3.1 TCP 套接字编程★
使用TCP 套接字编程可以实现基于TCP/IP 协议的面向连接的通信,它分为服务器和客户端两部分,其主要实现过程如图3-1所示。
TCP套接字编程中,服务器端实现的步骤:
- 使用socket()函数创建套接字;
- 为创建的套接字绑定到指定的地址结构;
- listen()函数设置套接字为监听模式,使服务器进入被动打开的转态;
- 接受客户端的连接请求,建立连接;
- 接收、应答客户端的数据请求;
- 终止连接。
客户器端实现的步骤相对比较简单:
- 使用socket()函数创建套接字;
- 调用connect函数建立一个与TCP服务器的连接;
- 发送数据请求,接收服务器的数据应答;
- 终止连接。
虽然整个TCP 套接字编程实现的过程较为复杂,但是它的模式相对固定。下面就介绍TCP 套接字编程中涉及到的基本套接字函数。
3.1.1 socket 函数
为了执行网络I/O ,无论是服务器还是客户端,首先必须调用 socket 函数,产生 TCP 套接字,作为 TCP 通信的传输端点。
结构
socket函数如下:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
参数
socket函数中 family 参数指明协议族。 type 参数指明产生套接字的类型。 protocol 参数是协议标志,一般在调用 socket 函数时将其置为 0 ,但如果是原始套接字,就需要为protocol指定一个常值。
family参数指明的协议族,确定了 socket 使用的协议类型,它的值通常为:
- AF_INET:IPv4协议
- AF_INET6:IPv6协议
- AF_ROUTE:路由套接口
type 参数指明产生套接字的类型,它常用的值包括:
- SOCK_STREAM:字节流套接口,TCP使用的是这种形式。
- SOCK_DGRAM:数据报套接口,UDP使用的是这种形式。
- SOCK_RAW:原始套接口。
并不是所有family 和 type 的组合都有效, 图 3 2 中给出了一些有效的组合,及其对应的协议。其中 Yes 表示 组合 有效,而 No 表示当前的组合是无效的。
返回值
该函数如果调用成功,将返回一个小的非负的整数值,它与文件描述符类似,这里称之为套接字描述符( socket descriptor ),简称套接字,之后的 I/O 操作都由该套接字完成。如果函数调用失败,则返回 1 。
实例
调用socket 函数的代码如下:
1. #include <sys/socket.h>
2. ……
3. int sockfd;
4. //crteate socket
5. if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) = = -1)
6. {
7. //handle exception
8. ……
9. }
3.1.2 connect 函数
TCP 客户端使用connect 函数来配置套接字,建立一个与TCP 服务器的连接。
结构
connect 函数如下:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect 函数用于激发TCP 的三路握手过程,建立与远程服务器的连接。
参数
参数sockfd 是由socket 函数返回的套接字描述符。
第二个参数addr 是指向服务器的套接字地址结构的指针,如果是IPv4 地址,server 指向的就是一个sockaddr_in 地址结构,在进行connect 函数调用时,必须将sockaddr_in 结构转换成通用地址结构sockaddr。
最后一个参数addrlen 是该套接字地址结构的大小。
返回值
调用成功返回0,出错则返回-1。
如果描述符是TCP 套接字,调用函数connect 就是建立一个TCP 的连接,只在连接建立成功或者出错时该函数才返回,返回的错误有如下几种情况:
- 如果客户没有收到SYN 分节的响应,返回ETIMEDOUT,这可能需要重发若干次SYN。
- 如果对客户的SYN 的响应是RST,则表明该服务器主机在指定的端口上没有进程在等待与之相连。客户端马上返回错误ECONNREFUSED。
- 如果客户发出的SYN 在中间路由器上引发一个目的地不可达ICMP 错误,客户端内核保存此消息,并按第一种情况,连续重传SYN,直到规定时间的超时时间,对方仍没有响应,则返回保存的消息(即ICMP错误)EHOSTUNREACH或ENETUNREACH错误返回给进程。
TCP 连接的状态
对于TCP 连接的状态, connect 导致客户端从 CLOSED 状态转到了 SYN_SENT 状态。若建立连接成功,也就是 connect 调用成功,状态会再变到 ESTABLISHED 状态。若函数 connect调用失败,则套接字不能再使用,必须关闭。如果想继续向服务器发起建立连接的请求,就需要重新调用 socket 函数,生成新的套接字。
实例
调用 connect 函数的代码如下:
#include <sys/socket.h>
……
int sockfd;
struct sockaddr _in server;
……
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(1234);
server.sin_addr .s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{
//handle exception
……
}
……
第6 行 为套接字地址结构 server 设置初始值 0 。
第7-9 行 为套接字 地 址 结 构中的成员赋值,客户端要建立连接的服务器的 IP 地址为127.0.0.1 ,端口号为 1234 。
第10 行 调用 connect 函数,与服务器建立连接。其中 connect 的第二个参数是将 IPv4的套接字地址结构强制转换成了通用套接字地址结构。
第12 行 如果调用 connect 函数失败,连接失败的异常处理。
3.1.3 bind 函数
绑定函数的作用就是为调用socket 函数产生的套接字分配一个本地协议地址,建立地址与套接字的对应关系。对于网际协议,协议 地址包括 32 位的 IPv4 地址或 128 位的 IPv6 地址和 16 位的 UDP 或 TCP 的端口号。对于绑定操作,地址信息必须是惟一的,在实际应用中,通过绑定的端口号来保证地址的惟 一性。
结构
bind函数如下:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *server , socklen_len addrlen);
参数
参数sockfd是套接字函数返回的套接字描述符。
server参数是指向特定于协议的地址结构的指针,指定用于通信的本地协议地址。
addrlen指定了该套接字地址结构的长度。
返回值
如果调用成功返回0 ,出错则返回 1 ,并置错误号 errno 。
绑定的套接字地址结构
对于绑定的套接字地址结构,可以指定端口号或 IP 地址中的任意一个,可以两个都指定,也可以一个也不指定。如果不绑定任何端口,当调用 connect 或 listen 时,内核会为套接字选择一个临时的端口。
进程如果绑定了一个特定的本地IP 地址到它的套接字上,对于 TCP 客户端,这就为在此套接字上发送的 IP 数据包分配了源 IP 地址。对于 TCP 服务器端,这就限制了该套接字只接收目的地址为此 IP 地址的客户连接。
TCP客户端一般不需要调用 bind ,也就是它不绑定一个 IP 地址到它的套接字上。当连接套接字时,由内核来选择 IP 源地址。如果 TCP 服务器不把 IP 地址绑定在套接字上,内核就把客户所发的 SYN 所在分组的目的 IP 地址作为服务器的源 IP 地址。表 3-2 给出 了 调用 bind函数时 IP 地址和端口号的取值和其对应的结果。
对于IPv4 通配地址由常数 INADDR_ANY 来指定,其值一般为 0 ,它通知内核选择 IP地址。若指定的端口号为 0 ,调用函数 bind 时,内核选择一个临时端口,但是由于 bind 的第二个参数有 const 限定词,使得其并不能返回所选择的 IP 地址和端口号。为了得到选择的值,就需要调用后面将要讲到的函数 getsockname 来返回协议地址。
bind函数返回的一个常见错误是 EADDRINUSE ,表示地址已使用,这个可以通过设置套接字选项 SO_REUSEADDR 来解决,在套接字选项一章会有 详细说明。
在调用bind 函数设置端口号时,一般不要将端口号设置为小于 1024 的值,因为 1~1024是保留端口号。
实例
调用 bind 函数的代码如下:
1. #include <sys/socket.h>
2. ……
3. int sockfd;
4. int port = 1234;
5. struct sockaddr_in server;
6. ……
7. bzero(&server, sizeof(server));
8. server.sin_family = AF_INET;
9. server.sin_port = htons(port);
10. server.sin_addr.s_addr = htonl(INADDR_ANY);
11. if (bind sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
12. {
13. //handle exception
14. ……
15. }
16. ……
第4 行 bind 的端口号为1234。
第7~10 行 对服务器的地址结构初始化并赋值,IP 地址是一个通配地址,由内核选择。
第11 行 调用bind 函数将描述符sockfd 与server 套接字地址结构中的协议地址绑定。
第13 行 如果调用bind 函数失败,进行异常处理。
3.1.4 listen 函数
当调用函数socket 创建一个套接字时,默认情况下它是一个主动套接字,也就是一个将调用connect 发起连接的客户端套接字。所以对于TCP 服务器,在绑定操作后,必须要调用listen 函数,将这个未连接的套接字转换成被动套接字,使它处在监听模式下,指示内核应接受发向该套接字的连接请求。在调用listen 函数后,服务器的状态从CLOSED 转换到了LISTEN 状态。
结构
listen 函数如下:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数
参数sockfd 是要设置的描述符。
backlog 参数规定了请求队列中的最大连接个数,它对队列中等待服务请求的数目进行了限制。如果一个服务请求到来时,输入队列已满,该套接字将拒绝连接请求。
返回值
该函数调用成功返回0,出错返回-1,并置errno 值。
监听套接字的两个队列
对于一个监听套接字,内核要为其维护两个队列。
- 未完成连接对列,为每个请求建立连接的SYN 分节开设一个条目,服务器正等待完成TCP 三次握手,当前的套接字处在SYN_RCVD 状态。
- 已完成连接对列,为每个已完成TCP 三次握手的客户端开设一个条目。当前的套接字的状态为ESTABLISHED。
在TCP 三次握手中监听套接字两个队列的位置如图3-3 所示。
当服务器收到客户发送的建立连接的SYN 分节, TCP 在未完成队列中创建一个新的条目,然后发送服务器的 SYN 分节给客户端,并附带对客户 SYN 的确认 ACK 。这个条目一直保存在未完成队列中,直到三次握手完毕,该条目将从未完成队列移到已完成队列的队尾。当进程调用 accept 函数时,取出已完成 队列中的队头条目返回给进程。当队列为空,表示没有等待处理的已完成连接,进程将睡眠,直到有新的条目到达时才唤醒。
backlog的大小
对于backlog 的大小,不同系统对 它有不同的定义, 曾被规定为两个队列总和的最大值。通常不要把 backlog 定义为 0 ,因为不同系统对其有不同的处理。有些系统允许有一个连接队列,有些系统不允许有连接队列。如果服务器不想接受任何客户的连接请求,最好关闭监听套接字。
若两个队列都是满的
当一个客户SYN 到达时,若两个队列都是满的, 服务器端 tcp 会 忽略此分节, 并 且不发送 RST ,这样 客户 tcp 将重发 SYN,服务器期望 在客户重发 SYN 的 这段时间里 能在队列中找到空闲条目 ,以回应客户的请求 。
如果 队列满时, 服务器 发送 RST 给客户端,客户的 connect函数将返回一个错误,而这时 客户 将 无法分辨这个错误产生的原因,是在此端口上没有服务器,还是服务器的队列满了。
实例
调用 listen 函数的代码如下:
1. #include <sys/socket.h>
2. ……
3. int sockfd;
4. int BACKLOG = 5;
5. ……
6. if (listen(sockfd, BACKLOG) == 1) {
7. //handle exception
8. ……
9. }
10. ……
第4 行 设置监听的最大连接数为 5 。
第6 行 调用 listen 函数,将 sockfd 描述符设置为监听描述符。
第7 行 当 listen 函数调用失败时的异常处理。
3.1.5 accept 函数
accept函数使服务器接受客户端的连接请求。它将完成队列中的队头条目返回给进程。并产生一个新的套接字描述符,这个新生成的描述符称之为“已连接套接字”。当已完成队列为空,则进程睡眠,直到有已完成连接到达时。
结构
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t *addrlen);
参数
listenfd 参数是由 socket 函数产生的套接字描述符,在调用 accept 函数前,已经调用 listen函数将此套接字变成了监听套接字。
client 和 addrlen 参数用来返回连接对方的套接字地址结构和对应的结构长度。这里的 addrlen 是一个值-结果参数,调用前,将 addrlen 指针所指的整数值置为client 所指的套接字地址结构的长度。函数返回时,此整数值变为内核写入此套接字地址结构中的准确字节数。
返回值
函数调用成功时,可以得到三个值。
- 一个是accept函数的返回值,已连接套接字描述符。这个与前面说的监听描述符是不同的描述符,所做的工作也完全不同。一个服务器常常只有一个监听套接字描述符,而且会一直存在,直到服务器关闭。而已连接套接字描述符是内核为每个被接受的客户都分别创建一个。当服务器完成与该客户的数据传输时,要关闭对应的已连接描述符。所以监听描述符负责接收客户的连接请求,而已连接描述符负责与对应的客户进行数据传输。
- 由client参数返回客户端的协议地址,包括IP地址和端口号等。
- 由addrlen参数返回客户端地址结构的大小。
如果对客户的协议地址和地址结构的长度不感兴趣,可以将client 和 addrlen两个参数都设为空指针。
如果函数调用失败, accept 函数将返回 1 ,并置 errno 值。
实例
调用 accept 函数的代码如下:
1. #include <sys/socket.h>
2. ……
3. int listenfd , connfd;
4. struct sockaddr_in client;
5. socklen_t addrlen;
6. addrlen = sizeof(client);
7. ……
8. connfd = accept(listenfd, (struct sockaddr *)&client, &addrlen);
9. if (connfd == -1) {
10. //handle exception
11. ……
12. }
13. ……
第 3 行 定义了两个套接字描述符,一个是监听套接字描述符,一个是已连接套接字描述符。
第 6 行 得到 client 当前的长度。
第 8 行 调用 accept 函数,接收连接请求,返回已连接套接字描述。与服务器连接的客户端的协议地址可以通过参数 client 得到, addrlen 返回内核写入 client 结构体中的准确字节数。
第 10 行 当 accept 函数调用失败时的异常处理。
3.1.6 数据传输函数
当服务器和客户端的连接建立起来后,就可以进行双向的数据传输了。服务器和客户用各自的套接字描述符进行读写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读写函数 read() 和 write() 进行接收和发送操作。
1. write和read函数
write()函数用于数据的发送。 write 函数如下:
#include <unistd.h>
int write(int sockfd , char *buf, int len);
- 参数sockfd 是套接字描述符。对于服务器是 accept() 函数返回的已连接套接字描述符。对于客户端是调用 socket() 函数返回的套接字描述符。
- 参数 buf 是指向一个用于发送信息的数据缓冲区。
- len 指明传送数据缓冲区的大小。
函数调用成功返回大于 0 的整数,也就是发送的字节数。出错则返回 -1 。
read函数用于数据的接收。 read 函数如下:
#include <unistd.h>
int read(int sockfd , char *buf, int len);
- 参数sockfd 是套接字描述符。对于服务器是 accept() 函数返回的已连接套接字描述符。对于客户端是调用 socket() 函数返回的套接字描述符。
- 参数 buf 是指向一个用于接收信息的数据缓冲区。
- len 指明接收数据缓冲区的大小。
函数调用成功返回大于 0 的整数,也就是接收的字节数。出错则返回 1 。
调用read函数的代码如下:
1. #include <unistd.h>
2. #include <sys/socket.h>
3. #define MAXDATASIZE 100
4. ……
5. int num, connfd;
6. char buf[MAXDATASIZE];
7. ……
8. if ( ( num = read(connfd, buf, MAXDATASIZE)) > 0 ) {
9. //handle data
10. ……
11.}
12.……
第3 行 定义接收信息的数据缓冲区的长度。
第8 行 接收数据,整数 num 返回接收的字节数。
第9 行 处理收到的数据。
2. send和recv函数
TCP套接字提供了 send 和 recv 函数,用来发送和接收操作。这两个函数与 write 和 read函数很相似,只是多了一个附加的参数。
send函数用于数据的发送操作。 send 函数如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void buf, size_t len, int flags);
它前三个参数与 write 相同,参数 flags 是传输控制标志,其值定义如下:
- 0:常规操作,所做的操作与write相同。
- MSG_DONTROUTE:告诉内核目的主机在直接连接的本地网络上,不需要查路由表。
- MSG_DONTWAIT:将单个I/O操作设为非阻塞模式,而不需要在套接字上打开非阻塞标志,执行I/O操作,然后关闭非阻塞标志。
- MSG_OOB:指明发送的是带外数据。
send函数调用成功返回写出的字节数,出错返回-1。
recv函数用于数据的发送操作。 recv 函数如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
它前三个参数与 read 相同,参数 flags 是传输控制标志,其值定义如下:
- 0:常规操作,所做的操作与read相同。
- MSG_DONTWAIT:将单个I/O操作设为非阻塞模式,而不需要在套接字上打开非阻塞标志,执行I/O操作,然后关闭非阻塞标志。
- MSG_OOB:指明要读的是带外数据而不是一般数据。
- MSG_PEEK:可以查看可读的数据,在接收数据后不会将这些数据丢弃。
- MSG_WAITALL:通知内核直到读到请求的数据字节数时,读操作才返回。
recv函数调用成功返回读入的字节数,出错返回-1。
调用 recv 函数的代码如下:
1. #include <unistd.h>
2. #include <sys/socket.h>
3. #define MAXDATASIZE 100
4. ……
5. int num, connfd;
6. char buf[MAXDATASIZE];
7. ……
8. if ( (num = recv(connfd, buf, MAXDATASIZE, 0)) > 0 ) {
9. //handle data
10. ……
11.}
12.……
第 8 行 接收数据,整数 num 返回接收的字节数,操作与 read 相同 。
3.1.7 close 函数
close函数用于关闭套接字,并立即返回到进程。关闭了以后的套接字描述符不能再接收和发送数据,再不能作为函数 read 或 write 的参数。 TCP 试着将已排队的待发数据发送完,然后按照正常 TCP 连接终止的操作关闭连接。 close 关闭描述符,其实只是将描述符的访问计数减 1 。如果这时描述符的访问计数仍大于 0 ,它不会引发 TCP 的终止连接操作,这个功能在并发服务器中非常重要。
close函数如下:
#include <unistd.h>
int close(int sockfd);
参数 sockfd 是要关闭的描述符。
函数调用成功返回 0 ,出错返回 -1 。
3.2 TCP 套接字编程实例
下面是一个简单的 TCP 套接字编程实例,将使用前面介绍的所有函数,涉及 TCP 套接字编程的主要方面,是一个完整的 TCP 客户 服务器 实例。套接字编程分为服务器和客户端两个部分,这里采用的是最简单的一服务器对应一客户的模式。
程序实现的功能是:
- 客户根据用户提供的IP地址,连接到相应的服务器。
- 服务器等待客户的连接,一旦连接成功,则显示客户的IP地址、端口号,并向客户端发送字符串。
- 客户接收服务器发送的信息并显示。
TCP服务器端程序
第1~7 行:所需的头文件。
第8~9 行:定义端口号和最大允许连接的数量。为了简单本例采用宏定义端口号和最大允许连接的数量,由于本例不是并发服务器,所以最大允许连接的数 量 BACKLOG 定义为 1 。
第17~22 行:调用 socket 函数,产生 TCP 套接字。如果出错打印错误信息。
第24~25 行:设置套接字选项 SO_REUSEADDR ,即地址重用选项。由于系统默认是只允许一个套接字绑定一个特定的协议地址上,并且当该套接字关闭后,系统仍不允许在该地址上绑定其他套接字。如果去掉这两行,程序运行时产生的错误信息为:“ Bind() error:Address already in use ”。
第26~29 行:初始化 server 套接字地址结构,并对地址结构中的成员赋值。当前的本地地址设为 INADDR_ANY ,接收目的地址是本机 IP 的客户端连接。这里的端口号和 IP 地址都要转换成网络字节序。
第30~35 行:将套接字和指定的协议地址绑定。
第36~40 行:将套接字描述符转换成监听描述符,等待客户的连接。
第41~46 行:接受客户连接,客户的地址信息存放在 client 地址结构中。
第47 行:显示客户的 IP 地址和端口号,通过 inet_ntoa() 函数将 IP 地址转换成可显示的ASCII 串,通过 htons() 函数将端口号转换成网络字节序。
第48 行:发送 Welcome 字符串给客户端。
第49~50 行: 关闭套接字。先关闭已连接套接字,再关闭监听套接字。
TCP客户端程序
第1~7 行:所需的头文件。
第8~9 行:定义端口号和接收缓冲器大小。这里的端口号是要与之通信的服务器的端口号。这里的缓冲区是采用静态方式分配的,也可采用动态分配方式提高内存的使用效率。
第16~20 行:检查用户的输入。如果用户输入不正确,提示用户正确的输入方式。
第21~25 行:通过用户输入的点分十进制形式的 IP 地址,获得服务器的相关地址信息。gethostbyname() 函数将在后面的章节介绍。
第26~30 行:调用 socket() 函数产生套接字描述符。
第31~ 34 行:初始化服务器的地址结构,并为地址结构的成员赋值。
第35~39 行:调用 connect() 函数连接到服务器 server 。
第40~44 行:接收服务器发过来的字符串,并保存在 buf 中。接收的真正字节数被存储在 num 中。
第45 行:以 0 标志字符串的结束。
第46 行:显示从服务器接收到的 buf 中信息。
第47 行:关闭套接字。
程序运行结果
首先运行服务器端程序。
服务器端的运行结果如下:
./tcpserver
You got a connection from cient's ip is 127.0.0.1 port is 37441
客户端的运行结果如下:
./tcpclient 127.0.0.1
server message: Welcome
3.3 服务器三种异常情况▽
下面介绍当服务器遇到以下三种异常情况时,对客户端的影响。
3.3.1 服务器主机崩溃
下面模拟服务器主机崩溃时的情况,首先在不同的主机上运行客户和服务器 ,先启动服务器,再启动客户。当连接建立后 ,从网络上断开服务器主机 ,这样同时也模拟了当客户发送数据时服务器主机不可达的情况 。
在客户端输入数据 ,由客户 TCP 当作一个数据分节发出 ,接下来 客户就阻塞于 recv 调用,等待接收数据。由于 当服务器主机崩溃时,已有的网络连接上发不出任何东西。 所以 客户 TCP 使用重传机制, 持续重传该数据分节,试图从服务器上接收一个 ACK 。 源自 Berkeley 的重传数据分节 的次数为 12 次,放弃前等待的时间约 9 分钟。当客户 TCP 最后终于放弃重传时(假设在这段时间内,服务器主机没有重新启动,或者是服务器主机没有崩溃而是 网络不可达的情况,假设当前网络仍然不可达),会返回客户进程一个错误。因为客户阻塞于 recv 的调用,它返回一个错误。 假设是由于服务器主机已崩溃,对客户的数据分节根本没有响应,则错误为 ETIMEDOUT ;但如果是某些中间路由器判定服务器主机不可达,且以一个目的地不可达的 ICMP 消息响应,则错误是 EHOSTUNREACH 或 ENETUNREACH 。
尽管最终客户还是会发现对方已崩溃或不可达, 但是要等待 9 分钟 。然而有时想尽快地检测出这种 崩溃情况,这就需要通过对 recv 的调用设置超时时间来实现。
假设客户端一直没有向服务器主机发送数据那么它就不能检测出对方是否已经崩溃。在后面的套接字选项章节中,会介绍一个套接字选项 SO_KEEPALIVE ,它可以在客户端不主动向服务器发送数据时,也能检测出服务器主机是否崩溃 。
3.3.2 服务器主机崩溃后重启
前一节介绍了当服务器崩溃后,客户端的处理过程,但是如果当服务器主机崩溃后又重新启动了,对客户端又有什么影响呢?
这种情况是首先让客户与服务器之间建立连接,然后使服务器主机崩溃并 重启 。 在 3.3.1 节中 客户 发送数据时,服务器主机仍处于崩溃状态 ,而本节 将在 客户 发送数据前重新启动服务器主机。模拟这种情况的最简单方法是:建立连接,从网络上断开服务器,关闭服务器主机,然后重启服务主机,重新连接服务器主机入网。
前一节已经了解,如果客户在服务器主机崩溃时不主动发数据给服务器客户是不会知道服务器已崩溃的(假设没有使用套接字选项 SO_KEEPALIVE )。 如果服务器主机崩溃后重启 ,它的 TCP 将丢失崩溃前的所有连接信息,所以服务器 的 TCP 对接收的所有客户数据均以 RST 响应。 在客户端, 当 RST 到达时,客户 当前正阻塞于 recv 的 调用,导致它返回ECONNRESET 错误。
实际应用中,有些客户端需要实时的检测服务器主机的状态,是否已经崩溃 ,当服务器主机崩溃时,即使客户不主动发送数据也 希望能及时知道, 这就需要有其他技术支持如前一节提到的套接口选项 SO_KEEPALIVE 等 。
3.3.3 服务器主机关闭
前面两节讨论了服务器主机崩溃和崩溃后重启两种情况,现在,考虑一下当服务器进程正在运行时,操作员关闭该服务器主机将会发生什么。
当系统关机时,一般是由 init 进程给所有进程发信号 SIGTERM (可以捕获此信号),等待一段固定时间(常常是 5 秒 ~ 20 秒),然后给还在运行的所有进程发信号 SIGKILI (此信号不能捕获)。这就给了所有运行进程一小段时间来清除和终止。如果不捕获信号 SIGTERM 和终止,服务器将由信号 SIGKILL 终止。
当进程终止时,所有打开的描述字都关闭,这引起了一个 FIN 发送给客户,客户 TCP 应以 ACK 响应。但是当前客户正堵塞于 fgets 调用,等待从终端上获取数据。如果这时在客户的终端再键入一行数据,客户端会调用 send ,将数据发送给服务器,这种情况在 TCP 中是允许的,因为客户 TCP 接收到 FIN 只表示服务器进程巳关闭了连接的服务器一端,它不再发送任何数据,但还可以接收数据。当服务器 TCP 接收到来自客户的数据时,由于先前打开那个套接字的进程已终止所以 服务器会 以 RST 响应。
客户进程在调用 send 后立即调用 recv ,这时会接收到服务器 TCP 先前发送的 FIN,recv 立即返回 0 (文件结束符),所以客户进程将看不到 RST 。由于客户预期接收的不是文件结束符,所以会以错误信息“ server terminated prematurely(服务器过早终止)”退出。 这时客户端会关闭所有打开的描绘符。
客户工作时有两个描述字——套接字和标准输入, 由于当 FIN 到达套接字时,客户正阻塞于标准输入 fgets 调用,无法立即接收,使得它不能正常处理服务器的关闭。所以对于客户,它不能只阻塞于套接字中的任何一个,这就需要在客户上使用后面将遇到的两个函数 select 或 poll ,使得客户在服务器进程开始终止时就检测到。