网络编程:TCP故障模式
故障模式总结
异常情况可归结为两大类:

第一类,是对端无FIN包发送出来的情况;第二类是对端有FIN包发出来
对端无FIN包发送出
- 网络终端造成对端无FIN包
很多原因都会造成网络中断,这种情况,TCP程序并不能及时感知异常信息。除非网络中的其他设备,如路由器发送出一条ICMP报文,说明目的网络或主机不可达,此时,通过read或write调用就会返回unreachable的错误
在没有ICMP报文的情况下,TCP程序并不能感应到连接异常。如果程序是阻塞在read调用上,程序无法从异常中恢复,可以通过read操作设置超时来解决
如果程序先调用了write操作发送了一段数据流,接下来阻塞在read调用上,结果又是另一种情况。Linux系统的TCP协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传12次,合计时间约9分钟之后,协议栈会标识连接异常,此时,阻塞的read调用会返回一个TIMEOUT的错误信息,如果程序还继续往这条连接上写数据,写操作会立即失败,返回一个SIGPIPE信号给应用程序
- 系统崩溃造成的对端无FIN包
当系统突然奔溃,如断电,网络连接来不及发出任何东西,和通过应用调用杀死应用程序非常不同的是,没有任何FIN包被发送出来。
这种情况和网络中端造成的结果非常类似,在没有ICMP报文的情况下,TCP程序只能通过read和write调用的到网络连接异常的消息,超时错误是一种常见的结果。
不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。
对端有FIN包发出
对端如果有 FIN 包发出,可能的场景是对端调用了** close 或 shutdown** 显式地关闭了连接,也可能是对端应用程序崩溃,操作系统内核代为清理所发出的。从应用程序角度上看,无法区分是哪种情形。
阻塞的 read 操作在完成正常接收的数据读取之后,FIN 包会通过返回一个 EOF 来完成通知,此时,read 调用返回值为 0。这里强调一点,收到 FIN 包之后 read 操作不会立即返回。你可以这样理解,收到 FIN 包相当于往接收缓冲区里放置了一个 EOF 符号,之前已经在接收缓冲区的有效数据不会受到影响。
服务端程序:
//服务端程序
int main(int argc, char **argv) {
int connfd;
char buf[1024];
connfd = tcp_server(SERV_PORT);
for (;;) {
int n = read(connfd, buf, 1024);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed \n");
}
sleep(5);
int write_nc = send(connfd, buf, n, 0);
printf("send bytes: %zu \n", write_nc);
if (write_nc < 0) {
error(1, errno, "error write");
}
}
exit(0);
}
服务端程序是一个简单的应答程序,在收到数据流之后回显给客户端,在此之前,休眠 5 秒,以便完成后面的实验验证。
客户端程序从标准输入读入,将读入的字符串传输给服务器端:
客户端程序:
//客户端程序
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: reliable_client01 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
char buf[128];
int len;
int rc;
while (fgets(buf, sizeof(buf), stdin) != NULL) {
len = strlen(buf);
rc = send(socket_fd, buf, len, 0);
if (rc < 0)
error(1, errno, "write failed");
rc = read(socket_fd, buf, sizeof(buf));
if (rc < 0)
error(1, errno, "read failed");
else if (rc == 0)
error(1, 0, "peer connection closed\n");
else
fputs(buf, stdout);
}
exit(0);
}
- read直接感知FIN包
依次启动服务器端和客户端程序,在客户端输入 good 字符之后,迅速结束掉服务器端程序,这里需要赶在服务器端从睡眠中苏醒之前杀死服务器程序。
屏幕上打印出:peer connection closed。客户端程序正常退出
$./reliable_client01 127.0.0.1
$ good
$ peer connection closed
这说明客户端程序通过 read 调用,感知到了服务端发送的 FIN 包,于是正常退出了客户端程序。

- 通过 write 产生 RST,read 调用感知 RST
们仍然依次启动服务器端和客户端程序,在客户端输入 bad 字符之后,等待一段时间,直到客户端正确显示了服务端的回应“bad”字符之后,再杀死服务器程序。客户端再次输入 bad2,这时屏幕上打印出”peer connection closed“。
$./reliable_client01 127.0.0.1
$bad
$bad
$bad2
$peer connection closed

在很多书籍和文章中,对这个程序的解读是,收到 FIN 包的客户端继续合法地向服务器端发送数据,服务器端在无法定位该 TCP 连接信息的情况下,发送了 RST 信息,当程序调用 read 操作时,内核会将 RST 错误信息通知给应用程序。这是一个典型的 write 操作造成异常,再通过 read 操作来感知异常的样例。
*向一个已关闭连接连续写,最终导致 SIGPIPE
为模拟该过程,对服务端程序和客户端程序进行了修改:
服务端:
nt main(int argc, char **argv) {
int connfd;
char buf[1024];
int time = 0;
connfd = tcp_server(SERV_PORT);
while (1) {
int n = read(connfd, buf, 1024);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed \n");
}
time++;
fprintf(stdout, "1K read for %d \n", time);
usleep(1000);
}
exit(0);
}
服务器端每次读取 1K 数据后休眠 1 秒,以模拟处理数据的过程。
客户端:
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: reliable_client02 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
signal(SIGPIPE, SIG_IGN);
char *msg = "network programming";
ssize_t n_written;
int count = 10000000;
while (count > 0) {
n_written = send(socket_fd, msg, strlen(msg), 0);
fprintf(stdout, "send into buffer %ld \n", n_written);
if (n_written <= 0) {
error(1, errno, "send error");
return -1;
}
count--;
}
return 0;
}
果在服务端读取数据并处理过程中,突然杀死服务器进程,我们会看到客户端很快也会退出,并在屏幕上打印出“Connection reset by peer”的提示。
$./reliable_client02 127.0.0.1
$send into buffer 5917291
$send into buffer -1
$send: Connection reset by peer
这是因为服务端程序被杀死之后,操作系统内核会做一些清理的事情,为这个套接字发送一个 FIN 包,但是,客户端在收到 FIN 包之后,没有 read 操作,还是会继续往这个套接字写入数据。这是因为根据 TCP 协议,连接是双向的,收到对方的 FIN 包只意味着对方不会再发送任何消息。 在一个双方正常关闭的流程中,收到 FIN 包的一端将剩余数据发送给对面(通过一次或多次 write),然后关闭套接字。
当数据到达服务器端时,操作系统内核发现这是一个指向关闭的套接字,会再次向客户端发送一个 RST 包,对于发送端而言如果此时再执行 write 操作,立即会返回一个 RST 错误信息。
全过程的描述图:

小结
因为可能故障的存在,TCP并不是那么的“可靠”。故障分为两大类,一类是对端无 FIN 包,需要通过巡检或超时来发现;另一类是对端有 FIN 包发出,需要通过增强 read 或 write 操作的异常处理,帮助我们发现此类异常。
网络编程:TCP故障模式的更多相关文章
- C#网络编程TCP通信实例程序简单设计
C#网络编程TCP通信实例程序简单设计 采用自带 TcpClient和TcpListener设计一个Tcp通信的例子 只实现了TCP通信 通信程序截图: 压力测试服务端截图: 俩个客户端链接服务端测试 ...
- Socket网络编程(TCP/IP/端口/类)和实例
Socket网络编程(TCP/IP/端口/类)和实例 原文:C# Socket网络编程精华篇 转自:微冷的雨 我们在讲解Socket编程前,先看几个和Socket编程紧密相关的概念: TCP/IP层次 ...
- 网络编程——TCP协议、UDP协议、socket套接字、粘包问题以及解决方法
网络编程--TCP协议.UDP协议.socket套接字.粘包问题以及解决方法 TCP协议(流式协议) 当应用程序想通过TCP协议实现远程通信时,彼此之间必须先建立双向通信通道,基于该双向通道实现数 ...
- Socket网络编程-TCP编程
Socket网络编程-TCP编程 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.socket介绍 1>.TCP/IP协议 2>.跨网络的主机间通讯 在建立通信连接的 ...
- GO语言练习:网络编程 TCP 示例
1.代码 2.编译及运行 1.网络编程 TCP 示例 simplehttp.go 代码 package main import ( "net" "os" &qu ...
- 网络编程TCP协议-聊天室
网络编程TCP协议-聊天室(客户端与服务端的交互); <span style="font-size:18px;">1.客户端发数据到服务端.</span> ...
- 32.网络编程TCP/UDP服务
网络编程TCP: 服务器端口了解: port:0~65535 web服务:80 邮箱服务:556 0~1024:为服务默认的公认端口,一般我们不能用 套接字:socket socket作用 ip:po ...
- 网络编程TCP/IP详解
网络编程TCP/IP详解 1. 网络通信 中继器:信号放大器 集线器(hub):是中继器的一种形式,区别在于集线器能够提供多端口服务,多口中继器,每个数据包的发送都是以广播的形式进行的,容易阻塞网络. ...
- 网络编程TCP总结及实践-C语言
网络变成首先要注意IP和port的转换,如今电脑基本上是主机字节序,存储依照小端方式,而在网络中传输统一使用大端方式,所以网络变成首先要注意字节序的转换. 一个经常使用的ip转换程序的实现: #inc ...
- 36 - 网络编程-TCP编程
目录 1 概述 2 TCP/IP协议基础 3 TCP编程 3.1 通信流程 3.2 构建服务端 3.3 构建客户端 3.4 常用方法 3.4.1 makefile方法 3.5 socket交互 3.4 ...
随机推荐
- Jupyter Notebook 一些常用的快捷键
命令模式(按 Esc 进入): A:在当前单元格上方插入新单元格. B:在当前单元格下方插入新单元格. D + D(按两次 D 键):删除当前单元格. Z:撤销单元格删除. Ctrl + Enter: ...
- Flume - [02] Spooling Directory Source
一.概述 可以通过将文件放入磁盘上的 "Spooldir" 目录中来获取数据.此源会监视指定目录中的新文件,并在新文件出现时解析新文件中的事件.事件解析逻辑是可插入的.在将指定 ...
- Linux - 服务器磁盘 Raid & 分区 & 挂载
一.流计算服务器 有一台流处理服务器(系统盘:2*600G.数据盘:6*600G)分区挂载如下: 设备名 分区 大小 挂载点 文件系统类型 磁盘用途 分区类别 /dev/sda /dev/sda1 3 ...
- 【自荐】Catime v1.0.4 一款贼好用的计时器
Github: https://github.com/vladelaina/Catime 仅1.3MB!!!!! 特点 极简设计: 透明界面.点击穿透.可调大小和位置.多语言支持 丰富字体: 47种字 ...
- 使用Windows任务计划程序实现每天更换一张Processing创意桌面壁纸
Windows任务计划程序(Windows Task Scheduler)是Windows操作系统中的一项系统工具,它允许用户安排自动执行的任务.通过任务计划程序,用户可以设定特定的时间或条件来运行各 ...
- 错误修正记录:对应的VMware Tools脚本未能成功运行
起因 装了台式机,想把笔记本里的vmware虚拟机迁移过来:复制过来后开机就出现这种情况,点开机.挂起.关机等操作会报错,然后无法挂起(再点一次就行) 或者使用下方的选项,而非上方包装过的功能,似乎也 ...
- Go map字典排序
前言 我们已经知道 Go 语言的字典是一个无序集合,如果你想要对字典进行排序,可以通过分别为字典的键和值创建切片,然后通过对切片进行排序来实现. 按照键进行排序 如果要对字典按照键进行排序,可以这么做 ...
- go 遍历修改切片数据
package main import "fmt" type good struct { id int64 sum int64 } func main() { good1 := g ...
- 【U-Boot】解决U-Boot的“Unknown command 'help' - try 'help'”问题
[U-Boot]解决U-Boot的"Unknown command 'help' - try 'help'"问题 零.起因 最近在玩U-Boot,自己编译U-Boot之后输入hel ...
- 重生之我是操作系统(五)----CPU调度
简介 当CPU有大量任务要处理,但由于资源有限,无法同时处理.所有就需要某种规则来决定任务处理的顺序,这就是调度. 调度层次 根据调度频率与层次,共分为三种 高级调度 也称为作业调度(Long-Tre ...