客户端主动发起连接的中断,将自己到服务器端的数据流方向关闭,此时,客户端不再往服务器端写入数据,服务器端读完客户端数据后就不会再有新的报文到达。
但这并不意味着,TCP 连接已经完全关闭,很有可能的是,服务器端正在对客户端的最后报文进行处理,这个套接字的状态此时是“半关闭”的。
当完成这些操作之后,服务器端把结果通过套接字写给客户端。最后,服务器端才有条不紊地关闭剩下的半个连接,结束这一段 TCP 连接的使命。
close函数
int close(int sockfd)这个函数会对套接字引用计数减一,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流。
close 函数具体是如何关闭两个方向的数据流呢?
- 在输入方向:系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
- 在输出方向:系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。
注:如果对端没有检测到套接字已关闭,还继续发送报文,就会收到一个 RST 报文。
shutdown函数
int shutdown(int sockfd, int howto)howto 是这个函数的设置选项,它的设置有三个主要选项:
- SHUT_RD(0):关闭连接的“读”这个方向,对该套接字进行读操作直接返回 EOF。从数据角度来看,套接字上接收缓冲区已有的数据将被丢弃,如果再有新的数据流到达,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。
- SHUT_WR(1):关闭连接的“写”这个方向,这就是常被称为“半关闭”的连接。此时,不管套接字引用计数的值是多少,都会直接关闭连接的写方向。套接字上发送缓冲区已有的数据将被立即发送出去,并发送一个 FIN 报文给对端。应用程序如果对该套接字进行写操作会报错。
- SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。
使用 SHUT_RDWR 来调用 shutdown 不是和 close 基本一样吗? 都是关闭连接的读和写两个方向。
- 第一个差别:close 会关闭连接,并释放所有连接对应的资源,而 shutdown 并不会释放掉套接字和所有的资源。
- 第二个差别:close 存在引用计数的概念,并不一定导致该套接字不可用;shutdown 则不管引用计数,直接使得该套接字不可用,如果有别的进程企图使用该套接字,将会受到影响。
- 第三个差别:close 的引用计数导致不一定会发出 FIN 结束报文,而 shutdown 则总是会发出 FIN 结束报文,这在我们打算关闭连接通知对端的时候,是非常重要的。
体会 close 和 shutdown 的差别
客户端:
如果用户输入了“close”,则会调用 close 函数关闭连接,休眠一段时间,等待服务器端处理后退出;
如果用户输入了“shutdown”,调用 shutdown 函数关闭连接的写方向,注意我们不会直接退出,而是会继续等待服务器端的应答,直到服务器端完成自己的操作,在另一个方向上完成关闭。
# include "lib/common.h" # define MAXLINE 4096 int main(int argc, char **argv) { if (argc != 2) { error(1, 0, "usage: graceclient <IPaddress>"); } int socket_fd; socket_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &server_addr.sin_addr); socklen_t server_len = sizeof(server_addr); int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len); if (connect_rt < 0) { error(1, errno, "connect failed "); } char send_line[MAXLINE], recv_line[MAXLINE + 1]; int n; fd_set readmask; fd_set allreads; FD_ZERO(&allreads); FD_SET(0, &allreads); FD_SET(socket_fd, &allreads); for (;;) { readmask = allreads; int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL); if (rc <= 0) error(1, errno, "select failed"); if (FD_ISSET(socket_fd, &readmask)) { n = read(socket_fd, recv_line, MAXLINE); if (n < 0) { error(1, errno, "read error"); } else if (n == 0) { error(1, 0, "server terminated \n"); } recv_line[n] = 0; fputs(recv_line, stdout); fputs("\n", stdout); } if (FD_ISSET(0, &readmask)) { if (fgets(send_line, MAXLINE, stdin) != NULL) { if (strncmp(send_line, "shutdown", 8) == 0) { FD_CLR(0, &allreads); if (shutdown(socket_fd, 1)) { error(1, errno, "shutdown failed"); } } else if (strncmp(send_line, "close", 5) == 0) { FD_CLR(0, &allreads); if (close(socket_fd)) { error(1, errno, "close failed"); } sleep(6); exit(0); } else { int i = strlen(send_line); if (send_line[i - 1] == '\n') { send_line[i - 1] = 0; } printf("now sending %s\n", send_line); size_t rt = write(socket_fd, send_line, strlen(send_line)); if (rt < 0) { error(1, errno, "write failed "); } printf("send bytes: %zu \n", rt); } } } } }
第一部分是套接字的创建和 select 初始化工作:
9-10 行: 创建了一个 TCP 套接字;
12-16 行:设置了连接的目标服务器 IPv4 地址,绑定到了指定的 IP 和端口;
18-22 行:使用创建的套接字,向目标 IPv4 地址发起连接请求;
30-32 行:为使用 select 做准备,初始化描述字集合;
第二部分是程序的主体部分,从 33-80 行, 使用 select 多路复用观测 在连接套接字和标准输入上的 I/O 事件,其中:
第二部分是程序的主体部分,从 33-80 行, 使用 select 多路复用观测 在连接套接字和标准输入上的 I/O 事件,其中:
38-48 行:当连接套接字上有数据可读,将数据读入到程序缓冲区中。
40-41 行:如果有异常则报错退出;
42-43 行:如果读到服务器端发送的 EOF 则正常退出。
49-77 行:当标准输入上有数据可读,读入后进行判断。如果输入的是“shutdown”,则关闭标准输入的 I/O 事件感知,并调用 shutdown 函数关闭写方向;如果输入的是“close”,则调用 close 函数关闭连接;
64-74 行:处理正常的输入,将回车符截掉,调用 write 函数,通过套接字将数据发送给服务器端;
服务端:
服务器端程序稍微简单一点,连接建立之后,打印出接收的字节,并重新格式化后,发送给客户端。(有一点需要注意,那就是对 SIGPIPE 这个信号的处理)
#include "lib/common.h"
static int count;
static void sig_int(int signo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
}
int main(int argc, char **argv) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
}
int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
error(1, errno, "listen failed ");
}
signal(SIGINT, sig_int);
signal(SIGPIPE, SIG_DFL);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
}
char message[MAXLINE];
count = 0;
for (;;) {
int n = read(connfd, message, MAXLINE);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed \n");
}
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
count++;
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
sleep(5);
int write_nc = send(connfd, send_line, strlen(send_line), 0);
printf("send bytes: %zu \n", write_nc);
if (write_nc < 0) {
error(1, errno, "error write");
}
}
}
第一部分是套接字和连接创建过程:
11-12 行:创建了一个 TCP 套接字;
14-18 行:设置了本地服务器 IPv4 地址,绑定到了 ANY 地址和指定的端口;
20-40 行:使用创建的套接字,依次执行 bind、listen 和 accept 操作,完成连接建立;
第二部分是程序的主体,通过 read 函数获取客户端传送来的数据流,并回送给客户端:
第二部分是程序的主体,通过 read 函数获取客户端传送来的数据流,并回送给客户端:
51-52 行:显示收到的字符串;
56 行: 对原字符串进行重新格式化,之后调用 send 函数将数据发送给客户端;(在发送之前,让服务器端程序休眠了 5 秒,以模拟服务器端处理的时间)
(1) 启动服务器,再启动客户端,依次在标准输入上输入 data1、data2 和 close,观察一段时间后看到:
$./graceclient 127.0.0.1 data1 now sending data1 send bytes:5 data2 now sending data2 send bytes:5 close
$./graceserver received 5 bytes: data1 send bytes: 9 received 5 bytes: data2 send bytes: 9 client closed客户端依次发送了 data1 和 data2,服务器端也正常接收到 data1 和 data2。在客户端 close 掉整个连接之后,服务器端接收到 SIGPIPE 信号,直接退出。客户端并没有收到服务器端的应答数据。
因为客户端调用 close 函数关闭了整个连接,当服务器端发送的“Hi, data1”分组到底时,客户端给回送一个 RST 分组;服务器端再次尝试发送“Hi, data2”第二个应答分组时,系统内核通知 SIGPIPE 信号。这是因为,在 RST 的套接字进行写操作,会直接触发 SIGPIPE 信号。
像这样注册一个信号处理函数,对 SIGPIPE 信号进行处理,避免程序莫名退出:
static void sig_pipe(int signo) { printf("\nreceived %d datagrams\n", count); exit(0); } signal(SIGPIPE, sig_pipe);
(2) 启动服务器,再启动客户端,依次在标准输入上输入 data1、data2 和 shutdown 函数,观察一段时间后看到:
$./graceclient 127.0.0.1 data1 now sending data1 send bytes:5 data2 now sending data2 send bytes:5 shutdown Hi, data1 Hi,data2 server terminated
$./graceserver received 5 bytes: data1 send bytes: 9 received 5 bytes: data2 send bytes: 9 client closed因为客户端调用 shutdown 函数只是关闭连接的一个方向,服务器端到客户端的这个方向 还可以继续进行数据的发送和接收,所以“Hi,data1”和“Hi,data2”都可以正常传送;当服务器端读到 EOF 时,立即向客户端发送了 FIN 报文,客户端在 read 函数中感知了 EOF,也进行了正常退出。
总结
使用 close 函数关闭连接有两个需要明确的地方
- close 函数只是把套接字引用计数减 1,未必会立即关闭连接;
- close 函数如果在套接字引用计数达到 0 时,立即终止读和写两个方向的数据传送。
基于这两点,在期望关闭连接其中一个方向时,应该使用 shutdown 函数。
思考题
1. 上面服务器端程序中,直接调用exit(0)完成了 FIN 报文的发送,这是为什么呢?为什么不调用 close 函数或 shutdown 函数呢?
因为在调用exit之后进程会退出,而进程相关的所有的资源,文件,内存,信号等内核分配的资源都会被释放,在linux中,一切皆文件,本身socket就是一种文件类型。(内核会为每一个打开的文件创建file结构 并维护指向该结构的引用计数,每一个进程结构中都会 维护本进程打开的文件数组,数组下标就是fd,内容就指向上面的file结构,close本身就可以用来操作所有的文件,做的事就是,删除本进程打开的文件数组中指定的fd项,并把指向的file结构中的引用计数减一,等引用计数为0的时候,就会调用内部包含的文件操作close,针对于socket,它内部的实现应该就是调用shutdown,只是参数是关闭读写端,从而比较粗暴的关闭连接。)
2. 关于信号量处理,上面的程序中,使用的是SIG_DFL默认处理,你知道默认处理和自定义函数处理的区别吗?
信号的处理有三种,默认处理,忽略处理,自定义处理。默认处理就是采用系统自定义的操作,大部分信号的默认处理都是杀死进程,忽略处理就是当做什么都没有发生。