TCP协议
TCP协议(Transmission Contral Protocol,传输控制协议)是最重要的传输层协议,TCP协议的主要内容有
- TCP协议的报头
- TCP协议的特点
- TCP协议的可靠性和性能
- TCP协议的粘包问题与异常情况
TCP协议的报头
TCP协议的报头如下
- 16位源端口号:标识发送数据的进程
- 16位目的端口号:标识接收数据的进程,TCP协议通过16位目的端口号确定需要将数据交付给上层哪一个进程
- 32位序号:是TCP保证可靠性的一个重要手段,接收端在收到TCP报文时,可以根据32位序号对报文进行排序
- 32位确认序号:对发送端数据的确认,表示在指定序号之前的数据已经全部收到
- 4位首部长度:标识TCP报头的长度,单位是4字节,一般4位首部长度被设置为二进制的
0101,表示没有选项字段(4位首部长度的单位是4字节,因此TCP报头中的选项字段最多为40字节) - 6个保留位:TCP报头中暂时没有被使用的字段
- 6个标志位:TCP报头中的6个标志位用于确定TCP报文的种类,6个标志位的含义
URG:标识紧急指针是否有效ACK:ACK标志位为应答标志位,用于告知对方你上一次发送的数据我已经收到了PSH:用于催促对方尽快将接收缓冲区的数据进行读取RST:请求与对方重置连接,重新进行三次握手SYN:三次握手期间请求与对方建立FIN:四次挥手时请求断开连接
- 16位窗口大小:用于告知对方自己接收缓冲区的剩余大小,让对方能够以此为依据动态调整发送的数据量
- 16位校验和:用于验证数据在发送过程中是否损坏,若校验和不通过,则数据直接被丢弃
- 16位紧急指针:用于指明要发送的紧急数据在原始数据中的偏移量,只有当URG标志位被设置为1时,紧急指针才起作用
TCP协议的报头在系统中是一个位段结构
struct tcp_header {
int src_port : 16;
int dst_port : 16;
int serial_number : 32;
int confirm_sequence_number : 32;
int head_length : 4;
int reserved_bit : 6;
int urg : 1;
int ack : 1;
int psh : 1;
int rst : 1;
int syn : 1;
int fin : 1;
int window : 16;
int check : 16;
int emergency_pointer : 16;
};
URG
TCP协议允许发送紧急数据,在调用send函数时,可以设置MSG_OOB选项,表示发送带外数据
send(socket,"urgent_data",strlen("urgent_data"),MSG_OOB);
在进行recv读取时,可以读取紧急数据
recv(sock,buf,strlen(buf),MSG_OOB);
TCP协议的特点
-
可靠性:TCP有确认应答、超时重传,快重传、流量控制、拥塞控制等一系列机制保证TCP的可靠性
-
面向连接:在进行TCP通信时,需要进行三次握手建立连接
-
面向字节流:TCP发送的数据是字节流式的,发送的数据量与接收的数据量没有必然联系,例如:发送端一次发送100字节数据,接收端可以分2次读取,每一次读取50字节数据
-
缓冲区:TCP存在接收缓冲区和发送缓冲区
TCP的可靠性与效率
TCP主要特点即可靠,TCP通过一系列机制保证其可靠性
确认应答
确认应答机制是TCP保证可靠的基础,当主机A给主机B发送数据时,若主机A收到了主机B的应答,那么主机A就可以100%确认数据被B收到了
确认应答只能保证上一次发送出去的数据100%被对方收到了,不能确保最新一次发送的数据是否被对方收到,有“应答”才能得到“确认”
确认应答体现在TCP报头上即为32位确认序号,确认序号表示的含义是在此序号之前的数据我全部收到了
超时重传
如果发送的数据在一定时间没有得到应答,那么发送端会进行超时重传,超时重传有2种情况:
- 发送端发送的报文丢失,接收端没有收到
- 接收端的应答丢失,发送端没有收到
无论是那种情况,都会触发超时重传
如果是接收端的应答丢失,发送端进行超时重传时接收端会收到重复的数据,可根据报头中的32位序号进行去重
超时重传的时间
发送端在时间t内没有收到应答,就进行超时重传,这个时间t应该设置为多少呢?
TCP为了保证重传的效率,t不能设置的太长,又为了保证发送端不会频繁的发送大量重复的数据,t不能设置的太短。因此会动态调整这个超时重传的时间,起始值为500ms,如果500ms之内没有收到应答,进行一次重传,如果重发之后,依然得不到应答,那么1000ms之后再次进行重传,每一次重传的时间是前一次的2倍。如果多次重传一直得不到应答,那么TCP会强制关闭连接
连接管理
TCP的连接管理即三次握手和四次挥手
三次握手的流程
- 客户端向服务端发送SYN被设置的TCP报文,表示客户端想要与服务端建立连接,客户端处于SYN_SENT状态
- 服务端接受到SYN报文之后,向客户端发送SYN和ACK被设置的报文,表示客户端的连接请求已经收到了,同时服务端也想与客户端建立连接,服务端处于SYN_RCVD状态
- 客户端发送ACK报文,表示服务端的连接请求收到,客户端处于ESTABLISHED状态,认为连接建立完成
- 服务端在收到最后一个ACK报文时,也认为连接建立完成,变为ESTABLISHED状态
三次握手中的特殊情况
-
情况1:客户端发送的SYN丢失
这种情况下,由于客户端收不到服务端的SYN+ACK,会触发超时重传的机制
-
情况2:服务端发送的SYN+ACK丢失
客户端收不到服务端的SYN+ACK,会触发超时重传的机制
-
情况3:最后一次ACK丢失
这种情况下,客户端会认为连接已经建立完毕,但是服务端还处于SYN_RCVD状态,认为连接没有建立完成,当客户端向服务端发送数据时,服务端会给客户端返回一个RST标志位被设置的TCP报文,请求重新进行三次握手建立连接
为什么是三次握手?
TCP三次握手的目的有如下三点
- 验证客户端主机是否正常
- 验证服务端主机是否正常
- 验证客户端与服务端之间的通信线路是否正常
进行三次握手,当客户端收到SYN+ACK时,客户端可以知道如下信息
- 我具有接收数据的能力
- 我第一次发送的SYN报文服务端一定收到了,我具有发送数据的能力,并且我到服务端的通信线路是正常的
- 我能收到服务端的SYN+ACK,说明服务端具有发送数据的能力,同时服务端具有接收数据的能力(因为我发送的SYN服务端一定收到了),并且服务端到客户端的通信线路是正常的
当服务端收到客户端的SYN请求时,服务端可以知道如下信息
- 客户端具有发送数据的能力
- 客户端到服务端的通信线路是正常的
- 我具有接收数据的能力
但是服务端并不知道客户端是否有接受数据的能力,也不知道自己是否有发送数据的能力,也不知道自己到客户端的通信线路是否正常。而这些信息,必须要服务端收到最后一次ACK才能确认。
如果是1次握手或2次握手是达不到上述目的的,同时一次握手或2次握手会有SYN洪水的问题。
SYN洪水
客户端与服务端建立连接后,系统会维护该链接,维护链接需要花费时间成本和空间成本,如果TCP建立连接只需要1次握手或2次握手,那么服务端很容易受到攻击(客户端只需要发起一次SYN就可消耗服务端的资源)。TCP建立连接采取三次握手,是为了服务器能够正常的给客户端提供服务,而不是被白白消耗资源。
四次挥手
四次挥手流程
- 客户端向服务端发起FIN,请求断开连接,表示客户端不会向服务端发送数据了,客户端处于FIN_WAIT_1状态,同时客户端的发送缓冲区关闭(接收缓冲区依然可以接收数据)
- 服务端收到FIN之后,向客户端发送ACK,表示收到客户端的断开连接请求,服务端处于CLOSE_WAIT状态
- 客户端收到ACK之后,进行等待,处于FIN_WAIT_2状态,此时客户端不在发送数据,但是仍然可以接收数据
- 服务端发送FIN,表示也想与客户端断开连接,此时服务端不在发送数据(关闭了发送缓冲区,接收缓冲区依然可以接收数据),处于LAST_ACK状态,表示等待最后一次ACK应答
- 客户端收到FIN之后,处于TIME_WAIT状态,进行等待,此时客户端的接收缓冲区依然可以接收数据。虽然服务端已经不在发送数据,关闭了发送缓冲区,但是服务端之前发送的数据可能还在进行路由,因此客户端处于TIME_WAIT状态,等待接收正在路由的数据
- 客户端收到FIN之后,处于TIME_WAIT状态,同时立刻向服务端发送最后一次ACK(此时客户端的发送缓冲区虽然已经关闭,但是由于最后一次ACK不携带有效载荷,因此依旧可以发送)
- 客户端TIME_WAIT状态等待完毕之后,立即关闭接收缓冲区,断开连接
- 服务端收到最后一次ACK后,关闭接收缓冲区,断开连接
- 四次挥手完毕
四次挥手中的异常情况
-
情况1:第一个FIN丢失或第一个ACK丢失
这种情况下会触发超时重传
-
情况2:第二个FIN丢失
服务器进行超时重传
-
情况3:第二个ACK丢失
服务端会进行超时重传,同时因为客户端处于TIME_WAIT状态,因此依然可以接收到服务端的FIN,给服务端反馈ACK,这也是TIME_WAIT状态存在的另外一个原因
四次挥手的理解
TCP断开连接,双方调用close并不意味着双方能够立即进入CLOSED状态,正常情况下应该是一方处于TIME_WAIT状态,另一方处于CLOSED状态
四次挥手能否缩减为三次?
从理论上是可以的,服务器可以将ACK与FIN同时发送,但是实际并没有选择这样做,因为服务器可能还有数据需要发送给客户端,因此服务器不会立即发送FIN与客户端断开连接。
TIME_WAIT状态
四次挥手过程中,主动断开连接的一方会处于TIME_WAIT状态,等待对方"在路上"的数据,如果服务器是主动断开连接的一方,服务器在收到客户端的FIN时会处于TIME_WAIT状态进行等待,客户端在收到ACK后处于CLOSED状态。此时服务端如果立即重新绑定端口号会产生"bind error"错误,原因是上层虽然已经调用close,但是底层依然处于TIME_WAIT状态,端口号依然被占用
tcp 0 0 127.0.0.1:8080 127.0.0.1:32794 TIME_WAIT -
为了解决这个问题,需要使用setsockopt设置端口复用,该操作需在创建套接字之后,绑定地址结构之前进行
int reuse=1;
setsockopt(sockfd/*用于监听的fd*/, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
TIME_WAIT状态的时间
TIME_WAIT状态的时间一般为2*MSL,其中MSL表示TCP报文的最大生存时间,一般为60s,可以通过以下命令查看
cat /proc/sys/net/ipv4/tcp_fin_timeout
服务器忘记调用close关闭连接
若客户端发起前2次挥手关闭连接,但是服务器忘记调用close完成最后2次挥手,那么在服务端会充满大量处于CLOSE_WAIT状态的连接,占用服务器的资源,因此,双方在通信完成之后,均应该调用close关闭连接
tcp 0 0 127.0.0.1:8080 127.0.0.1:35650 CLOSE_WAIT
滑动窗口
TCP存在确认应答机制来保证可靠性,客户端发送一条数据,服务端返回一个应答。若是单纯的确认应答的话TCP的传输效率就会比较低,因为客户端在发送下一次数据之前必须等到上次数据的应答,一来一回比较耗费时间,基于此,TCP引入了滑动窗口,滑动窗口指的是发送方暂时没有收到应答的情况下能够发送的最大数据量。
可以将TCP的发送缓冲区分为如下三部分
其中第2部分可以发送的数据大小即为滑动窗口的大小,滑动窗口的大小由2部分决定:
- 接收方的接收缓冲区的剩余容量,即接收方的窗口大小
- 网络状况
如果接收方的窗口大小很大,并且网络状况良好,那么发送方的滑动窗口大小可以被设置为一个比较大的值,表示发送方可以连续、顺畅的发送数据;如果接收方的窗口大小比较小,并且网络状态较差,那么此时发送方就不应该发送大量数据,应该将窗口大小设置为一个较小值,否则即使数据到了接收方,也极有可能因为接收方窗口大小太小而被丢弃。
TCP滑动窗口可以让发送方在暂时没有收到应答的情况下可以连续的向接收方发送数据,如果在中途数据包丢失,可能会触发快重传(高速重发控制)
例如上图客户端连续发送数据,但是序号为1001~2000的报文丢失,此时服务器对于2001-3000报文的确认序号只能是2001,对3001-4000报文的确认序号也只能是2001,4001-5000的确认序号也是2001,因为确认序号的含义是在此编号之前的数据我都收到了。当客户端连续收到3个相同的确认序号时,客户端意识到1001-2000的报文没有发送过去,便会触发快重传的机制,重新发送1001-2000的报文。
快重传与超时重传
- 快重传的触发条件是连续3次及以上收到相同的确认序号,此时会触发快重传
- 超时重传是在一定时间之内没有收到应答导致的重传
连续发送数据的过程中ACK丢失
如图,在连续发送数据的过程中确认序号为1001、3001、4001的ACK均丢失,仅收到了确认序号为2001、5001的ACK,这种情况下,客户端不需要进行超时重传,因为确认序号的含义是在此编号之前的数据我都收到了,只要收到了确认序号为5001的响应报文,即说明客户端发送的数据服务器已经全部收到了,不需要进行超时重传
流量控制
流量控制主要通过TCP报头中的16位窗口大小完成,流量控制的主要目的是限制发送方的滑动窗口大小,防止发送方发送过量的数据,而接收方因为接收缓冲区剩余容量不足,导致接收到的报文被丢弃,白白浪费了网络资源。在三次握手期间,双方除了发送SYN请求建立连接,还会协商各自的窗口大小,以控制后续发送的数据量。
当接收方的窗口大小很小或为0时,向发送方反馈的TCP报文中16位窗口大小会被设置为极小值或0,此时发送方意识到自己不应该继续发送数据了,应该等待接收方的上层将数据读走在发送数据,在此期间,TCP采取以下策略使发送方得知接收方的窗口大小:
- 发送方会定期向接收方发送窗口探测报文,该报文不携带任何有效载荷,接收方在收到窗口探测报文后,向发送方反馈自己的窗口大小
- 接收方还会每隔一段时间主动向发送方发送窗口更新通知,以告知发送方我接收缓冲区的数据已经被读走了,你可以继续向我发送数据了
16位窗口大小
TCP报头中,窗口大小是16位,也就意为着接收缓冲区的大小最大为65535字节,实际上在TCP报头的选项字段中可以包含一个窗口扩大因子M,实际的窗口大小为原始的窗口大小左移M位,不过窗口扩大因子不被经常使用。
拥塞控制
拥塞控制是TCP为了应对网络状况拥堵而采取的策略,在使用TCP发送报文时,如果发送100个报文,只有一个得到了应答,丢包率非常高,那么此时TCP就不应该进行超时重传,此时的情况应该是网络出现问题,TCP应该少发或不发,等到网络恢复之后再发送报文。
TCP存在慢启动的机制,在正式发送大量数据之前,会先发送少量报文探测网络状况,如果网络状况良好,增大数据量继续发送,直到网络发生拥塞,将当前网络状况下允许发送的最大数据量称为拥塞窗口,滑动窗口的大小=min(拥塞窗口的大小,接收方的窗口大小),发送方连续向接收方发送数据,既要保证不会造成网络拥塞,又要保证对方具有足够的接收数据的能力。
流量控制与拥塞控制的区别:
- 流量控制的主要依据是接收方的窗口大小,依据接收方的窗口大小,限制发送方的发送速率
- 拥塞控制的主要依据是网络状况,依据网络的好坏,限制发送方的发送速率
延迟应答
延迟应答指的是接收方在收到数据之后,不是立即向发送方反馈自己的窗口大小,而是等待一段时间之后在进行反馈,目的是让上层把数据读走,这样反馈的窗口大小值就会大一些。延迟应答一般采取的策略:
- 每收到N个包才进行一次应答,N一般为2
- 等待t时间之后才应答,t一般为200ms
捎带应答
捎带应答指的是一个报文既可以是应答报文,又可以是向对方发送的数据,一般的报文都是捎带应答的报文,TCP三次握手中的第二次握手也是捎带应答的报文,即是对第一次SYN的确认,又是发送的SYN报文
TCP的粘包问题与异常情况
TCP的粘包问题
TCP协议是面向字节流的,意味着发送与读取没有必然联系,例如发送方一次发送的数据为"1+1=2",接收方一次可能读取到"1+",第2次读取到"1",多次读取才能读到完整数据;也有可能发送方发送了2个报文,内容为"1+1=2","1+2=3",而接收方读取了2次,读取到的内容为"1+1=21+","2=3",这种因为TCP面向字节流而导致的数据混乱的问题称为粘包问题,粘包问题TCP无法解决,因为TCP是负责传输策略的,解决粘包问题需要由应用层完成,通过应用层定制的协议来对读取到的数据进行解释与分包。例如HTTP协议通过Content-Length自描述字段以及使用空行作为分隔来对读取到的数据进行解释和分包。
TCP的异常情况
-
进程终止
进程终止,会自动关闭打开的所有文件描述符,相当于调用了close,此时底层会自动完成四次挥手
-
机器重启
机器重启之前,需要关闭该机器上运行的所有进程,其效果同进程终止一样
-
机器断电
在这种情况下,正常的一方发送的数据一直得不到ACK,会尝试重新进行三次握手,多次尝试失败之后,最终会释放连接;同时也可能在多次进行超时重传后关闭连接。如果正常一方没有发送数据的操作,TCP协议也内置了一些保活定时器,会定时向对方发送心跳报文以检测对方是否在线,如果不在线就会释放连接。
TCP与UDP
TCP与UDP的主要区别:
- TCP可靠,UDP不可靠
- TCP面向字节流,UDP面向数据报
- TCP面向连接,UDP无连接
基于UDP的应用层协议:
- DHCP协议:给局域网中的主机动态分配ip
- DNS协议:进行域名解析
基于TCP的应用层协议:
- HTTP协议
- HTTPS协议
- SSH协议
- Telnet协议
- SMTP协议
- FTP协议
如何用UDP实现可靠传输?
TCP有众多可靠性策略,UDP不可靠,那么能否使用一些方法让UDP实现可靠传输?
可以考虑将TCP中的一些特性加入到UDP中,例如引入序号和确认序号,引入确认应答、超时重传的机制,引入连接管理机制,同时可以考虑增加拥塞控制和流量控制的机制。
TCP套接字中listen函数的第二个参数
int listen(int sockfd, int backlog);
listen函数的第二个参数表示的是全连接队列的最大长度减1,例如backlog设置为2,表示全连接队列的最大长度是3
TCP在完成三次握手之后,底层只是建立好了连接,需要通过accept将连接获取上来,如果底层已经建立好连接但是上层没有调用accept,或是上层采用的是单进程服务,进程正在为一个链接提供服务,无法调用accept获取连接,此时在底层就会导致连接堆积,将底层堆积的连接称为全连接队列。
全连接队列的意义
- 当服务器压力比较大时,可能不能立马处理底层已经建立好的连接,此时该链接在全连接队列中排队,稍等片刻,服务器即可将该链接获取上来,为其提供服务
- 全连接队列不能过长,否则会导致排在全连接队列后面的连接迟迟得不到服务
- 当全连接队列满时,客户端向服务端发起connect,客户端会处于SYN_SENT状态,直到服务端获取一个全连接队列中的连接,客户端才会完成三次握手,被放到全连接队列中
可以使用listen(sock,2)设置服务端全连接队列的长度为3,同时服务端不调用accept,使用多个客户端连接服务端,即可观察到处于SYN_SENT状态的客户端
tcp 0 1 127.0.0.1:56152 127.0.0.1:8080 SYN_SENT 4955/./client