传输层协议
传输层负责端与端之间的数据传输,其中典型协议为TCP
协议和UDP
协议。
TCP
协议是TCP/IP协议栈中的传输层的典型协议,叫传输控制协议,面向连接,可靠传输,提供字节流服务。
UDP
协议是TCP/IP协议栈中的传输层的典型协议,叫用户数据报协议,无连接,不可靠,提供数据报传输服务。
UDP协议
协议字段
协议字段都包含在UDP协议数据报的报头中,每次发送数据都会将这些信息和数据一起发出。
1、16位源端口:标识数据从哪个进程来。
2、16位目的端口:描述数据到哪个进程去。
3、16位校验和:对数据的二进制反码求和,用于校验接收到的数据是否和发出的数据一致。
4、16位数据报长度:由于数据报长度一共只有16位因此udp一个数据报的最大大小为64k,加上udp数据报头部信息一共64位还要占8个字节,因此数据大小不能超过64k-8。并且sendto
这个接口会将我们传入的数据直接封装报头进行传输,所以我们在使用sendto
接口的时候buf的长度最大不能超过64k-8。
如果我们要传输的数据大于64-8。则需要用户在应用层进行分包传输。并且udp协议不保证安全也不保证包序,因此在接收数据后我们还需要在应用层进行整理。
根据udp报文中的数据报长度大小,我们的udp协议能且仅能一次收发一条完整的udp报文,不会将报文拆分,报文的长度可以从报头中获得。
应用
因为udp无连接不可靠,因此udp不能应用于有安全性要求的传输,但是因为报头短,并且不需要保证安全性因此传输速度快,因此多应用于实时性要求高的场景。
udp在协议栈层面实现了广播机制,通过向一个地址发送信息可以向局域网内所有主机发送信息。
在应用层基于udp协议实现的协议:DNS
协议,DHCP
协议。
TCP协议
协议字段
1、16位源端口:标识数据从哪个进程来。
2、16位目的端口:标识数据到哪个进程去。
3、32位序号:用于进行包序管理。
4、32位确认序号:用于进行包序管理。
5、4位首部长度:用于描述头部信息长度,单位是4个字节。tcp协议头部长度最小为20字节,最大为当4位首部长度最大时即为15 * 4 = 60字节
。
6、6位保留:用于保留下来存储新的属性和数据。
7、6位标志位:用于标志当前tcp信息的属性和类型,常见的有URG/ACK/PSH/RST/SYN/FIN
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15CWR(Congestion Window Reduce):拥塞窗口减少标志被发送主机设置,用来表明它接收到了设置ECE标志的TCP包,发送端通过降低发送窗口的大小来降低发送速率
ECE(ECN Echo):ECN响应标志被用来在TCP3次握手时表明一个TCP端是具备ECN功能的,并且表明接收到的TCP包的IP头部的ECN被设置为11。更多信息请参考RFC793。
URG(Urgent):该标志位置位表示紧急(The urgent pointer) 标志有效。该标志位目前已经很少使用参考后面流量控制和窗口管理部分的介绍。
ACK(Acknowledgment):取值1代表Acknowledgment Number字段有效,这是一个确认的TCP包,取值0则不是确认包。后续文章介绍中当ACK标志位有效的时候我们称呼这个包为ACK包,使用大写的ACK称呼。
PSH(Push):该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
RST(Reset):用于复位相应的TCP连接。通常在发生异常或者错误的时候会触发复位TCP连接。
SYN(Synchronize):同步序列编号(Synchronize Sequence Numbers)有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4294967295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。类似的后续文章介绍中当这个SYN标志位有效的时候我们称呼这个包为SYN包。
FIN(Finish):带有该标志置位的数据包用来结束一个TCP会话,但对应端口仍处于开放状态,准备接收后续数据。当FIN标志有效的时候我们称呼这个包为FIN包。
8、16位窗口大小:标志传输窗口的大小,这块牵扯到tcp协议的传输特性和机制,在后续进一步讲解。
9、16位校验和:校验数据一致性。
10、16位紧急指针:带外数据。
11、40字节选项数据:有时没有有时有,但长度必须是4的整数倍字节。
除选项外其他头信息一共20字节,这是最小头信息长度。
TCP连接管理机制
三次握手建立连接
首先客户端处于SYN_SENT
状态服务端处于LISTEN
状态,客户端会向服务端发送一个SYN
包,服务端收到后会转变状态为SYN_REVD
状态,成功后向客户端发送SYN+ACK
报文表示收到连接请求,之后客户端会再次确认,转换状态为ESTABLISHED
并返回ACK
包表示连接成功,服务端收到ACK
后叶转换状态为ESTABLISHED
。
四次挥手断开连接
首先客户端(假设客户端先发起断开连接请求)处于FIN_WAIT1
状态会发送FIN
包请求关闭连接,服务端收到后切换状态为CLOSE_WAIT
状态,并且发送ACK
包确认收到请求,客户端收到数据包后会会转变状态为FIN_WIAT2
状态。随后服务端切换状态为LAST_ACK
并且主动发送FIN
包确认是否断开连接,客户端收到后切换状态为TIME_WAIT
并发送ACK
包给服务端,服务端收到后切换状态为CLOSED
正式断开连接,但是客户端此时并不会真正断开连接,处在TIME_WAIT
状态的客户端会在此继续等待一段时间,之后才会改变状态为CLOSED
。
为什么?
在tcp连接和断开过程中有很多疑问,为什么要三次握手四次挥手?为什么客户端断开连接还要等待?
为什么握手要三次才能建立连接?两次行不行?四次呢?
我们都注意到握手和挥手都需要客户端和服务端双方各发送一个SYN
和收到一个ACK
包才能算成功,这是为了防止网络传输过程中由于延迟,丢包等问题对数据造成丢失或延迟,因此客户端和服务端此时都要确认对方此时是在线的是可以连接的。我们可以想象这样的场景,在挥手过程中如果只有两次的话,客户端发送一个连接请求,服务端收到后就表示已经建立连接,这样是十分不安全的,如果这个连接请求因为延迟被耽误了很久,服务端接收到报文的时候客户端此时已经关闭了,那么这个连接就是失败的,但是服务端却表示成功了,那着肯定是不合理不安全的。因此服务端也必须要发送一个SYN
包确认此时客户端在线,需要服务端回复后才能确认连接建立成功,因此两次握手是不安全的,但是四次连接也是没有必要的,服务端在建立连接时确认客户端在线的SYN
报文是可以和回复报文一起发送的,因此三次即可。
为什么挥手要四次,三次可以么?
和建立连接同理,服务端和客户端需要各发送一次SYN
进行一次确认,但是这里为什么不可以将服务端的SYN
和ACK
放在一起发送?因为在连接关闭后,连同socket及其相应的缓冲区都会关闭,清空数据,那么如果此时缓冲区中还有信息的话信息也会一起丢失,因此客户端发送FIN
包后服务端不能将回应ACK
和确认FIN
一起发送,服务端会在用户接收缓冲区中所有数据后才会发送SYN
确认关闭请求,然后关闭套接字。
TIME_WAIT有什么用,为什么客户端关闭连接后要等待?要等待多久?
如果没有TIME_WAIT
,客户端在发送最后一次ACK
后就直接关闭的情况下,如果此时最后一次ACK
包文丢失,服务端很久并没有收到包文,它会再次重发最后一次FIN
包,如果此时客户端又重启了一个同端口的客户端准备再次建立连接,此时却接收到服务端发来的FIN
包就会陷入混乱,因此客户端在此时需要等待。至于等待多久此时需要2个MSL
的时间,一个MSL
时间表示数据包在网络中的生命周期,它最久只能在网络中存活这么久,如果还没有成功则会销毁这个包,也就是丢包,等待两个MSL
时间是为了让客户端自己发送的包以及以及服务端如果重发FIN
包都能够要么接收到,要么都消亡在网络中不对后续连接造成影响,等待2个MSL
时间已经足够保守极大减少之前连接会影响到之后连接情况。
保活机制
默认情况下,通信双方7200s没有数据往来,每隔75s向对方发送一个保活探测数据包,要求对方进行相应,若是得到相应则认为连接正常,若是连续9次没有得到相应,则认为连接断开,将socket状态置为CLOSE_WAIT
。
确认应答机制
TCP给发送的所有数据中的每一字节都进行了编号,假如说客户端首先给服务端发送了1-1000号数据,服务端如果接收到所有数据会返回1001ACK
表示前1000字节的数据都已经收到了,但是如果返回的不是1001或者压根没有返回,客户端就会知道中间有丢包,于是为了可靠传输则会重传,于是有了超时重传机制。
超时重传机制
如果在客户端发送了1-1000号数据后服务端返回了1001ACK
,此时客户端发送了1001-2000号数据,但是服务端给我们迟迟不见发送ACK
确认,于是客户端就会认为1001-2000号数据丢包则会重新发送1001-2000号数据。
当然客户端收不到ACK
也有可能是ACK
丢包,因此客户端会依然重发数据,此时服务端可能会存在大量重复数据,服务端会根据编号来辨别重复数据,并且如果客户端之后发送了2001-3000号数据,并且收到了3001号ACK
则客户端虽然没有收到2001号ACK
也会认为之前所有的数据都已经收到。
至于多久才算超时,Linux是以500ms作为一个单位以整数倍作为超时时间,以指数形式增长,当重传到一定次数后还收不到ACK
则认为网络出现异常,强制关闭连接。
确认数据有序和无误
超时重传机制和确认应答机制只能保证所有的包都不丢包,但是并不能保证包有序到达并且数据无误,这就要用到TCP头信息中的序号/确认序号来对包文进行排序,如果服务端先收到了2001-3000号包但没有收到1001-2000号数据服务端则暂时不会向用户表示自己收到了2001-3000号数据,会在此等待收到1001-2000号数据为止,并且收到数据后对包文进行排序和整理,以及通过校验和对包文数据进行校验。
滑动窗口机制
我们之前讨论都是在一次传输一次应答的情况下讨论的,但是这种通信方式过于缓慢,于是TCP是可以同时发送多个数据传输而不用非要每一条传输都接收ACK
才会发送下一条数据,这样大大可以提高性能。但是如果发送过快,而网络状况不好,全部丢包,服务端一条都收不到或者只能零零散散接收到一些片段,则会导致客户端大量的重传包文,十分影响效率,因此引入滑动窗口机制。
在介绍滑动窗口之前,我们还要知道一个概念即MSS
,最大数据长度,这个大小往往是在三次握手期间就已经确定下来的,标识一个包文最大只能传输这么大的数据。知道这个概念后,我们就要开始思考一个问题,TCP允许同时发送多个数据包,但是为了防止大量丢包该怎么做呢?没错,呢就是限制能够同时发送的数据包的数量,这就是滑动窗口成立的基础。
滑动窗口大小决定了能够无需等待确认应答的情况下可以继续发送数据的最大值。假如说我们在三次握手期间确定MSS=1024
,滑动窗口win=4096
,则表示客户端向服务端可以暂时不等待确认应答最多可以发送4096/1024=4
个数据包,假设一开始客户端窗口框住的数据区域为1-4096,并且同时发送4个数据包完毕后,客户端就必须停下来等待确认应答。如果我们的第一个包1-1024号数据已经成功发送并且收到了响应ACK
1025则表示一号包中数据发送成功,则客户端的窗口会向后滑动一个MSS
大小框住接下来的数据1025-5120(假设滑动窗口大小并没有改变),然后继续向服务端发送窗口滑动后出现的新的一个数据包,以此类推,直到发出所有数据包并接受所有响应,此时滑动窗口应该已经滑倒了数据末尾。窗口大小越大,网络吞吐量越高。
快速重传机制
因为滑动窗口机制我们知道了TCP允许同时发送多个数据包,但是如果其中发生丢包会怎样呢?假设窗口还是win=4096
,MSS=1024
我们的第一个包1-1024已经成功接收到了ACK
,说明服务端接收到了1-1024的数据,但是服务端此时迟迟收不到1025-2048的数据,尽管后面的数据都已经收到,此时服务端会连续不断发送1025ACK
提醒客户端我想要的是1025-2048数据。一旦客户端连续三次收到相同的ACK
则会知道对应的数据包丢失重新发送对应的数据包,这个机制叫做快速重传机制。
流量控制
同时得益于滑动窗口,我们可以控制传输数据的速度和大小,控制网络吞吐量。但是滑动窗口的大小并不是一成不变的,服务端在接收数据过程中如果缓冲区满了无法继续接收数据则会造成大量丢包情况,于是此时服务端可以通过改变窗口大小控制数据传输大小。服务端可以将自己缓冲区的大小放进TCP头部信息的窗口大小字段中,告诉客户端自己缓冲区大小,当缓冲区逐渐变满的过程中服务端每次发送ACK
都会重新调整滑动窗口大小,告诉客户端你传满点我收不下了,当大小为0时表示缓冲区已满,只有等用户从缓冲区提取信息后服务端才会加大窗口大小继续接收数据,由此做到流量控制。
拥塞控制
滑动窗口的大小并不是一开始就是最大的,因为要考虑到网络状况,一开始滑动窗口的大小只有1,用于试探网络状况,当确认网络状况没有问题的时候才会逐渐以指数级别增长速度继续增长,但是到达一个阈值后停止继续增长。如果传输过程中出现丢包需要重传情况则会将阈值缩小为原先的一半,并将滑动窗口置回1。这个过程用一句话形容就是慢启动,快增长,客户端想尽快将数据发送给对方,但是又要避免网络造成太大压力的折中方案。
延迟应答机制
客户端将数据发送给服务端后,服务端收到数据后并不会立刻回复ACK
,因为刚接收完数据存储在缓冲区中用户还没有取走数据,因为流量控制机制此时必然要调小窗口大小,这样会影响传输速度,影响网络吞吐量,于是服务端会稍微在此等待片刻再做应答,这个时间一般是200ms左右,操作系统不同时间也不同,而操作系统也许处理数据速度很快10ms就取走数据,此时缓冲区数据被取走缓冲区又恢复到无数据状态,也就不用调小窗口大小控制传输速度了,这样可以加大网络吞吐量,提高传输效率。
捎带应答机制
不光客户端可以给服务端发送数据,服务端也会给客户端发送数据,假如客户端发送数据你好呀
,服务端想要回复我还行
,服务端为了传输效率则会将客户端数据的ACK
和要发送的我还行
数据一起发送给客户端,这就是捎带应答机制。
TCP面向字节流
提供字节流服务
一个TCP套接字在被创建后,同时在内核中创建一个发送缓冲区和一个接收缓冲区,既可以读数据也可以写数据,这样的模式被称为全双工。在调用send
发送数据后会将数据先读取到缓冲区中,操作系统会将数据积累到合适的长度在合适的时机进行发送。在接收数据时也是从接收缓冲区中读取数据。正因为缓冲区的存在,使得TCP的数据传输十分自由,写一个数据可以一次写完也可以分段多次写,读也一样,正因为字节流这样灵活的服务导致了接下来的问题——粘包问题。
TCP粘包问题
首先要明确一点UDP不会粘包,因为在UDP头信息中就已经存储了数据大小信息,根据这个信息可以分辨出这个包的边界,因此不会出现取数据时取出下一个报文的数据的情况。但是由于TCP面向字节流传输灵活因此无法明确两个包之间的边界。
如何解决呢?明确两个包之间的边界。
要想解决粘包问题我们只能通过在应用层对数据的处理来明确数据边界了。有以下几种做法。
1、如果数据是定长的,例如传输的是结构或者类这样的块状结构,只需要每次都保证读取定长的数据即可。
2、如果是边长的数据,可以约定在数据头部添加一个字段,表示这个包数据的长度,获取这个包的话只读取这么长的数据即可。
3、也可以在不影响的数据下在数据末尾添加分割符,标识这个包数据的结束。
以上这些方法都可以区分包与包之间数据的边界,从而解决粘包问题。
TCP总结
以上讲了TCP这么多的机制,我们可以将其分类总结,TCP之所以复杂的原因是它为了保证可靠性但又尽可能的在提升效率。
可靠性
1、校验和。
2、序列号。
3、确认应答机制。
4、超时重传机制。
5、流量控制。
6、拥塞控制。
7、连接管理。
提高性能
1、快速重传。
2、滑动窗口。
3、延迟应答机制。
4、捎带应答机制。
TCP应用
基于TCP的应用层协议:HTTP/HTTPS/SSH/FTP/SMTP
。
TCP与UDP的对比
通过对TCP与UDP的介绍,我们现在已经很清楚TCP与UDP各自的特点及各自的优势。
对于TCP协议,适用于要求安全性高,传输可靠的情况下,例如文件传输,重要状态更新等情况。
对于UDP协议,适用于实时性高,速度要求快的情况下,例如视频传输,通信领域等。
如何用UDP实现可靠传输
UDP本身是不可靠的,可能会有大量丢包的情况发生,因此我们想要UDP实现可靠传输可以参考TCP在应用层实现TCP的一些可靠机制,例如在应用层利用序号确定包序,引入确认应答和超时重传保证不丢包等。