• 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("
    [" + 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 "
    ".'['.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');
    View Code

    查看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  
  • 相关阅读:
    MySQL之数据库优化
    cookie和session
    php自动加载
    php函数之strtr和str_replace的区别
    php函数之substr()
    阶段总结(一)
    json和xml
    sqlserver交换数据行中的指定列
    3 宏、条件编译
    5 常量与变量
  • 原文地址:https://www.cnblogs.com/songjianming/p/13111068.html
Copyright © 2020-2023  润新知