文章目录
5 如何优雅地断开套接字连接
5.1 基于TCP的半关闭
Linux 的 close 函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。因此在某些情况下,通信一方调用 close 函数,显得不太优雅。例如,主机 A 发送完最后的数据后,调用 close 函数断开最后的连接,之后主机 A 无法再接受主机 B 传输的数据。最终,由主机 B 传输的、主机 A 必须要接受的数据也销毁了。
为了解决这类问题,“只关闭一部分数据交换中使用的流的方法应运而生”。断开一部分连接是指,可以传输数据但是无法接收,或可以接受数据但无法传输。
5.1.1 套接字和流(Stream)
两台主机通过套接字建立连接后进入可交换数据的状态,又称流形成的状态。
一旦两台主机之间建立了套接字连接,每个主机就会拥有单独的输入流和输出流。其中一个主机的输入流与另一个主机的输出流相连,而输出流则与另一个主机的输入流相连。断开连接方式只断开其中 1 个流,而非同时断开两个流。Linux 的 close 函数和 Windows 的 closesocket 函数将同时断开这两个流,这样的方式确实不够优雅。
5.1.2 针对优雅断开的shutdown函数
在 Socket 编程中,shutdown 函数的作用已在相关章节中进行了说明,但之前的示例中均使用close 来断开连接。为了更清晰地理解其用途,这里再次给出shutdown函数的定义。
int shutdown(int sockfd,int how);
/**
* 关闭socket的一部分或全部连接
* sockfd:Socket描述符。
* how:指定关闭的类型,可以取值为:
* SHUT_RD:断开输入流,无法接收数据,即使输入缓冲收到数据也会抹去,而且无法调用相关函数
* SHUT_WR:断开输出流,无法传输数据。如果输出缓冲中还有未传输的数据,则将传递给目标主机
* SHUT_RDWR:同时输入流和输出流,当于分 2 次调用 shutdown ,其中一次以SHUT_RD为参数,
* 另一次以 SHUT_WR为参数
* return:成功返回0,失败返回-1,同时设置errno
*/
5.1.3 为何需要半关闭
(1)为什么需要半关闭?是否只要留出足够长的时间,保证完成数据交换即可?只要不急于断开连接,好像也没必要使用半关闭?(客户端断开连接前还有数据要传输的情况)
我们假设这样一个场景:“一旦客户端连接到服务器端,服务器端将约定的文件传给客户端,客户端收到后发送字符串 “Thankyou' 给服务器端。”
传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序阻塞。
(2)是否可以让服务器和客户端约定一个代表文件尾的字符?
这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束,客户端通过函数返回值接收 EOF ,这样可以避免与文件内容冲突。
(3)服务器何时传递EOF?
断开输出流时向主机传输 EOF。调用 close 函数的会关闭 I/O 流,这样也会向对方发送 EOF ,但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串(Thank you)。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。
5.1.4 基于半关闭的文件传输程序
上述文件传输服务器端和客户端的数据流可以整理如图
(1)服务器端 file_server.c
#define BUF_SIZE 1024
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
return -1; \
} \
int main(int argc, char const *argv[])
{
struct sockaddr_in serv_addr,clnt_addr;
char* buf = malloc(sizeof(char)*BUF_SIZE);
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 2) {
printf("Usage:%s <port>\n",argv[0]);
exit(1);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);
int serv_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",serv_sock);
int temp=bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
handle_error("bind",temp);
temp= listen(serv_sock,128);
handle_error("listen",temp);
socklen_t clnt_len = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock,(struct sockaddr *)&clnt_addr,&clnt_len);
handle_error("accept",clnt_sock);
int fd = open("file_server.c",O_RDWR);
if(fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
while (1)
{
//将file_server.c里的数据读入缓冲区buf
int read_cnt = read(fd,buf,BUF_SIZE);
//读到文件末尾
if(read_cnt < BUF_SIZE) {
send(clnt_sock,buf,read_cnt,0);
break;
}
send(clnt_sock,buf,BUF_SIZE,0);
}
//关闭写端
shutdown(clnt_sock,SHUT_WR);
recv(clnt_sock,buf,BUF_SIZE,0);
printf("Message from client:%s\n",buf);
close(fd);
close(serv_sock);
free(buf);
return 0;
}
(2)客户端 file_clint.c
#define BUF_SIZE 1024
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
return -1; \
} \
int main(int argc, char const *argv[])
{
// FILE* file;
struct sockaddr_in serv_addr,clnt_addr;
char* buf = malloc(sizeof(char)*BUF_SIZE);
char message[] = "Thank you";
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 3) {
printf("Usage:%s <IP> <port>\n",argv[0]);
exit(1);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&serv_addr.sin_addr);
int clnt_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",clnt_sock);
int temp=connect(clnt_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
handle_error("connect",temp);
printf("Connected......\n");
int read_cnt;
int fd = open("received_file.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
handle_error("open", fd);
while ((read_cnt = recv(clnt_sock,buf,BUF_SIZE,0)) != 0) {
write(fd,buf,read_cnt);
}
printf("Received file data\n");
send(clnt_sock,message,sizeof(message),0);
close(fd);
close(clnt_sock);
free(buf);
return 0;
}
(3)测试结果