认识TCP协议
TCP全称为“传输控制协议”,这是传输层的一个协议,对数据的传输进行一个详细的控制。
特点:
- 面向字节流
- 安全可靠
- 面向连接
TCP协议段格式
- 源端口号与目的端口号:这里与UDP的一样,每个数据都要知道从哪个进程来,要到哪个进程去。
- 32位序号与32位确认序号:这里的序号与确认信号可以理解成两个通信进程在收发数据的时候互相答复的信息。比如说:A进程从序列号1000开始给B进程发送数据,发送五个数据。那么在B收到数据回复的时候,这里A的确认序列号应该是从1006,如果不是1006,比如说是1003,那就意味着1004、1005数据包B没有收到,于是A启动重发机制。这也就保证了数据的可靠性,也是TCP的特点之一。序列号是进程发送消息的号码,而确认是期望目的进程返回的号码。进行比对,从而验证数据包是否到达。
- 4位TCP报头长度:这里的四位TCP报头长度,可以理解成四个比特位表示长度,四位比特位表示的值乘以四就是该TCP头部的长度。由图可知,报头最短长度为20字节,也就是说这里的四位TCP报头长度默认为0101。并且TCP报头长度不可超过15*4=60个字节。
- 6位标志位:①URG:紧急指针是否有效②ACK:确认号是否有效③PSH:提示接收端应用程序立刻从TCP缓冲区内部把数据读走④RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段⑤SYN:请求建立连接,我们把携带SYN标识的称为同步报文段⑥FIN:通知对方,本端要关闭了,我们把携带FIN标识的称为结束报文段。
- 16位窗口大小:这里的窗口大小可以看做一个标志,标志着TCP缓冲区内部剩余空间的大小。起到一个流量控制的作用。如果16为窗口满了,那么这个时候是不允许数据接收的。后面到达的数据会被丢失。
- 16位校验和:这里的校验和由发送端填充,CRC校验。接收端校验数据的时候如果校验不通过,那么认为数据有问题。此处的校验和不仅仅校验TCP首部,还校验数据部分。
- 16位紧急指针:标识哪部分的数据为紧急数据。
连接过程
我们知道,TCP协议是面向连接的,也就是说它需要客户端与服务器成功连接之后才可使用。那么客户端与服务器的连接过程是怎样的呢?简单的来说就是三次握手,四次挥手,大意为客户端连接服务器需要三次握手,通信完毕过后断开连接需要四次挥手。
-
三次握手
客户端与服务器在握手之前都做了一些前期的准备。服务器在开始先分配一个描述符,然后填充一下sockaddr_in结构体,绑定创建的文件描述符及服务器端口,接着listen监听,使得刚才的文件描述符成为一个监听描述符,最后阻塞至accept等待客户端的连接。而客户端相较来说简单一些,就是分配文件描述符,填充sockaddr_in结构体,最后进行connect请求服务器的连接,直至服务器响应。
在客户端经过connect请求服务器响应的时候,向服务器发送同步报文段也就是SYN请求,发送完毕后等待服务器响应。服务器如果收到了SYN同步报文段,那么就会给客户端发送ACK响应,意为收到了客户端发送的同步报文段,在此同时,服务器也会发送SYN同步报文段,请求客户端的响应。客户端在接收到SYN同步报文段后也会发送ACK响应来回复服务器。这个过程就是三次握手的过程。
这样看来,客户端与服务器的连接是双方的,两者都得发送请求同样两者也都得响应。图上来看,SYN_SENT就是请求连接状态,SYN_RCVD就是等待连接状态。在三次握手成功后,服务器与客户端都会进入ESTABLISHED状态,也就是TCP连接成功态,这个时候就可以进行数据的传送了。
这个过程中如果客户端的SYN请求如果丢包,服务器不会响应,而客户端会有一个等待时间,等待时间到达,未收到ACK响应,这个时候客户端会发起再次请求。如果多次请求都未成功,此时客户端可能会判断网络异常,不会再次请求。同样再服务器接收到客户端的SYN请求之后也会发送ACK响应同时发送SYN请求。如果客户端迟迟不给服务器ACK响应,服务器也会进行重发,直至判断网络异常。所以说,三次握手任意一个缺失都不会连接成功,也就无法通信。所以三次握手也是确保TCP可靠的一种方式。
三次握手的目的是什么?
答:个人理解简单易懂的就是同步连接双方的序列号和确认号,并交换tcp窗口的大小信息。
为什么需要二次握手就能完成解决反而需要三次呢?
答:需要三次握手是为了防止已经失效的连接请求报文段又突然传送到服务端,因而产生错误。
四次挥手
在客户端与服务器数据传输完毕之后,客户端没有请求了,所以此时调用close关闭文件描述符,开始进入FIN_WAIT_1状态,同时向服务器发送FIN结束报文段。等待服务器的响应。当服务器这里收到了FIN结束报文段时,这个时候,服务器进入CLOSE_WAIT状态。并给客户端进行应答发送ACK。当客户端收到服务器的ACK响应时,进入FIN_WAIT_2状态。当服务器调用close时,会向客户端发送FIN结束报文段。此时进入LAST_ACK状态。此时的客户端收到服务器发送的FIN时,会向服务器进行响应ACK,并且客户端进入TIME_WAIT状态,TIME_WAIT结束之后,进入CLOSED,断开连接成功。当服务器收到客户端最后的ACK时,进入CLOSED状态。断开连接成功。
CLOSE_WAIT与LAST_ACK状态
在三次握手的时候服务器可以将SYN与ACK同时发送,但是为什么这里服务器发送的FIN与ACK是分开的发送的呢??其实是这样的。
首先FIN信号是由于调用close所以才发送的。而客户端调用close时,发送FIN结束报文段并进入FIN_WAIT_1状态。而这个报文段在服务器中用户态其实是无法感知的,内核会自己处理这个报文段,也就是说由内核进行ACK响应。这个过程中不是由用户代码决定的,服务器的FIN是由用户代码调用close发送的,所以内核与服务器不一定是同时处理这个信息的。所以FIN与ACK不一定是同时发送出去的。注意:这里是不一定!!!但是三次握手的时候发送SYN是由内核直接完成的,所以这就可以达到一个同步发送的情况。
如果服务器的代码没有调用close,那么意味着并没有发送FIN结束报文段。那么也就是说,此连接的服务器长期保持在CLOSE_WAIT状态,这会有什么影响?
服务器长期保持在CLOSE_WAIT状态,也就是说分配的文件描述符并没有关闭并归还。那么大量的CLOSE_WAIT存在的话,就会导致一种资源的泄漏。可能到最后就没有可分配的文件描述符了,那么就会使一些客户端无法连接,从而造成不可估量的影响。
TIME_WAIT
在客户端最后一次发送ACK响应后,进入TIME_WAIT状态,而这个状态的时候客户端在做什么呢?
答案就是等待!在客户端最后发送ACK响应后,进入TIME_WAIT状态,这是为了防止最后发送的ACK响应丢包。在这里,TIME_WAIT状态会等待2MSL的时间。
这里的单位 MSL就是 Max Segment Life意思就是报文的最大生存时间,这里的生存时间指的是一个报文从发生到被接收到的整个过程,这个过程的时间就是MSL。
在Linux下可以利用cat /proc/sys/net/ipv4/tcp_fin_timeout来查看MSL的值。
而客户端最后一次发送ACK响应后,为什么要等待2MSL呢?
这是为了确保最后一条ACK消息的到达。因为客户端在发送最后一条ACK响应后进入TIME_WAIT状态,如果这条ACK报文丢失,那么服务器在等待一个MSL的时间过后发现没有收到ACK响应,那么它会重新发送一条FIN报文。这样一条ACK响应的时间加上重发的FIN的时间正好就是2MSL。如果客户端等待2MSL后没有收到FIN报文,那么意味着服务器收到了客户端发送的ACK报文,这样就断开连接。
这里可以看到客户端退出后,进入到了TIME_WAIT状态。
也就是说,在TIME_WAIT的时候,客户端与服务器之间的TCP连接还是存在的。
在某些情况下服务器也有可能请求断开连接,由服务器先进入FIN_WAIT_1,这样的话最终就是服务器进入TIME_WAIT状态。那么在这个状态下会有什么问题呢?
这里,我们终止掉服务器,发现服务器进入了TIME_WAIT状态。这时候接着再次启动服务器,发现启动不了。此时启动不了的原因的端口号绑定失败。这是为什么?
其实这就是因为在TIME_WAIT状态的时候,TCP连接还是存在的,那么刚才的端口号仍旧被绑定着。再次启动服务器的时候,此时的端口号并未被释放。所以才提示绑定失败。
如果服务器需要处理大量的客户端连接,每个连接的生存时间很短,但是每秒都有大量的客户端请求。这个时候如果服务器主动关闭连接,就会产生大量的TIME_WAIT连接。由于我们的需求量很大,就会导致TIME_WAIT的连接数很多,从而导致服务器的端口不够用,无法处理新的连接。这个时候如何解决呢?
这时候可以利用函数setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)来解决这个问题。这个函数的作用就是允许创建端口号相同但是IP地址不同的多个socket描述符。在socket()与bind()之间调用即可。