0.1、索引
https://waterflow.link/articles/1664591292871
1、tcp的3次握手(建设连贯)
- 客户端的协定栈向服务器端发送了 SYN 包,并通知服务器端以后发送序列号 j,客户端进入 SYNC_SENT 状态;
- 服务器端的协定栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,示意对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,通知客户端以后我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
- 客户端协定栈收到 ACK 之后,使得应用程序从 connect 调用返回,示意客户端到服务器端的单向连贯建设胜利,客户端的状态为 ESTABLISHED,同时客户端协定栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
- 应答包达到服务器端后,服务器端协定栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连贯也建设胜利,服务器端也进入 ESTABLISHED 状态。
2、tcp的4次挥手(敞开连贯)
- 一方应用程序调用 close,咱们称该方为被动敞开方,该端的 TCP 发送一个 FIN 包,示意须要敞开连贯。之后被动敞开方进入 FIN_WAIT_1 状态。
- 接管到这个 FIN 包的对端执行被动敞开。这个 FIN 由 TCP 协定栈解决,咱们晓得,TCP 协定栈为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序能够通过 read 调用来感知这个 FIN 包。肯定要留神,这个 EOF 会被放在已排队等待的其余已接管的数据之后,这就意味着接收端应用程序须要解决这种异常情况,因为 EOF 示意在该连贯上再无额定数据达到。此时,被动敞开方进入 CLOSE_WAIT 状态。
- 被动敞开方将读到这个 EOF,于是,应用程序也调用 close 敞开它的套接字,这导致它的 TCP 也发送一个 FIN 包。这样,被动敞开方将进入 LAST_ACK 状态。
- 被动敞开方接管到对方的 FIN 包,并确认这个 FIN 包。被动敞开方进入 TIME_WAIT 状态,而接管到 ACK 的被动敞开方则进入 CLOSED 状态。进过 2MSL 工夫之后,被动敞开方也进入 CLOSED 状态。
3、socket中的连贯建设和敞开
我看先看下流程:
- 服务端调用socket、bind绑定ip端口、listen开启服务端监听。
- accept阻塞期待下次调用,并返回一个tcp连贯。
- 客户端调用connect连贯服务端。
- 此时服务端accept完结阻塞,代表客户端和服务端胜利建设连贯。
- 而后就是数据交互读写读写。
- 当客户端连贯敞开时,服务端的read办法会读取一个io.EOF的谬误,代表客户端敞开连贯。服务端收到敞开连贯的谬误后也调用close敞开连贯。
4、golang中的连贯建设
咱们先看下服务端:
package mainimport ( "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) {}
- 首先咱们定义好ip和端口,开启监听
- 而后调用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 2022tcp46 0 0 *.8330 *.* LISTEN
能够看到此时8330端口曾经开启监听
客户端:
package mainimport ( "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 2022tcp4 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 mainimport ( "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)}
首先要明确,操作系统内核会为每个连贯的客户端和服务端调配发送缓冲区和接收缓冲区。
- 当客户端须要发送数据到服务端,调用conn.Write从客户端缓冲区发送数据到操作系统内核的发送缓冲区。理论所做的事件是把数据从应用程序缓冲区中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
- 数据通过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 mainimport ( "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 mainimport ( "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)}
参考:
《极客工夫:网络编程实战》