协议解析
协议格式
协议特性
编程影响
UDP协议解析
协议格式:
首先我们来看看udp在文件(/usr/include/netinet/udp.h)中是如何组织数据的:
可以观察到,一个完整的udp数据格式由下面几部分组成:
16位源端-对端端口:描述通信双方
16位报文长度:描述报文长度。
由于它是16位的,最多也是一个1111 1111 1111 111,即最大为65535
也就决定了,udp长度不能超过64k,一个udp报文的长度最多不能超过 64k-8
16位校验和:用于校验接收的数据是否与发送的数据完全一致
二进制反码求和算法:
①将报文的每个字节取反相加,得到一个数。这个数就是校验和。在计算之前,校验和默认为0,计算之后将这个数放进去。
②对端收到udp数据之后,对报文从头到尾进行二进制反码求和,如果这个数是0即认为收到的数据和发送的数据是一样的。
③报文每个字节加起来的数字总有大于65535的时候,而校验和是16位2字节的整形,意味着有字节越界了,这时,将高位月结的字节数截取出来,在与低16为相加
UDP协议特性:
无连接,
不可靠,
面向数据报
解析:
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
无连接:通信时不需要进行连接,只要知道对方的ip地址就可以发送数据
不可靠:不保证数据有序、安全到达接收方
面向数据报:有最大长度限制,并且是整条交付
体会整条交付:udp数据接收时,对头部进行解析,得到要取的数据为数据报长度-8,这时就会去缓冲区中取这么多数据。
编程影响:
传输层协议的特性,影响着应用层的操作方式。这需要我们程序员针对传输层不同协议的特性,来制定特定的数据传输方式。
影响:
如果应用层要发送的数据过大(超过64k),就需要在应用程进行分包操作,将数据分成一个一个64k以内大小的包,按顺序传输
如果上层进行了分包操作,数据在传输的时候,由于网络原因或者其他原因,有可能造成后来的数据先到了,缓冲区数据混乱,为了避免这种情况,传输层就需要对数据进行包序管理
接收方recvfrom的接收缓冲区也要足够大,至少要能完整取出一条数据
TCP协议解析
传输控制协议
协议格式:
在/usr/include/netinet中打开tcp.h发现里面包含的数据很多,大概能看到的是它使用条件编译,对TCP头部做出了很多解释,我们来画图理解一下TCP的大概组成:
16位源端-对端端口:描述通信两端
32位序号-32位确认序号:用于实现包序管理
4位报头长度:以4个字节为单位,4位表示最大数字15,
报头长度为1表示有4个字节,2表示8个字节
15x4=60,即报头长度最大60个字节
若没有数据,一个tcp报文也有上图的前5行的内容,共有20个字节
即TCP报头长度最少20个字节,最多60个字节
6位保留
6位标志位:用于标注当前报文的类型
URG:紧急指针有效位,表示紧急指针字段有没有效
ACK:确认应答报文
PSH:告诉对端数据应该尽快被取出,不在缓冲区中排队
RST:重置连接,用于复位出错的连接,拒绝非法数据访问
SYN:建立连接请求标志位
FIN:断开连接请求标志位
16位窗口大小:用于实现滑动窗口机制,进行流量控制
16位校验和:校验数据一致性
16位紧急指针:表示带外数据的位置,带外数据一般需要立即处理
0~40字节选项数据:
协议特性:
面向连接
可靠传输
面向字节流
解析:
面向连接:通信双方建立连接之后才能进行通信
确保通信双方都有收发数据的能力
面向连接
三次握手建立连接:
在讲套接字时,说到服务端发送连接请求,服务端监听套接字接收到请求之后,创建新的套接字获取新建连接。
我们来画图细致描述一下这个过程:
问题:
为什么是三次握手,而不是两次或四次?
总结:两次不安全,四次没必要。
前提:建立连接是建立在通信双方都知道对方具有数据收发能力的基础上的。
服务端第一次接收到客户端的SYN报文之后,不能确定客户端是否具有数据收发能力的原因是:预防客户端发送一个请求就没影了。所以必须再发送SYN获取对方的状态。这一步必不可少。(问什么不能两次)
四次的情况就是服务端ACK和SYN分别发送,我们知道发送请求实际上是发送报文头,每一个报文头中都有各种报文类型标志位,只需要将对应的标志位置1就是发送对应请求了。完全可以在一个报文中将ACK和SYN对应位置1即可。(四次没必要)
握手失败是怎么处理的?
SYN泛洪攻击:有一种网络攻击手段就是这种泛洪攻击,同一时间向服务端发送很多SYN请求却不建立连接,希望导致服务端资源耗尽崩溃。
TCP针对这种攻击的处理方式就是,
1、我等待超时就发送RST重置连接,释放资源,要想再连接你就重新发送请求。
2、防火墙,如果同一时间收到来自相似ip的很多请求,就把这个ip加入黑名单,不接受来自这个ip的连接请求。
失败情况和默认处理:
第一次握手失败:第一次客户端发送的SYN请求丢包,服务端不需要有什么处理,因为就没有感知到有人传来请求,客户端的处理方式是:过一段时间之后重传。
第二次握手失败:服务端收到客户端的SYN请求,进行ACK和SYN回复丢包。这种情况两边都受影响,我们分别说说客户端和服务端的处理方式:
若这个ACK+SYN请求丢失,
- 客户端等待第二次握手超时,就会怀疑,是不是我第一次握手发送的SYN请求丢失了。就重新向服务端发送SYN请求进行第一次握手。
- 服务端等待第三次握手超时,会怀疑这个客户端是不是恶意攻击。这时就不会重新发送ACK+SYN请求,而是等待超时之后向客户端发送RST重置连接报文,关闭新建的套接字释放资源。如果你还想跟我建立连接就重新请求吧。
第三次握手失败:第三次握手客户端回复的ACK丢失,只会影响服务端,就是服务端等待超时。处理方式就是向客户端发送RST重置连接报文,关闭套接字释放资源。
四次挥手断开连接:
在理解TCP通信断开连接之前,我们来了解一个接口:
close(sockfd) 关闭读写端并释放资源
shutdown(int sockfd,int how) 按照指定方式关闭socket断开连接
socked 套接字操作句柄
how
SHUT_RD 关闭读端 SHUT_WR 关闭写端 SHUT_RDWR 关闭读写端
画图理解过程:
为什么挥手要四次?
首先我们需要知道,上层调用到close才会发送FIN包。主动关闭方调用到close发送FIN包,有可能被动关闭方还没有调用到close,还有可能要发送数据,所以FIN只能是确定自己不再发送数据了,而必须还要具有接收来自对方数据的能力。
主动关闭方调用到close/shutdown发送FIN断开连接请求,表示不再发送数据。被动关闭方收到之后发送ACK确认回复。
当被动关闭方上层调用到close/shutdown,才会向主动关闭方发送FIN包,表示我也不再发送数据了。
ACK回复之后,双方都不再发送数据,就没有通讯的必要了,通讯就结束了,此时就可以断开连接。
因此默认的是被动关闭方发送ACK包和FIN包需要独立进行。
四次挥手过程中的状态分析:
一台主机上出现了大量的CLOSE_WAIT状态连接是什么原因,是如何解决的?
一台主机接收到FIN包,进行ACK回复之后,状态就会置为CLOSE_WAIT,等这台主句做完自己的事情遇到close/shutdown向对方发送FIN包,才会将状态置为LAST_ACK。
因此出现大量CLOSE_WAIT的原因就是,收到FIN包发送了ACK但是一直没有调用到close发送FIN包,很有可能就是代码中没有对连接断开的套接字进行close操作。
解决方案就是:检查代码是否出现问题。
TIME_WAIT状态有什么用?
主动关闭方在收到对方的FIN包之后,进行最后一次回复之后就会进入TIME_WAIT状态。
/假设主动关闭方最后一次回复的ACK丢失,导致的结果就是被动关闭方接收不到这个ACK,被动关闭方就会怀疑是不是对方没有接收到我上面发的FIN包。处理的方式就是,重新发送FIN包等待回复。因为一些原因这个FIN包只会重传一次。/
假设主动关闭方在回复最后一次ACK之后没有进行TIME_WAIT而是直接关闭套接字释放资源,之后又有新建的套接字占用了这个刚刚关闭的IP和端口,但是此时被动关闭方还是处于等待ACK状态,等待超时之后,就会向对端重新发送FIN断开连接。这种情况下,导致的一种可能就是,新建的套接字上来啥也没干就收到一个FIN包关闭连接请求,这不懵逼了。
而TIME_WAIT的作用就是等待这些有可能重传的FIN包。
TIME_WAIT等待2MSL个时间,MSL(Maximum Segment Lifetime)就是报文最长存活时间,就是等待这两个报文重传FIN和重传回复ACK。确保本次通信的数据都消失在网络中。
一台主机上大量出现TIME_WAIT连接,是什么原因,如何解决?
TIME_WAIT是主动发关闭方收到FIN请求最后一次发送ACK回复之后的状态,一台主机大量出现TIME_WAIT原因就是大量主动关闭套接字,常见于爬虫客户端主机。
解决方案就是:
1.将TIME_WAIT等待时间设置更短
2.设置套接字选项,开启地址复用
int setsocketopt(int fd,int level,int optname,int* optval,int* optlen)
level:SOL_SOCKET
optname: SO_REUSEADDR 设置地址复用
常用于服务端,由于一个端口两个套接字,复用之前需要考虑一个数据发送过来到底是谁接收,主要就是解决由于关闭套接字出现TIME_WAIT
爬虫就是,打开一个连接,再打开很多其他连接,最终找到自己想要的连接,类似于我们在百度查找一个东西,不一定一次能找到,要找多次。
保活机制
使用TCP通信中,连接双方如果长时间没有进行数据通信,套接字的资源占用着就有点浪费,服务端每隔一段时间就会向客户端发送一条保活探测数据报,要求客户端回应,如果发送了多次都没有收到回复,服务端就认为连接断开,就会关闭套接字释放资源。
长时间:最晚一次通信后的7200s
每隔一段时间:75s
默认保活探测报文次数:9
以上数据为操作系统默认数据,可以使用setsocketopt()自定义设置
连接断开在上层的表现:recv返回0/send出发SIGPIPE异常
可靠传输
安全有效传输
保证数据可靠到达对端,并有序交付。
面向连接:
确保双方都有数据收发的能力
确认应答机制:
接收方对接收到的每条数据都要进行回复,发送方收到确认回复认为传输成功,否则认为数据丢包。
超时重传机制:
发送方等待超时没有收到确认回复,则对数据进行重传。
默认等待200ms,但是是根据网络状况动态变化的,不固定
时间太长?快速重传协议
协议字段中的序号和确认序号:
进行包序管理实现数据有序交付
seq:本条数据的起始序号
ack:对方发送数据的确认序号,告诉对方自己接收到了数据
seq、ack和ACK标志位不一样,总体过程大概就是:
三次握手建立连接中,双方协商起始信号,确认信号就是起始信号加1
数据通信阶段,确认信号是对方的起始序号加数据长度告诉对方该序号之前的数据已经全部接收到了。
校验和字段:
检验数据一致性,如果不一致,就丢弃数据,要求重传
避免无谓丢包
丢包:
通信中不可避免出现丢包的情况,常见的两种就是:
1.因为发送方发送数据过多,接收方来不及处理,缓冲区溢出时产生的丢包
2.因为网络状态不好产生的大量丢包
-
滑动窗口机制
依赖于协议字段中的窗口字段实现,避免因为发送方发送数据过快,接收方来不及处理,缓冲区溢出之后产生的丢包。 原理:接收方收到数据之后就会应答,这时候会通过窗口大小字段告诉发送方最多再给自己发送多少数据。 大小:窗口大小不能大于接收缓冲区中剩余空间的大小 实现:通信双方分别有维护一个发送窗口和接收窗口, 在三次握手阶段,双方通过数据包头会协商一个MMS最大报文段长度(MTU-40)。通讯双方发送一个自己能接受的最大报文长度,取其中较小的一个作为MMS 发送方窗口:记录发送方所能发送的数据大小和序号 后沿:发送起始信号,收到确认回复之后向前移动 前沿:结束发送序号(位置),根据窗口大小变化 前沿减后沿不能大于对方回复的窗口大小 接收方窗口: 后沿:接收起始序号,受到起始序号的数据向前移动 前沿:接受的结束序号,根据窗口大小和剩余缓冲区空间向前移动 几种不同场景的丢包预防机制: 停等协议:发送数据之后,收到确认回复之后才会发送下一条。适应于网络状况极差的情况。 回退n步协议:哪一条数据丢了,从丢失的这一个数据开始,往后的数据全部重传。网络状况不好的情况下,丢失一条数据,有可能后续也有数据丢失了,为了方便,就回退n步重传。 选择重传协议:那条丢了就重传哪一条,适用于网络状况极好的场景
-
拥塞窗口机制
解决因为网络状况不好产生的大量丢包 原理:发送方维护了一个拥塞窗口,用于限制当前所能发送的数据量 机制:慢启动,快增长的形式进行传输控制 理解:就是每次发送数据的时候 第一次发送1条数据,收到回复,网络状态良好 第二次发送2条数据,收到回复,网络状态良好 第三次发送4条数据,收到回复,网络状态良好 第四次发送8条数据,收到回复,网络状态良好(发送数据的量呈指数增长) 第五次发送16条数据,收到回复,有三条没收到,网络状态不好。 第六次发送1条数据,收到回复,网络状态良好 第七次发送2条数据,收到回复,网络状态良好 ………… 网络良好时,发送数据的大小并不能无限增长,最大不能超滑动窗口大小,滑动窗口的大小是一个阈值
提高传输性能
TCP为了实现可靠传输,牺牲了传输性能,而在传输中,有些性能的损失是没有必要的。
下面介绍几种性能挽回方案:
-
快速重传协议:
概念:在传输过程中,若接收方接收到的数据并非是接收窗口的前沿数据,则有理由认为该条数据丢失,这时每收到一条数据就会发送一个前·沿数据的重传请求,一旦发送方连续收到三条重传请求则直接对这块数据进行重传。
作用:丢包后不用完全等待超时了,减少了丢包后的超时等待时间
画图理解:
为什么是三次之后重传? 三条是避免数据延迟到达情况,发了三条都没收到,就不考虑延迟了。 -
捎带应答机制:
受确认应答机制的影响,接收方对收到的每一条数据都要进行确认回复,然而回复的最主要信息就是确认序号,每一条确认回复至少是一个空报头的传输。
如果收到数据之后,刚好要给对方也发送数据,则将即将要发送的数据和确认回复放到一起进行发送。 -
延迟应答机制:
接收方对接收的每一条数据都会进行确认回复,其中还
包含当前的窗口字段大小,如果立即进行回复,窗口大小是不断减小的(前沿不动,后沿一直向前),
因此延迟进行回复,有可能上层将数据取出,维持窗口大小(数据取出之后,前沿向前移动),通过这种方式保证传输吞吐量不会降低。 -
延迟发送机制
tcp传输过程中,如果每次send的数据都直接封装报头进行发送,若发送的数据很小,但是次数比较多,就会增多IO次数,降低效率,这时没有必要的。
所以延迟发送,将数据在发送缓冲区堆积起来,在合适的时候进行发送,减少了IO次数,提高效率。
延迟发送由一种nagle算法支撑,是默认开启的,可以通过设置过套接字选项关闭的。关闭之后,send直接发送。
面向字节流
可靠的、有序的、双向的、基于连接的传输方式
传输时,并不限制上次recv和send发送或接受的数据大小
优势:相较于面向数据报来说,传输更加灵活
缺陷:产生粘包问题
粘包问题:
将多条数据当作一条数据进行处理
产生本质原因:
TCP发送端send发送数据的时候, 并不会将数据直接进行发送,而是先将数据放到发送缓冲区中,选择合适的时候从缓冲区中取出合适大小的数据进行封装发送。
接收端recv接收数据的时候, 并不会一条一条的向上交付,而是会将接收到的数据放到接收缓冲区中取出指定长度的数据进行交付。
而TCP并不会对边界进行处理,这就导致,有可能多条数据当作一条数据进行处理。这就是粘包问题。
对比:udp接收方在接收数据时,将报头和数据一起放入缓冲区,从缓冲区中取出数据时,就可以对头部进行解析,取出特定长的数据。
解决方案:
程序员在应用层进行数据得边界管理,解决的方式有:
特殊字符为间隔 - 缺点是数据中如果也有特殊字符就需要转义处理
数据定长 - 缺点是大量传输短小数据时,需要填补空位,这是资源的的浪费。
使用TLV格式数据
很常见的格式。比如udp数据发送时将报头和数据一起放入接收缓冲区,http数据包头和数据一起放入缓冲区。
就是在应用程设置TCP报头长度,取的时候先取出数据头部,根据头部里面的长度信息Content-Length再取出特定长度的数据。
编程影响
1.连接断开:recv返回0,send触发异常
recv一旦返回0,就要考虑套接字描述符的回收
send因为会触发异常导致程序退出,若不想退出就需要信号处理
2.tcp传输存在粘包问题,需要在应用层对数据进行边界管理。udp由于缓冲区中有报头,就不存在粘包问题。
总结
tcp协议与udp的区别:
实现上的差别:
特性上的差别:
应用上的差别:
tcp如何实现可靠传输?udp如何实现可靠传输?
tcp在传输层通过面向连接,确认应答机制,超时重传机制还有tcp报头校验和字段,序号和确认序号来实现数据安全有序到达接收方,通过滑动窗口机制和拥塞窗口机制避免丢包问题实现安全可靠传输。udp在传输层并没有实现可靠传输,如果需要使用udp进行可靠传输,就需要程序员在应用层对一些可靠传输方法进行实现。