当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。
这样就能够==让 TCP 连接的主动关闭方在它变成TIME_WAIT状态以后发送的最后一个 ACK 丢失的情况下,重新发送最终的 ACK。==最后这个ACK如果没有被B接收到(超时重传的计时内,小于2MSL),那么B就会再发一个FIN给A。
主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
在握手的时候,第二次握手如果服务器不同意建立连接,直接不发ACK就可了。如果同意就发送ACK。当中没有数据牵扯。
但是!断开连接的时候,比如A发完了所有消息准备断开。就会有前两次挥手。但此时B很有可能还有信息要发送给A。所以二三次挥手之间可能会间隔很长时间。直到B也决定断开连接,才会有三四次挥手。
注意:一旦一二次挥手成功,A就只能回复B的消息收到了,而不能再给B发送新的报文。因为此时A到B的连接以及断开。只是B到A还没断开。
B一般会受到所有ACK以后才会进行第三四次挥手。
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
tcp_server.c
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
//int optval = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
return -1;
}
// 监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
return -1;
}
// 接收客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accpet");
return -1;
}
// 获取客户端信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
// 输出客户端的信息
printf("client's ip is %s, and port is %d\n", cliIp, cliPort );
// 接收客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
if(len == -1) {
perror("recv");
return -1;
} else if(len == 0) {
printf("客户端已经断开连接...\n");
break;
} else if(len > 0) {
printf("read buf = %s\n", recvBuf);
}
// 小写转大写
for(int i = 0; i < len; ++i) {
recvBuf[i] = toupper(recvBuf[i]);
}
printf("after buf = %s\n", recvBuf);
// 大写字符串发给客户端
ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
if(ret == -1) {
perror("send");
return -1;
}
}
close(cfd);
close(lfd);
return 0;
}
tcp_client.c
#include
#include
#include
#include
#include
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
while(1) {
char sendBuf[1024] = {0};
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
端口复用最常见的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
(没有被释放的端口就无法再次建立连接)
#include
#include
//设置套接字属性(不仅仅能设置端口复用)set socket option
int setsockopt(int sockfd,int level,int optname,const void* optval,socklen_t optlen);
-sockfd:打开的套接字的文件描述符
-level:级别 SOL_SOCKET(端口复用的级别)
-optname:
-SO_REUSEPORT 允许重用端口复用
-SO_REUSEADDR 允许重用地址复用
-optval:端口复用的值(整形)
-1:可以复用
-0:不可以复用
-optlen:optval参数的大小
端口复用,设置的时机是在服务器端口绑定端口之前
常看网络相关信息的命令:netstat(netstate)
参数:
a 所有的socket (all)
p 显示正在使用socket的程序名称(programs)
n 直接使用IP地址,而不通过域名服务器(numric)
netstat -anp|grep 9999
有三个socket是因为服务器端有一个监听还有个数据通信的。
grep 9999:这个命令会筛选出包含 “9999” 这个数字的行。
所以整个命令的目的是查看哪些进程或连接正在使用端口 “9999”。
在通信过程中,状态不发生改变