[TOC]

0x00 Go语言基础之Socket网络编程

现在的我们几乎每天都在使用互联网,但是你知道程序是如果通过网络互相通信吗?
描述: 相信大部分人通常是一知半解的,作为一个程序员👨‍💻‍,对于网络模型你应该了解,知道网络到底是怎么进行通信的,进行工作的,为什么服务器能够接收到请求,做出响应。这里面的原理应该是每个 Web 程序员应该了解的。

本章我们就一起来学习下Go语言中的网络编程,关于网络编程其实是一个很庞大的领域,本文只是简单的演示了如何使用net包进行TCP和UDP通信。

1.基础概念介绍

描述: 互联网的核心是一系列协议,总称为互联网协议(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网,并通过各种协议实现不同的功能, 下面简单介绍一些协议涉及的基础知识概念。

此处不得不老生重谈OSI七层网络模型 ,OSI是国际标准化组织 1984 提出的模型标准,简称 OSI(Open Systems Interconnection Model),主要统一标准来规范网络协议让各个硬件厂商可以协同工作,相信如果你学习过网络工程或者操作系统方面的课程至少你是了解它的。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持,通常用户接触到的只是最上面的那一层。

WeiyiGeek.OSI七层网络与TCP/IP四层网络模型

WeiyiGeek.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.一张解释互联网的传输过程的图

WeiyiGeek.一张解释互联网的传输过程的图


知识扩展

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.Socket图解

WeiyiGeek.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.go

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
func main() {
// 1.设置服务端监听端口
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)
}

// 2.等待客户端建立连接
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Accept failed,err: %v\n", err)
return
}
defer conn.Close() // 程序结束时,关闭与客户端打开的TCP连接通道

// 3.与客户端进行消息通信(读取客户端法过来的信息)
var msg [1024]byte
n, err := conn.Read(msg[:]) // 注意读取的类型是byte的slice
if err != nil {
fmt.Printf("Read from Client conn failed, err:%v\n", err)
return
}

// 4.打印客户端发送的信息,注意此需要将[]byte类型的切片,转为字符串类型进行输出.
fmt.Println(string(msg[:n]))
}


客户端: Client.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
// 1.与Server端建立TCP通信链接
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
}

// 2.发送字符串数据到Server端
sendMsg := "Hello World! Server, I'm client"
conn.Write([]byte(sendMsg))

// 3.关闭建立的TCP通信链接
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.简单示例1执行结果

WeiyiGeek.简单示例1执行结果


进阶示例2: TCP Server端与多个Client端持续通信。
Server.go

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
48
49
50
51
52
53
54
55
56
57
58
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func SendReceiveProccess(conn net.Conn) {
// 3.与客户端进行消息通信(循环读取客户端法过来的信息)
defer conn.Close() // 关闭当前链接通信对象
reader := bufio.NewReader(conn)
var msg [1024]byte // 每次读取1024B
for {
n, err := reader.Read(msg[:]) // 注意读取的类型是byte的slice
// 末尾标识
if err == io.EOF {
fmt.Printf("Close conn %v\n", conn.RemoteAddr())
break
}
// 异常时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() {
// 1.设置监听端口
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() // 关闭服务端监听

// 2.等待客户端建立连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Accept failed,err: %v\n", err)
return
}
// 不同的客户端利用Goroutine分配不同的线程进行响应。
go SendReceiveProccess(conn)
}
}


Client.go

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
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
)

func main() {
// 1.与Server端建立TCP链接
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()

// 2.发送初始连接信息到Server端
sendMsg := fmt.Sprintf("Hello Server, I'm %v client.", conn.LocalAddr())
inputReader := bufio.NewReader(os.Stdin) // 复习点
conn.Write([]byte(sendMsg))

// 3.循环从服务端接收以及从终端输入发送信息到服务端
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') // 复习点 (以\n截至读取)
sendMsg = strings.TrimSpace(sendMsg) // 复习点 处理输入字符串的前后空格
sendMsg = strings.Trim(sendMsg, "\n") // 复习点 处理输入字符串的最后的换行符

// 但客户端输入quit则退出与服务端建立的TCP通信连接.
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 && ./Server
go build && ./Client

WeiyiGeek.进阶示例2执行结果

WeiyiGeek.进阶示例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.go

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
package main
import (
"fmt"
"net"
"strings"
"time"
)
func main() {
// 1.服务端开启监听 UDP 通信的相关设置
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)
}
// 程序结束时关闭conn资源
defer conn.Close()

// 2.循环接收和响应数据给客户端,非常主要此处不需要建立连接,直接收发数据。
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]))

// 并将接收到的信息更改为大写,再返还给Client。
reply := strings.ToUpper(string(recvMsg[:count]))
conn.WriteToUDP([]byte(reply), addr) // 发送数据
}
}


Client.go

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
48
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
"time"
)
func main() {
// (1) 与服务端建立UDP通信链接
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()

// (2) 发送与接收服务端返回的信息
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.实现UDP服务端与多客户端进行连接通信!

WeiyiGeek.实现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.go
    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
    48
    49
    50
    package proto
    import (
    "bufio"
    "bytes"
    "encoding/binary"
    )
    // (1) Encode 将消息编码
    func Encode(message string) ([]byte, error) {
    // 1.读取消息的长度,转换成int32类型(占4个字节)以后可以按照需要进行自定义
    var length = int32(len(message))
    var pkg = new(bytes.Buffer) // 向系统为具有读写方法的字节大小可变的缓冲区申请内存。
    // 2.写入消息头
    err := binary.Write(pkg, binary.LittleEndian, length) //注意此处以小端的方式写入.在后续解包时也必须采用小端方式读取
    if err != nil {
    return nil, err
    }
    // 3.写入消息实体
    err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    if err != nil {
    return nil, err
    }
    // 4.返回封包完毕的缓冲区中数据
    return pkg.Bytes(), nil
    }

    // (2) Decode 解码消息
    func Decode(reader *bufio.Reader) (string, error) {
    // 1.读取消息的长度
    lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
    lengthBuff := bytes.NewBuffer(lengthByte) // NewBuffer使用buf作为初始内容创建并初始化一个新缓冲区,此处指定要读取数据的长度.
    var length int32
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {
    return "", err
    }
    // 2.Buffered返回缓冲中现有的可读取的字节数,如果获取的字节数小于消息的长度则说明数据包有误.
    if int32(reader.Buffered()) < length+4 {
    return "", err
    }

    // 3.读取真正的消息数据
    pack := make([]byte, int(4+length))
    _, err = reader.Read(pack)
    if err != nil {
    return "", err
    }

    // 4.利用slice切片返回四个字节后的消息数据
    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 main
    import (
    "bufio"
    "fmt"
    "io"
    "net"
    "weiyigeek.top/studygo/Day08/03StickBag/proto"
    )
    func process(conn net.Conn) {
    // 4.退出时关闭conn资源
    defer conn.Close()
    // 5.NewReader返回其缓冲区具有默认大小的新读取器。
    reader := bufio.NewReader(conn)
    for {
    // 6.解包: 将通过conn对象中获取的缓冲区数据进行解包.
    msg, err := proto.Decode(reader)
    if err == io.EOF {
    return
    }
    if err != nil {
    fmt.Println("decode msg failed, err:", err)
    return
    }
    // 7.打印解包后的数据
    fmt.Println("收到client发来的数据:", msg)
    }
    }
    func main() {
    // 1.设置TCP Server端监听地址和端口
    listen, err := net.Listen("tcp", "127.0.0.1:30000")
    if err != nil {
    fmt.Println("listen failed, err:", err)
    return
    }
    // 2.函数结束后关闭监听资源
    defer listen.Close()

    // 3.循环接收客户端发送过来的数据,利用gorontine执行process任务
    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 main
    import (
    "fmt"
    "net"
    "weiyigeek.top/studygo/Day08/03StickBag/proto"
    )
    func main() {
    // 1.连接到server端.
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {
    fmt.Println("dial failed, err", err)
    return
    }

    // 2.程序结束时关闭conn网络连接资源.
    defer conn.Close()

    // 3.循环发送20次msg给server端
    for i := 0; i < 20; i++ {
    msg := `Hello, Hello. How are you?`
    // 4.将要发送的信息进行封包处理
    data, err := proto.Encode(msg)
    if err != nil {
    fmt.Println("encode msg failed, err:", err)
    return
    }
    // 5.将处理过的封包进行发送
    conn.Write(data)
    }
    }

执行结果: 此时发现它不会将多个hello…字符串放在一行了,并且输出也是我们预定的20次。

WeiyiGeek.黏包解决办法

WeiyiGeek.黏包解决办法


补充知识:字节序列的存储格式之大端(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.大端和小端

WeiyiGeek.大端和小端


补充知识:CPU存储一个字节的数据时其字节内的8个比特之间的顺序是否也有big endianlittle 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
# Big Endian
msb------------------------>lsb
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


# Little Endian
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
// 1.Handle在DefaultServeMux中注册给定模式的处理程序函数
type httpServer struct {}
func (server httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.URL.Path))
}
var fooHandler httpServer
http.Handle("/foo", fooHandler)

// 2.HandleFunc在DefaultServeMux中注册给定模式的处理程序函数
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

// 3.指定的本地网卡的8080断开作为监听地址和处理器启动一个HTTP服务端。
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 main

import (
"fmt"
"log"
"net/http"
)

// 自定义结构体
type httpServer struct{}

// httpServer 自定义实现http请求处理程序函数
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() {
// 1.声明serve变量的类型为我们自定义结构体
var server httpServer
serveraddr := "0.0.0.0:8080"

// 2.Handle在DefaultServeMux中注册给定模式的处理程序。
http.Handle("/", server)

// 3.启动httpServer监听并采用包变量DefaultServeMux作为处理器。
err := http.ListenAndServe(serveraddr, nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Http Server %v Started......", serveraddr)
}
}

执行结果如下图所示
WeiyiGeek.http.Handle结果

WeiyiGeek.http.Handle结果


示例2.http.HandleFunc实现用户请求处理

index.html

1
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 main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
func main() {
// 1.声明定义server监听端口地址
serveraddr := "0.0.0.0:8080"

// 2.HandleFunc在DefaultServeMux中注册给定模式的处理程序。
http.HandleFunc("/hello", hello)
http.HandleFunc("/blog", blog)
http.HandleFunc("/htmlfile", htmlfile)

// 3.启动httpServer监听并采用包变量DefaultServeMux作为处理器。
err := http.ListenAndServe(serveraddr, nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Http Server %v Started......", serveraddr)
}
}

// (1) 方式1.此种方式不能直接写入HTML标签代码并返还给客户端.
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World! Go <b>测试页面0</b> Time : %s", time.Now())
}

// (2) 方式2.可以直接写入HTML标签代码并返还给客户端.
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))
}

// (3) 读取本机上的网页为文件返还给客户端.
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.http.HandleFunc实现用户请求处理

WeiyiGeek.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 main

import (
"fmt"
"log"
"net/http"
"time"
)

// 自定义myHandler结构体,注册Handler处理程序使用
type myHandler struct {
name string
}
// 自定义myHandler结构体的方法注册到Handler处理程序,【非常注意】、【非常注意】方法名必须为 ServeHTTP
func (handler myHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 现在handler基于URL的路径部分(req.URL.Path)来决定执行什么逻辑。
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") // WeiyiGeek -> https://weiyigeek.top
default:
// 如果这个handler不能识别这个路径,它会通过调用返回客户端一个HTTP404错误,并响应给客户端表明请求的路径不存在.
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}

func main() {
// 1.实例化一个处理所有请求的Handler接口
handler := myHandler{name: "WeiyiGeek"}

// 2.创建一个自定义的Server参数,注意如果Handler为nil则采用http.DefaultServeMux进行处理响应,否则需要我们自己实现结构体的ServeHTTP方法.
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}

// 3.启动并监听HTTP服务端
log.Fatal(server.ListenAndServe())
}

执行结果:
WeiyiGeek.自定义http.Server结构体参数与Handler实现

WeiyiGeek.自定义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 http
type 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) }
// Map类型的变量声明
type MyHandler map[string]dollars
// 类型方法
func (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) // 404
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/httpnet/url以便于我们进行网站API接口(GETPOSTPUT)请求和处理服务端响应的数据。

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
# Get 请求
resp, err := http.Get("http://example.com/")
...
# Post 请求
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
# Form 表单请求
resp, err := http.PostForm("http://example.com/form",
url.Values{"key": {"Value"}, "id": {"123"}})
...
if err != nil {
// handle error
}
...
# 程序在使用完response后必须关闭回复的主体。
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 main

import (
"fmt"
"io/ioutil"
"net/http"
)

func getMethod(url string) {
// 1.Get请求指定地址
resp, err := http.Get(url)
if err != nil {
fmt.Printf("get failed, err:%v\n", err)
return
}
// 2.程序完毕时关闭回复的主体.
defer resp.Body.Close()

// 3.读取响应的网页源代码
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("read from resp.Body failed, err:%v\n", err)
return
}
html := string(body)

// 4.网站请求响应数据
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指定处理重定向的策略。如果CheckRedirect不是nil,则客户端在执行HTTP重定向之前调用它。
CheckRedirect: redirectPolicyFunc,
// 超时指定此客户端发出的请求的时间限制。超时包括连接时间、任何重定向和读取响应正文。Get、Head、Post或Do返回后,计时器将保持运行,并将中断Response.Body的读取。
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{
// TLS 配置
TLSClientConfig: &tls.Config{RootCAs: pool},
// 是否禁用压缩
DisableCompression: true,
// 是否保持连接(长连接、短连接)
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Tips: ClientTransport类型都可以安全的被多个goroutine同时使用, 出于效率考虑,应该一次建立、尽量重用。
Tips: 如果取数据比较频繁的场景建议使用长连接,否则使用短连接即可。


3.综合实践

3.1 Get 请求示例

get_client.go

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
func getMethod(urlstr string) {
// 1.URL格式校验解析
urlParse, err := url.Parse(urlstr)
if err != nil {
fmt.Printf("Url %v Format Error!\nerr: %v\n", urlParse, err)
return
}
// 2.URL参数设置与编码
data := url.Values{}
data.Set("id", "1024")
data.Set("name", "唯一极客")
// 处理URL中包含的中文参数,此处采用encode进行编码.
queryStr := data.Encode()
// URL参数设置并输出处理过后的请求字符串
urlParse.RawQuery = queryStr // encoded query values, without '?'
fmt.Println("QueryStr => ", queryStr)

// 3.NewRequest使用后台上下文包装NewRequestWithContext,返回请求对象
req, err := http.NewRequest("Get", urlParse.String(), nil)
if err != nil {
fmt.Printf("NewRequest %v faile!\n[error]: %v\n", urlstr, err)
return
}

// 5.DefaultClient是默认客户端,由Get、Head和Post请求使用,此时传入上面处过的req请求对象
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("Request %v faile!\n[error]: %v\n", urlstr, err)
return
}

// 6.程序完毕时关闭回复的主体(非常重要).
defer resp.Body.Close()

// 7.从resp中把服务端返回的数据读出来
// 方式1
// var data []byte
// response.Body.Read(data)
// 方式2
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("read from resp.Body failed, err:%v\n", err)
return
}

// 8.网站请求响应数据集合
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%9E%81%E5%AE%A2 //url 编码后
resp.StatusCode : 200,
resp.Status: 200 OK,
resp.Request: &http.Request{Method:"Get", URL:(*url.URL)(0xc0000fe090), 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.go

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
// POST 示例
func postMethos(urlstr string) {
// (1) 定义Post请求上传的参数( 表单数据/json数据)
//contentType := "application/x-www-form-urlencoded"
//data := "name=小王子&age=18"
contentType := "application/json"
data := `{"id":128,"name":"Weiyi"}`

// (2) 进行 Post 请求传入请求url,contentType以及Post上传的data数据
// resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
resp, err := http.Post(urlstr, contentType, strings.NewReader(data))
if err != nil {
fmt.Printf("post failed, err:%v\n", err)
return
}

// (3) 程序结束则关闭resp资源
defer resp.Body.Close()

// (4) 读取POST请求返回的数据包
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println("Body : ", string(body))

// (5) 响应头输出
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.go

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
func postFormMethod(urlstr string) {
// (1) 方式1定义Post请求上传的参数( 表单数据)
//contentType := "application/x-www-form-urlencoded"
//data := "id=256&name=唯一极客"

// (2) 方式2定义Post请求上传的参数( 表单数据)
data := url.Values{}
data.Set("id", "256")
data.Set("name", "WeiyiGeek-唯一极客")

// (3) 进行表单上传请求
// resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})
resp, err := http.PostForm(urlstr, data)
if err != nil {
fmt.Printf("postForm failed, err:%v\n", err)
return
}

// (4) 程序结束则关闭resp资源
defer resp.Body.Close()

// (5) 读取POST请求返回的数据包
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println("Body : ", string(body))

// (6) 响应头输出
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.go

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
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 main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
)

// 4.自定义Handler结构体
type myHandler struct {
Id int `json:"id"`
Name string `json:"name"`
}

// 5.自定义myHandler结构体的方法注册到Handler处理程序,【非常注意】、【非常注意】方法名必须为 ServeHTTP
func (handler myHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 6.现在handler基于URL的路径部分(req.URL.Path)来决定执行什么逻辑。
switch req.URL.Path {
case "/get":
// 7.对于客户端Get请求参数获取
fmt.Printf("Method : %v, URL : %v \n", req.Method, req.URL)

// 8.自动识别请求URL中的参数,参数利用Map类型变量进行存储key-value
fmt.Println("QueryParam : ", req.URL.Query())
queryParam := req.URL.Query()
id := queryParam.Get("id")
name := queryParam.Get("name")

// 9.打印输出queryParam存储的value
fmt.Printf("id = %v,name = %v\n", id, name)

// 10.服务端响应头header自定义
w.Header().Add("RequestMethod", "Get") // 此处将响应的RequestMethod header字段设置为Get
w.Header().Add("Content-Type", "application/json;charset=UTF-8") // 此处将响应的类型设置为JSON
w.Header().Add("Cookies", fmt.Sprintf("id=%v;name=%v", id, name)) // 此处将响应的cookies设置为请求传入的参数

// 11.返回给客户端的JSON数据组装
uid, err := strconv.Atoi(id) // 将get到的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)

// 12.返回响应数据给客户端
w.Write([]byte(reply))

case "/post":
// 7.对于客户端Request请求信息获取
fmt.Printf("Method : %v, URL : %v \n", req.Method, req.URL)

// 8.服务端打印客户端发来的请求,当请求类型是application/json时才能从req.Body读取数据
body, err := ioutil.ReadAll(req.Body)
if err != nil {
fmt.Println("req Body Read Error,", err)
return
}
fmt.Println("req.Body => ", string(body))

//9.JSON反序列化
json.Unmarshal(body, &handler)

// 10.打印输出queryParam存储的value
fmt.Printf("id = %v,name = %v\n", handler.Id, handler.Name)

// 11.服务端响应头header自定义
w.Header().Add("RequestMethod", "Post") // 此处将响应的RequestMethod header字段设置为POST
w.Header().Add("Content-Type", "application/json;charset=UTF-8") // 此处将响应的类型设置为JSON
w.Header().Add("Cookies", fmt.Sprintf("id=%v;name=%v", handler.Id, handler.Name)) // 此处将响应的cookies设置为请求传入的参数

// 12.返回给客户端的JSON数据组装
reply := fmt.Sprintf("{\"method\":\"%v\",\"status\":\"ok\",\"data\":{\"id\":%v,\"name\":\"%v\"}}", "POST", handler.Id, handler.Name)
fmt.Printf("reply => %v\n\n", reply)

// 13.返回响应数据给客户端
w.Write([]byte(reply))

case "/postform":

// 7.对于客户端Request请求信息获取
fmt.Printf("Method : %v, URL : %v \n", req.Method, req.URL)

// 8. 请求类型是application/x-www-form-urlencoded时解析form数据并打印
req.ParseForm()
fmt.Println(req.PostForm)

// 9.获取postform表单中指定的字段值.
id := req.PostForm.Get("id")
name := req.PostForm.Get("name")
fmt.Printf("id = %v, name = %v\n", id, name)

// 10.服务端打印客户端发来的请求Body此处为[],因为但客户端请求类型是application/json时才能从req.Body读取数据
body, err := ioutil.ReadAll(req.Body)
if err != nil {
fmt.Println("req Body Read Error,", err)
return
}
fmt.Println("req.Body => ", string(body))

// 11.服务端响应头header自定义
w.Header().Add("RequestMethod", "PostForm") // 此处将响应的RequestMethod header字段设置为PostForm
w.Header().Add("Content-Type", "application/json;charset=UTF-8") // 此处将响应的类型设置为JSON
w.Header().Add("Cookies", fmt.Sprintf("method=form;id=%v;name=%v", id, name)) // 此处将响应的cookies设置为请求传入的参数

// 12.返回给客户端的JSON数据组装
uid, err := strconv.Atoi(id) // 将get到的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:
// 如果这个handler不能识别这个路径,它会通过调用返回客户端一个HTTP404错误,并响应给客户端表明请求的路径不存在.
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}

func main() {
// 1.实例化一个处理所有请求的Handler接口
handler := myHandler{Name: "WeiyiGeek"}

// 2.创建一个自定义的Server,注意如果Handler为nil则采用http.DefaultServeMux进行处理响应,否则需要我们自己实现结构体的ServeHTTP方法.
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
MaxHeaderBytes: 1 << 20,
}

// 3.启动并监听HTTP服务端
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"}}