目录node
《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判断可读条件能当即检测出来 | 无 | 无 |
服务端接收客户端的数据并丢弃:c#
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\n"; 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\n"; 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分节。socket
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分节。tcp
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信号,那么客户端不会崩溃。ui
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
这种状况本端TCP会超时,且套接字待处理错误会被置为ETIMEDOUT。.net
服务端主机关机和崩溃不一样,关机时会关闭进程打开的描述符,因此会发送FIN分节,客户端若是处理得当,就能检测到。可是若是是对端主机崩溃,除非设置了SO_KEEPALIVE
选项,不然本端没法得知对端主机已经崩溃。
这一种状况对应表格中的第3、四行。
TCP自己是可靠,可是若是使用不当会给人形成TCP不可靠的错觉。
假设服务端接收链接后调用后打开一个本地文件,而后将文件内容经过socket发送给客户端。
int main(int argc, char* argv[]) { if (argc < 3) { printf("Usage:%s filename port\n", argv[0]); return 0; } int sockfd = acceptOrDie(atoi(argv[2])); printf("accept client\n"); FILE* fp = fopen(argv[1], "rb"); if (!fp) { return 0; } printf("sleeping 10 seconds\n"); 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\n", 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\n", argv[0]); return 0; } int sockfd = acceptOrDie(atoi(argv[2])); printf("accept client\n"); FILE* fp = fopen(argv[1], "rb"); if (!fp) { return 0; } printf("sleeping 10 seconds\n"); 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\n", 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数据。
参考资料:
若是一个 socket 在接收到了 RST packet以后,程序仍然向这个socket写入数据,那么就会产生SIGPIPE信号。
具体例子见“本端TCP发送数据时对端进程已经崩溃”这一节。
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
直接忽略该信号,此时write()会返回-1,而且此时errno的值为EPIPE。
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
经过TCP_NODELAY选项关闭Nagle算法,通常都须要。
TCP主动关闭的一端在发送最后一个ACK后,必须在TIME_WAIT状态等待2倍的MSL(报文最大生存时间)。
在链接处于2MSL状态期间,由该插口对(src_ip:src_port, dest_ip:dest_port)定义的链接不能被再次使用。对于服务端,若是服务器主动断开链接,那么在2MSL时间内,该服务器没法在相同的端口,再次启动。
可使用SO_REUSEADDR选项,容许一个进程从新使用处于2MSL等待的端口。
这样能够防止最后一个ACK丢失,若是丢失了,在2倍的MSL时间内,对端会重发FIN,而后主动关闭的一端能够再次发送ACK,以确保链接正确关闭。
假设处于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); }