我们知道,TCP是一个双全工协议,从协议层面,我们了解到,如果client发了FIN包给服务端,在收到ACK之后,状态切换成FIN_WAIT2。此时,从协议层面,只是关闭了client->server这个方向的数据传输。而server->client端,则可以继续往client端发数据。可以参考《tcp/ip详解》卷1,第18.5章节(TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力)。既然是这样,我们就自己亲测一把,看是否如理论所言。
//tcpserver.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void sigroutine(int no) {
cout << "sigroutine call no=" << no << endl;;
}
int main(int argc, char *argv[])
{
signal(SIGPIPE, sigroutine);
//创建套接字
int sk = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[1]));
server.sin_addr.s_addr = htonl(INADDR_ANY);
//端口绑定
bind(sk, (struct sockaddr*)&server, sizeof(server));
//监听
listen(sk, 5);
struct sockaddr_in client;
bzero(&client, sizeof(client));
socklen_t len = sizeof(client);
//接受连接请求
int talk = accept(sk, (struct sockaddr*)&client, &len);
//发送数据
send(talk, "Hello", strlen("Hello") + 1, 0);
//接收数据
char buff[1024] = {'\0'};
recv(talk, buff, sizeof(buff), 0);
cout << buff << endl;
cout << "sleep 3s" << endl;
sleep(3);
cout << "now client has close, try to send msg to client" << endl;
send(talk, "Hello", strlen("Hello") + 1, 0);
//关闭套接字
close(talk);
close(sk);
return 0;
//tcpclient.cpp
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
//创建套接字
int sk = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//连接服务器
connect(sk, (struct sockaddr*)&server, sizeof(server));
char buff[1024] = {'\0'};
//接收数据
recv(sk, buff, sizeof(buff), 0);
cout << buff << endl;
//发送数据
send(sk, "I am hahaya", strlen("I am hahaya") + 1, 0);
//关闭套接字
close(sk);
cout << "sleep 10s, then close" << endl;
sleep(10);
/*
shutdown(sk, SHUT_WR);
memset(buff, 0, sizeof(buff));
int ret = recv(sk, buff, sizeof(buff), 0);
cout << "ret = " << ret << ":" << buff << endl;
cout << "I have close, but I don't exit process!" << endl;
sleep(100);
*/
return 0;
}
#makefile
TARGET:=server client
all:$(TARGET)
$(TARGET):%:%.o
>---g++ -g -o $@ $<
%.o:%.cpp
>---g++ -g -c $<
clean:
>---rm -f *.o $(TARGET)
将代码放到同一个目录下,make之后,会出现两个可执行文件:server client
执行步骤:
1、启动server,不妨监听12354端口,命令:
./server 12354
2、启动tcpdump抓包看交互细节。tcpdump -i any port 12354 -nn
3、启动client,假设与server同机,命令:
./client 127.0.0.1 12354
看到tcpdump运行结果:
在客户端执行close之后,服务端尝试给客户端再发数据之后,客户端认为是一个非法包,直接回了一个RST。而此时,连接从FIN_WAIT2状态在收到非法包之后,回复了RST包之后,client状态从FIN_WAIT2直接重置成CLOSE(CLOSE是假想的一个状态,在机器上是看到没有这个连接了),如下图所示:
RST包产生之后,通过netstat已经看不到这个连接了。
这里可能与linux的实现细节相关,具体可以参考Beej’s networking guide里面提到的,我这里摘抄过来。
Whew! You’ve been send()ing and recv()ing data all day long, and you’ve had it. You’re ready to close the connection on your socket descriptor. This is easy. You can just use the regular Unix file descriptor close() function:
close(sockfd);
This will prevent any more reads and writes to the socket. Anyone attempting to read or write the socket on the remote end will receive an error.
Just in case you want a little more control over how the socket closes, you can use the shutdown() function. It allows you to cut off communication in a certain direction, or both ways (just like close() does.) Synopsis:
简要说明,调用了close,表示从OS层面,告诉OS,我这个socket即不发,也不收了。如果需要更加细节的控制,可以使用shutdown函数替换之。
如果对于socket,后续不需要收且不需要发数据的情况下,即直接调用close就可以。而对于对方已经调用close操作的服务而言,再往其发送数据,会收到RST包。前面已验证。
如果关闭了连接之后,还需要细节的控制,则可以选择使用shutdown。另外,shutdown中有两个控制参数。SHUT_RD表示不再收数据,SHUT_WR表示不再写数据。前面的示例中,如果使用shutdown,无论使用什么参数(SHUT_RD或者SHUT_WR或者SHUT_RDWR),服务端再往client发数据的话,能正常发数据,不会收到RST包。
如果设置参数为SHUT_RD,则应用层无法收到服务端后续发过来的数据。如果设置为SHUT_RDWR,则客户端通过API是能够正常收到服务端发过来的数据。
接着上面的场景继续往后讨论,如果客户端调用了close之后,服务端没有close,而是继续发一次数据,则客户端会返回一个RST包。此时,连接已经不存在了。而服务端再发消息的话,进程会收到一个SIGPIPE信号。(可能不能版本的linux的实现不同,我的测试机器上的linux上,是默认忽略了这个信号,需要显示安装信号处理函数才能捕获)。
SIGPIPE信号,可以理解为”管道破裂“的意思。当你尝试往一个对端已经关闭读操作的管道中写数据中,因为永远不会有进程读到数据,所以从OS的角度来说,有必要提醒你,这是一个不会有结果的操作,所以产生了SIGPIPE信号来提醒你。