我們知道,基於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("\n[" + 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 "\r\n".'['.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:\r\n".$clientRequestData); //讀服務端響應報文 $responLengthStr = socket_read($socketConnectToServer, self::TCP_HEADER_LEN, PHP_BINARY_READ); self::logLog("responLengthStr:\r\n" .$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 "\r\nwait 100s\r\n"; //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 '\r\n' >> /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