1 TCP模型
在Go的网络编程过程中,无需关注socket是否是non-block的,也无需亲自注册文件描述符(FD),只需要将每个连接对应的goroutine以block I/O的方式对待socket处理就可以。
具体的流程也不需要向Unix/Linux socket编程那样以socket,bind,listen,accept,write,read来实现,go的socket编程流程就相对简单:即server端通过Listen+Accept模式实现,例如一个典型的Go Server端程序如下
func handler(conn net.Conn) {
defer conn.Close()
for {
// read connection
// ...
// write connection
}
}
func main() {
// 监听端口
log.Printf("Listening...")
server, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
// 死循环等待客户端连接
for {
conn, err := server.Accept()
if err != nil {
log.Printf("Accept error: %#v\n", err)
break
}
log.Printf("Get conn from port 8080")
go handler(conn)
}
}
主要的作用就是监听获取连接然后通过log打印状态,具体我们用户层眼中看到的goroutine中的“block socket”的实现细节暂时没有去细究,但是在搜索相关资料时大致了解到是通过Go runtime中的netpoller通过Non-block socket+I/O复用机制“模拟”出来的,真实的underlying socket实际上是non-block的。
1.1 TCP连接的建立
1.1.1 TCP数据的读取
有了刚才服务端的结构,接下来就需要客户端和服务端进行三次握手来进行通信。连接建立过程中,客户端Go语言使用的是net.Dial进行连接。
func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Println("Connect error: ", err)
return
}
defer conn.Close()
log.Println("Dial ok")
}
接下来从命令行分别运行server.go以及client.go,可以得到如下结果
server端
$ go run server.go
2024/11/15 22:07:14 Listening...
2024/11/15 22:07:17 Get conn from port 8080
client端
$ go run client.go
2024/11/15 22:07:17 Dial ok
根据开始尝试的正常的连接建立,经过学习以及查阅相关资料跟着学习了TCP连接建立后几种特殊的异常情况。①当本机8080端口未有服务程序监听,执行client.go后,Dial会立马返回错误:
$ go run client.go
2024/11/16 00:39:00 Connect error: dial tcp :8080: connect: connection refused
当socket中有部分数据,并且长度小于Read一次读取的长度,那么Read会一次性返回读取的结果
func handler(conn net.Conn) {
defer conn.Close()
for {
var buf = make([]byte, 10)
log.Println("Start to read...")
n, err := conn.Read(buf)
if err != nil {
log.Println("Read error: ", err)
return
}
log.Printf("Read %d bytes, content is %s\n", n, string(buf))
}
}
func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Println("Connect error:", err)
return
}
defer conn.Close()
log.Println("Dial ok")
time.Sleep(time.Second * 2)
data := os.Args[1]
_, err = conn.Write([]byte(data))
if err != nil {
log.Printf("write error %v", err)
}
time.Sleep(time.Second * 10000)
}
会得到如下结果:
$ go run server.go
2024/11/16 17:18:31 Listening...
2024/11/16 17:18:34 Get conn from port 8080
2024/11/16 17:18:34 Start to read...
2024/11/16 17:18:36 Read 5 bytes, content is hello
$ go run client.go hello
2024/11/16 17:18:34 Dial ok
②而当socket中的数据大于Read一次能够读取的长度,Read先读取期望的长度,然后在第二次读取的时候把剩余的数据读出。通过client.go向server发送hello,marscode,server会先将前10个字节读出来(hello,mars),再将剩下的4个字节读取出来(code)
$ go run client.go hello,marscode
2024/11/16 17:28:49 Dial ok
$ go run server.go
2024/11/16 17:28:41 Listening...
2024/11/16 17:28:49 Get conn from port 8080
2024/11/16 17:28:49 Start to read...
2024/11/16 17:28:51 Read 10 bytes, content is hello,mars
2024/11/16 17:28:51 Start to read...
2024/11/16 17:28:51 Read 4 bytes, content is code
2024/11/16 17:28:51 Start to read...
③在server读取数据时,client端主动关闭了socket,这里分为两种情况:一种是“有数据关闭”,另一种是“无数据关闭”。当client在关闭时,socket中还有server未读取的数据称为“有数据关闭”,我们再以同样的方式给server端发送数据hello,可以得到
$ go run server.go
2024/11/16 17:54:08 Listening...
2024/11/16 17:54:11 Get conn from port 8080
2024/11/16 17:54:11 Start to read...
2024/11/16 17:54:13 Read 5 bytes, content is hello
2024/11/16 17:54:13 Start to read...
2024/11/16 17:54:15 Read error: EOF
可以看到server在第一次成功读出client所发送的数据后,由于在第二次Read时client端的socket关闭,导致了Read的结果为EOF,而我们发送的数据长度本就小于server期望的数据长度,所以不难推测出在“无数据关闭”情况下的Read结果会直接返回EOF
1.1.2 TCP连接中数据的写入
既然Read中会出现各种异常的情况(举例的并不充分),那么调用Write操作也同样会遇到一些问题。那么如何判断是否Write成功,其中的一个依据就是在通过调用conn.Write()获得返回值n与预期写入的数据长度是否相同并且err=nil。
异常情况①写阻塞,由于TCP的全双工特性,接收方和发送方都会有自己的接收缓冲以及发送缓冲,所以当发送方将对方的接收缓冲区以及自己的发送缓冲区写满后,就会发生写阻塞现象,模拟一下实验场景
server端前10秒不进行任何Read操作,这样可以模拟client在写入一定量时会发生阻塞
func handler(conn net.Conn) {
defer conn.Close()
time.Sleep(time.Second * 10)
for {
time.Sleep(5 * time.Second)
var buf = make([]byte, 60000)
log.Println("Start to read...")
n, err := conn.Read(buf)
if err != nil {
log.Println("Read error: ", err)
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
}
log.Printf("Read %d bytes, content is %s\n", n, string(buf))
}
}
func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Println("Connect error:", err)
return
}
defer conn.Close()
log.Println("Dial ok")
buf := make([]byte, 65536)
var total int
for{
n, err := conn.Write(buf)
if err != nil {
total += n
log.Printf("Write %d bytes, errors: %v\n", n, err)
break
}
total += n
log.Printf("Write %d bytes this time, %d in total\n", n, total)
}
log.Printf("Write %d bytes in total\n", total)
time.Sleep(time.Second * 10000)
}
分别运行server和client会得到如下情景
$ go run client.go
2024/11/16 18:39:53 Dial ok
2024/11/16 18:39:53 Write 65536 bytes this time, 65536 in total
2024/11/16 18:39:53 Write 65536 bytes this time, 131072 in total
....
2024/11/16 18:39:53 Write 65536 bytes this time, 2555904 in total <-----
2024/11/16 18:40:18 Write 65536 bytes this time, 2621440 in total
...
2024/11/16 18:40:19 Write 65536 bytes this time, 6356992 in total
可以看到在写入2555904字节的时候会发生阻塞,此时server端开始进行Read操作,client才可以继续进行Write操作
$ go run server.go
2024/11/16 18:39:49 Listening...
2024/11/16 18:39:53 Get conn from port 8080
2024/11/16 18:40:08 Start to read...
2024/11/16 18:40:08 Read 60000 bytes, content is
2024/11/16 18:40:13 Start to read...
2024/11/16 18:40:13 Read 60000 bytes, content is
2024/11/16 18:40:18 Start to read...
2024/11/16 18:40:18 Read 38666 bytes, content is
②写入部分数据,当client在进行数据写入时我们挂掉server,这时可以看到client输出的日志:
....
2024/11/16 20:00:38 Write 65536 bytes this time, 2555904 in total
2024/11/16 20:00:38 Write 30689 bytes, errors: write tcp 127.0.0.1:47406->127.0.0.1:8080: write: connection reset by peer
2024/11/16 20:00:38 Write 2586593 bytes in total
可以看到在我们挂掉server的时候,并不是在2555904阻塞的,在后续又写入了30689个字节后发生了阻塞,也就是说经过Write操作返回了err!=nil且n=30689,所以程序需要对这30689字节做其他特定的处理
③写入超时如果client端写入超时会发生什么,在client的Write的操作前加入一个
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))
的操作,代表如果在当前时间内的10微秒未能成功将数据写入到conn,任何的Write操作都会返回一个error,这个error通常会是timeout,进行实验得到如下结果:
...
2024/11/16 20:21:36 Write 30689 bytes, errors: write tcp 127.0.0.1:55486->127.0.0.1:8080: i/o timeout
2024/11/16 20:21:36 Write 2586593 bytes in total
当client发生写入超时时,并不会完全阻塞数据写入,此时也会有30689字节的数据写入
1.2 TCP实战抓包
1.2.1 正常通讯情况下的抓包
经过对之前异常状况的简单学习,现在开始抓包server端和client端之间的通信,使用tcpdump工具进行抓包,由于只是对本机间的两个进程进行通讯,先运行server.go,再通过另一个命令行运行
tcpdump -i lo tcp port 8080 -w tcp_local.pcap
再运行client.go,这个抓包实验中我们实现client向server发送hello字符,通过使用Wireshark打开tcp_local.pcap文件,可以看到如下过程
在上述的过程中,我们可以看到从端口56710向8080发起SYN请求发起三次握手请求,三次握手建立后,端口56710向8080发送长度(len)为5的数据,8080端口回复ACK报文,其中Ack=6就向课程中所讲希望下一个报文的seq为6
点开其中一个请求发送过程,可以看到详细的信息,例如传输协议,传输的报文类型等等
1.2.2 模拟丢包情景
因为是在同一台虚拟机上,所以无法通过像小林coding作者那样使用拔网线这种物理手段来让服务端挂掉,而如果不启动server端让client端直接发起三次握手又会直接返回panic导致进程中断无法发起重传,所以使用一种在客户端上加防火墙限制,把来自服务端的输入输出都丢弃,配置规则如下,它的作用是将来自服务端的数据都丢弃
iptables -I INPUT -p tcp dport 8080 -j DROP
像之前正常启动通讯的流程一样,配置防火墙规则后,启动server、再启动tcpdump、再启动client,抓包后的结果如下
可以看到针对第一次握手的情况一共超时重传了3次,并且重传的时间是指数变化
2 UDP模型
UDP不像TCP那样在通讯前先建立连接,而是可以直接发送数据包,传输过程中也没有相应报文的确认信息,首部资源的开销也较TCP少,通常用于音视频传输。但就是因为没有确认和重传的机制,导致了UDP不可靠的问题,所以我们也同样需要防止丢包使UDP变得相对可靠,通过的方式就是模拟TCP在传输层实现的机制,只不过我们是在应用层模拟,实现:
1.通过UDP socket实现ack机制,确保数据发送到对端
2.添加重传机制,并感知在UDP中的丢包重传
2.1 UDP连接的建立
对于UDP连接的建立我们需要几个函数:
1.func ResolveUDPAddr(network, address string) (*UDPAddr, error)用于解析UDP字符串地址
2.func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)用于创建UDP监听器
3.func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)用于创建一个UDP连接
从上面三个函数我们可以看到它们都有一个返回值UDPConn,它同样也是Conn的接口实现,那么需要用到的函数也和其有关,例如:
func (c *conn) Write(b []byte) (int, error):发送数据func (c *conn) Read(b []byte) (int, error):读取数据func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error):从UDP连接中读取数据func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error):向UDP连接中写入数据
从上面的方法我们可以看出,与TCP不同的是,UDP缺少对客户端连接请求的Accept方法,用这几个函数我们可以编写一个简单的基于UDP实现的server端以及client端:
Server端
创建监视器监听端口,等待客户端连接,连接后会读取客户端所发送的数据并打印出来,再将所读取的数据发送回客户端,和TCP的server端一样使用死循环持续接收客户端所发送来的信息,这样就实现了UDP数据的接收和发送
func handleClient(conn *net.UDPConn) {
var buf [512]byte
n, udpAddr, err := conn.ReadFromUDP(buf[:])
if err != nil {
log.Printf("Read from UDP conn error: %v", err)
return
}
log.Printf("Read %d bytes, content is %s", n, string(buf[:n]))
n, err = conn.WriteToUDP(buf[:n], udpAddr)
if err != nil {
log.Printf("Write to UDP conn error: %v", err)
return
}
log.Printf("Write %d bytes in total", n)
}
func main() {
udpAddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:8080")
if err != nil {
log.Printf("Resolve error: %v", err)
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
for {
handleClient(conn)
}
}
Client端
解析出要进行连接的地址,随后创建UDP对所解析的地址的IP以及端口进行连接,连接成功后,通过对连接进行写入hello字符发送给server端,然后从UDP中读取由server端的响应数据,实现UDP的基本发送和接收功能
func main(){
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
if err != nil {
log.Printf("Resolve error: %v", err)
}
log.Println("Start listening...")
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
log.Printf("Dial error: %v", err)
return
}
defer conn.Close()
n, err := conn.Write([]byte("hello"))
if err != nil {
log.Printf("Write error: %v", err)
return
}
var buf [512]byte
n, addr, err := conn.ReadFromUDP(buf[:])
log.Printf("Server response %v, content is %s, %d bytes in total", addr, string(buf[:n]), n)
if err != nil {
log.Printf("Get response error: %v", err)
}
// 丑陋阻塞
time.Sleep(time.Second * 10000)
}
分别运行server.go和client.go,可以从控制台中看到以下响应:
server端
$ go run server.go
2024/11/17 21:36:54 Start listening...
2024/11/17 21:36:56 Read 5 bytes, content is hello
2024/11/17 21:36:56 Write 5 bytes in total
client端
$ go run client.go
2024/11/17 21:36:56 Server response 127.0.0.1:8080, content is hello, 5 bytes in total
至此,我们就实现了UDP socket编程中最基础的客户端等待服务端ack(也就是response)再发包(只不过我们没有在client端做下一步的响应)
2.2 实现seq/ack
我们可以在此基础上在server端维护一个ack信息,在client端维护一个seq以及数据,在收到seq后我们简单假设ack=seq+1(其实ack=seq+发送消息的长度)。并且在client端维护一个等待时间,用于等待server端数据的回送,如果在时间内没有收到数据,则判定为丢包。
Client端
通过定义一个结构体Message来维护一个Seq以及Msg变量,每次client端发送数据时就将Message打包发送过去。每次发送过后就等待server端的响应,接收到ack响应时判断ack.Seq == seq,若不是则提示ack错误并停止发包,如果是那就继续发包。
type Message struct {
Seq int
Msg string
}
func main(){
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
if err != nil {
log.Printf("Resolve error: %v", err)
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
log.Printf("Dial error: %v", err)
}
defer conn.Close()
input_message := []string{"test 1", "test 2", "test 3", "test 4"}
seq := 0
for _, msg := range input_message {
seq++
message := Message{Seq: seq, Msg: msg}
log.Printf("Send msg seq: %d, content: %s", message.Seq, message.Msg)
_, err = conn.Write([]byte(fmt.Sprintf("%d:%s",message.Seq, message.Msg)))
if err != nil {
log.Printf("Write error: %v", err)
return
}
buf := make([]byte, 512)
// 设置超时时间5s
conn.SetReadDeadline(time.Now().Add(time.Second * 5))
n, addr, err := conn.ReadFromUDP(buf[:])
if err != nil {
log.Printf("Get response error: %v", err)
} else {
ack := decodeMsg(buf[:n])
if ack.Seq == seq + 1 {
log.Printf("Get response ACK: %d\n", ack.Seq)
log.Printf("Server response %v, content is %s, %d bytes in total", addr, ack.Msg, n)
} else {
log.Printf("Invalid ACK. Please retry.")
return
}
}
}
}
func decodeMsg(data []byte) Message {
subpart := strings.Split(string(data), ":")
seq, err := strconv.Atoi(subpart[0])
if err != nil {
log.Printf("Decode message error: %v", err)
}
msg := subpart[1]
return Message{Seq: seq, Msg: msg}
}
Server端
在读取到client发送来的数据时,打印序列号以及读取到的内容,然后对序列号进行加1操作并作为ack发回给client,也就是希望下一个收到的包的seq=seq+1。
type Message struct {
Seq int
Msg string
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
n, udpAddr, err := conn.ReadFromUDP(buf[:])
if err != nil {
log.Printf("Read from UDP conn error: %v", err)
return
}
message := decodeMsg(buf[:n])
log.Printf("Received seq=%d from %v, content is %s\n", message.Seq, udpAddr, message.Msg)
ack := Message{Seq: message.Seq + 1, Msg: "ACK"}
n, err = conn.WriteToUDP([]byte(fmt.Sprintf("%d:%s", ack.Seq, ack.Msg)), udpAddr)
if err != nil {
log.Printf("Write to UDP conn error: %v", err)
return
}
log.Printf("Write %d bytes in total", n)
}
func main() {
udpAddr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:8080")
if err != nil {
log.Printf("Resolve error: %v", err)
return
}
log.Println("Start listening...")
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
for {
handleClient(conn)
}
}
func decodeMsg(data []byte) Message {
subpart := strings.Split(string(data), ":")
seq, err := strconv.Atoi(subpart[0])
if err != nil {
log.Printf("Decode message error: %v", err)
}
msg := subpart[1]
return Message{Seq: seq, Msg: msg}
}
分别运行server和client得到以下运行结果
Server端:
$ go run server.go
2024/11/18 15:44:22 Start listening...
2024/11/18 15:44:24 Received seq=1 from 127.0.0.1:40150, content is test 1
2024/11/18 15:44:24 Write 5 bytes in total
2024/11/18 15:44:24 Received seq=2 from 127.0.0.1:40150, content is test 2
2024/11/18 15:44:24 Write 5 bytes in total
2024/11/18 15:44:24 Received seq=3 from 127.0.0.1:40150, content is test 3
2024/11/18 15:44:24 Write 5 bytes in total
2024/11/18 15:44:24 Received seq=4 from 127.0.0.1:40150, content is test 4
2024/11/18 15:44:24 Write 5 bytes in total
Client端:
$ go run client.go
2024/11/18 15:44:24 Send msg seq: 1, content: test 1
2024/11/18 15:44:24 Get response ACK: 2
2024/11/18 15:44:24 Server response 127.0.0.1:8080, content is ACK, 5 bytes in total
2024/11/18 15:44:24 Send msg seq: 2, content: test 2
2024/11/18 15:44:24 Get response ACK: 3
2024/11/18 15:44:24 Server response 127.0.0.1:8080, content is ACK, 5 bytes in total
2024/11/18 15:44:24 Send msg seq: 3, content: test 3
2024/11/18 15:44:24 Get response ACK: 4
2024/11/18 15:44:24 Server response 127.0.0.1:8080, content is ACK, 5 bytes in total
2024/11/18 15:44:24 Send msg seq: 4, content: test 4
2024/11/18 15:44:24 Get response ACK: 5
2024/11/18 15:44:24 Server response 127.0.0.1:8080, content is ACK, 5 bytes in total
通过对通信过程抓包结果如下:
能够看到client发送长度为8的Message(例如test 1:1),server端回应长度为5的Message(例如ACK:1)
3 总结
至此,我们就在UDP中模拟实现了TCP中的一个简单seq/ack机制,其他的机制例如超时重传、滑动窗口、拥塞控制等等都还没有实现,旨在对基于Golang实现socket编程进行一个基础的学习,也通过抓包等看到了一些具体的通信过程而不是单调的背八股,能够在一定程度上加深自己的理解,后面的基础还需要再进行巩固,需要赶紧跟上青训营的进度。