七、TCP半关闭
套接字开启过程很简单,基本不会出现太大问题;但是在关闭套接字过程当中会出现各种意想不到的情况发生。因此需要单独说一下TCP套接字的关闭。
7.1 单方面断开连接的问题
在Linux系统当中调用close()关闭套接字意味着完全断开连接,如下图所示
在主机A给B发完信息之后,随即调用close()进行断开连接,此时主机B收到A的信息之后准备发送数据给A,但此时A的断开连接导致双向通信完全断开,导致A无法收到B发送的信息因此被销毁了,我们不希望出现这种情况,理想的情况应该是,A断开的只是A---->B的单向通路,但没断开B—>A的通路,比较形象一点的解释就是:主机A与主机B之间的通信建立在两条信息流之上,一条从A流向B,另外一条从B流向A;A如果关闭应该关闭的是流向B的信息流,而不应该关闭从B流过来的信息(可能需要接收B的回馈信息)。因此我们需要制定特定规则来解决这个问题。在此之前,我们需要弄清楚流的概念。
7.2 针对优雅断开的shutdown函数
进行半关闭操作的第一个函数
#include<sys/socket.h>
int shutdown(int sock,int howto);
//socket:需要关闭的套接字。
//howto:传递断开方式的信息。这个值决定断开连接方法,一般采用宏进行控制且主要有以下宏定义可以使用:
// 1. SHUT_RD: 断开输入流
// 2. SHUT_WR: 断开输出流
// 3. SHUT_RDWR:同时断开I/O流
7.3 为什么需要半关闭
这个问题通俗一点的解释就是:半关闭是否是解决问题的最好的方法?那我们现在看看有什么其他的可靠方案,注意我们讨论的是如何优雅的断开套接字,而非仅仅关闭了事。
-
假如A向B发送完数据之后,等待一段时间然后关闭是否可行?这样也能接收B发过来的信息
答:可行,但是没必要。有几个问题,比如:等多久?假如此时B发生了阻塞,那么A是否也要一直等下去?亦或是A觉得B发送的信息没有什么价值只是一些问候性回馈,那么此时就没必要等,直接关闭。
-
在A发送给B的信息最后传递一个约定俗成的断开连接字符例如:EOF,告诉B我要断开连接了。
可行,但是A即使告诉了B断开连接,A调用close()函数之后,依然接收不到来自B的信息,因此这种方案不符合我们解决问题的初衷(优雅)。
7.4 基于半关闭的文件传输程序
我们写一个服务器\客户端接收文件的程序来实际体验以下这个半关闭。服务端向客户端传递server.c源文件,客户端接收源文件并写入receive.dat文件当中。
//server.c
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char* argv[])
{
int serv_sd,clnt_sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr,clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2)
{
printf("Usage: %s,<port>\n",argv[0]);
exit(1);
}
//读取源文件
fp=fopen("file_server.c","rb");
serv_sd=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
bind(serv_sd,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
listen(serv_sd,5); //监听,同时可以处理5个客户端请求
clnt_adr_sz=sizeof(clnt_adr);
clnt_sd=accept(serv_sd,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
//循环读取并写入发送缓冲区当中
while(1)
{
read_cnt=fread((void *)buf,1,BUF_SIZE,fp);
if(read_cnt<BUF_SIZE)
{
write(clnt_sd,buf,read_cnt);
break;
}
write(clnt_sd,buf,BUF_SIZE);
}
//读取完毕,半关闭套接字(注意关闭的套接字描述符),但不关闭接收缓冲区。
shutdown(clnt_sd,SHUT_WR); //关闭向客户端套接字进行写端,也就是关闭接收数据端
//读取接收缓冲区内容,(此处证明我们采用的是半关闭,否则不能读取),此时如果客户端没有信息发过来的话,程序就在此处阻塞!
read(clnt_sd,buf,BUF_SIZE);
printf("Message from client:%s \n",buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
//client.c
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
int sd;
FILE *fp;
int read_cnt=0;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!=3)
{
printf("Usage:%s ,<IP> <port> \n",argv[0]);
exit(1);
}
//新建一个文件用于存放接收过来的数据。
fp=fopen("receive.dat","wb");
sd=socket(PF_INET,SOCK_STREAM,0);
memset((&serv_adr),0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
connect(sd,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
//循环读取数据,直至缓冲区为空。
while((read_cnt=read(sd,buf,BUF_SIZE))!=0)
{
fwrite((void*)buf,1,read_cnt,fp);
}
puts("Receive file data.....");
//发送问候信息。此时服务端继续运行,并全关闭,客户端直接全关闭。
write(sd,"Thank you",10);
fclose(fp);
close(sd);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
编译运行
gcc server.c -o server
gcc client.c -o client
-----------------------------
./server 9091
./client 127.0.0.1 9091
结果
client:Receive file data.....
server:Message from client:Thank you
此时工程文件内的Receive.dat文件的内容与server.c一样。