如果你搜过TCP的四次挥手相关信息,你一定会看到类似这样的话:“主动方选择关闭连接后,被动方马上回ACK,此时可以继续传输数据,传完后再发送FIN”
(图片来源:CS-Notes/计算机网络 - 传输层.md at master · CyC2018/CS-Notes · GitHub)
为了验证CLOSE-WAIT状态是否真的能发数据,我先是写了这样一段代码:
代码如下,大致效果是:启动一个TCP服务端和客户端,服务端accept连接后关闭,此时理论上应该进入FIN-WAIT-1阶段。然后客户端不关闭,尝试在CLOSE-WAIT阶段发送数据,看看服务端能否在FIN-WAIT2阶段接收到数据。
// main.go
package main
import (
"net"
"time"
"github.com/sirupsen/logrus"
)
const (
serverAddr = "127.0.0.1:6666"
)
func serverMain() {
listener, err := net.Listen("tcp", serverAddr)
if err != nil {
panic(err)
}
conn, err := listener.Accept()
if err != nil {
panic(err)
}
go func() {
time.Sleep(time.Second)
conn.Close()
}()
go func() {
for {
buf := make([]byte, 100)
n, err := conn.Read(buf)
if err != nil {
logrus.Errorf("cannot read %v", err)
return
}
logrus.Infof("read:%v", buf[:n])
}
}()
time.Sleep(time.Minute)
}
func clientMain() {
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
panic(err)
}
for {
time.Sleep(time.Millisecond * 300)
_, err := conn.Write([]byte("hello world"))
if err != nil {
logrus.Errorf("error write:%v", err)
return
}
}
}
func main() {
go serverMain()
time.Sleep(time.Second)
go clientMain()
time.Sleep(time.Minute * 3)
}
运行后,在关闭后发现不能实现想要的效果
此时的抓包结果显示如下
10. [FIN,ACK] 服务端发送FIN,进入FIN-WAIT1
11. [ACK] 客户端回ACK,服务端此时进入FIN-WAIT2,客户端进入CLOSE-WAIT
12. 客户端发送数据
13. 服务端回了个RST,表示连接异常
为什么会和协议上说的不一样呢?经过一番艰难的搜索,发现关闭连接的第二种方式:shutdown
下面用C语言来展示一下用shutdown的情况:
注意close_conn函数里的shutdown(connfd,SHUT_WR); 它表示关闭写方向的连接
然后就是client_read函数里的if (n==0) 判断,对面无论是close还是shutdown,都会发送FIN,此时可以通过n==0来判断FIN,但是无法知道对面是close还是shutdown,如果是close,此时就不应该继续往这条连接写东西了。
// main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
void* close_conn(void* conn) {
int connfd = *((int*)conn);
sleep(1);
// close(connfd);
shutdown(connfd,SHUT_WR);
printf("close conn\n");
return NULL;
}
void* server_main() {
int listenfd,connfd;
struct sockaddr_in servaddr;
char buf[100];
pthread_t conn_closer;
printf("server start\n");
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
printf("socket error %s\n",strerror(errno));
return NULL;
}
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
servaddr.sin_port = htons(6666);
if ( bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0) {
printf("bind error %s\n",strerror(errno));
return NULL;
}
if ( listen(listenfd,10) < 0 ) {
printf("listen error %s\n",strerror(errno));
return NULL;
}
if ( (connfd = accept(listenfd,NULL,NULL)) < 0 ){
printf("accept error %s\n",strerror(errno));
return NULL;
}
pthread_create(&conn_closer,NULL,close_conn,&connfd);
while(1) {
int n = recv(connfd,buf,sizeof(buf),0);
if (n <0) {
printf("cannot read %s\n",strerror(errno));
return NULL;
}
buf[n] = 0;
printf("read:%s\n",buf);
}
return NULL;
}
void* client_read(void* fd) {
int connfd = *((int*)fd);
char buf[100];
printf("start client read\n");
while(1) {
int n = recv(connfd,buf,sizeof(buf),0);
if (n==0) {
printf("maybe shutdown?\n");
return NULL;
}
if (n<0) {
printf("wtf server:%s",strerror(errno));
return NULL;
}
}
return NULL;
}
void* client_main() {
int sockfd,connfd;
struct sockaddr_in servaddr;
char buf[100];
pthread_t read_loop;
printf("client start\n");
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
printf("socket error %s\n",strerror(errno));
return NULL;
}
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
servaddr.sin_port = htons(6666);
if ( connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0){
printf("connect %s\n",strerror(errno));
return NULL;
}
pthread_create(&read_loop,NULL,client_read,&sockfd);
const char *msg = "hello world";
while(1) {
usleep(300 * 1000);
int n = send(sockfd,msg,strlen(msg),0);
if (n<0){
printf("error write: %s\n",strerror(errno));
return NULL;
}
}
}
int main() {
pthread_t server,client;
pthread_create(&server,NULL,server_main,NULL);
sleep(1);
pthread_create(&client,NULL,client_main,NULL);
printf("wating\n");
sleep(5);
printf("wati done\n");
return 0;
}
结果如下,在FIN后确实还可以继续发数据
上面的代码其实是从go代码翻译过去的,如果是go语言,应该如何达到shutdown的效果呢?
net.Conn接口并没有shutdown,可能是因为这个接口面向的协议包括了UDP在内的其他协议。
而要使用shutdown,则需要用断言或者别的手段转为net.TCPConn类才能使用CloseWrite来实现。(CloseWrite的实现就是fd.shutdown(syscall.SHUT_WR))
func serverMain() {
listener, err := net.Listen("tcp", serverAddr)
if err != nil {
panic(err)
}
conn, err := listener.Accept()
if err != nil {
panic(err)
}
go func() {
time.Sleep(time.Second)
// conn.Close()
conn.(*net.TCPConn).CloseWrite()
}()
go func() {
for {
buf := make([]byte, 100)
n, err := conn.Read(buf)
if err != nil {
logrus.Errorf("cannot read %v", err)
return
}
logrus.Infof("read:%v", buf[:n])
}
}()
time.Sleep(time.Minute)
}