我们知道,基于TCP/IP协议的网络数据传输大致过程:
- 发送端将数据加上tcp报头(包含发送方端口和目的方端口信息)交给自己的IP模块;
- 发送端IP模块再加上IP报头(包含发送端IP地址和目的端IP地址),并根据路由表选择将封好的IP包交给哪个IP路由;
- 发送端数据链路层在当前局域网根据路由IP查询或从arp缓存找到路由IP对应的硬件MAC地址,加上MAC头,发给路由节点,路由节点收到数据帧去掉MAC头得到IP包,以同样的方式给下一个路由节点,直到IP数据包到达目标主机;
- 目标主机拿到tcp报文根据目的端口将数据交给绑定该端口的应用程序处理;
附上:
tcp报头结构:
IPv4报头结构:
基于tcp协议传输数据前要先建立到目的IP:Port的连接,服务端需要先绑定监听某个端口给数据传输使用,就是告诉机器:发给这个端口的数据交给我处理。
数据传输完成连接不再使用要断开连接。
就是常说的tcp建立连接“三次握手”和断开连接“四次挥手”。
一般过程就是下面这个经典的图:
建立连接:
- 服务端先要绑定监听一个端口(比如提供http服务的nginx监听80,我服务端监听的是12345端口),这时服务端能看到一个连接在LISTEN状态;
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
- 客户端向服务端发出一个报头SYN标志位为1的tcp报文,序号seq为n,并将这个连接状态标记为SYN_SENT;
- 服务端收到客户端发来的这个报头SYN标志位为1的tcp报文,会回给客户端一个报头SYN标志位和ACK标志位都为1的tcp包,序号假设是m,确认号为n+1, 并在服务端把这个连接状态标记为SYN_RCVD;
- 客户端收到服务端这个SYN标志位和ACK标志位都为1的tcp包响应,知道连接没问题,将连接状态标记为ESTABLISHED,并给服务端回一个ACK标志位为1的包,确认号给m+1;
- 服务端收到这个确认包后也知道了连接没问题,将此连接标记为ESTABLISHED,建立连接过程结束。
tcp4 0 0 127.0.0.1.12345 127.0.0.1.51594 ESTABLISHED tcp4 0 0 127.0.0.1.51594 127.0.0.1.12345 ESTABLISHED tcp4 0 0 127.0.0.1.12345 *.* LISTEN
断开连接:
- 主动关闭方发送 报头FIN标志位为1的报文给对方,序号seq为n,并将此连接标记为FIN_WAIT_1;(告诉对方,我这边不会再write了)
- 被动关闭方收到这个报头FIN标志位为1的报文,会回一个报头ACK标志位为1的报文,确认号为n+1,并将该连接的状态标记为CLOSE_WAIT;
- 主动关闭方收到被动方的确认后将连接标记为FIN_WAIT_2;
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61331 CLOSE_WAIT tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 FIN_WAIT_2 tcp4 0 0 127.0.0.1.12345 *.* LISTEN
- 此时,主动关闭方不会再写,但是被动关闭方还可以正常write,主动关闭方还可以正常read!!(为了全双工共,所以连接断开要4次,被动方FIN可能要晚点发)
- 当被动关闭方判断往主动方这个方向的通道可以关闭时,发送报头FIN标志位为1的报文给主动方(序号seq为m),并将状态标记为LAST_ACK;
- 主动关闭方收到被动方的FIN包,回ACK包给被动方(确认号为m+1),将连接状态从FIN_WAIT_2标记为TIME_WAIT,等待2MSL(Maximum Segment Lifetime)后释放连接资源;
- 被动关闭方收到主动方对FIN包的确认直接释放连接资源;
tcp4 0 0 127.0.0.1.12345 *.* LISTEN tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 TIME_WAIT
为什么主动关闭方要在TIME_WAIT状态等待?
我的理解:
- 为对方(被动关闭方)负责:如果被动方没有收到我对它FIN包的ACK,被动方将重发FIN,2MSL时间内我会收到被动方重发的FIN,如果没收到被动方重发的FIN,我认为我的ACK它收到了。
- 防止重用这客户端端口建立新连接,收到给旧连接的报文。
有没有注意到:
我这里说的一直是“主动关闭方”和“被动关闭方”。
我发现书上和网上关于tcp连接关闭过程的时序图画的都是客户端主动关闭。
实际上,主动关闭也可能是服务端主动发起,最后TIME_WAIT等待在服务端。
这是我今天想说的重点。
我的实验环境:
服务端:golang,做完数据传输5s关闭。
package main import ( "fmt" "net" "strconv" "strings" "time" ) //主结构--主要操作都定义为它的方法 type TcpServer struct { work bool proxyTransListener *net.TCPListener //用于关闭,绑定清理 } //主方法 func main() { fmt.Println("main program started") tcpServer := new(TcpServer) //模拟打开服务开关 tcpServer.startTcpService() time.Sleep(3600 * 1e9) fmt.Println("main program end") defer tcpServer.stopProxyService() //主界面终止时,关闭服务 } //统一记日志方法 func (tcpServer *TcpServer) Loglog(msg string) { timeObj := time.Now() fmt.Println(" [" + timeObj.Format("2006-01-02 15:04:05") + "] " + msg) } //开启服务 func (tcpServer *TcpServer) startTcpService() { tcpServer.work = true tcpServer.Loglog("startTcpService run") go tcpServer.proxyTransService() } //关闭服务,并清理端口绑定 func (tcpServer *TcpServer) stopProxyService() { tcpServer.work = false tcpServer.Loglog("stopProxyService run") tcpServer.proxyTransListener.Close() } //tcp报头填充 func (tcpServer *TcpServer) headPadding(wantLength int, content string) string { contentLenth := len(content) contentLenthStr := strconv.Itoa(contentLenth) repeatCount := wantLength - len(contentLenthStr) return contentLenthStr + strings.Repeat(" ", repeatCount) } //请求报文转发服务 func (tcpServer *TcpServer) proxyTransService() { tcpServer.Loglog("proxyTransService run") var proxy_host, proxy_trans_port string proxy_host = "127.0.0.1" proxy_trans_port = "12345" serverHostPort := proxy_host + ":" + proxy_trans_port serverAddr, rsvAdrsErr := net.ResolveTCPAddr("tcp", serverHostPort) if rsvAdrsErr != nil { tcpServer.Loglog(fmt.Sprintf("Resolving address:port failed: %v", rsvAdrsErr)) return } proxyTransListener, lisnErr := net.ListenTCP("tcp", serverAddr) tcpServer.proxyTransListener = proxyTransListener if lisnErr != nil { tcpServer.Loglog(fmt.Sprintf("ListenTCP err: %v", lisnErr)) return } for { if tcpServer.work == true { tcpServer.Loglog("trans_wait") clientConnection, acptErr := proxyTransListener.Accept() tcpServer.Loglog(fmt.Sprintf("accept err: %v", acptErr)) if acptErr == nil { go tcpServer.bussDeal(clientConnection) } } else { tcpServer.Loglog("trans_stop_wait") return } } } //接到客户端连接业务处理 func (tcpServer *TcpServer) bussDeal(clientConnection net.Conn) { clientIpPort := clientConnection.RemoteAddr().String() tcpServer.Loglog(fmt.Sprintf("clientIpPort: %v", clientIpPort)) var clientRequestProxyLengthStrByte []byte = make([]byte, 10) clientConnection.Read(clientRequestProxyLengthStrByte) clientRequestProxyLength, _ := strconv.Atoi(strings.TrimSpace(string(clientRequestProxyLengthStrByte))) tcpServer.Loglog(fmt.Sprintf("clientRequestProxyLength: %v", clientRequestProxyLength)) var clientRequestProxy string if clientRequestProxyLength > 0 { var clientRequestProxyByte []byte = make([]byte, clientRequestProxyLength) clientConnection.Read(clientRequestProxyByte) clientRequestProxy = string(clientRequestProxyByte) tcpServer.Loglog(fmt.Sprintf("clientRequestProxy: %v", clientRequestProxy)) } proxyResp := "backToClientMsg" tcpServer.clientConnectionEnd(clientConnection, proxyResp) } //返回财务并关闭连接 func (tcpServer *TcpServer) clientConnectionEnd(clientConnection net.Conn, proBk string) { proBkLengthStr := tcpServer.headPadding(10, proBk) bkToClientData := proBkLengthStr + proBk clientConnection.Write([]byte(bkToClientData)) clientIpPort := clientConnection.RemoteAddr().String() fmt.Println("server close before") time.Sleep(1 * 1e9) tcpServer.Loglog("5") time.Sleep(1 * 1e9) tcpServer.Loglog("4") time.Sleep(1 * 1e9) tcpServer.Loglog("3") time.Sleep(1 * 1e9) tcpServer.Loglog("2") time.Sleep(1 * 1e9) tcpServer.Loglog("1") time.Sleep(1 * 1e9) clientConnection.Close() tcpServer.Loglog("server close after") fmt.Println(proBkLengthStr+proBk+"Closed connection: ", clientIpPort) }
客户端:php, 做完数据传输直接结束程序(关闭),或者等100s模拟让服务端先关。
<?php class tcpClient { private static $serverHost = '127.0.0.1'; private static $serverPort = '12345'; const TCP_HEADER_LEN = 10;//约定报头长度 /** * * 统一日志方法 * @param string $msg content * @author:songjm * @return void */ private static function logLog($msg) { echo " ".'['.date('Y-m-d H:i:s').']'.$msg; } /** * * tcp客户端测试 * @param string $requestMsg 发送消息 * @author:songjm * @return array */ public static function hello($requestMsg) { try { //创建socketConnectToServer $socketConnectToServer = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($socketConnectToServer === false) { $socketCreateErr = "socket_create() failed:reason:" . socket_strerror(socket_last_error()); self::logLog($socketCreateErr); throw new Exception($socketCreateErr); } self::logLog("socketConnectToServer created."); //尝试连接服务端socket if (!socket_connect($socketConnectToServer, self::$serverHost, self::$serverPort)) { $connectToSignServerErr = "connect to server failed :reason:" . socket_strerror(socket_last_error($socketConnectToServer)); self::logLog($connectToSignServerErr); throw new Exception($connectToSignServerErr); } self::logLog("connect to server ".self::$serverHost.":".self::$serverPort."."); //发送请求报文 $clientRequestData = str_pad(strlen($requestMsg), self::TCP_HEADER_LEN, ' ', STR_PAD_RIGHT).$requestMsg; if (socket_write($socketConnectToServer, $clientRequestData, strlen($clientRequestData)) === false) { $reqErr = "socket_write() failed reason:" . socket_strerror(socket_last_error($socketConnectToServer)); self::logLog($reqErr); throw new Exception($reqErr); } self::logLog("send requestMsg: ".$clientRequestData); //读服务端响应报文 $responLengthStr = socket_read($socketConnectToServer, self::TCP_HEADER_LEN, PHP_BINARY_READ); self::logLog("responLengthStr: " .$responLengthStr); $responLength = (int)$responLengthStr; $responStr = ''; $responLeftLength = $responLength; $responReadStartTime = time(); do { $responThisTime = socket_read($socketConnectToServer, $responLeftLength, PHP_BINARY_READ); if ($responThisTime !== false && $responThisTime != '') { $responStr .= $responThisTime; } $responLeftLength = $responLength - strlen($responStr); } while (($responLeftLength > 0) && (time() - $responReadStartTime < 5));//读5秒超时 self::logLog("respon:".$responStr); $bkArr = array( 'code' => 1, 'msg' => 'ok', 'respData' => $responStr, ); //echo " wait 100s "; //sleep(1000);//让服务端先关闭 return $bkArr; } catch (Exception $exception) { return array( 'code' => 0, 'msg' => $exception->getMessage(), 'respData' => '', ); } } } $respArr = tcpClient::hello('client跟server说'); echo "<pre>"; print_r($respArr); die('program end');
查看tcp连接状态:
netstat -p tcp -an | grep '12345' >> /a.txt && echo ' ' >> /a.txt
客户端主动关闭:
客户端方法结束主动关闭后:
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61331 CLOSE_WAIT
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 FIN_WAIT_2
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
服务端关闭之后:(被动)
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 TIME_WAIT
服务端主动关闭:
服务端主动关闭: tcp4 0 0 127.0.0.1.12345 127.0.0.1.61493 FIN_WAIT_2 tcp4 0 0 127.0.0.1.61493 127.0.0.1.12345 CLOSE_WAIT tcp4 0 0 127.0.0.1.12345 *.* LISTEN 客户端方法结束关闭之后:(被动) tcp4 0 0 127.0.0.1.12345 *.* LISTEN tcp4 0 0 127.0.0.1.12345 127.0.0.1.61493 TIME_WAIT