本文基于阅读《UNIX网络编程第3版卷1》第五章TCP客户/服务端程序实例,介绍一个简单的TCP回射程序网络的基本流程和所要考虑的各种异常问题。下图是这个程序所要用到的基本套接字函数。
套接字函数简介
int socket(int domain, int type, int protocol);
执行网络IO,第一件事使用socket函数,指定期望的通信协议类型。似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),套接字描述符,简称sockfd。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
服务端套接字绑定自己的IP地址与端口号,客户端那边如果没有绑定端口号,内核会给它分配一个临时的端口。绑定成功,返回0,失败返回-1,IP地址不指定,内核会为其选择。
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
TCP客户端通过connect函数与服务端连接,进行通信。它会激发TCP的三路握手过程,仅在连接建立成功或者出错时才会返回。
出错情况:
- 1, TCP客户端没有收到SYN分节的响应,则返回ETIMEOUT错误。
- 2,若对应客户端的SYN响应是RST(复位),则表示服务器主机在我们指定的端口上没有进程在等待与之连接,硬错误,会返回ECONNREFUSED错误。(RST错误的几种类型:补充)
- 3,路由错误,不可达,软错误。destination unreachable
connect函数会导致套接字状态从CLOSED到SYN_SENT状态,再成功转到ESTABLISHED状态。
int listen(int sockfd, int backlog);
socket函数创建一个套接字时,是一个主动套接字,下一步会使用connect函数,listen函数则将主动套接字转换为被动套接字,等着别人来连接,它导致套接字状态从CLOSED转为LISTEN状态,关于第二个backlog参数,则涉及到监听套接字的两个队列,backlog为两个队列长度之和:
- 1,未完成连接队列:客户端发过来的SYN到达服务器,存在其中,等待服务器完成对应的三路握手过程。这些套接字处于SYN_RCVD状态。
-
2,已完成连接队列:每个已完成TCP三路握手协议的客户对应其中一项。
队列中项的转换过程如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。
一个服务器通常通常仅仅只创建一个监听socket描述字(listen socket),它在该服务器的生命周期内一直存在。
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字(connected socket),当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
关于UNIX网络编程第5章回射网络函数考虑问题点说明
一、正常终止过程
1,客户端终止时,会关闭所有打开的描述符,这会导致客户TCP发送一个FIN给服务器,服务器会以ACK响应,这是tcp终止序列的铅笔部分,至此服务器套接字处于CLOSE_WAIT状态,客户端则处于FIN_WAIT2状态。
2,服务器TCP收到FIN是,子进程会通过调研exit来终止,导致tcp连接终止序列的最后两个分节,一个服务端到客户端的FIN和ACK,至此客户端套接字进入TIME_WAIT状态。
3,服务器子进程终止时,会给父进程发一个SIGCHLD的信号,如果父进程没有处理,子进程进入僵死状态。
二、子进程信号处理
1,fork子进程时,必须捕获SIGCHLD信号
2,当捕获信号是,必须被处理中断的系统调用
3,SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵死进程
三、服务器子进程异常终止:
服务器端fork的子进程被杀死时,会想客户端发送一个FIN,然而此时客户端正被阻塞在fgets函数上,等待从终端输入一行文本,此时终端如果输入一行文本,会导致server terminated prematurely错误,服务器端过早终止
四、数据格式问题:
客户端、服务端不同的实现以不同格式存储二进制数。
不同架构C数据类型上有差异
不同实现结构打包方式不一样
解决办法:
把所有的数值数据转换为文本串
显示顶一个所支持的数据类型的二进制格式
五、SIGPIPE信号:返回broken pipe错误,
在网络编程中经常会遇到SIGPIPE信号,默认情况下这个信号会终止整个进程,当然你并不想让进程被SIGPIPE信号杀死。我们不禁会这样思考:
在什么场景下会产生SIGPIPE信号?
要怎样处理SIGPIPE信号?
SIGPIPE产生的原因是这样的:如果一个 socket 在接收到了 RST packet 之后,程序仍然向这个 socket 写入数据,那么就会产生SIGPIPE信号。
这种现象是很常见的,譬如说,当 client 连接到 server 之后,这时候 server 准备向 client 发送多条消息,但在发送消息之前,client 进程意外奔溃了,那么接下来 server 在发送多条消息的过程中,就会出现SIGPIPE信号。对 server 来说,为了不被SIGPIPE信号杀死,那就需要忽略SIGPIPE信号。
参考http://senlinzhan.github.io/2017/03/02/sigpipe/
六、服务器崩溃后重启:
客户端发送一个tcp包到服务器,服务器端崩溃,由于没有客户端的连接信息,将会对这个客户端发送一个RST,此时客户端收到RST时,由于客户端阻塞与read调用,将会导致返回ECONNREST错误,因此客户端必须能检测服务器主机端是否崩溃
参考资料:
《UNIX网络编程卷1》第5章
https://www.cnblogs.com/zhangshenghui/p/6095297.html
http://senlinzhan.github.io/2017/03/02/sigpipe/