1. UDP协议
(一)UDP协议端格式
注意:
- 16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度
- 16位UDP检验和,能判断是否出现数据丢失等问题
- 如果校验和出错, 就会直接丢弃
UDP报头本质上也是一个结构体:
操作系统内有多个报头,需要管理 ----- 先描述再组织
(二)UDP 的特点
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接
- 不可靠: 没有确认机制, 没有重传机制(即使因为校验和出错丢失数据,接收方也不会收到任何信息提示)
- 面向数据报: 不能够灵活的控制读写数据的次数和数量(发送方发送次数就是接收方接收次数,发送方每次发送数据的大小,就是接收方接收数据的大小),也容易导致乱序(数据发送顺序混乱等等)
注意:
UPD 对于 TCP 发送数据的不同:TCP 的读取必须是每次读取一次完整的报文,而不是读取全部数据
(三)面向数据报
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
举例,如果用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个
字节,而不能循环调用10次recvfrom, 每次接收10个字节
(四)UDP的缓冲区
- UDP没有真正意义上的 发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作
- UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致(乱序问题)
- 如果缓冲区满了, 再到达的UDP数据就会被丢弃
- UDP的socket既能读, 也能写, 这个概念叫做 全双工
(五)UDP使用注意事项
我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64KB(包含UDP首部).
然而64K在当今的互联网环境下, 是一个非常小的数字.
如果我们的数据超过64KB就需要在应用层手动的分包, 将数据分成多个64K大小的数据,分多次发送
(六)基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议
2. TCP 协议
TCP全称为 "传输控制协议" , 要对数据的传输进行一个详细的控制
(一)TCP协议段格式
注意:
- 不管是服务端还是客户端,在 tcp 通信的时候,必须发送的是完整的报头
- 为了确保数据的可靠安全,发送方发送了一个完整报文,只有接收到了接受方的回应,这一个过程的发送数据才是可靠的
- 无法保证整个 tcp通信时的全过程都是可靠安全的,但是局部通信是可以保证的
- 发送方发送数据是会控制速度的(防止接收方的缓冲区满了,从而丢弃数据),主要取决于接收方接收缓冲区剩余空间的大小
- 发送方发送数据的顺序,可能和接收方接收数据的顺序不同,从而导致乱序,但是可以根据报文中的32位序号排号(TCP 给每个字节的数据都进行了编号,即序列号,本质就是数组下标,发送的数据块的最后一个字节的下标就是32位序号),恢复原先顺序
- 确认序号,填充的是:收到的报文的32位序号 + 1 ,代表确认序号之前的报文(不包括确认序号),接收方都收到了,下次发送请从确认序号的指定序号发送,这样就允许一些回应的报文丢失了
如 : 收到的报文32文序号是 1000,即 确认序号是 1001,代表0 - 1000 的数据,接收方都接收到了
- 设置两个序号 -- 32位序号和确认序号 的好处:可以避免一些情况:比如客户端发送了一个报文,服务端发送给客户端的报文中,既有回应,又有发送的数据,回应可以查看服务端的32位确认序号,发送的数据可以查看服务端的32位序号
- tcp 通信的时候,可以建立连接,发送数据,断开连接,这些都需要发送 tcp 报文,因此报文有不同的类型,而服务器要根据不同的报文类型,做出不同的行为,6 位标志可以判断报头的类型
- 16位紧急指针:标明该报头的重要数据的起始地址(没有偏移量),所以得到的数据是只有1个字节的,与标志 URG 配合使用
- 6 位标志(设置成1代表有效或者需要):
- URG: 紧急指针是否有效
- ACK: 确认序号是否有效 (即回应是否有效)
- PSH: 是否需要提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 是否需要对方重新建立连接。 我们把携带RST标识的称为复位报文段
连接是需要被管理的 --- 先描述再组织
要服务端和客户端都连接上了,才能互发信息,即如果只有一端没有连接,就需要重新连接(三次握手中的数据可能会有丢失现象)
- SYN: 请求建立连接。我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了
(二)确认应答(ACK)机制
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据,下一次你从哪里开始发
(三)超时重传
发送端发送数据,只有得到回应,才能确定数据是被接收方接收的,但是如果迟迟不回应,有很多原因:发送的数据被丢包,得到的回应的丢失,发送的数据被接收但是仍在接收端的等待队列中等待回应,因此,在一段时间得到不回应的时候,就需要发送端超时重传
发送数据丢包:
回应丢失:
定义超时时间:
- 超时重传的时间是动态的,跟网络的速度有关
- 时间设定以 500毫秒为单位
动态计算超时时间:
- 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
(四)连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
三次握手的必要:
- 确保 tcp 通讯的顺畅(双方都能发送和接收数据),即全双工通路流程
- 奇数次握手,可以确保一般情况握手失败的情况下,连接成本是由客户端承受的,使得服务端的压力没有那么大
三次握手的作用:
- 建立连接
- 协商起始序号
- 协商双方的接收缓冲区大小
四次挥手:
注意:
- 只有两方都发起断连接并且接收到了回应,才是真正的断了连接
- 断开连接意味着,双方都没有数据发给对方,所以需要断两次
- 如果只有一方断了连接,仍然能接收到另一方发送的数据,因为它只是写端关闭了,不能发送数据(不代表不可以发送报文,报文可以没有有效载荷)
完整 tcp 通信流程图:
注意:
- 连接建立成功与 accept 函数无关,三次握手都是双方操作系统自动完成的
- 当三次握手之后,服务端和客户端的状态都是 ESTABUSHED,此时服务端的连接完成,并且连接是被管理在全连接队列里
- 当服务端收到客户端的 SYN 时,此时的状态是 SYN_RCVD,它也会维护一个队列是半连接队列,而只有服务端的状态变成 ESTABUSHED 时,才会把半连接队列里的一些连接放入全连接队列里
- listen 函数 的第二次参数 backlog 表明的是最多允许 backlog + 1 个节点在全连接队列里(所以服务端的连接是有限的)
- 服务端不会长时间维护处于半连接队列的客户端,最后有可能直接被释放掉
- 半连接队列维护的节点个数 和 全连接队列维护的节点个数 都是有限的,而全连接维护个数与 listen 函数有关,如果达到上限个数,当再有客户端发起连接的时候,三次握手中的最后一次 ACK被客户端接收到了,但是不做任何反应(状态不会变成 ESTABUSHED)
- 主动断开连接的一方,最后一次发送完 ACK 后,它的状态 TIME_WAITE 会维持一段时间(此时不算完全断开,因此端口号仍然属于这个进程,如果是主动断开的是服务端,再次 bind 可能会发送错误,因为端口号不能被多个进程绑定),才会变成 CLOSED 状态
(五)理解 CLOSE_WAIT 状态
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.
(六)理解 TIME_WAIT 状态
好处:
- 让通信双方把历史数据得以消散
- 让断开连接的4次挥手有容错性(如四次挥手中的最后一次 ACM 被丢失,有可能还有时间可以重新发送)
注意:
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL的时间后才能回到CLOSED状态.
- 使用Ctrl-C 终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口
- MSL在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s
- 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值
TIME_WAIT的时间是2MSL的原因:
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话 ,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发 ACK)
(七)流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制
注意:
- 第一次发送的时候,如何确认发送数据的大小是合理的 -- 3次握手中,双方交换了报文,知道对方的接收能力
- 流量控制,给 tcp 通信带来了可靠性(丢包减少),间接也提高了效率(超时重传少了)
控制过程:
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度
- 如果接收端缓冲区满了, 就会将窗口置为0 , 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
接收端把窗口大小告诉发送端,根据TCP首部中有一个16位窗口字段(就是存放了窗口大小信息),16位数字最大表示65535, 但是TCP窗口最大不是65535字节
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位
(八)滑动窗口
每一个发送的数据段, 都要给一个ACK确认应答,收到ACK后再发送下一个数据段
这样做有一个比较大的缺点, 就是性能较差, 尤其是数据往返的时间较长的时候
所以真正通信的策略是:一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时
间重叠在一起了)
现在又有的一个问题是,如果发送报文丢失,就会导致超时重发,那么发送端是如何维护需要超时重发的数据呢?
注意:
- 滑动窗口是发送缓冲区的一部分
- 滑动窗口的范围是和对方接收窗口的大小有关
- 发送缓冲区的区域划分:通过指针/下标来进行区分
从目前看来,滑动窗口的大小不能超过接收方的接收大小,即窗口大小(具体规定在拥塞控制)
深刻理解滑动窗口原理操作,如图:
情况一:如果接收方接收到了报文,回应 ACK 丢失
情况二:如果发送方的报文丢失
注意:
- 这种情况下,还涉及到了快重传:如果三次确认序号都是一样的(说明出现丢包),此时不采取超时重传,而是发送端立刻补发报文,这种策略就叫快重传
- 快重传提高了效率,减少了重传次数
- 快重传和超时重传都是必要的,当等待发送的报文少于三个,如果发送端发生报文丢失,此时采取的是超时重传
滑动窗口移动和大小变化问题:
- 滑动窗口的两个指针都是向右移动的
- 滑动窗口的大小是动态变化的
- 滑动窗口的大小可能为0,意味着没有带发送的报文,或者接收方的窗口大小为0(应用层没从内核里读取数据)
- 滑动窗口像一个环形的数组,在发送缓冲区中不会越界
(九)延迟应答
为了提高 tcp通信 效率,窗口大小要尽可能大,而使得窗口大小可能增大,我们有时候采取延迟应答,即接收端接收到报文后,不会立刻发送ACK应答,而是晚一点(这个动作只是在 tcp协议 这一层发生,与应用层无关,应用层仍然可以读取数据)
延迟发送的时间是远小于超时重传的时间,在延迟的时间内,有可能应用层读取了数据,使得窗口大小变大
采取延迟应答的可能时期:
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异, 一般N取2, 超时时间取200ms
(十)捎带应答
捎带应答:
报文在 ACM应答 的同时,也携带了数据
好处:
减少了报文的个数,提高了效率
(十一)阻塞控制
发送报文的过程出现了问题,不仅仅可能是双方主机的问题,也可能是网络的问题
发送报文丢失情况:
- 少量报文丢失 ---- 正确情况
- 大量报文丢失 ---- 网络出问题(有很多原因,可能是硬件,也可能是发送报文数据量过大,引起阻塞)
判断报文丢失是网络的问题:
如果出现大量报文超时重传,则表明大量报文丢失,网络出现问题
注意:
- 如果网络出现了问题,发送端的主机基本都能识别出来(少量的发送端发送报文少,可能不会意识到网络问题),达成一种共识,并进行相应措施
- 如果网络出现了问题,发送主机不能再像之前一样,把所有超时报文都重传(如果都超时重传,网络中的报文数量只会更多,仍然会有大量报文丢失),而是试探网络传输中可以接收的报文个数
试探网络传输中可以接收的报文个数,举例:
可以一开始只发送一个报文,得到应答之后
尝试发送两个报文,都得到应答之后
再尝试发送 n 个报文(指数增长),以此来判断此时网络中可以接收的报文的最大个数(拥塞窗口)
注意:
- TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
- 像上面这样的拥塞窗口(是动态的)增长速度, 是指数级别的,"慢启动" 只是指初使时慢, 但是增长速度非常快
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值 ,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 发送数据个数不可能一直增长,会受到限制
纠正滑动窗口的大小:
left = 确认序号
right = min(窗口大小,有效数据,拥塞窗口)
(十二)粘包问题
粘包问题中的 "包" , 是指的应用层的数据包,因为数据从内核里面是全部读出来的,导致报文可能不完整,多出来的数据包就是发送了粘包问题
解决粘包问题,需要明确每一个报文之间的边界
解决粘包问题:
- 定长报文
- 特殊字符分隔
- 使用自描述字符 + 定长报头
- 使用自描述字符 + 特殊字符
(十三)TCP异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别
机器重启: 和进程终止的情况相同
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset重新连接,并且接收方也会定期询问对方是否还使用写入操作,如果对方不在 , TCP自己也内置了一个保活定时器 , 也会把连接释放