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  
原文地址:https://www.cnblogs.com/songjianming/p/13111068.html