传输层里面比较重要的两个协议一个是TCP一个是UDP
TCP和UDP的区别
TCP是面向连接的,UDP 面向无连接的,面向连接的协议会先建立连接,例如TCP会三次握手,
所谓的建立连接,是为了在客户端和服务端服务连接而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。
例如TCP提供可靠交付,通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达,IP数据包是没有任何可靠性保证的,而TCP连接就能保证IP数据包的准确性,而UDP继承了IP包的特性,不保证不丢失 ,不保证按序到达。
再如,TCP是面向字节流的,发送的时候发的是一个流没头没尾,IP包是一个个的数据包,之所以变成了流也是TCP自己的状态维护做的事情;而UDP继承了IP的特性,基于数据报的一个个的发和收
有TCP 是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况 调整自己的行为,看看是不是发快了,要不要发慢点。UDP就不会,应用让我发,我就 发,管它洪水滔天。
所以TCP是一个有状态的、有脑子的服务;UDP是无脑的、无状态的服务。
UDP
UDP包的头结构
当包传输给了目标机器之后,发现MAC地址匹配,于是把MAC头取下来交给IP层处理,发现IP地址匹配就把IP头取下来,在IP头中有个8位协议,这里存放到底是TCP还是UDP,比如这里如果是UDP,就能从数据里面解析出来,到这里传输层的事情处理完了,内核的事情基本干完了,剩下的数据交给应用程序自己去处理,无论是通过TCP还是UDP传数据都要监听一个端口,用来区分应用程序
UDP的特点:
第一,沟通简单,不需要一肚子花花肠子(大量的数据结构、处理逻辑、包头字段)。前提是它相信网络世界是美好的,秉承性善论,相信网络通路默认就是很容易送达的,不容易被丢弃的。
第二,轻信他人。它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他 数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。
第三,愣头青,做事不懂权变。不知道什么时候该坚持,什么时候该退让。它不会根据网络 的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。
UDP的使用场景
1、需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。DHCP就是基于UDP协议的。
2、不需要一对一沟通,建立连接,而是可以广播的应用。UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的。
3、需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。如:
- 网页或者 APP 的访问,原本的网页访问是基于HTTP协议的,而HTTP协议是基于TCP协议的,后续采用了Google提出基于UDP的QUIC协议
- 流媒体协议
- 实时游戏
- 物联网
- 移动通信领域
TCP协议
TCP包头格式
1、源端口号和目标端口号:用于确认发送给哪个引用
2、包的序号:包的序号是为了解决乱序问题
3、确认序号:为了解决不丢包的问题,发出去的包应该有确认,如果没有收到就应该重新发送,直到送达。
4、有一些是状态位:SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN是结束链接等
5、窗口大小:TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力。
TCP的三次握手
为什么是三次握手不是两次、不是四次?
假设这个通路是非常不可靠的,A 要发起一个连接,当发了第一个请求杳无音信的 时候,会有很多的可能性,比如第一个请求包丢了;再如没有丢,但是绕了弯路,超时了; 还有 B 没有响应,不想和我连接。 A 不能确认结果,于是再发,再发。终于,有一个请求包到了 B,但是请求包到了 B 的这个事情,目前 A 还是不知道的,A 还有可能再发。 B 收到了请求包,就知道了 A 的存在,并且知道 A 要和它建立连接。如果 B不乐意建立连 接,则 A 会重试一阵后放弃,连接建立失败,没有问题;如果 B 是乐意建立连接的,则会发送应答包给 A。 当然对于 B 来说,这个应答包也是一入网络深似海,不知道能不能到达 A。这个时候 B 自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者 A 已经挂了都有可能。 而且这个时候 B 还能碰到一个诡异的现象就是,A 和 B 原来建立了连接,做了简单通信后,结束了连接。但是A 建立连接的时候,请求包重复发了几次,有的请求包绕了一 大圈又回来了,B 会认为这也是一个正常的的请求的话,因此建立了连接,可以想象,这个连接不会进行下去,也没有个终结的时候,纯属是B的单相思了。因而两次握手肯定不行。(A在请求连接的时候有可能会重复发几个包,当A和B结束连接之后才收到A超时的请求包,这时候B返回一个包应答,但是A此时并没有要请求的意思,所以两次请求不行)
四次握手其实也是可以的,甚至四十次也可以,但是就算是四百次也不能保证真的可靠了,所以双方的消息都有去有回就可以了。好在大部分情况下,A 和 B 建立了连接之后,A 会马上发送数据的,一旦 A 发送数据,则 很多问题都得到了解决。例如 A 发给 B 的应答丢了,当 A 后续发送的数据到达的时候,B 可以认为这个连接已经建立,或者 B 压根就挂了,A 发送的数据,会报错,说 B 不可达, A 就知道 B 出事情了。
三次握手除了建立连接之外,还为了沟通TCP包的序号问题:互相告诉对方我发送的包从哪个序号开始。为什么不都从1号开始?例如,AB建立连接之后发送了1、2、3三个包,但是发送3的时候中间丢了或者绕路了于是重新发送,这时候A掉线了,之后又重新与B建立了连接,序号又从1开始、发送2但是并不需要发送3,但是上次绕路的那个3又回来了,B就会以为这个3也是A要发送的包,就出现了错误。
TCP的四次挥手
为什么发送方发送完最后一个ACK后还要等待2MSL时间?
1、保证发送方发送的最后一个ACK报文能到达B
2、保证B原来发送的包都死翘翘了。
TCP如何保证传输的可靠性
为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。
为了记录发送的包和接受的包,TCP需要缓存来保存这些记录,在发送端的缓存记录里分成了以下四个部分:
第一部分:发送了并且已经确认的。
第二部分:发送了并且尚未确认的。
第三部分:没有发送,但是已经等待发送的。
第四部分:没有发送,并且暂时还不会发送的。
在 TCP 里,接收端会给发送端报一个窗口的大 小,叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送 。
对于接收端来说,缓存分为三部分:
第一部分:接受并且确认过的。
第二部分:还没接收,但是马上就能接收的。
第三部分:还没接收,也没法接收的。
确认与重发机制
假设 4 的确认到了,不幸的是,5 的 ACK 丢了,6、7 的数据包丢了,这该怎么办呢?
一种方法就是超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器, 超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时 间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长, 访问就变慢了。由于重传时间是不断变化的,我们称为自适应重传算法。TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就检测 到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之 前,重传丢失的报文段。
还有一种方式称为Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。
流量控制问题
在对于包的确认中,同时会携带一个窗口的大小。当接收 方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上 又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为 空,才更新窗口。
拥塞控制问题
拥塞窗口 cwnd,是怕把网络塞满。LastByteSent - LastByteAcked <= min {cwnd, rwnd}是拥塞窗口和滑动窗口共同控制发送的速度。TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。
要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动:一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的 时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个 确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是指数性的 增长。有一个值 ssthresh 为 65535 个字节,当超过这个值的时候,就 要小心一点了,不能倒这么快了,可能快满了,再慢下来。每收到一个确认后,cwnd 增加 1/cwnd,拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 cwnd/2,将cwnd 设为 1,重新开始慢启动。
快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因 为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高 的值,呈线性增长。
套接字Socket
在网络层,Socket 函数需要指定到底是 IPv4 还是 IPv6,分别对应设置为 AF_INET 和 AF_INET6。另外,还要指定到底是 TCP 还是 UDP。还记得咱们前面讲过的,TCP 协议是 基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因而设置为 SOCK_DGRAM。
基于 TCP 协议的 Socket 程序函数调用过程
TCP 的服务端要先监听一个端口,一般是先调用 bind 函数,给这个 Socket 赋予一个 IP 地址和端口。为什么需要端口呢?要知道,你写的是一个应用程序,当一个网络包来的时 候,内核要通过 TCP 头里面的这个端口,来找到你这个应用程序,把包给你。为什么要 IP 地址呢?有时候,一台机器会有多个网卡,也就会有多个 IP 地址,你可以选择监听所有的 网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。
当服务端有了 IP 和端口号,就可以调用 listen 函数进行监听。
在内核中,为每个 Socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次 握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三 次握手还没完成,处于 syn_rcvd 的状态。接下来,服务端调用 accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成, 就要等着。
监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket.
TCP 的 Socket 就是一个文件流,Socket 在 Linux 中就是以文件的形式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。