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

《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\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分节。

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\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数据。

参考资料:

  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);
}

TCP使用注意事项总结的更多相关文章

  1. tcp/udp注意事项

  2. TCP使用

    TCP使用注意事项总结   目录 发送或者接受数据过程中对端可能发生的情况汇总 本端TCP发送数据时对端进程已经崩溃 本端TCP发送数据时对端主机已经崩溃 本端TCP发送数据时对端主机已经关机 某个连 ...

  3. 【TCP/IP网络编程】:09套接字的多种可选项

    本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...

  4. mina框架详解

     转:http://blog.csdn.net/w13770269691/article/details/8614584 mina框架详解 分类: web2013-02-26 17:13 12651人 ...

  5. MINA2 框架详解(转)

    Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP.UDP/IP协议栈的通信框架(当然,也可以提供JAVA 对象的序列化服务.虚拟机管道通信服务等),M ...

  6. mina

    http://bsr1983.iteye.com/blog/1886296 http://blog.csdn.net/defonds/article/category/1844073(这个网站原创最全 ...

  7. mina 通讯框架

    Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP.UDP/IP协议栈的通信框架(当然,也可以提供JAVA 对象的序列化服务.虚拟机管道通信服务等),M ...

  8. Android C# java 长连接框架

    mina框架详解 Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP.UDP/IP协议栈的通信框架(当然,也可以提供JAVA 对象的序列化服务.虚拟机管 ...

  9. wireshark抓包实战(六),过滤器

    目录 一.抓包过滤器 1.语法来源 2.语法 二.显示过滤器 1.语法来源 2.关键要素 wireshark中,过滤器有两种,一种是抓包过滤器,一种是显示过滤器! 抓包过滤器适合大网络环境,配置与抓包 ...

随机推荐

  1. 《深入浅出WPF》笔记——模板篇

    原文:<深入浅出WPF>笔记--模板篇 我们通常说的模板是用来参照的,同样在WPF中,模板是用来作为制作控件的参照. 一.认识模板 1.1WPF菜鸟看模板 前面的记录有提过,控件主要是算法 ...

  2. Spring Boot中集成Spring Security 专题

    check to see if spring security is applied that the appropriate resources are permitted: @Configurat ...

  3. 利用反汇编手段解析C语言函数

    1.问题的提出函数是 C语言中的重要概念.利用好函数能够充分利用系统库的功能写出模块独立.易于维护和修改的程序.函数并不是 C 语言独有的概念,其他语言中的方法.过程等本质上都是函数.可见函数在教学中 ...

  4. 新版本MenuDemo——使用Duilib模拟Windows本机菜单

    相信玩Duilib朋友已经开始期待一个很长的文章.由于我的文章在一周前公布--"无焦点窗体的实现"里面提到了无焦点窗体在菜单里面的应用,并承诺大家,写一个关于Menu实现的Demo ...

  5. crossplatform----文本编辑器工具Atom安装

    1.简介 Atom 是 Github 专门为程序员推出的一个跨平台文本编辑器.具有简洁和直观的图形用户界面,并有很多有趣的特点:支持CSS,HTML,JavaScript等网页编程语言.它支持宏,自动 ...

  6. JQuery采纳CSS实现DOM显示和隐藏要素

    今天参加了Code Review活动.阅读编写代码的同事,感到满足当前功能的实现,但是从长远来看,,无论角度还是从代码重用是来看显然不佳维修点. 有什么需要看的权利.通过选择不同的选项下拉框需求,为了 ...

  7. 从零开始学习 asp.net core 2.1 web api 后端api基础框架(一)-环境介绍

    原文:从零开始学习 asp.net core 2.1 web api 后端api基础框架(一)-环境介绍 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.ne ...

  8. WPF与缓动(一) N次缓动

    原文:WPF与缓动(一) N次缓动   WPF与缓动(一)  N次缓动                                                                  ...

  9. sdutoj1225--编辑距离(dp:字符串转换)

    编辑距离 nid=24#time" style="padding-bottom:0px; margin:0px; padding-left:0px; padding-right:0 ...

  10. 【转】关于在.Net开发中使用Sqlite的版本选择问题

    如果一个.NET应用要自适应32位/64位系统,只需要在项目的“目标平台”设置为“Any CPU”.但是如果应用中使用了SQLite,情况就不同了. SQLite的.NET开发包来自是System.D ...