TCP状态转换

重复bind同一端口出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define SERV_PORT 9000 //本服务器要监听的端口号,一般1024以下的端口很多都是属于周知端口,所以我们一般采用1024之后的数字做端口号

int main(int argc, char *const *argv)
{
//这些演示代码的写法都是固定套路,一般都这么写

//服务器的socket套接字【文件描述符】
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //创建服务器的socket,大家可以暂时不用管这里的参数是什么,知道这个函数大概做什么就行

struct sockaddr_in serv_addr; //服务器的地址结构体
memset(&serv_addr,0,sizeof(serv_addr));

//设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。

int result;
result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//绑定服务器地址结构体
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}
result = listen(listenfd, 32); //参数2表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成,c/s之间进入正常通讯后,请求数-1
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}


/*
{
//再绑定一个
int listenfd2 = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in serv_addr2;
memset(&serv_addr2,0,sizeof(serv_addr2));
serv_addr2.sin_family = AF_INET;
serv_addr2.sin_port = htons(SERV_PORT); //端口重复,bind会失败
serv_addr2.sin_addr.s_addr = htonl(INADDR_ANY);
result = bind(listenfd2, (struct sockaddr*)&serv_addr2, sizeof(serv_addr2));
char *perrorinfo = strerror(errno); //根据资料不会返回NULL;
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo); //bind返回的值为-1,错误码为:98,错误信息为:Address already in use;
}*/

int connfd;
const char *pcontent = "I sent sth to client!\n"; //指向常量字符串区的指针
for(;;)
{
//卡在这里,等客户单连接,客户端连入后,该函数走下去【注意这里返回的是一个新的socket——connfd,后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接】
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

//发送数据包给客户端
write(connfd,pcontent,strlen(pcontent)); //注意第一个参数是accept返回的connfd套接字
printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");

//只给客户端发送一个信息,然后直接关闭套接字连接;
close(connfd);
} //end for
close(listenfd); //实际本简单范例走不到这里,这句暂时看起来没啥用
return 0;
}

同一个IP(INADDR_ANY),同一个端口SERV_PORT,只能被成功的bind()一次,若再次bind()就会失败,并且显示:Address already in use

image-20220208171606212

就好像一个班级里不能有两个人叫张三;
结论:相同IP地址的相同端口,只能被bind一次;第二次bind会失败;

命令netstat:显示网络相关信息

-a:显示所有选项
-n:能显示成数字的内容全部显示成数字
-p:显示段落这对应程序名
netstat -anp | grep -E 'State|9000'

image-20220208172140382

执行./server程序后执行命令netstat -anp | grep -E 'State|9000'可以观察到网络状态为LISTEN,9000端口仍然被监听当中。

打开两个终端并且执行telnet 192.168.200.129 9000语句,我们用两个客户端连接到服务器,服务器给每个客户端发送一串字符”I sent sth to client!\n”,并关闭客户端;

image-20220208172713501

我们用netstat观察,原来那个监听端口 一直在监听【listen】,但是当来了两个连接之后【连接到服务器的9000端口】,虽然这两个连接被close掉了,但是产生了两条TIME_WAIT状态的信息【因为你有两个客户端连入进来】
只要客户端 连接到服务器,并且 服务器把客户端关闭,那么服务器端就会产生一条针对9000监听端口的 状态为 TIME_WAIT 的连接;

过了一段时间后将会处于TIME_WAIT状态的连接将会消失,如下图所示

image-20220208173828449

TCP状态转换图

preview

TCP状态转换图【11种状态】 是 针对“一个TCP连接【一个socket连接】”来说的;
客户端: CLOSED ->SYN_SENT->ESTABLISHED【连接建立,可以进行数据收发】
服务端: CLOSED ->LISTEN->【客户端来握手】SYN_RCVD->ESTABLISHED【连接建立,可以进行数据收发】
谁主动close连接,谁就会给对方发送一个FIN标志置位的一个数据包给对方;【服务器端发送FIN包给客户端】
服务器主动关闭连接:ESTABLISHED->FIN_WAIT1->FIN_WAIT2->TIME_WAIT
客户端被动关闭:ESTABLISHED->CLOSE_WAIT->LAST_ACK

TIME_WAIT状态

具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样;当这种状态存在的时候,服务器程序退出并重新执行会失败,会提示:
//bind返回的值为-1,错误码为:98,错误信息为:Address already in use
所以,TIME_WAIT状态是一个让人不喜欢的状态;
连接处于TIME_WAIT状态是有时间限制的(1-4分钟之间) = 2 MSL【最长数据包生命周期】;

image-20220208175501919

引入TIME_WAIT状态【并且处于这种状态的时间为1-4分钟】 的原因:

  • 可靠的实现TCP全双工的终止:如果服务器最后发送的ACK【应答】包因为某种原因丢失了,那么客户端一定会重新发送FIN,这样因为服务器端有TIME_WAIT的存在,服务器会重新发送ACK包给客户端,但是如果没有TIME_WAIT这个状态,那么无论客户端收到ACK包,服务器都已经关闭连接了,此时客户端重新发送FIN,服务器给回的就不是ACK包,而是RST【连接复位】包,从而使客户端没有完成正常的4次挥手,不友好,而且有可能造成数据包丢失;也就是说,TIME_WAIT有助于可靠的实现TCP全双工连接的终止;

  • 允许老的重复的TCP数据包在网络中消逝;

RST标志位:

对于每一个TCP连接,操作系统是要开辟出来一个收缓冲区,和一个发送缓冲区 来处理数据的收和发;(发数据即向发送缓冲区写数据)当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭。FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;

反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;

SO_REUSEADDR选项

定义函数:int setsockopt(int s, int level, int optname, const void * optval, ,socklen_toptlen);

头文件:#include <sys/types.h> #include <sys/socket.h>

函数说明:setsockopt()用来设置参数s 所指定的socket 状态. 参数level 代表欲设置的网络层, 一般设成SOL_SOCKET 以存取socket 层. 参数optname 代表欲设置的选项, 有下列几种数值:
SO_DEBUG 打开或关闭排错模式
SO_REUSEADDR 允许在bind ()过程中本地地址可重复使用
SO_TYPE 返回socket 形态.
SO_ERROR 返回socket 已发生的错误原因
SO_DONTROUTE 送出的数据包不要利用路由设备来传输.
SO_BROADCAST 使用广播方式传送
SO_SNDBUF 设置送出的暂存区大小
SO_RCVBUF 设置接收的暂存区大小
SO_KEEPALIVE 定期确定连线是否已终止.
SO_OOBINLINE 当接收到OOB 数据时会马上送至标准输入设备
SO_LINGER 确保数据安全且可靠的传送出去.

参数 optval 代表欲设置的值, 参数optlen 则为optval 的长度.

返回值:成功则返回0, 若有错误则返回-1, 错误原因存于errno.

附加说明:
1、EBADF 参数s 并非合法的socket 处理代码
2、ENOTSOCK 参数s 为一文件描述词, 非socket
3、ENOPROTOOPT 参数optname 指定的选项不正确.
4、EFAULT 参数optval 指针指向无法存取的内存空间.

setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前
SO_REUSEADDR的能力:
(1)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;【即便TIME_WAIT状态存在,服务器bind()也能成功】
(2)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;
(4)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持,同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字[TCP不行];

所有TCP服务器都应该指定本套接字选项,以防止当套接字处于TIME_WAIT时bind()失败的情形出现;
试验程序server.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define SERV_PORT 9000 //本服务器要监听的端口号,一般1024以下的端口很多都是属于周知端口,所以我们一般采用1024之后的数字做端口号

int main(int argc, char *const *argv)
{
//这些演示代码的写法都是固定套路,一般都这么写

//服务器的socket套接字【文件描述符】
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //创建服务器的socket,大家可以暂时不用管这里的参数是什么,知道这个函数大概做什么就行

struct sockaddr_in serv_addr; //服务器的地址结构体
memset(&serv_addr,0,sizeof(serv_addr));

//设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。


//setsockopt():设置一些套接字参数选项;
//参数2:是表示级别,和参数3配套使用,也就是说,参数3如果确定了,参数2就确定了;
//参数3:允许重用本地地址
int reuseaddr=1; //开启
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, (const void *) &reuseaddr,sizeof(reuseaddr)) == -1)
{
char *perrorinfo = strerror(errno);
printf("setsockopt(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n",-1,errno,perrorinfo);
}

int result;
result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//绑定服务器地址结构体
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}
result = listen(listenfd, 32); //参数2表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成,c/s之间进入正常通讯后,请求数-1
if(result == -1)
{
char *perrorinfo = strerror(errno);
printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo);
return -1;
}

/*
{
//再绑定一个(socket不同,但绑定的ip地址,端口相同),失败,后续bind会失败
int listenfd2 = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in serv_addr2;
memset(&serv_addr2,0,sizeof(serv_addr2));
serv_addr2.sin_family = AF_INET;
serv_addr2.sin_port = htons(SERV_PORT); //端口重复,bind会失败
serv_addr2.sin_addr.s_addr = htonl(INADDR_ANY);
int reuseaddr=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, (const void *) &reuseaddr,sizeof(int)) == -1)
{
char *perrorinfo = strerror(errno);
printf("setsockopt2(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n",-1,errno,perrorinfo);
}
result = bind(listenfd2, (struct sockaddr*)&serv_addr2, sizeof(serv_addr2));
if(result == -1)
{
char *perrorinfo = strerror(errno); //根据资料不会返回NULL;
printf("bind2返回的值为%d,错误码为:%d,错误信息为:%s;\n",result,errno,perrorinfo); //bind返回的值为-1,错误码为:98,错误信息为:Address already in use;
}
}*/

int connfd;
const char *pcontent = "I sent sth to client!\n"; //指向常量字符串区的指针
for(;;)
{
//卡在这里,等客户单连接,客户端连入后,该函数走下去【注意这里返回的是一个新的socket——connfd,后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接】
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

//发送数据包给客户端
write(connfd,pcontent,strlen(pcontent)); //注意第一个参数是accept返回的connfd套接字
printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");

//只给客户端发送一个信息,然后直接关闭套接字连接;
close(connfd);
} //end for
close(listenfd); //实际本简单范例走不到这里,这句暂时看起来没啥用
return 0;
}

image-20220208182113242

结论:

  1. 两个进程,绑定同一个IP和端口:bind()失败[一个班级不能有两个人叫张三]
  2. TIME_WAIT状态时的bind绑定:bind()成功

SO_REUSEADDR:主要解决TIME_WAIT状态导致bind()失败的问题;