我们是调用close 或closesocket 函数只是单方面断开连接。
————————————————————————————————————————————————
基于TCP 的半关闭
TCP中的断开连接过程比建立连接过程更重要,因为连接过程中一般不会出现大的变数,但断开过程有可能发生预想不到的情况,因此应准确掌控。只有掌握了下面要讲解的半关闭(Half-close), 才能明确断开过程。
单方面断开连接带来的问题
Linux 的close函数和Windows的closesocket函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。因此, 在某些情况下,通信一方调用close或closesocket函数断开连接就显得不太优雅,如图所示。
上图描述的是2 台主机正在进行双向通信。主机A发送完最后的数据后,调用close函数断开了连接,之后主机A无法再接收主机B传输的数据。实际上,是完全无法调用与接收数据相关的函数。最终,由主机B传输的、主机A必须接收的数据也销毁了。
为了解决这类问题,“只关闭一部分数据交换中使用的流" ( Half-close ) 的方法应运而生。断开一部分连接是指 “可以传输数据但无法接收,或可以接收数据但无法传输”。顾名思义就是只关闭流的一半。
————————————————————————————————————————————————
套接字和流( Stream )
两台主机通过套接字建立连接后进入可交换数据的状态,又称“流形成的状态"。也就是把建立套接字后可交换数据的状态看作一种流。
此处的流可以比作水流。水朝着一个方向流动,同样,在套接字的流中,数据也只能向一个方向移动。因此,为了进行双向通信,需要如图所示的2个流。
一旦两台主机间建立了套接字连接,每个主机就会拥有单独的输入流和输出流。当然,其中一个主机的输入流与另一主机的输出流相连,而输出流则与另一主机的输入流相连。另外,“优雅地断开连接方式”只断开其中1 个流,而非同时断开两个流。Linux的close和Windows的closesocket函数将同时断开这两个流,因此与“优雅” 二字还有一段距离。
————————————————————————————————————————————————
针对优雅断开的 shutdown 函数
#include <sys/socket.h>
int shutdown(int sock, int howto);
//成功时返回0, 失败时返回-1。
#sock 需要断开的套接字文件描述符。
#howto 传递断开方式信息。
调用上述函数时, 第二个参数决定断开连接的方式,其可能值如下所示。
- SHUT_RD: 断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用输入相关函数。
- SHUT_WR: 中断输出流,也就无法传输数据。但如果输出缓冲还留有未传输的数据,则将传递至目标主机。
- SHUT RDWR: 同时中断I/O流。这相当于分2次调用shutdown ,其中一次以SHUT_RD为参数,另一次以SHUT_WR为参数。
————————————————————————————————————————————————
为何需要半关闭
close与shutdown的区别主要表现在:
close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。
而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。
调用close函数的同时关闭1/0流,这样也会向对方发送EOF 。但此时无法再接收对方传输的数据。换言之,若调用close函数关闭流,就无法接收客户端最后发送的字符串"Thank you " 。这时需要调用shutdown函数,只关闭服务器的输出流( 半关闭)。这样既可以发送EOF, 同时又保留了输入流,可以接收对方数据。
实例代码:
服务器:
#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 = NULL;
char buf[BUF_SIZE] = {0};
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("message.txt", "rb"); //从这个文件读取信息
if (fp == NULL) {
printf("error\n");
exit(-1);
}
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);
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);
}
客服端:
#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;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
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) {
printf("cli:%s\n", (char *)buf);
}
puts("Received file data");
write(sd, "Thank you", 10);//在服务器半关闭后,再回复一个信息
close(sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
服务器:
$ ./ser 9190
Message from client: Thank you
客户端:
$ ./cli 192.168.1.141 9190
cli:aaaaaa
Received file data