TCP通信
三次握手建立连接
三次握手就是三次通信,完成后就是建立通信完成
三次握手是内核完成的,在用户空间是accpet()、connect()成功执行返回了
![](https://i-blog.csdnimg.cn/blog_migrate/ec72ca9083a62cf2a50685482fcbdc20.png)
SYN标志位是专门建立连接通信的标志位,服务器一旦收到此标志位表明客户端要和服务器建立连接;
1000表示的是包号(默认从0开始),后面的0表示所携带的数据的大小;
<mss 1460>表示传输数据的上限。
ACK表示服务器同意建立连接
1001表示1001号包以前的数据都收到了(其意味着1001号是SYN)
ACK 8001到达服务器:意味着建立通信连接完成
注:mss传输数据的上限为1460的原因是:以太网帧协议中最多传输数据1500字节,去掉ip首部和TCP首部就是1460字节
![](https://i-blog.csdnimg.cn/blog_migrate/c23f3d995c0674596636f90a87e36ec6.png)
数据通信(滑动窗口)
4.5.6是数据通信
并不是一次发送,一次应答。也可以批量应答
4到5的1001(20)从1001号包写了20个字节的数据传过去,ACK8001是为了前面通信断了的话,保证通信,服务器收到数据包后给客户端回执(TCP特点,UDP不是)。ACK1021就是回执,而8001(10)表示的是服务器向客户端发送的数据,接着客户端回执给服务器ACK 8011表示8011以前数据都收到了。
以上方法可能会很慢,因为每次发送都要回执
TCP有滑动窗口,多次发送,一次回执
![](https://i-blog.csdnimg.cn/blog_migrate/2b2c3537d084cae982111c6b04e4b947.png)
客户端发送数据,服务器接收到内核的缓冲区,当客户端发送数据过多,服务器的缓冲区被写满了,这样就不好了。滑动窗口确定缓冲区大小
Win4096告诉服务器端,客户端的(滑动窗口)缓冲区大小,win6144告诉客户端,服务器端的(滑动窗口)缓冲区大小。
后面的win也是,9不再发了是阻塞等待服务器腾出缓冲区,10,11就是腾出的缓冲区
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
四次握手关闭连接
客户端向服务器发送FIN标志位,沿用之前的包号发送0字节数据(FIN,1021(0)),并且保证抵达(ACK 8011),在回执了ACK 1022后处于半关闭完成(客户端只能收数据,不能发数据)
接着服务器向客户端发送FIN标志位,类似上面
导致四次连接原因:半关闭
当ACK 8012由于某种原因服务器没有收到,服务器就会反复向客户端发送FIN,8011(0),ACK 1022
完成两次挥手后,不是说两端的连接断开了,主动端关闭了写缓冲区,不能再向对端发送数据,被动端关闭了读缓冲区,不能再从对端读取数据。然而主动端还是能够读取对端发来的数据。
TCP通信与代码对应关系
![](https://i-blog.csdnimg.cn/blog_migrate/fe2dc7a70f2b4f4d642a35df86ec80f3.png)
TCP状态转换
下图为TCP状态转换图,用于排除和定位网络或系统故障。
要记住这张图的每个状态及转换过程
![](https://i-blog.csdnimg.cn/blog_migrate/8eea306cc9deadec6f845aacd593a1c6.png)
TCP状态转换图
CLOSED:表示初始状态。
LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
ESTABLISHED:表示连接已经建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态
netstat -apn | grep client 查看客户端网络连接状态
netstat -apn | grep port 查看端口的网络连接状态
TCP状态时序
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。
1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)
2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)
-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)
-- 等 2MSL时长 -- CLOSE
3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)
4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK
-- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE
重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)
2MSL时长
一定出现在【主动关闭连接请求端】。--- 对应 TIME_WAIT 状态。
保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
2MSL (Maximum Segment Lifetime) TIME_WAIT状态的存在有两个理由:
(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。
(2)防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
所以TIME_WAIT时间是1分钟-4分钟
端口复用函数
Linux网络编程——端口复用(多个套接字绑定同一个端口) - 知乎 (zhihu.com)
默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这时候,别的套接字就无法使用这个端口( 8000 )---端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错
设置socket的SO_REUSEADDR选项,即可实现端口复用:
int opt = 1;
// sockfd为需要端口复用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四种情况下。
当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。
SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
具体例子:
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;// 设置端口复用。
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
shutdown函数
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2
close(cfd);
可以使用shutdown关闭,有多种模式选择
shutdown(int fd, int how);
how: SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。