tcp連接建立斷開過程及狀態變化


 

我們知道,基於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)
}
View Code

客戶端: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');
View Code

 

查看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  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM