您的位置:首页 > 理论基础 > 计算机网络

【TCP】TCP连接建立和终止

2015-03-03 16:38 351 查看
对于TCP连接建立和终止状态分析,基于自己编写的代码来逐步来分析,而现成的例子并不能全面的进行状态分析;比如listen中backlog对三次握手的影响;

TCP连接的建立和终止示意图



TCP状态转换图



服务器端核心代码

void TcpServer::run()
{
  assert(!_started);

  sockets::bind(_listenfd, _serverAddress);
  
  sockets::listen(_listenfd);

  printf("TcpServer start...\n");

  _started = true;
  while (_started)
    {
      int connfd = sockets::accept(_listenfd, NULL); 
      
      if (connfd < 0)
        continue;
      
      char buf[20];
      int n;
      while ((n = sockets::read(connfd, buf, sizeof buf)) > 0) {
        sockets::writen(connfd, buf, n);
      }
      
      if (n <= 0)
        ::close(connfd);
    }
}
说明:上述代码是一个TCP迭代服务器的核心代码;服务器端口号为6002;

客户端核心代码

void TcpClient::request(const struct sockaddr_in& serverAddr)
{
  int err = sockets::connect(_connectfd, serverAddr);
  if (err < 0)
    {
      printf("In TcpClient::request, connect error : %s\n",  strerror_r(errno, g_errorbuf, sizeof g_errorbuf));
      return;
    }

  char buf[10];
  while (fgets(buf, sizeof buf, stdin))
    {

      int n =  sockets::writen(_connectfd, buf, strlen(buf) + 1);
      printf("client send (%d bytes): %s\n", n , buf);
      
      if ((n = sockets::readn(_connectfd, buf, n)) == 0)    //receive Fin
        {
          printf("server exit\n");
          break;
        }

      printf("client receive : %s\n", buf);
    }
}
说明:上述代码是一个TCP客户端的核心代码;请求的服务器端口号为6002;

TCP连接建立

(1)在服务器端核心代码中sockets::listen(_listenfd);后加入语句::sleep(100);使用netstat查看如下,选项-a表示查看所有的状态;TCP服务器被动打开,从CLOSED至LISTEN;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN


使用lsof查看监听描述符,编号为3;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ lsof -i@0.0.0.0:6002
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server  23180 sykpour    3u  IPv4 249295      0t0  TCP *:x11-2 (LISTEN)


(2)三次握手的过程由内核完成的,在accept返回之前三次握手已经完成;三次握手具体过程具体如下:

(a)客户端执行connect主动打开时,将会发送SYN,客户端从CLOSED变为SYN_SENT;

(b)服务器端接收SYN,发送SYN+ACK;服务器从LISTEN至SYN_RCV;

(c)客户端接收SYN+ACK,发送ACK;客户端从SYN_SENT至ESTABLISHED

(d)服务器端接收ACK;服务器从SYN_RCVD至ESTABLISHED;

仍在sockets::listen(_listenfd);后加入语句::sleep(100);观察三次握手的过程;打开一个客户端,使用netstat查看如下,其中会产生两个套接字对(ESTABLISHED),第1个是客户端的,第2个是服务端的;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:41922 ESTABLISHED
tcp 0 0 127.0.0.1:41922 127.0.0.1:6002 ESTABLISHED

三次握手wireshark截图:



使用lsof查看描述符,会增加一个连接描述符号,client的连接描述符编号为3;由于server未从accept中返回,因此虽然TCP的连接已经建立,但是服务器端连接描述符还没有返回;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ lsof -i@0.0.0.0:6002
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server  24128 sykpour    3u  IPv4 252661      0t0  TCP *:x11-2 (LISTEN)
client  24155 sykpour    3u  IPv4 252728      0t0  TCP localhost:41922->localhost:x11-2 (ESTABLISHED)
(3)在listen中,有一个backlog参数,它影响已完成连接的队列(ESTABLISHD状态, 如今已经不影响未完成连接队列(SYN_RCVD状态);本程序中backlog为2;从下面输出可知,服务器端已经完成ESTABLISHED状态的实际数目为4,客户端(端口号为41985)已经进入了ESTABLISHED状态,而服务器端处于SYN_RECV状态(原因是已完成连接队列已满,服务器端不会接收41895的ACK);就算此时客户端(41985)发送数据也只是写入了对应的内核缓冲区,不能代表服务器已经接收了这些数据,客户端的内核程序将会超时重传这些数据;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:41985 SYN_RECV
tcp 0 0 127.0.0.1:6002 127.0.0.1:41970 ESTABLISHED
tcp 0 0 127.0.0.1:41972 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41971 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41972 ESTABLISHED
tcp 0 0 127.0.0.1:41985 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41969 ESTABLISHED
tcp 0 0 127.0.0.1:41969 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:41971 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:41970 127.0.0.1:6002 ESTABLISHED

wireshark截图:服务端超时重传发送SYN+ACK,客户端发送ACK确认,但是服务器由于已完成连接的队列(ESTABLISHED状态)已满,丢弃ACK,继续超时重传SYN+ACK,5次后放弃;



使用netstat查看如下,服务器超时重传5次后放弃,消除了SYN_RECV状态,此时此连接已经断开;但是客户端(端口号为41985)的连接仍是ESTABLISHED状态,如果客户端发送数据,服务器将会发送RESET报文

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:41970 ESTABLISHED
tcp 0 0 127.0.0.1:41972 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41971 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41972 ESTABLISHED
tcp 0 0 127.0.0.1:41985 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:41969 ESTABLISHED
tcp 0 0 127.0.0.1:41969 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:41971 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:41970 127.0.0.1:6002 ESTABLISHED


RESET报文wireshark截图:



TCP连接终止

客户端主动关闭

(1)启动服务器,由上backlog=2,可打开4个客户端1, 2, 3 , 4,服务端和客户端都可以建立好ESTABLISHED状态;而由于服务器是一个迭代服务器,打开第5个客户端时,服务器状态为SYN_RECV状态;具体状态如下:

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:43892 SYN_RECV
tcp 0 0 127.0.0.1:6002 127.0.0.1:43889 ESTABLISHED
tcp 0 0 127.0.0.1:43889 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43888 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43891 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43888 ESTABLISHED
tcp 0 0 127.0.0.1:43892 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43890 ESTABLISHED
tcp 0 0 127.0.0.1:43890 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43891 127.0.0.1:6002 ESTABLISHED


(2)此时关闭第一个客户端1(端口号为43888),那么服务器端将会read到0(read返回0前,就已经发送ACK,客户端状态已变为FIN_WAIT_2),从而执行到close后客户端1转变为TIME_WAIT;而且已完成连接队列将会多出一个位置,当服务端内核再次超时重传发送SYN+ACK时,客户端(端口号为43892)发送SYN的ACK,服务端将会从SYN_RECV状态变为ESTABLISHED状态;具体如下:

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:43892 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43889 ESTABLISHED
tcp 0 0 127.0.0.1:43889 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43888 127.0.0.1:6002 TIME_WAIT
tcp 0 0 127.0.0.1:6002 127.0.0.1:43891 ESTABLISHED
tcp 0 0 127.0.0.1:43892 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43890 ESTABLISHED
tcp 0 0 127.0.0.1:43890 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43891 127.0.0.1:6002 ESTABLISHED
报文交互wireshark截图:



(3)若此时不关闭第一个客户端1,而关闭客户端3;此时由于服务器仍阻塞在执行read客户端1数据上,因此不能执行close,服务端(端口号为43826)将处于CLOSE_WAIT状态;客户端3(端口号为43826)将位于FIN_WAIT_2状态;端口号为43828的服务端状态仍旧为SYN_RECV;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:43828 SYN_RECV
tcp 0 0 127.0.0.1:43826 127.0.0.1:6002 FIN_WAIT2
tcp 0 0 127.0.0.1:6002 127.0.0.1:43824 ESTABLISHED
tcp 0 0 127.0.0.1:43828 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43827 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43824 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43825 127.0.0.1:6002 ESTABLISHED
tcp 1 0 127.0.0.1:6002 127.0.0.1:43826 CLOSE_WAIT
tcp 0 0 127.0.0.1:6002 127.0.0.1:43825 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43827 ESTABLISHED
(3.1)经过一段时间后,查看状态如下;此时客户端3(端口号为43826)的FIN_WAIT_2状态消失,但是服务端(端口号为43826)的CLOSE_WAIT仍旧存在,而且端口号为43828的客户端SYN_RECV状态已经消失;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:43824 ESTABLISHED
tcp 0 0 127.0.0.1:43828 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43827 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43824 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43825 127.0.0.1:6002 ESTABLISHED
tcp 1 0 127.0.0.1:6002 127.0.0.1:43826 CLOSE_WAIT
tcp 0 0 127.0.0.1:6002 127.0.0.1:43825 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43827 ESTABLISHED


(3.2)此时若关闭客户端1,2,服务器将依次read为0(read返回0前,就已经发送了ACK,客户端状态已变为FIN_WAIT_2),close后客户端状态将转变为TIME_WAIT,因此客户端1,2将处于TIME_WAIT状态;服务器端程序将会accept返回到43826,read为0,(服务端状态为CLOSE_WAIT)close发送FIN,但是客户端3上的连接已经从FIN_WAIT_2状态消失,将会已RESET报文进行回复;

RESET报文wireshark截图:



服务端主动关闭

(1)启动服务器,打开3个客户端1, 2, 3 服务端和客户端都可以建立好ESTABLISHED状态如下;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 0.0.0.0:6002            0.0.0.0:*               LISTEN   
tcp 0 0 127.0.0.1:6002 127.0.0.1:43901 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43897 TIME_WAIT
tcp 0 0 127.0.0.1:6002 127.0.0.1:43899 ESTABLISHED
tcp 0 0 127.0.0.1:6002 127.0.0.1:43900 ESTABLISHED
tcp 0 0 127.0.0.1:43901 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43900 127.0.0.1:6002 ESTABLISHED
tcp 0 0 127.0.0.1:43899 127.0.0.1:6002 ESTABLISHED


(2)关闭服务器,状态如下;服务器只针对端口号为43899的客户端进行正常的终止,也就是和第一个客户端进行正常的序列终止;而对于端口号43900和43901都是直接使用RESET报文进行响应的,因为此时服务器端的连接已经消失;

sykpour@sykpour:~/Desktop/MyCode/Linux/build/bin$ netstat -a -n | grep 6002
tcp        0      0 127.0.0.1:6002          127.0.0.1:43897         TIME_WAIT  
tcp        0      0 127.0.0.1:6002          127.0.0.1:43899         FIN_WAIT2  
tcp        1      0 127.0.0.1:43899         127.0.0.1:6002          CLOSE_WAIT


关闭服务器wireshark截图:



代码运行


完整目录

|-- Net

| |-- CMakeLists.txt

| |-- SocketsOps.cc

| |-- SocketsOps.h

|-- test4

| |-- CMakeLists.txt

| |-- TcpClient.cc

| |-- TcpClient.h

| |-- TcpServer.cc

| `-- TcpServer.h

(注,其中Net目录下,本程序目前只需要使用SocketsOps.h,SocketsOps.cc)


编译构建环境

线程模型:posix

gcc 版本: 4.9.0 (GCC),注(支持C++11 )

Cmake版本:cmake 2.8.0


完整代码下载链接


https://github.com/sykpour/MyCode/tree/master/Linux/test4

(注,本节关键代码位于test4中)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: