写点什么

golang 中的 socket 编程

作者:六月的
  • 2022-10-19
    上海
  • 本文字数:4720 字

    阅读完需:约 1 分钟

0.1、索引

https://waterflow.link/articles/1664591292871

1、tcp 的 3 次握手(建立连接)


  1. 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;

  2. 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;

  3. 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;

  4. 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

2、tcp 的 4 次挥手(关闭连接)


  1. 一方应用程序调用 close,我们称该方为主动关闭方,该端的 TCP 发送一个 FIN 包,表示需要关闭连接。之后主动关闭方进入 FIN_WAIT_1 状态。

  2. 接收到这个 FIN 包的对端执行被动关闭。这个 FIN 由 TCP 协议栈处理,我们知道,TCP 协议栈为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。一定要注意,这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着接收端应用程序需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,被动关闭方进入 CLOSE_WAIT 状态。

  3. 被动关闭方将读到这个 EOF,于是,应用程序也调用 close 关闭它的套接字,这导致它的 TCP 也发送一个 FIN 包。这样,被动关闭方将进入 LAST_ACK 状态。

  4. 主动关闭方接收到对方的 FIN 包,并确认这个 FIN 包。主动关闭方进入 TIME_WAIT 状态,而接收到 ACK 的被动关闭方则进入 CLOSED 状态。进过 2MSL 时间之后,主动关闭方也进入 CLOSED 状态。

3、socket 中的连接建立和关闭


我看先看下流程:


  1. 服务端调用 socket、bind 绑定 ip 端口、listen 开启服务端监听。

  2. accept 阻塞等待下次调用,并返回一个 tcp 连接。

  3. 客户端调用 connect 连接服务端。

  4. 此时服务端 accept 结束阻塞,代表客户端和服务端成功建立连接。

  5. 然后就是数据交互读写读写。

  6. 当客户端连接关闭时,服务端的 read 方法会读取一个 io.EOF 的错误,代表客户端关闭连接。服务端收到关闭连接的错误后也调用 close 关闭连接。

4、golang 中的连接建立

我们先看下服务端:


package main
import ( "fmt" "net")
func main() { server := ":8330" tcpAddr, err := net.ResolveTCPAddr("tcp", server) if err != nil { fmt.Println("resolve err:", err) return }
// 监听某个端口的tcp网络 listen, err := net.ListenTCP("tcp", tcpAddr) if err != nil { fmt.Println("listen err:", err) return } defer listen.Close()
for { // 等待下次请求过来并建立连接 conn, err := listen.Accept() if err != nil { fmt.Println("accept err:", err) continue }
// 在这个连接上做一些事情 go handler(conn)
}}
func handler(conn net.Conn) {}
复制代码


  1. 首先我们定义好 ip 和端口,开启监听

  2. 然后调用 accept 等待下次请求过来,并建立 tcp 连接


我们运行下上面的代码:


go run server.go
复制代码


然后在另一个 shell 中执行下面的命令:


watch -d 'netstat -nat |grep "8330"'
Every 2.0s: netstat -nat |grep "8330" userdeMacBook-Pro.local: Thu Sep 29 16:38:42 2022
tcp46 0 0 *.8330 *.* LISTEN
复制代码


可以看到此时 8330 端口已经开启监听


客户端:


package main
import ( "fmt" "net")
func main() { serverAddr := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr) if err != nil { fmt.Println("resolve err:", err) return }
// 发起一个tcp的网络拨号 _, err = net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("dial err:", err) return }

closed := make(chan bool)

// 客户端阻塞不直接关闭 for { select { case <-closed: fmt.Println("服务端关闭") return } }
}
复制代码


其中核心的方法就是 net.DialTCP,第一个参数会返回一个建立成功的连接,第二个参数会返回没建立成功的错误信息。


然后我们命令行执行下:


go run client.go
复制代码


接着看下watch -d 'netstat -nat |grep "8330"'的返回,这个命令是实时的,所以不需要重复执行


Every 2.0s: netstat -nat |grep "8330"                                                 userdeMacBook-Pro.local: Thu Sep 29 16:45:57 2022
tcp4 0 0 127.0.0.1.8330 127.0.0.1.59146 ESTABLISHEDtcp4 0 0 127.0.0.1.59146 127.0.0.1.8330 ESTABLISHEDtcp46 0 0 *.8330 *.* LISTEN
复制代码


可以看到客户端服务端,服务端和客户端都成功建立了连接(连接是否建立成功不是看是否有条线真连上了,连接状态是维护在各个端的)


同时我们也可以在 wireshark 中看到三次握手建立连接的流程:


5、golang 中的读和写

我们现在稍微修改下服务端的代码:


package main
import ( "fmt" "io" "net" "time")
func main() { server := ":8330" tcpAddr, err := net.ResolveTCPAddr("tcp", server) if err != nil { fmt.Println("resolve err:", err) return }
listen, err := net.ListenTCP("tcp", tcpAddr) if err != nil { fmt.Println("listen err:", err) return } defer listen.Close()
for { conn, err := listen.Accept() if err != nil { fmt.Println("accept err:", err) continue }
go handler(conn)
}}
func handler(conn net.Conn) {
go func() { for { // 指定从buffer中读取数据的最大容量 var buf = make([]byte, 1024) // 从buffer中读取数据并保存到buf中,n代表实际返回的数据大小 n, err := conn.Read(buf) if err != nil { // 客户端关闭会触发EOF if err == io.EOF { conn.Close() return } fmt.Println("read err:", err) return }
fmt.Println("read data ", n, ":", string(buf)) } }()

curTime := time.Now().String() // 数据写到缓冲区 _, err := conn.Write([]byte(curTime)) if err != nil { fmt.Println("write err:", err) return } fmt.Println("send data:", curTime)
}
复制代码


首先要明白,操作系统内核会为每个连接的客户端和服务端分配发送缓冲区接收缓冲区


  1. 当客户端需要发送数据到服务端,调用 conn.Write 从客户端缓冲区发送数据到操作系统内核的发送缓冲区。实际所做的事情是把数据从应用程序缓冲区中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。

  2. 数据通过 tcp 发送到服务端的接收缓冲区,然后服务端的程序从接收缓冲区读取数据。


非阻塞 I/O,当应用程序调用非阻塞 I/O 完成某个操作时,内核立即返回,不会把 CPU 时间切换给其他进程,应用程序在返回后,可以得到足够的 CPU 时间继续完成其他事情。


读操作:如果套接字对应的接收缓冲区没有数据可读,在非阻塞情况下 read 调用会立即返回,一般返回 EWOULDBLOCK 或 EAGAIN 出错信息。


写操作:在非阻塞 I/O 的情况下,如果套接字的发送缓冲区已达到了极限,不能容纳更多的字节,那么操作系统内核会尽最大可能从应用程序拷贝数据到发送缓冲区中,并立即从 write 等函数调用中返回。可想而知,在拷贝动作发生的瞬间,有可能一个字符也没拷贝,有可能所有请求字符都被拷贝完成,那么这个时候就需要返回一个数值,告诉应用程序到底有多少数据被成功拷贝到了发送缓冲区中,应用程序需要再次调用 write 函数,以输出未完成拷贝的字节。


**非阻塞 I/O 操作:**拷贝→返回→再拷贝→再返回。


**阻塞 I/O 操作:**拷贝→直到所有数据拷贝至发送缓冲区完成→返回。


golang 中底层使用的还是非阻塞的 I/O,但是在代码层面做了一些处理,让用户感觉是以阻塞方式调用的。


...
for { n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p) if err != nil { n = 0 // 非阻塞方式调用,如果遇到syscall.EAGAIN报错,代表没拿到数据,继续循环 if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } } err = fd.eofError(n, err) return n, err }
...
复制代码

6、golang 中的关闭

在 socket 中,当客户端调用 close()方法时,其实就是发送一个 FIN 标志位,意思就是我要主动关闭 TCP 连接了。Close 方法会让对端的所有读写操作结束阻塞,并返回。


在 golang 中调用 Close 方法,会让对端的 Read 读取到 EOF 的错误,此时就代表我想关闭连接。对端接收到关闭的请求后也可以调用 Close 方法关闭连接。


客户端:


package main
import ( "fmt" "io" "net")
func main() { serverAddr := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr) if err != nil { fmt.Println("resolve err:", err) return }
conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("dial err:", err) return }

closed := make(chan bool) go func() { for { var buf = make([]byte, 1024) n, err := conn.Read(buf) if err != nil { // 读取到EOF,服务端关闭连接 if err == io.EOF { conn.Close() closed <- true return } fmt.Println("read err:", err) return }
fmt.Println("read data ", n, ":", string(buf)) } }()
for { select { case <-closed: fmt.Println("服务端关闭") return } }
}
复制代码


服务端:


package main
import ( "fmt" "io" "net" "time")
func main() { server := ":8330" tcpAddr, err := net.ResolveTCPAddr("tcp", server) if err != nil { fmt.Println("resolve err:", err) return }
listen, err := net.ListenTCP("tcp", tcpAddr) if err != nil { fmt.Println("listen err:", err) return } defer listen.Close()
for { conn, err := listen.Accept() if err != nil { fmt.Println("accept err:", err) continue }
go handler(conn)
}}
func handler(conn net.Conn) {
go func() { for { var buf = make([]byte, 1024) n, err := conn.Read(buf) if err != nil { // 读取到EOF,客户端关闭连接 if err == io.EOF { conn.Close() return } fmt.Println("read err:", err) return }
fmt.Println("read data ", n, ":", string(buf)) } }()

curTime := time.Now().String() _, err := conn.Write([]byte(curTime)) if err != nil { fmt.Println("write err:", err) return } fmt.Println("send data:", curTime)
}
复制代码


参考:


《极客时间:网络编程实战》

用户头像

六月的

关注

还未添加个人签名 2019-07-23 加入

还未添加个人简介

评论

发布
暂无评论
golang中的socket编程_golang_六月的_InfoQ写作社区