纯属个人见解,如有错误欢迎指正
目录
传输层实现的功能
传输层是在网络层提供的主机和主机间通信的基础上实现两主机的进程与进程的通信。(通过端口号来标识唯一进程)
根据协议的不同又分成了可靠的传输和不可靠的传输
常用的两个协议中:TCP提供了可靠的传输而UDP提供的是不可靠的传输(尽力而为的传输)
TCP
TCP提供的可靠传输是指:
- 无错误 依靠 校验和 重传机制
- 无丢失 依靠 超时重传、快重传
- 无乱序 依靠 序列号 确认号 滑动窗口 累计确认
- 无重复 依靠 序列号 确认号 滑动窗口 累积确认
- 拥塞控制 依靠 滑动窗口 拥塞窗口 慢启动 拥塞避免 拥塞发生 快重传 快恢复
- 流量控制 依靠 滑动窗口 流量窗口
三次握手与四次挥手
众所周知,TCP是面向连接的,在我看来面向连接的最大作用是通过面向连接提供了序列号和确认号这一机制,这一机制是提供可靠传输的基础,也就是说面向连接是可靠传输的基础
三次握手的过程(结合系统调用)
- 客户端调用系统调用connect来发起连接(之前客户端需要打开socket),connect调用后内核向目标服务器发送SYN请求,客户端进入SYN_SENT状态,SYN请求中应该会包含seq序列号、窗口大小、并且标志位SYN置1
- 服务器接收到SYN报文后(在这之前服务器需要打开socket、bind绑定源IP和源端口、listen转换为监听套接字同时创建半连接队列和全连接队列),将连接放入半连接队列,再向客户端发送SYN_ACK报文(由于SYN和ACK一起发送所以只需3次握手),之后服务器进入SYN_RECV状态
- 客户端接收到SYN_ACK报文后从connect函数中返回,向服务器发送最后一个ACK(可以携带数据)报文并进入establish状态
- 服务器接收到ACK报文将连接已完成的连接放入全连接队列,再从全连接队列中取出一个已完成三次握手的连接(在此之前需要调用accept函数),accept函数返回连接的文件描述符。
- 三次握手就此完成
为什么要有三次握手?
连接的建立依靠三次握手,三次握手的最大作用是防止旧的重复连接初始化造成混乱(其实也是依靠序列号和确认号来实现的)。
假设在客户端发送完第一个SYN请求后机器出现故障重启了,重启完成后又重新发送了一个序列号不一样的SYN请求,之后服务器就会收到两个seq不一样的SYN请求,这时服务器无法判断哪个请求是有效的(或许都是有效的)只能向这两个请求分别发送相应的SYN_ACK报文,客户端在受到错误的SYN_ACK报文(ACK与seq不匹配)后立即发送RST报文通知服务器连接有误,从而避免了错误连接的初始化,而受到正确SYN报文后向服务器发送ACK报文确保可以正常建立连接。
如果没有三次握手只有两次握手的话,显然两次握手无法确保客户端和服务器双方的得到了对方约定的序列号。假设第一次握手时客户端发送了SYN报文和序列号,第二次握手时服务器发送了SYN_ACK报文,如果没有第三次握手的话服务器根本无法得知客户端是否真的收到了SYN_ACK报文,就无法确保可以同步序列号
四次挥手过程(结合系统调用)
- 主动发(一般是客户端,因为要进入timewait状态)起方调用close调用,此时向被动方发送FIN报文,在这之后主动方不能再发送消息只能接收消息进入FIN_WAIT_1状态
- 被动方接收到FIN报文后(也就是从read中读到0个字节)调用close函数,发送ACK报文告知主动方已经收到FIN请求,并进入CLOSE_WAIT状态等待被动方发送完所有消息(这是为什么挥手需要四次而不是三次的原因,因为ACK和FIN不一定能一次发送),主动方收到ACK报文后进入FIN_WAIT_2状态,等待被动方发送FIN报文
- 被动方发送完所有信息后发送FIN报文,进入LAST_ACK状态等待最后一个ACK报文
- 主动方收到FIN报文后发送ACK报文并进入TIME_WAIT状态,被动方收到ACK报文后从close返回并断开连接
TIME_WAIT状态
TIME_WAIT需要主动方等待2MSL(两个最大往返时间),原因有两个:
- 最后一次发送的ACK可能丢失
- 为了让可能阻塞在网络中的FIN报文能顺利丢弃(2MSL正好能顺利丢弃),避免就的报文对新连接产生影响。
滑动窗口
TCP协议是一个停等协议,需要确认接收方收到消息后才能继续发送,为了提高协议的效率引入的滑动窗口的概念,每次发送方可以发送窗口大小的数据(流水线的形式),接收到窗口内序号最低的数据后移窗口(窗口的大小取拥塞窗口和流量窗口的最小值)
由于移动窗口的存在使得TCP一次可以发送多个数据,TCP的滑动窗口移动采用累计确认的方式进行,通过接收方回复收到的seq+1来做回为确认号,代表当前需要接收序列号为seq+1的数据,如果收到已经收到的seq之间丢弃,收到当前需求的seq之后的数据时(乱序数据)会先缓存起来,再次发送当前需求的seq来申请当前需求的seq
累计确认既不是属于RGB也不属于SR像是两种的结合体
超时重传和快重传:
当发生超时重传时,证明网络可能存在堵塞,会将当前窗口减为1,门限改为发生超时重传的大小的一半,由于是累计确认,所以会重传当前未确认的所有数据。如果再次重传失败则将超时时间间隔设为先前值的两倍,再次重传,直到达到最大重传次数后就关闭连接
当发生快重传时(也就是收到三个相同的ack时),证明可能不是网络堵塞只是单纯的丢包,这时,会将窗口减为一半,重传ack所需求的数据,再进入快恢复状态
流量控制
流量控制的存在是为了减少不必要的数据发送。
在系统编程时我们知道,每个socket连接描述符都有内核的缓冲区,当我们收到TCP传来的数据时是,数据并不是直接发送到应用层而是先存放到内核的接收缓冲区,当我们应用层从内核中取出数据的速度比TCP发送数据的速度慢时,就会接收内核缓冲区就会不断变小直到为0。为了能够减少丢包的情况发生,必须有某种机制来根据内核的接收缓冲区大小来控制发送方窗口的大小,这种机制就是流量控制机制。
流量控制的实质就是根据内核的接收缓冲区大小来控制发送方窗口的大小,依靠接收方回复消息时发送的窗口大小来动态调整发送窗口大小,注意当窗口减为0时,发送方会定时发送检测消息来确认是否可以继续发送。
拥塞控制
拥塞控制的存在是为了避免在网络拥堵时还不断向网络中发送数据,导致更多的数据丢失,需要引发更多的超时重传这样会导致网络彻底瘫痪的死循环。
拥塞控制四大步骤:
慢启动
首先将拥塞窗口置为1,每收到1个ack就将窗口扩大一位(也就是指数增长),当到达慢启动门限时进入拥塞避免状态。
拥塞避免
当到达慢启动门限时进入拥塞避免状态,在拥塞避免状态时每收到1个ack就将窗口扩大(1/窗口大小)(也就是线性增长),直到遇到超时重传或快重传
拥塞发生
当遇到超时重传时说明网络拥堵就进入拥塞发生,此时将窗口大小置为1,慢启动门限设为发生拥塞时的窗口的一半,然后再次进入慢启动状态
快速恢复
当连续收到三个相同的ACK时,说明可能丢包,此时发送窗口变为原来的一半,慢启动门限变为新的窗口大小,然后进入快速恢复状态,拥塞窗口变为慢启动门限+3,如果再收到重复的ACK,就将窗口加1,直到收到新的ACK就将门限设为快重传前,再次进入拥塞避免
字节流与粘包
TCP是以字节流的形式来传输的,消息与消息之间没有边界,并且由于拥塞窗口和流量窗口的存在,我们调用write系统调用来发送信息时并不是就向对方主机发送了wrie调用所返回的字节 长度,write调用只是将消息发送到内核缓冲区,至于内核什么时候发送,发送多少字节给对方主机,完全是未知的。因此主机收到TCP消息时是无法判断消息是哪一条消息,这就是TCP粘包问题。
解决办法:
- 消息定长(几乎不用,灵活性太差)
- 特殊字符分割(参考HTTP)
- 自定义结构体(参考UDP,先一个指定的字节数来存放数据长度,后面再接数据)
UDP
UDP提供的是尽力而为的服务,不一定可靠
这里的不可靠可以对应TCP的可靠服务:
1、保证无错误但是发生错误也不会重传 依靠 校验和
2、不保证无丢失 因为没有确认号
3、不保证无乱序 因为没有序列号、确认号
4、不保证无重复 因为没有序列号、确认号
5、没有拥塞控制 因为没有连接无法约定窗口
6、没有流量控制 因为没有连接无法约定窗口
优点:无连接通信快
缺点:不可靠、无拥塞控制会占满带宽
数据包协议
UDP协议不同于TCP协议,属于数据包协议,每次数据都是以一个数据包的形式发送(没有发送缓冲区),由于UDP头部是固定的8个字节,使用报文UDP头部长度字段的内容再减去8个字节就能得到报文数据的内容。