[TOC]
0x00 Go语言基础之Socket网络编程 现在的我们几乎每天都在使用互联网,但是你知道程序是如果通过网络互相通信吗? 描述: 相信大部分人通常是一知半解的,作为一个程序员👨💻,对于网络模型你应该了解,知道网络到底是怎么进行通信的,进行工作的,为什么服务器能够接收到请求,做出响应。这里面的原理应该是每个 Web 程序员应该了解的。
本章我们就一起来学习下Go语言中的网络编程,关于网络编程其实是一个很庞大的领域,本文只是简单的演示了如何使用net包进行TCP和UDP通信。
1.基础概念介绍 描述: 互联网的核心是一系列协议,总称为互联网协议(Internet Protocol Suite)
,正是这一些协议规定了电脑如何连接和组网,并通过各种协议实现不同的功能, 下面简单介绍一些协议涉及的基础知识概念。
此处不得不老生重谈OSI七层网络模型
,OSI是国际标准化组织 1984 提出的模型标准,简称 OSI(Open Systems Interconnection Model),主要统一标准来规范网络协议让各个硬件厂商可以协同工作,相信如果你学习过网络工程或者操作系统方面的课程至少你是了解它的。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持,通常用户接触到的只是最上面的那一层。
weiyigeek.top-OSI七层网络与TCP/IP四层网络模型
如上图所示按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件,在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型,即应用层、传输层、网络层、数据链路层、物理层
。
下面简单介绍各层: (如有兴趣想深入学习的可以自行Google)
(1) 物理层 : 即通过我们计算机或其它设备通过网络硬件与外界互联网通信,它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号(比特流)。
例如: 以太网、无线Lan、PPP、双绞线、光纤、无线。
(2) 数据链路层 : 确定了物理层传输的0和1的分组方式及代表的意义, 通过以太网(Ethernet)的协议规定一组电信号构成一个数据包,叫做帧
(Frame)。
每一帧分成两个部分:标头(Head)和数据(Data) Head : 包含数据包的一些说明项,比如发送者(MAC地址)
、接受者(MAC地址)
、数据类型等等;(其长度固定为18字节
) Data : 则是数据包的具体内容。(其长度,最短为46字节,最长为1500字节
) Tips: 因此整个帧
最短为64字节,最长为1518字节, 所以如果数据很长就必须分割成多个帧进行发送。
(3) 网络层 : 使得我们能够区分不同的计算机是否属于同一个子网络(子网),该地址就叫做网络地址
(即IP地址),此时每台计算机有了两种地址,一种是MAC地址(硬件网卡唯一标识),另一种是IP地址。
IP地址: 则是网络管理员分配的,它可以帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。 根据IP协议发送的数据就叫做IP数据包
,IP数据包也分为标头
和数据
两个部分:标头
部分主要包括版本、长度、IP地址等信息(长度为20到60字节
)数据
部分则是IP数据包的具体内容,整个数据包的总长度最大为65535字节
。
(4) 传输层 : 有了上述的MAC地址和IP地址可以就可以在互联网上任意两台主机上建立通信。但是如果是要与主机上某一程序进行通信我们还需要一个端口
(Port),从而便可以让两个程序通过网络进行收发数据。
IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信, 此外可以选择常用的TCP或者UDP协议进行通信。端口
: 是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口 TCP (Transmission Control Protocol
): 面向连接的、可靠的、基于字节流的传输层通信协议(经过三次握手四层挥手
),由IETF的RFC 793定义,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。 (主要用于对通信的信息比较重要的场景
) UDP (User Datagram Protocol
) : 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法,我们可以持续的发送信息但并不关心其是否正常到达, 由IETF的 RFC 768 定义, 其数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。(主要用于视屏直播流、非实时性的控制指令发送的场景
)
(5) 应用层 : ”应用层”的作用就是规定应用程序使用的数据格式,由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。
例如: 我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。
如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。
weiyigeek.top-一张解释互联网的传输过程的图
知识扩展
Q:那么,发送者和接受者是如何标识呢? 答: 以太网规定,连入网络的所有设备都必须具有网卡
接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示例如00-FF-81-D5-15-F8
。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号
, 此时有了MAC地址就可以定位网卡和数据包的路径了。
Q: 有了MAC地址之后,如何把数据准确的发送给接收方呢? 描述: 首先通过ARP协议来获取接受方的MAC地址, 然后通过广播
(broadcasting)的方式,向本网络内所有计算机都发送,让每台计算机读取这个包的标头
,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同就接受这个包,做进一步处理,否则就丢弃这个包。
Q: 网络地址的协议? 描述: 规定网络地址的协议叫做IP协议,目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0~255.255.255.255
,当然里面包含三个私有网段,以及保留的网段,此处不细讲知道即可。
2.Socket 基础介绍 其实学习其它开发语言你将会发现, 基本高级语言中都有专门进行网络通信的包来提供两个程序的网络通信, 通常针对程序网络通信的开发都描述为Socket编程。
Q: 什么是Socket编程? 描述: Socket(也称作”套接字”
)是BSD UNIX
的进程通信机制,用于描述IP地址和端口是一个通信链的句柄,Socket可理解为TCP/IP网络的API
,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序,所以在我们计算机运行的应用程序通常通过"套接字"
向网络发出请求或者应答网络请求。
Socket 是应用层
与传输层(TCP/IP协议族)
通信的中间软件抽象层,在设计模式中Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。
weiyigeek.top-Socket图解
3.Go实现C/S端TCP通信 描述: TCP/IP(Transmission Control Protocol/Internet Protocol)
即传输控制协议/网间协议,是一种面向连接(连接导向)
的、可靠的、基于字节流的传输层(Transport layer)
通信协议,因为是面向连接的协议,数据像水流一样传输,但它会存在黏包问题(将会在后续演示解决方案)。
1) TCP 服务端 描述: 通常一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网、京东商城。
由于Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序常规流程: 1.设置监听网络地址与端口 2.接收客户端请求建立链接 3.创建goroutine处理链接
在Go语言中的可以采用net包来实现的TCP服务端的创建,下述罗列出net包中使用的相关方法原型:
func net.Listen(network string, address string) (net.Listener, error)
: 指定通信协议类型版本、本地网络地址和通信端口进行监听,注意 network参数值必须是“tcp”、“tcp4”、“tcp6”、“unix”或“unixpacket”
之一。
func (net.Listener).Accept() (net.Conn, error)
: 等待Listener对象并返回到侦听器的下一个连接。
func (net.Conn).Read(b []byte) (n int, err error)
: 从服务端或者客户端连接中读取数据。
2) TCP 客户端 TCP客户端进行TCP通信的流程大致如下: 1.与服务端的建立链接 2.与服务端进行数据收发 3.关闭与服务端的链接
下述罗列出net包中Tcp客户端创建使用的相关方法原型:
func net.Dial(network string, address string) (net.Conn, error)
: 客户端连接到指定网络上的地址, 同样 networks 参数可选值如下"tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket"
.
func (net.Conn).Write(b []byte) (n int, err error)
: 写入将数据写入连接即发送给连接的网络对象,并返回发送的字节数。(注意中文字符占3字节)
func (net.Conn).LocalAddr() net.Addr
: 获取本地客户端连接到服务端的网络地址信息。
简单示例1:TCP Server端与Client端一次通信。 服务端: Server.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func main () { address := "10.20.172.108:22022" listener, err := net.Listen("tcp" , address) if err != nil { fmt.Printf("Start Tcp Server on %v Failed!\nerr:%v\n" , address, err) return } else { fmt.Printf("Server Listen : %v\n" , address) } conn, err := listener.Accept() if err != nil { fmt.Printf("Accept failed,err: %v\n" , err) return } defer conn.Close() var msg [1024 ]byte n, err := conn.Read(msg[:]) if err != nil { fmt.Printf("Read from Client conn failed, err:%v\n" , err) return } fmt.Println(string (msg[:n])) }
客户端: Client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { address := "10.20.172.108:22022" conn, err := net.Dial("tcp" , address) if err != nil { fmt.Printf("Connect Server failed!\nerr:%v\n" , err) return } sendMsg := "Hello World! Server, I'm client" conn.Write([]byte (sendMsg)) defer conn.Close() }
将上面的代码保存之后分别编译成server和client可执行文件,具体操作如下:1 2 3 4 5 6 ➜ Server go build ➜ Server ./Server Server Listen : 10.20.172.108:22022 ➜ Client go build ➜ Client ./Client
weiyigeek.top-简单示例1执行结果
进阶示例2: TCP Server端与多个Client端持续通信。 Server.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package mainimport ( "bufio" "fmt" "io" "net" ) func SendReceiveProccess (conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) var msg [1024 ]byte for { n, err := reader.Read(msg[:]) if err == io.EOF { fmt.Printf("Close conn %v\n" , conn.RemoteAddr()) break } if err != nil { fmt.Printf("Read from Client conn failed, Close conn %v\n" , conn.RemoteAddr()) break } fmt.Println(conn.RemoteAddr(), "->" , string (msg[:n])) _, err = conn.Write([]byte (msg[:n])) if err != nil { fmt.Printf("Send failed, Close Client conn %v\n" , conn.RemoteAddr()) break } } } func main () { address := "10.20.172.108:22022" listener, err := net.Listen("tcp" , address) if err != nil { fmt.Printf("Start Tcp Server on %v Failed!\nerr:%v\n" , address, err) return } else { fmt.Printf("Server Listen %v Start......\n" , address) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { fmt.Printf("Accept failed,err: %v\n" , err) return } go SendReceiveProccess(conn) } }
Client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package mainimport ( "bufio" "fmt" "net" "os" "strings" ) func main () { address := "10.20.172.108:22022" conn, err := net.Dial("tcp" , address) if err != nil { fmt.Printf("Connect Server failed! \n [err]: %v\n" , err) return } else { fmt.Printf("Connect Server %v successful!\n" , address) } defer conn.Close() sendMsg := fmt.Sprintf("Hello Server, I'm %v client." , conn.LocalAddr()) inputReader := bufio.NewReader(os.Stdin) conn.Write([]byte (sendMsg)) for { reply := [1024 ]byte {} n, err := conn.Read(reply[:]) if err != nil { fmt.Println("recv failed, err:" , err) return } fmt.Printf("Server > %v\n" , string (reply[:n])) fmt.Printf("请输入消息:" ) sendMsg, _ = inputReader.ReadString('\n' ) sendMsg = strings.TrimSpace(sendMsg) sendMsg = strings.Trim(sendMsg, "\n" ) if strings.ToUpper(sendMsg) == "QUIT" { fmt.Printf("exit conn......." ) break } _, err = conn.Write([]byte (sendMsg)) if err != nil { return } } }
Server.go与Client.go编译&运行&执行结果:1 2 go build && ./Servergo build && ./Client
weiyigeek.top-进阶示例2执行结果
4.Go实现C/S端UDP通信 描述: UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)
参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
Tips : UDP 通信相比较于 TCP 通信使用简单, 下述我将UDP服务端和客户端实现的相关方法原型进行展示。
func net.ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error)
: 设置监听UDP相关的网络地址与端口信息、网络必须是UDP网络名称。
func (*net.UDPConn).ReadFromUDP(b []byte) (int, *net.UDPAddr, error)
: 接收conn对象里发送的信息, ReadFromUDP与ReadFrom类似,但返回一个UDPADD。
func (*net.UDPConn).WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
: 发送信息到conn对象里, WriteToUDP的行为类似于WriteTo,但使用UDPADD。
func net.DialUDP(network string, laddr *net.UDPAddr, raddr *net.UDPAddr) (*net.UDPConn, error)
: 作用于与UDP服务端建立连接, DialUDP的作用类似于UDP网络的拨号。
进阶示例1.实现UDP服务端与多客户端进行连接通信! Server.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" "net" "strings" "time" ) func main () { server_ip := [4 ]byte {10 , 20 , 172 , 108 } server_port := 30000 conn, err := net.ListenUDP("udp" , &net.UDPAddr{ IP: net.IPv4(server_ip[0 ], server_ip[1 ], server_ip[2 ], server_ip[3 ]), Port: server_port, }) if err != nil { fmt.Printf("Listen UDP Server (%v:%v) Failed, err: %v\n" , server_ip, server_port, err) return } else { fmt.Printf("[%v] Listening UDP Server %v:%v is successful!\n" , time.Now().Format("2006-01-02 15:04:05" ), server_ip, server_port) } defer conn.Close() for { var recvMsg [1024 ]byte count, addr, err := conn.ReadFromUDP(recvMsg[:]) if err != nil { fmt.Println("Read from UDP Client failed. Err:" , err) return } fmt.Printf("[%v] %v - %v\n" , time.Now().Format("2006-01-02 15:04:05" ), addr.String(), string (recvMsg[:count-1 ])) reply := strings.ToUpper(string (recvMsg[:count])) conn.WriteToUDP([]byte (reply), addr) } }
Client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package mainimport ( "bufio" "fmt" "net" "os" "strings" "time" ) func main () { server_ip := [4 ]byte {10 , 20 , 172 , 108 } server_port := 30000 socket, err := net.DialUDP("udp" , nil , &net.UDPAddr{ IP: net.IPv4(server_ip[0 ], server_ip[1 ], server_ip[2 ], server_ip[3 ]), Port: server_port, }) if err != nil { fmt.Printf("Connect UDP Server (%v:%v) Failed! err: %v\n" , server_ip, server_port, err) return } else { fmt.Printf("[%v] - Connect UDP Server %v:%v successful!\n" , time.Now().Format("2006-01-02 15:04:05" ), server_ip, server_port) } defer socket.Close() var reply [1024 ]byte inputData := bufio.NewReader(os.Stdin) for { fmt.Print("<- 请输入将要发送的内容:" ) sendMsg, _ := inputData.ReadString('\n' ) sendMsg = strings.TrimSpace(sendMsg) socket.Write([]byte (sendMsg)) if err != nil { fmt.Printf("发送数据失败,err: %v\n" , err) return } count, _, err := socket.ReadFromUDP(reply[:]) if err != nil { fmt.Printf("接收数据失败, err: %v\n" , err) return } fmt.Printf("Server -> [%v Bytes] %v \n" , count, string (reply[:count])) } }
执行结果:
weiyigeek.top-实现UDP服务端与多客户端进行连接通信!
5.课外知识扩展 1) TCP 黏包 在讲解TCP黏包前我们先来看看TCP黏包会导致什么问题?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func process (conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) var buf [1024 ]byte for { n, err := reader.Read(buf[:]) if err == io.EOF { break } if err != nil { fmt.Println("read from client failed, err:" , err) break } recvStr := string (buf[:n]) fmt.Println("收到client发来的数据:" , recvStr) } } func main () { listen, err := net.Listen("tcp" , "127.0.0.1:30000" ) if err != nil { fmt.Println("listen failed, err:" , err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:" , err) continue } go process(conn) } }
1 2 3 4 5 6 7 8 9 10 11 12 func main () { conn, err := net.Dial("tcp" , "127.0.0.1:30000" ) if err != nil { fmt.Println("dial failed, err" , err) return } defer conn.Close() for i := 0 ; i < 20 ; i++ { msg := `Hello, Hello. How are you?` conn.Write([]byte (msg)) } }
将上面的代码保存后分别编译, 先启动服务端然后再启动客户端,可以看到服务端输出结果如下:1 2 3 4 5 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
从客户端示例代码中可以看见客户端分20次发送的数据,但是在服务端并没有成功的输出20次,而是多条数据“粘”到了一起,这就是TCP黏包带来的问题。
Q: 此时可能会问为什么会出现粘包?
答:主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发, 而TCP黏包可以发生在在发送端也可发生在接收端, 主要是由于Nagle算法导致的。 Nagle算法是一种改善网络传输效率的算法, 通常在发送端由于Nagle算法导致的黏包问题,而接收端接收不及时也会造成的接收端粘包。 发送端: 简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去,所以说Nagle算法特性其并不适用于某些场景。 接收端: TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
Q: 有何解决办法?
答: 出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。 此时需要自己定义一个协议,比如数据包的前4个字节为包头里面存储的是发送的数据的长度,然后通过发送端进行封包、接收端进行拆包的操作来解决此问题。
Q: 什么是封包?
答: 封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
实践示例:
自定义协议: proto.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package protoimport ( "bufio" "bytes" "encoding/binary" ) func Encode (message string ) ([]byte , error) { var length = int32 (len (message)) var pkg = new (bytes.Buffer) err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil , err } err = binary.Write(pkg, binary.LittleEndian, []byte (message)) if err != nil { return nil , err } return pkg.Bytes(), nil } func Decode (reader *bufio.Reader) (string , error) { lengthByte, _ := reader.Peek(4 ) lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "" , err } if int32 (reader.Buffered()) < length+4 { return "" , err } pack := make ([]byte , int (4 +length)) _, err = reader.Read(pack) if err != nil { return "" , err } return string (pack[4 :]), nil }
然后在服务端和客户端分别使用上面定义的proto包的Decode 和 Encode
函数处理数据。
服务端代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "bufio" "fmt" "io" "net" "weiyigeek.top/studygo/Day08/03StickBag/proto" ) func process (conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { msg, err := proto.Decode(reader) if err == io.EOF { return } if err != nil { fmt.Println("decode msg failed, err:" , err) return } fmt.Println("收到client发来的数据:" , msg) } } func main () { listen, err := net.Listen("tcp" , "127.0.0.1:30000" ) if err != nil { fmt.Println("listen failed, err:" , err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println("accept failed, err:" , err) continue } go process(conn) } }
客户端代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "fmt" "net" "weiyigeek.top/studygo/Day08/03StickBag/proto" ) func main () { conn, err := net.Dial("tcp" , "127.0.0.1:30000" ) if err != nil { fmt.Println("dial failed, err" , err) return } defer conn.Close() for i := 0 ; i < 20 ; i++ { msg := `Hello, Hello. How are you?` data, err := proto.Encode(msg) if err != nil { fmt.Println("encode msg failed, err:" , err) return } conn.Write(data) } }
执行结果: 此时发现它不会将多个hello…字符串放在一行了,并且输出也是我们预定的20次。
weiyigeek.top-黏包解决办法
补充知识 :字节序列的存储格式
之大端(Big-endian
)和小端(LittleEndian
)存储
Big-endian:将高序字节存储在起始地址(高位编址),此种方式便于人类理解。
Little-endian:将低序字节存储在起始地址(低位编址)一般在x64/x32的系统中都是小端存储。
举个例子: 如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为;
address
big-endian(大端)
little-endian (小端)
0x0000
0x12
0xcd
0x0001
0x34
0xab
0x0002
0xab
0x34
0x0003
0xcd
0x12
注:每个地址存1个字节,2位16进制数是1个字节(0xFF=11111111)
;
weiyigeek.top-大端和小端
补充知识 :CPU存储一个字节的数据时其字节内的8个比特之间的顺序是否也有big endian
和little endian
之分?或者说是否有比特序的不同?
实际上,这个比特序是同样存在的只是多个两个表示名称(MSB 和 LSB)。 MSB的意思是:全称为Most Significant Bit
,在二进制数中属于最高有效位,MSB是最高加权位,与十进制数字中最左边的一位类似。 LSB的意思是:全称为Least Significant Bit
,在二进制数中意为最低有效位,一般来说,MSB位于二进制数的最左侧,LSB位于二进制数的最右侧。
下面以数字0xB4(10110100)用图加以说明。1 2 3 4 5 6 7 8 9 10 11 12 msb------------------------>lsb +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ lsb-------------------------->msb +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
实际上,由于CPU存储数据操作的最小单位是一个字节,其内部的比特序是什么样对我们的程序来说是一个黑盒子。也就是说,你给我一个指向0xB4这个数的指针,对于big endian方式的CPU来说,它是从左往右依次读取这个数的8个比特;而对于little endian方式的CPU来说,则正好相反,是从右往左依次读取这个数的8个比特。而我们的程序通过这个指针访问后得到的数就是0xB4,字节内部的比特序对于程序来说是不可见的,其实这点对于单机上的字节序来说也是一样的。
0x01Go语言基础之HTTP C/S实现 描述: 在Socket网络编程中我们学习TCP协议与UDP协议的服务端与客户端代码的编写实践, 今天我们来学习在应用层中我们使用最多的HTTP协议,利用Go语言分别实现HTTP的Client端与Server端服务。
Go语言内置的net/http
包十分的优秀,其为我们提供了HTTP客户端和服务端的实现。
Q: 什么是 HTTP 协议?
答: 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准,设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。 例如,我们在浏览器中访问的http的网站,其传输协议便是采用HTTP。
1.HTTP 服务端 描述: 利用Go语言提供的net/http包我们可以非常便利的使用并创建一个服务端, 值得说明的是如果仅仅是实现简单的API接口可以采用原生的http包中提供的方法, 而如果是编写一些Web后端项目通常是采用框架来实现,所以本章节主要对Go语言创建HTTP服务端的基础示例进行说明学习,而Go语言的Web应用开发框框在我后续笔记中将会进行实践讲解。
HTTP服务端实现常用方法原型:
func http.ListenAndServe(addr string, handler http.Handler) error
: 使用指定的监听地址和处理器启动一个HTTP服务端, 处理器参数通常是nil表示采用包变量DefaultServeMux作为处理器。
func http.Handle(pattern string, handler http.Handler){ DefaultServeMux.Handle(pattern, handler) }
: Handle在DefaultServeMux中注册给定模式的处理程序函数。
func http.HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)){DefaultServeMux.HandleFunc(pattern, handler)}
: HandleFunc在DefaultServeMux中注册给定模式的处理程序函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type httpServer struct {}func (server httpServer) ServeHTTP (w http.ResponseWriter, r *http.Request) { w.Write([]byte (r.URL.Path)) } var fooHandler httpServerhttp.Handle("/foo" , fooHandler) http.HandleFunc("/bar" , func (w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %q" , html.EscapeString(r.URL.Path)) }) log.Fatal(http.ListenAndServe(":8080" , nil ))
Tips: go http http.Handle 和 http.HandleFunc 区别?
http.Handle()
需要自己去定义struct实现这个Handler接口。
http.HandleFunc()
则不需要我们自己定义structqi实现,只需要传入访问连接路径以及DefaultServeMux中注册给定模式的处理程序函数。
通常是使用HandleFunc方法,其更加方便简单。
示例1.http.Handle自定义实现Handler接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "fmt" "log" "net/http" ) type httpServer struct {}func (server httpServer) ServeHTTP (w http.ResponseWriter, r *http.Request) { path := r.URL.Path fmt.Println(path) switch path { case "/" : w.Write([]byte ("根路径 : " + r.URL.Path)) case "/index" : w.Write([]byte ("首页路径 : " + r.URL.Path)) case "/hello" : w.Write([]byte ("子网页路径 : " + r.URL.Path)) default : w.Write([]byte ("<b>未知路径</b> : https://weiyigeek.top" + r.URL.Path)) } } func main () { var server httpServer serveraddr := "0.0.0.0:8080" http.Handle("/" , server) err := http.ListenAndServe(serveraddr, nil ) if err != nil { log.Fatal(err) } else { fmt.Printf("Http Server %v Started......" , serveraddr) } }
执行结果如下图所示
weiyigeek.top-http.Handle结果
示例2.http.HandleFunc实现用户请求处理
index.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 测试页面2</title > </head > <body > <p id ="tips" style ="color: red;font-size:medium;font-weight: bolder;" > Welcome to Visit weiyigeek.top web site</p > <button id ="msg" > 点击提示</button > <script > var tips = document .getElementById("tips" ).textContent; document .getElementById("msg" ).onclick=function ( ) { alert(tips); } </script > </body > </html >
Http Server 端代码:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "fmt" "io/ioutil" "log" "net/http" "time" ) func main () { serveraddr := "0.0.0.0:8080" http.HandleFunc("/hello" , hello) http.HandleFunc("/blog" , blog) http.HandleFunc("/htmlfile" , htmlfile) err := http.ListenAndServe(serveraddr, nil ) if err != nil { log.Fatal(err) } else { fmt.Printf("Http Server %v Started......" , serveraddr) } } func hello (w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World! Go <b>测试页面0</b> Time : %s" , time.Now()) } func blog (w http.ResponseWriter, r *http.Request) { reply := fmt.Sprintf("<b>测试页面1</b><p> 标题.Demo1 Test(Go net/http) </p> <i>I'm WeiyiGeek</i><br/> Time : %s" , time.Now().Format("2006-01-02 15:04:05" )) w.Write([]byte (reply)) } func htmlfile (w http.ResponseWriter, r *http.Request) { res, err := ioutil.ReadFile("./index.html" ) if err != nil { w.Write([]byte (fmt.Sprintf("Error: %v" , err))) return } w.Write([]byte (res)) }
将上面的代码编译之后执行,打开电脑上的浏览器在地址栏输入127.0.0.1:8080
回车,此时返回如下页面。
weiyigeek.top-http.HandleFunc实现用户请求处理
示例3.自定义http.Server结构体参数与Handler实现 描述: 我们可以创建一个自定义的 Server
Handler实现, 通过http.Server指定设置结构体参数来管理服务端的行为。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "log" "net/http" "time" ) type myHandler struct { name string } func (handler myHandler) ServeHTTP (w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/index" : fmt.Fprintf(w, "%s\n" , "This is Index path" ) case "/weiyigeek" : fmt.Fprintf(w, "%s -> %s\n" , handler.name, "https://weiyigeek.top" ) default : w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "no such page: %s\n" , req.URL) } } func main () { handler := myHandler{name: "WeiyiGeek" } server := &http.Server{ Addr: ":8080" , Handler: handler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20 , } log.Fatal(server.ListenAndServe()) }
执行结果:
weiyigeek.top-自定义http.Server结构体参数与Handler实现
Tips: 显然我们可以继续向ServeHTTP方法中添加case,但在一个实际的应用中,将每个case中的逻辑定义到一个分开的方法或函数中会很实用。对于更复杂的应用我们可以通过一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中,通过组合来处理更加错综复杂的路由需求。
Tips: 【非常注意】【非常注意】【非常注意】自己实现的 http.Handler 且必须包含一个 ServeHTTP 方法名, 才能接受和响应客户端。
示例4.ServeMux.HandleFunc实现http服务端 语句http.HandlerFunc(handler.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型, 它有如下的定义:1 2 3 4 5 package httptype HandlerFunc func (w ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP (w ResponseWriter, r *Request) { f(w, r) }
简单示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type dollars float32 func (d dollars) String () string { return fmt.Sprintf("$%.2f" , d) } type MyHandler map [string ]dollarsfunc (self MyHandler) list (w http.ResponseWriter, req *http.Request) { for item, price := range self { fmt.Fprintf(w, "%s: %s\n" , item, price) } } func (self MyHandler) price (w http.ResponseWriter, req *http.Request) { item := req.URL.Query().Get("item" ) price, ok := self[item] if !ok { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "no such item: %q\n" , item) return } fmt.Fprintf(w, "%s\n" , price) } func main () { handler := MyHandler{"shoes" : 50 , "socks" : 5 } http.HandleFunc("/list" , handler.list) http.HandleFunc("/price" , handler.price) log.Fatal(http.ListenAndServe("localhost:8000" , nil )) }
Tips: 为了方便net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数, 前面说过我们只需要传入nil值即可。
2.HTTP 客户端 描述: 如果你学过Python爬虫那么你也肯定知道,我们可以采用编程语言提供的相应函数或者方法就要可以对指定的网站进行请求,并可以将请求响应的数据进行清洗然后存储进数据中。
Go语言也为我们提供相应的内置包net/http
和net/url
以便于我们进行网站API接口(GET
、POST
、PUT
)请求和处理服务端响应的数据。
HTTP客户端请求函数原型:
func http.Get(url string) (resp *http.Response, err error)
: 向指定的URL发出Get请求,它将随重定向,最多10个重定向。
func http.Post(url string, contentType string, body io.Reader) (resp *http.Response, err -error)
: 指定的URL发出Post请求,调用方在完成读取后应关闭相应的主体。
func http.PostForm(url string, data url.Values) (resp *http.Response, err error)
: PostForm向指定的URL发出POST,并将数据的键和值URL编码为请求正文,Content-Type header
设置为application/x-www-form-urlencoded
。
func http.NewRequest(method string, url string, body io.Reader) (*http.Request, error)
: NewRequest使用后台上下文包装NewRequestWithContext.
func (*http.Client).Do(req *http.Request) (*http.Response, error)
: 按照客户端上配置的策略(如重定向、cookie、身份验证),发送到HTTP请求并返回到HTTP响应。
func url.Parse(rawurl string) (*url.URL, error)
: Parse将rawurl解析为URL结构。
func (url.Values).Encode() string
: Encode将值编码为按键排序的“URL编码”形式(“bar=baz&foo=qux”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 resp, err := http.Get("http://example.com/" ) ... resp, err := http.Post("http://example.com/upload" , "image/jpeg" , &buf) ... resp, err := http.PostForm("http://example.com/form" , url.Values{"key" : {"Value" }, "id" : {"123" }}) ... if err != nil { // handle error } ... defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body)
Tips : GET请求的参数需要使用Go语言内置的net/url
标准库来处理。
简单示例.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "io/ioutil" "net/http" ) func getMethod (url string ) { resp, err := http.Get(url) if err != nil { fmt.Printf("get failed, err:%v\n" , err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("read from resp.Body failed, err:%v\n" , err) return } html := string (body) fmt.Printf("resp.StatusCode : %v,\nresp.Status: %v,\nresp.Request: %#v,\nresp.Header: %#v\nresp.Cookies: %#v,\nresp.TLS: %#v\n" , resp.StatusCode, resp.Status, resp.Request, resp.Header, resp.Cookies(), resp.TLS) fmt.Println("网站响应长度: " , len (html)) } func main () { getMethod("https://www.weiyigeek.top" ) }
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 resp.StatusCode : 200 , resp.Status: 200 OK, resp.Request: &http.Request{Method:"GET" , URL:(*url.URL)(0xc000176750 ), Proto:"" , ProtoMajor:0 , ProtoMinor:0 , Header:http.Header{"Referer" :[]string {"https://www.weiyigeek.top" }}, Body:io.ReadCloser(nil ), GetBody:(func () (io.ReadCloser, error) )(nil ) , ContentLength :0, TransferEncoding :[]string (nil ) , Close :false , Host :"", Form :url .Values (nil ) , PostForm :url .Values (nil ) , MultipartForm :(*multipart.Form) (nil ) , Trailer :http .Header (nil ) , RemoteAddr :"", RequestURI :"", TLS :(*tls.ConnectionState) (nil ) , Cancel :(<-chan struct {}) (nil ) , Response :(*http.Response) (0xc000176630) , ctx :(*context.emptyCtx) (0xc000134010) }, resp .Header : http .Header {"Access-Control-Allow-Origin" :[]string {"*" }, "Age" :[]string {"340" }, "Alt-Svc" :[]string {"h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400, h3-28=\":443\"; ma=86400, h3-27=\":443\"; ma=86400" }, "Cache-Control" :[]string {"max-age=600" }, "Cf-Cache-Status" :[]string {"DYNAMIC" }, "Cf-Ray" :[]string {"6b222396a8d10d24-LAX" }, "Content-Type" :[]string {"text/html; charset=utf-8" }, "Date" :[]string {"Mon, 22 Nov 2021 12:25:11 GMT" }, "Expect-Ct" :[]string {"max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"" }, "Expires" :[]string {"Mon, 22 Nov 2021 12:27:16 GMT" }, "Last-Modified" :[]string {"Fri, 08 Oct 2021 11:30:52 GMT" }, "Nel" :[]string {"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}" }, "Report-To" :[]string {"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=6dx48uJJynNOFy1TpNXdJU0V%2FfBjazc15i3SLtiT4xoXr8vXl0MZTRuVE11vx33KNqsU05DfmNKKuPrzDTBstP32mSG%2B%2FNWUtG%2B4vlX5Ro3hApIow24bII5D0uT8FtSh\"}],\"group\":\"cf-nel\",\"max_age\":604800}" }, "Server" :[]string {"cloudflare" }, "Vary" :[]string {"Accept-Encoding" }, "Via" :[]string {"1.1 varnish" }, "X-Cache" :[]string {"HIT" }, "X-Cache-Hits" :[]string {"1" }, "X-Fastly-Request-Id" :[]string {"de2ef3b07dec51677274e7fe8eb68162dc77c39d" }, "X-Github-Request-Id" :[]string {"C002:9402:1A6C52:23CF10:619B8A4C" }, "X-Proxy-Cache" :[]string {"MISS" }, "X-Served-By" :[]string {"cache-bur17563-BUR" }, "X-Timer" :[]string {"S1637583911.481935,VS0,VE1" }}resp.Cookies: []*http.Cookie{}, resp.TLS: &tls.ConnectionState{Version:0x304 , HandshakeComplete:true , DidResume:false , CipherSuite:0x1301 , NegotiatedProtocol:"h2" , NegotiatedProtocolIsMutual:true , ServerName:"weiyigeek.top" , PeerCertificates:[]*x509.Certificate{(*x509.Certificate)(0xc000324000 ), (*x509.Certificate)(0xc000324580 )}, VerifiedChains:[][]*x509.Certificate{[]*x509.Certificate{(*x509.Certificate)(0xc000324000 ), (*x509.Certificate)(0xc000324580 ), (*x509.Certificate)(0xc000267180 )}}, SignedCertificateTimestamps:[][]uint8 (nil ), OCSPResponse:[]uint8 {0x30 , 0x82 , 0x1 , 0x12 , 0xa , 0x1 , 0x0 , 0xa0 , 0x82 , 0x1 , 0xb , 0x30 , 0x82 , 0x1 , 0x7 , 0x6 , 0x9 , 0x2b , 0x6 , 0x1 , 0x5 , 0x5 , 0x7 , 0x30 , 0x1 , 0x1 , 0x4 , 0x81 , 0xf9 , 0x30 , 0x81 , 0xf6 , 0x30 , 0x81 , 0x9e , 0xa2 , 0x16 , 0x4 , 0x14 , 0xa5 , 0xce , 0x37 , 0xea , 0xeb , 0xb0 , 0x75 , 0xe , 0x94 , 0x67 , 0x88 , 0xb4 , 0x45 , 0xfa , 0xd9 , 0x24 , 0x10 , 0x87 , 0x96 , 0x1f , 0x18 , 0xf , 0x32 , 0x30 , 0x32 , 0x31 , 0x31 , 0x31 , 0x31 , 0x38 , 0x32 , 0x30 , 0x34 , 0x32 , 0x33 , 0x38 , 0x5a , 0x30 , 0x73 , 0x30 , 0x71 , 0x30 , 0x49 , 0x30 , 0x9 , 0x6 , 0x5 , 0x2b , 0xe , 0x3 , 0x2 , 0x1a , 0x5 , 0x0 , 0x4 , 0x14 , 0x12 , 0xd7 , 0x8b , 0x40 , 0x2c , 0x35 , 0x62 , 0x6 , 0xfa , 0x82 , 0x7f , 0x8e , 0xd8 , 0x92 , 0x24 , 0x11 , 0xb4 , 0xac , 0xf5 , 0x4 , 0x4 , 0x14 , 0xa5 , 0xce , 0x37 , 0xea , 0xeb , 0xb0 , 0x75 , 0xe , 0x94 , 0x67 , 0x88 , 0xb4 , 0x45 , 0xfa , 0xd9 , 0x24 , 0x10 , 0x87 , 0x96 , 0x1f , 0x2 , 0x10 , 0x9 , 0x98 , 0xa5 , 0x9a , 0x26 , 0x72 , 0xc7 , 0x24 , 0x4a , 0x4d , 0xc5 , 0x92 , 0x80 , 0xfb , 0x65 , 0x5a , 0x80 , 0x0 , 0x18 , 0xf , 0x32 , 0x30 , 0x32 , 0x31 , 0x31 , 0x31 , 0x31 , 0x38 , 0x32 , 0x30 , 0x32 , 0x37 , 0x30 , 0x32 , 0x5a , 0xa0 , 0x11 , 0x18 , 0xf , 0x32 , 0x30 , 0x32 , 0x31 , 0x31 , 0x31 , 0x32 , 0x35 , 0x31 , 0x39 , 0x34 , 0x32 , 0x30 , 0x32 , 0x5a , 0x30 , 0xa , 0x6 , 0x8 , 0x2a , 0x86 , 0x48 , 0xce , 0x3d , 0x4 , 0x3 , 0x2 , 0x3 , 0x47 , 0x0 , 0x30 , 0x44 , 0x2 , 0x20 , 0x5f , 0x88 , 0xf2 , 0xc1 , 0x99 , 0xcf , 0x99 , 0x2b , 0x57 , 0xd6 , 0xd0 , 0x38 , 0x2e , 0x7 , 0x72 , 0xc7 , 0x7d , 0x48 , 0x34 , 0x57 , 0x60 , 0x19 , 0xe , 0x42 , 0xd1 , 0x32 , 0x6b , 0xea , 0x5f , 0xbd , 0xfa , 0x36 , 0x2 , 0x20 , 0x67 , 0xc7 , 0xc1 , 0x3 , 0xd4 , 0xed , 0x1e , 0x32 , 0xb3 , 0x5f , 0x7e , 0xb3 , 0xc8 , 0x10 , 0xb4 , 0xdf , 0x88 , 0x47 , 0x1c , 0xf3 , 0xee , 0xab , 0x3b , 0x86 , 0xc7 , 0xe4 , 0xbc , 0xcf , 0x5c , 0x1d , 0x69 , 0x48 }, TLSUnique:[]uint8 (nil ), ekm:(func (string , []uint8 , int ) ([]uint8 , error) )(0x679b80) } 网站响应长度: 16853
自定义 Client&Transport 描述: 要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:1 2 3 4 5 6 7 8 9 10 11 12 13 14 client := &http.Client{ CheckRedirect: redirectPolicyFunc, Timeout: 30 * time.Second, } resp, err := client.Get("http://example.com" ) req, err := http.NewRequest("GET" , "http://example.com" , nil ) req.Header.Add("If-None-Match" , `W/"wyzzy"` ) resp, err := client.Do(req)
描述: 要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:1 2 3 4 5 6 7 8 9 10 tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, DisableCompression: true , DisableKeepAlives: true , } client := &http.Client{Transport: tr} resp, err := client.Get("https://example.com" )
Tips: Client
和Transport
类型都可以安全的被多个goroutine同时使用, 出于效率考虑,应该一次建立、尽量重用。 Tips: 如果取数据比较频繁的场景建议使用长连接,否则使用短连接即可。
3.综合实践 3.1 Get 请求示例 get_client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package mainimport ( "fmt" "io/ioutil" "net/http" "net/url" "strings" ) func getMethod (urlstr string ) { urlParse, err := url.Parse(urlstr) if err != nil { fmt.Printf("Url %v Format Error!\nerr: %v\n" , urlParse, err) return } data := url.Values{} data.Set("id" , "1024" ) data.Set("name" , "唯一极客" ) queryStr := data.Encode() urlParse.RawQuery = queryStr fmt.Println("QueryStr => " , queryStr) req, err := http.NewRequest("Get" , urlParse.String(), nil ) if err != nil { fmt.Printf("NewRequest %v faile!\n[error]: %v\n" , urlstr, err) return } resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf("Request %v faile!\n[error]: %v\n" , urlstr, err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("read from resp.Body failed, err:%v\n" , err) return } fmt.Printf("resp.StatusCode : %v,\nresp.Status: %v,\nresp.Request: %#v,\nresp.Header: %#v\nresp.Cookies: %#v,\nresp.TLS: %#v\n" , resp.StatusCode, resp.Status, resp.Request, resp.Header, resp.Cookies(), resp.TLS) fmt.Println("网站响应长度: " , len (body)) } func main () { getMethod("http://10.20.172.108:8080/get" ) }
执行结果:1 2 3 4 5 6 7 8 QueryStr => id=1024 &name=%E5%94 %AF%E4%B8%80 %E6%9 E%81 %E5%AE%A2 resp.StatusCode : 200 , resp.Status: 200 OK, resp.Request: &http.Request{Method:"Get" , URL:(*url.URL)(0xc0000f e090), Proto:"HTTP/1.1" , ProtoMajor:1 , ProtoMinor:1 , Header:http.Header{}, Body:io.ReadCloser(nil ), GetBody:(func () (io.ReadCloser, error) )(nil ) , ContentLength :0, TransferEncoding :[]string (nil ) , Close :false , Host :"10.20.172.108:8080", Form :url .Values (nil ) , PostForm :url .Values (nil ) , MultipartForm :(*multipart.Form) (nil ) , Trailer :http .Header (nil ) , RemoteAddr :"", RequestURI :"", TLS :(*tls.ConnectionState) (nil ) , Cancel :(<-chan struct {}) (nil ) , Response :(*http.Response) (nil ) , ctx :(*context.emptyCtx) (0xc0000b0010) }, resp .Header : http .Header {"Content-Length" :[]string {"68" }, "Content-Type" :[]string {"application/json;charset=UTF-8" }, "Cookies" :[]string {"id=1024;name=唯一极客" }, "Date" :[]string {"Tue, 23 Nov 2021 05:25:41 GMT" }, "Requestmethod" :[]string {"Get" }}resp.Cookies: []*http.Cookie{}, resp.TLS: (*tls.ConnectionState)(nil ) 网站响应长度: 68
3.2 Post 请求示例 post_client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func postMethos (urlstr string ) { contentType := "application/json" data := `{"id":128,"name":"Weiyi"}` resp, err := http.Post(urlstr, contentType, strings.NewReader(data)) if err != nil { fmt.Printf("post failed, err:%v\n" , err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("get resp failed, err:%v\n" , err) return } fmt.Println("Body : " , string (body)) fmt.Println("resp.Header : " , resp.Header) } func main () { postMethos("http://10.20.172.108:8080/post" ) }
执行结果:1 2 Body : {"method" :"POST" ,"status" :"ok" ,"data" :{"id" :128,"name" :"Weiyi" }} resp.Header : map[Content-Length:[64] Content-Type:[application/json;charset=UTF-8] Cookies:[id=128;name=Weiyi] Date:[Tue, 23 Nov 2021 05:25:41 GMT] Requestmethod:[Post]]
3.3 PostForm 请求示例 postForm_client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func postFormMethod (urlstr string ) { data := url.Values{} data.Set("id" , "256" ) data.Set("name" , "WeiyiGeek-唯一极客" ) resp, err := http.PostForm(urlstr, data) if err != nil { fmt.Printf("postForm failed, err:%v\n" , err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("get resp failed, err:%v\n" , err) return } fmt.Println("Body : " , string (body)) fmt.Println("resp.Header : " , resp.Header) } func main () { postFormMethod("http://10.20.172.108:8080/postform" ) }
执行结果:1 2 Body : {"method" :"POSTFORM" ,"status" :"ok" ,"data" :{"id" :256,"name" :"WeiyiGeek" }} resp.Header : map[Content-Length:[72] Content-Type:[application/json;charset=UTF-8] Cookies:[method=form;id=256;name=WeiyiGeek-唯一极客] Date:[Tue, 23 Nov 2021 05:25:41 GMT] Requestmethod:[PostForm]]
3.4 Http 服务响应示例 描述: 下述http_serve.go代码包含上面三种请求示例的响应,以及请求参数获取以及输出。
http_serve.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 package mainimport ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "strconv" "time" ) type myHandler struct { Id int `json:"id"` Name string `json:"name"` } func (handler myHandler) ServeHTTP (w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/get" : fmt.Printf("Method : %v, URL : %v \n" , req.Method, req.URL) fmt.Println("QueryParam : " , req.URL.Query()) queryParam := req.URL.Query() id := queryParam.Get("id" ) name := queryParam.Get("name" ) fmt.Printf("id = %v,name = %v\n" , id, name) w.Header().Add("RequestMethod" , "Get" ) w.Header().Add("Content-Type" , "application/json;charset=UTF-8" ) w.Header().Add("Cookies" , fmt.Sprintf("id=%v;name=%v" , id, name)) uid, err := strconv.Atoi(id) if err != nil { errMsg := fmt.Sprintf("uid convert err! %v\n" , err) fmt.Println(errMsg) w.Write([]byte (errMsg)) return } reply := fmt.Sprintf("{\"method\":\"%v\",\"status\":\"ok\",\"data\":{\"id\":%v,\"name\":\"%v\"}}" , "GET" , uid, handler.Name) fmt.Printf("reply => %v\n\n" , reply) w.Write([]byte (reply)) case "/post" : fmt.Printf("Method : %v, URL : %v \n" , req.Method, req.URL) body, err := ioutil.ReadAll(req.Body) if err != nil { fmt.Println("req Body Read Error," , err) return } fmt.Println("req.Body => " , string (body)) json.Unmarshal(body, &handler) fmt.Printf("id = %v,name = %v\n" , handler.Id, handler.Name) w.Header().Add("RequestMethod" , "Post" ) w.Header().Add("Content-Type" , "application/json;charset=UTF-8" ) w.Header().Add("Cookies" , fmt.Sprintf("id=%v;name=%v" , handler.Id, handler.Name)) reply := fmt.Sprintf("{\"method\":\"%v\",\"status\":\"ok\",\"data\":{\"id\":%v,\"name\":\"%v\"}}" , "POST" , handler.Id, handler.Name) fmt.Printf("reply => %v\n\n" , reply) w.Write([]byte (reply)) case "/postform" : fmt.Printf("Method : %v, URL : %v \n" , req.Method, req.URL) req.ParseForm() fmt.Println(req.PostForm) id := req.PostForm.Get("id" ) name := req.PostForm.Get("name" ) fmt.Printf("id = %v, name = %v\n" , id, name) body, err := ioutil.ReadAll(req.Body) if err != nil { fmt.Println("req Body Read Error," , err) return } fmt.Println("req.Body => " , string (body)) w.Header().Add("RequestMethod" , "PostForm" ) w.Header().Add("Content-Type" , "application/json;charset=UTF-8" ) w.Header().Add("Cookies" , fmt.Sprintf("method=form;id=%v;name=%v" , id, name)) uid, err := strconv.Atoi(id) if err != nil { errMsg := fmt.Sprintf("uid convert err! %v\n" , err) fmt.Println(errMsg) w.Write([]byte (errMsg)) return } reply := fmt.Sprintf("{\"method\":\"%v\",\"status\":\"ok\",\"data\":{\"id\":%v,\"name\":\"%v\"}}" , "POSTFORM" , uid, handler.Name) fmt.Println("reply => " , reply) w.Write([]byte (reply)) default : w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "no such page: %s\n" , req.URL) } } func main () { handler := myHandler{Name: "WeiyiGeek" } server := &http.Server{ Addr: ":8080" , Handler: handler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 15 * time.Second, MaxHeaderBytes: 1 << 20 , } fmt.Printf("[%v] Http Server Start.....\n" , time.Now().Format("2006-01-02 15:04:05" )) log.Fatal(server.ListenAndServe()) defer fmt.Printf("[%v] Http Server Close.....\n" , time.Now().Format("2006-01-02 15:04:05" )) }
执行结果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ demo4 go build && ./demo4 [2021-11-23 13:25:30] Http Server Start..... Method : Get, URL : /get?id=1024&name=%E5%94%AF%E4%B8%80%E6%9E%81%E5%AE%A2 QueryParam : map[id:[1024] name:[唯一极客]] id = 1024,name = 唯一极客 reply => {"method" :"GET" ,"status" :"ok" ,"data" :{"id" :1024,"name" :"WeiyiGeek" }} Method : POST, URL : /post req.Body => {"id" :128,"name" :"Weiyi" } id = 128,name = Weiyi reply => {"method" :"POST" ,"status" :"ok" ,"data" :{"id" :128,"name" :"Weiyi" }} Method : POST, URL : /postform map[id:[256] name:[WeiyiGeek-唯一极客]] id = 256, name = WeiyiGeek-唯一极客 req.Body => reply => {"method" :"POSTFORM" ,"status" :"ok" ,"data" :{"id" :256,"name" :"WeiyiGeek" }}