zoukankan      html  css  js  c++  java
  • TCP使用注意事项总结

    发送或者接受数据过程中对端可能发生的情况汇总

    《UNP》p159总结了如下的情况:

    情形 对端进程崩溃 对端主机崩溃 对端主机不可达
    本端TCP正主动发送数据 对端TCP发送一个FIN,这通过使用select判断可读条件立即能检测出来,如果本端TCP发送另一个分节,对端TCP就以RST响应。如果本端TCP在收到RST后应用进程仍试图写套接字,我们的套接字实现就给该进程发送一个SIGPIPE信号 本端TCP将超时,且套接字的待处理错误被置为ETIMEDOUT 本端TCP将超时,且套接字的待处理错误被置为EHOSTUNREACH
    本端TCP正主动接收数据 对端TCP发送一个FIN,我们将把它作为一个EOF读入 我们将停止接收数据 我们将停止接收数据
    连接空闲,保持存活选项已设置 对端TCP发送一个FIN,这通过select判断可读条件能立即检测出来 在无数据交换2小时后,发送9个保持存活探测分节,然后套接字的待处理错误被置为ETIMEDOUT 在无数据交换2小时后,发送9个保持存活探测分节,然后套接字的待处理错误被置为HOSTUNREACH
    连接空闲,保持存活选项未设置 对端TCP发送一个FIN,这通过select判断可读条件能立即检测出来

    本端TCP发送数据时对端进程已经崩溃

    服务端接收客户端的数据并丢弃:

    int acceptOrDie(uint16_t port)
    {
      int listenfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      assert(listenfd >= 0);
    
      int yes = 1;
      if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)))
      {
        perror("setsockopt");
        exit(1);
      }
    
      struct sockaddr_in addr;
      bzero(&addr, sizeof(addr));
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = INADDR_ANY;
      if (::bind(listenfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)))
      {
        perror("bind");
        exit(1);
      }
    
      if (::listen(listenfd, 5))
      {
        perror("listen");
        exit(1);
      }
    
      struct sockaddr_in peer_addr;
      bzero(&peer_addr, sizeof(peer_addr));
      socklen_t addrlen = 0;
      int sockfd = ::accept(listenfd, reinterpret_cast<struct sockaddr*>(&peer_addr), &addrlen);
      if (sockfd < 0)
      {
        perror("accept");
        exit(1);
      }
      ::close(listenfd);
      return sockfd;
    }
    
    void discard(int sockfd)
    {
    	char buf[65536];
      	while (true)
      	{
        	int nr = ::read(sockfd, buf, sizeof buf);
        	if (nr <= 0)
          		break;
      }
    }
    
    int main(int argc, char* argv[]) {
    	if (argc < 2) {
    		cout << "usage:./server port
    ";
    		exit(0);
    	}
    
    	int sockfd = acceptOrDie(atoi(argv[1]));  //创建socket, bind, listen
    	discard(sockfd);    //读取并丢弃所有客户端发送的数据
    
    	return 0;
    }
    

    客户端从命令行接受字符串并发送给服务端:

    struct sockaddr_in resolveOrDie(const char* host, uint16_t port)
    {
      struct hostent* he = ::gethostbyname(host);
      if (!he)
      {
        perror("gethostbyname");
        exit(1);
      }
      assert(he->h_addrtype == AF_INET && he->h_length == sizeof(uint32_t));
      struct sockaddr_in addr;
      bzero(&addr, sizeof(addr));
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr = *reinterpret_cast<struct in_addr*>(he->h_addr);
      return addr;
    }
    
    int main(int argc, char* argv[]) {
    	if (argc < 3) {
    		cout << "usage:./cli host port
    ";
    		exit(0);
    	}
    	struct sockaddr_in addr =  resolveOrDie(argv[1], atoi(argv[2]));
    
    	int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      	assert(sockfd >= 0);
      	int ret = ::connect(sockfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
      	if (ret)
      	{
        	perror("connect");
    		exit(1);
      	}
    
    	char sendline[1024];
    	while (fgets(sendline, sizeof sendline, stdin) != NULL) { //从命令行读数据
    		write_n(sockfd, sendline, strlen(sendline));  //发送给服务端
    	} 
    	return 0;
    }
    

    先启动tcpdump观察数据包的流动,然后分别启动服务端和客户端。
    下面是三次握手的数据包:

    15:33:21.184993 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [S], seq 1654237964, win 64240, options [mss 1412,nop,wscale 8,nop,nop,sackOK], length 0
    15:33:21.185027 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [S.], seq 3710209371, ack 1654237965, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
    15:33:21.230698 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [.], ack 1, win 259, length 0
    

    然后终止服务端进程,观察数据包的情况。服务端进程终止后,会向客户端发送一个FIN分节,客户端内核回应一个ACK。此时客户端阻塞在fgets,感受不到这个FIN分节。

    15:33:49.310810 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [F.], seq 1, ack 8, win 229, length 0
    15:33:49.356453 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [.], ack 2, win 259, length 0
    

    如果这时客户端继续发送数据,因为服务端进程已经不在了,所以服务端内核响应一个RST分节。

    15:34:31.198332 IP 221.218.38.144.53186 > 172.19.0.16.1234: Flags [P.], seq 8:16, ack 2, win 259, length 8
    15:34:31.198360 IP 172.19.0.16.1234 > 221.218.38.144.53186: Flags [R], seq 3710209373, win 0, length 0
    

    如果客户端在收到RST分节后,继续发送数据,将会收到SIGPIPE信号,如果使用默认的处理方式,客户端进程将会崩溃。

    如果我们在客户端代码中忽略SIGPIPE信号,那么客户端不会崩溃。

    signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
    

    本端TCP发送数据时对端主机已经崩溃

    这种情况本端TCP会超时,且套接字待处理错误会被置为ETIMEDOUT。

    本端TCP发送数据时对端主机已经关机

    服务端主机关机和崩溃不同,关机时会关闭进程打开的描述符,所以会发送FIN分节,客户端如果处理得当,就能检测到。但是如果是对端主机崩溃,除非设置了SO_KEEPALIVE
    选项,否则本端无法得知对端主机已经崩溃。

    某个连接长时间没有数据流动

    这一种情况对应表格中的第三、四行。

    1. 如果没有设置SO_KEEPALIVE选项,那么如果对端只是进程崩溃,那么本端还是可以通过select检测到的,但是如果对端主机崩溃或者变得不可达,那么本端没有办法得知,这个连接也得不到正常的关闭。
    2. 如果设置了该选项。
      这个选项是用来检测对端是否主机崩溃或者变得不可达(比如网线断开),而不是检测对端进程是否崩溃,如果是进程崩溃的话会发送一个FIN,本端可以用select检测到。但是如果对端长时间没有数据流动,我们除了设置这个选项,没有办法得知对端是不是主机崩溃或者变得不可达。
      设置该选项后,如果2小时内该套接字任一方向上都没有数据交换,TCP就自动给对端发送一个探测分节,可能出现三种情况:
      1. 对端响应ACK。表示一切正常,应用进程不会得到任何通知。
      2. 对端响应RST,表示对端已崩溃且以重新启动,该套接字的待处理错误被置为ECONNRESET,套接字被关闭。
      3. 对端没有任何响应,那么隔一段时间再次发送探测分节,如果还是没有响应,套接字错误被置为ETIMEOUT,套接字被关闭。

    TCP发送数据不全

    TCP本身是可靠,但是如果使用不当会给人造成TCP不可靠的错觉。

    TCP数据发送不全实例

    假设服务端接收连接后调用后打开一个本地文件,然后将文件内容通过socket发送给客户端。

    int main(int argc, char* argv[]) {
    	if (argc < 3) {
    		printf("Usage:%s filename port
    ", argv[0]);
    		return 0;
    	}
    
    	int sockfd = acceptOrDie(atoi(argv[2]));
    	printf("accept client
    ");
    
    	FILE* fp = fopen(argv[1], "rb");
    	if (!fp) {
    		return 0;
    	}
    
    	printf("sleeping 10 seconds
    ");
    	sleep(10);
    
    	char buf[8192];
    	size_t nr = 0;
    	while ((nr = fread(buf, 1, sizeof buf, fp)) > 0) {  //读文件
    		write_n(sockfd, buf, nr);                       //发送给客户端
    	}
    
    	fclose(fp);
    	printf("finish sending file %s
    ", argv[1]);
    }
    

    首先在在服务端启动该程序./send file_1M_size 1234。file_1M_size的1M大小的文件。

    用nc作为客户端nc localhost 1234 | wc -c
    连接建立后,服务端会sleep 10秒,然后拷贝文件,最终客户端输出:

    1048576
    

    这里没问题,确实发送了1M数据的文件。

    如果我们在服务端sleep 10秒期间,在客户端输入了一些数据:

    root@DESKTOP-2A432QS:/mnt/c/Users/12401/Desktop/network_programing/recipes-master/tpc# nc localhost 1234 | wc -c
    abcdfef
    976824
    

    abcdfef是我们发送给服务端的,976824是收到的字节数。显然不够1M。

    为什么会出现数据发送不全的现象?

    建立连接后,客户端也向服务端发送了一些数据,这些数据到达服务端后,保存在服务端的内核缓冲区中。服务端读取文件后调用write发送出去,虽然write返回了,但这仅仅代表要发送的数据已经被放到了内核发送缓冲区,并不代表已经被客户端接收了。这时服务端while循环结束,直接退出了main函数,这会导致close连接,当接收缓冲区还有数据没有读取时调用close,将会向对端发送一个RST分节,该分节会导致发送缓冲区中待发送的数据被丢弃,而不是正常的TCP断开连接序列,从而导致客户端没有收到完整的文件。
    问题的本质是:在没有确认对端进程已经收到了完整的数据,就close了socket。那么如何保证确保对端进程已经收到了完整的数据呢?

    如何解决(如何正确关闭连接)?

    一句话:read读到0之后才close。

    发送完数据后,调用shutdown(第二个参数设置为SHUT_WR),后跟一个read调用,该read返回0,表示对端也关闭了连接(这意味着对端应用进程完整接收了我们发送的数据),然后才close。

    发送方接收方程序结构如下:
    发送方:1.send() , 2.发送完毕后调用shutdown(WR), 5.read()->0(此时发送方才算能确认接收方已经接收了全部数据), 6.close()。
    接收方:3.read()->0(说明没有数据可读了), 4.如果没有数据可发调用close()。
    序号表明了时间的顺序。

    我们修改之前的服务端代码:

    int main(int argc, char* argv[]) {
    	if (argc < 3) {
    		printf("Usage:%s filename port
    ", argv[0]);
    		return 0;
    	}
    
    	int sockfd = acceptOrDie(atoi(argv[2]));
    	printf("accept client
    ");
    
    	FILE* fp = fopen(argv[1], "rb");
    	if (!fp) {
    		return 0;
    	}
    
    	printf("sleeping 10 seconds
    ");
    	sleep(10);
    
    	char buf[8192];
    	size_t nr = 0;
    	while ((nr = fread(buf, 1, sizeof buf, fp)) > 0) {
    		write_n(sockfd, buf, nr);
    	}
    
    	fclose(fp);
    
    	shutdown(sockfd, SHUT_WR);      //新增代码,发送FIN分节
    	while ((nr = read(sockfd, buf, sizeof buf)) > 0) {      //新增代码,等客户端close
    		//do nothing
    	}
    	printf("finish sending file %s
    ", argv[1]);
    }
    

    这次在while循环结束后,不是直接退出main,而是shutdown,然后循环read,等客户端先close,客户端close后,read会返回0,然后退出main函数。这样就能保证数据被完整发送了。

    root@DESKTOP-2A432QS:/mnt/c/Users/12401/Desktop/network_programing/recipes-master/tpc# nc localhost 1234 | wc -c
    abcdefg
    1048576
    

    这次就算客户端发送了数据,也能保证收到了完整的1M数据。

    参考资料:

    1. why is my tcp not reliable

    SIGPIPE信号

    什么场景下会产生SIGPIPE信号?

    如果一个 socket 在接收到了 RST packet之后,程序仍然向这个socket写入数据,那么就会产生SIGPIPE信号。
    具体例子见“本端TCP发送数据时对端进程已经崩溃”这一节。

    如何处理SIGPIPE信号?

    signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
    

    直接忽略该信号,此时write()会返回-1,并且此时errno的值为EPIPE。

    Nagle算法,TCP_NODELAY

    Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

    通过TCP_NODELAY选项关闭Nagle算法,一般都需要。

    SO_RESUSEADDR

    TCP主动关闭的一端在发送最后一个ACK后,必须在TIME_WAIT状态等待2倍的MSL(报文最大生存时间)。
    在连接处于2MSL状态期间,由该插口对(src_ip:src_port, dest_ip:dest_port)定义的连接不能被再次使用。对于服务端,如果服务器主动断开连接,那么在2MSL时间内,该服务器无法在相同的端口,再次启动。
    可以使用SO_REUSEADDR选项,允许一个进程重新使用处于2MSL等待的端口。

    为什么要设计2MSL状态?

    这样可以防止最后一个ACK丢失,如果丢失了,在2倍的MSL时间内,对端会重发FIN,然后主动关闭的一端可以再次发送ACK,以确保连接正确关闭。

    为什么处于2MSL状态时该插口对定义的连接不能被再用?

    假设处于2MSL状态的插口对,能再次被使用,那么前一个连接迟到的报文对这个新的连接会有影响。

    示例

    以前文的sender为例,在服务端执行./sender file_1M_size 1234,然后客户端进行连接 nc localhost 1234 | wc -c,连接后,终止sender进程。
    用netstat查看会发现这个连接处于TIME_WAIT状态,然后试图再在1234端口启动sender会发现:

    bind: Address already in use
    

    解决办法

    开启套接字的SO_REUSEADDR选项。

      int yes = 1;
      if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)))
      {
        perror("setsockopt");
        exit(1);
      }
    
  • 相关阅读:
    转换进制,十六进制数相加
    一个人的旅行(Dijkstra算法)
    畅通工程续(Dijkstra算法)
    免费书下载
    http://d3js.org/
    React.js model
    jtable更新数据
    java knowledge record
    ActionListener三种实现
    java接口理解(转载)
  • 原文地址:https://www.cnblogs.com/gatsby123/p/11099239.html
Copyright © 2011-2022 走看看