趣谈计算机网络2 - 传输层

前言:

极客时间《趣谈网络协议》学习笔记

一、UDP协议

1. TCP 和 UDP 有哪些区别

建立连接:为了在客户端和服务端维护连接,建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。

TCP与UDP的区别:

  • TCP提供可靠交付,使用TCP传输数据,无差错、不丢失、不重复、并且按序到达。UDP 继承了 IP 包的特性,不保证不丢失,不保证按顺序到达。
  • TCP是面向字节流的。而 UDP 继承了 IP 的特性,基于数据报的,一个一个地发,一个一个地收。
  • TCP是可以有拥塞控制的,它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己发送速率。UDP 没有拥塞控制。
  • TCP是一个有状态服务,精确地记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。UDP 则是无状态服务。

2. UDP 包头

UDP包解包流程:

  1. UDP 包到达目标机器后,发现 MAC 地址匹配,取下来,将剩下的包传给处理 IP 层的代码。
  2. 目标机器将 IP 头取下来,发现目标 IP 匹配
  3. IP 头中有个 8 位协议,用以区分TCP还是UDP,此处是UDP
  4. 处理完传输层的事情,内核的事情基本就干完了,里面的数据根据端口号交给应用程序处理

UDP包头结构:

3. UDP 的三大特点

特点:

  • 简单,没有大量的数据结构、处理逻辑、包头字段
  • 不会建立连接,虽然有端口号,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。
  • 它不会根据网络的情况进行发包的拥塞控制

4. UDP 的三大使用场景

三种场景:

  • 需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用(DHCP/TFTP)
  • 不需要建立连接,或是可以广播的应用。UDP 的不面向连接的功能,可以用于承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的。
  • 处理速度快,时延低,可以容忍少数丢包,无拥塞控制。

组播原理:

  1. 使用组播地址,可以将包组播给一批机器。
  2. 当一台机器上的某个进程想监听某个组播地址的时候,需要发送 IGMP 包,所在网络的路由器就能收到这个包,知道有个机器上有个进程在监听这个组播地址。
  3. 当路由器收到这个组播地址的时候,会将包转发给这台机器,这样就实现了跨路由器的组播。

5. 基于 UDP 的“城会玩”的五个例子

a. 网页或者 APP 的访问

场景:

  • 原来访问网页和手机 APP 都是基于 HTTP 协议的。HTTP 协议是基于 TCP 的,建立连接都需要多次交互
  • 目前的 HTTP 协议,往往采取多个数据通道共享一个连接的情况,这样本来为了加快传输速度,但是 TCP 的严格顺序策略使得共享通道也需要等待。

QUIC:基于UDP协议, 在应用层上,会自己实现快速连接建立、减少重传时延,自适应拥塞控制

b. 流媒体的协议

场景:直播协议多使用 RTMP,该协议使用TCP

应用:很多直播应用,都基于 UDP 实现了自己的视频传输协议。

c. 实时游戏

场景:

  • 实时游戏中客户端和服务端要建立长连接,来保证实时传输。
  • 由于维护 TCP 连接需要在内核维护一些数据结构,因而一台机器能够支撑的 TCP 连接数目是有限的,然后 UDP 由于是没有连接的,在异步 IO 机制引入之前,常常是应对海量客户端连接的策略。

应用:游戏对实时要求较为严格的情况下,采用自定义的可靠 UDP 协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响。

d. IoT 物联网

场景:

  • 嵌入式系统维护 TCP 协议代价太大
  • 物联网对实时性要求也很高,TCP时延高

应用:物联网通信协议 Thread,就是基于 UDP 协议的。

e. 移动通信领域

GTP-U协议是基于 UDP

二、TCP协议三次握手与四次挥手

1. TCP包头格式

包头格式:

  • 源端口号和目标端口号
  • 包的序号(编号是为了解决乱序问题)
  • 确认序号(发出去的包需要确认,如果没有收到就重新发送)
  • 状态位:SYN(发起连接),ACK(回复),RST(重新连接),FIN(结束连接)
  • 窗口大小:TCP做流量控制,通信双方需要各自声明一个窗口,标识自己当前能够处理的能力。TCP还要做拥塞控制,控制发送速度

TCP协议的特点:

  • 通过重传等算法,保证传输的可靠性。
  • 需要控制包传输顺序,处理丢包问题
  • 传输数据时需要维护连接,开始和结束时都需要处理
  • 需要做流量控制与拥塞控制

2. TCP三次握手

a. 三次握手流程

流程:

  1. 一开始,客户端和服务端处于closed状态,服务端主动监听某个端口,处于LISTEN状态
  2. 客户端发起连接,发送SYN包(SYN标志位置为1,并包含序号x),之后处于SYN-SENT状态
  3. 服务端收到发起的连接,返回SYN包(SYN标志位置为1,序号为y,确认序号为x+1),之后处于SYN-RCVD状态
  4. 客户端接收到服务端发送的SYN和ACK之后,发送ACK包(ACK标志位置为1,序号为x+1,确认序号为y+1),客户端处于ESTABLISHED状态

b. 为什么是三次握手

只进行一次握手:客户端发送连接请求,无法确认服务端是否收到

只进行两次握手:

  • 客户端发送连接请求,服务端会发送SYN包,因此客户端可以确认服务端是否收到请求。然而,服务端无法确认发送的响应包客户端能否收到。若客户端没有收到,则客户端不会发送数据,后续的传输也就停止了
  • 服务端B接收到客户端A的请求后,客户端A挂了,服务端会建立一个空连接

四次握手:浪费资源

三次握手:

  • 客户端和服务端都可以确认对方能否收到
  • 若客户端的应答之应答丢失了,后续客户端给服务端传输数据,服务端客确定连接是否建立。即使客户端不发送数据,也可开启keepalive 机制,定时发送探活包

c.  TCP 包的序号的问题

TCP包计数问题:

  • TCP包序号不能从1开始,因为若第一次连接,发送了序号为3的包,因为网络延迟没有送达目的结点。第二次连接,需要发送序号为3的包,此时第一次连接的包到达,则会导致目的结点错误接受原理的包。
  • TCP每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一

3. TCP四次挥手

a. 四次挥手流程

流程:

  1. 一开始,客户端和服务端处于ESTABLISHED状态。
  2. 客户端发送包,FIN位置为1,序号置为p。客户端处于FIN_WAIT_1状态
  3. 服务端接收到客户端断开连接的请求后,发送响应包,ACK为置为1,确认序号为p+1。此时,服务端处于CLOSED_WAIT状态,客户端在接受到响应后,处于FIN_WAIT_2状态
  4. 服务端传输剩余数据,传输完毕后,发送包,FIN位/ACK位置为1,序号设为q,确认序号设置为p+1。此时,服务端处于LAST_ACK
  5. 客户端接收到服务端的包后,发送包,ACK位置为1,确认序号设置为q+1,此时,客户端处于TIME_WAIT状态。等待2MSL后,进入CLOSED状态
  6. 服务端接收到客户端发送的ACK后,进入CLOSED状态

FIN_WAIT_2状态:客户端在接收到服务器端对FIN包的ACK之后,处于FIN_WAIT_2状态,服务器端做收尾处理。通过linux的tcp_fin_timeout参数可以设置客户端处于FIN_WAIT_2状态的超时时间阈值

MSL:报文最大生存时间。该时间是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等

客户端TIME_WAIT状态等待2MSL的原因:

  • 客户端在接收到服务端FIN包之后,按理来说可以断开连接了。但由于服务端接收不到客户端的ACK,会重新发送FIN包给客户端,为了处理这一种情况,客户端会等待2MSL(让服务端有时间重发且重发的包能顺利到达客户端)。
  • 客户端在接收到服务端发送的ACK响应后,可能还有部分服务端的数据包没有到达。客户端直接关闭连接,若新的连接建立,新的客户端会收到上一次连接的数据(虽然序号重新生成,但是为了保险起见),因此客户端在FIN_WAIT_2状态后,需要等旧的数据包全部传输过来,或者旧数据包因为超时被丢弃

4. TCP状态机

三、TCP流量控制与拥塞控制

1. TCP协议可靠性

累计确认模式:

  1. TCP为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么
  2. TCP在传输时按照 ID 一个个发送,为了保证不丢包,对于发送的包都要进行应答
  3. TCP不是接收一个数据应答一次,而是会应答某个之前的 ID,表示都收到了

发送端缓存数据结构:

  • 第一部分:发送了并且已经确认的
  • 第二部分:发送了并且尚未确认的
  • 第三部分:没有发送,但是已经等待发送的
  • 第四部分:没有发送,并且暂时还不会发送的
  • 在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分。超过这个窗口的,接收端做不过来,就不能发送了。

接收端缓存数据结构:

  • 第一部分:接受并且确认过的。
  • 第二部分:还没接收,但是马上就能接收的。
  • 第三部分:还没接收,也没法接收的。
  • AdvertisedWindow = MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)

2. 顺序问题与丢包问题

a. 例子

发送端:

1/2/3已经发送且确认;4/5/6/7/8/9发送但是未确认;10/11/12还未发送;13、14、15 是接收方没有空间,不准备发的

接收端:

1/2/3/4/5已经完成 ACK,但是没被应用层读取;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK。

综上:

  • 1/2/3已经完成了;
  • 4/5接收端已经ACK,发送端还未接收到应答,可能ACK在网络传输中,也可能丢失了;
  • 6/7已经发送了,客户端未接收到
  • 8/9客户端已经接收到了,但需要等待6/7到达才能ACK

b. 确认与重发的机制

超时重试:对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。

超时自适应重传算法:

  • 通过采样 RTT 的时间,然后进行加权平均,估算往返时间。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法
  • 每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。(例子:发送端发送5/6/7后,都超过定时器的时间了,就会重新发送。5/6收到后,发送 ACK,要求下一个是 7。若7丢失了,TCP会将超时间隔加倍。)

超时触发重传的问题:超时周期可能相对较长。

快速重传机制:

  • 当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
  • 例子:接收方发现 6 收到了,8 也收到了,但是 7 还到,就会发送 6 的 ACK,要求下一个是 7,接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。

3. 流量控制问题

流量控制流程:

  • 先假设窗口初始大小为9,发送窗口初始状态如下。

  • 发送端每一个数据被确认(接收到ACK)后,窗口(LastByteAcked与AdvertisedWindow之间的部分)会向右移动,可发送的数据会增加(5的ACK到达后,13为新增的可发送数据)
  • 接收端初始状态如下。接收端缓存如果处理太慢,可以通过确认信息修改窗口的大小。例如:若接收端应用层一直不处理接收到的数据,则数据6到达后,可用空间变小,窗口大小应当从9变为8,因此在对6进行ACK时,会将窗口大小设置为8,告诉发送端,需要调整可用窗口大小

  • 若接收端窗口置为0后,发送端会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收端处理速度比较慢时,为了防止低能窗口综合征,可当可用窗口达到一定大小后,才更新窗口大小。 

4. 拥塞控制

a. 概述

滑动窗口(rwnd):防止发送方将接收方缓存塞满

拥塞窗口(cwnd):防止将网络塞满。

公式:LastByteSent - LastByteAcked <= min {cwnd, rwnd} 

拥塞控制的作用:避免包丢失和超时重传。

b. 拥塞控制流程

普通拥塞控制:

  1. TCP连接开始时,cwnd 设置为一个报文段,一次只能发送一个
  2. 若发送的数据按时收到了ACK,cwnd加1,此时可以发送两个数据段。若两个数据段也按时收到了ACK,对应于每个ACK,cwnd也加1(两个到达,因此+2)。以此类推,后续能发送的数据段数量为4,8,16,32 ...,直到超过ssthresh(默认是65535 个字节)后,每收到一个确认后,cwnd 增加 1/cwnd(发送的数据全收到ACK后,+1)。可见,TCP拥塞窗口大小一开始呈指数增加,超过阈值后,呈线性增长
  3. 若拥塞窗口大小大于一定值,导致网络拥塞后,将 sshresh 设为 cwnd/2,将cwnd设置为1。

快重传拥塞控制:

  1. 当接收端发现中间丢失了一个包后,发送三次前一个包的ACK。
  2. 发送端接收到ACK后,会立即重传,不需要等到超时才重传。
  3. TCP认为这种情况下,网络拥塞不严重(后续的包能接收到),于是将cwnd设置为cwnd/2,sshthresh = cwnd。后续拥塞窗口大小呈线性增长

c. TCP BBR 拥塞算法 

TCP拥塞控制算法的问题:

  1. 丢包并不代表着网络拥塞。公网上带宽不满也会丢包。
  2. TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满

TCP BBR企图找到一个平衡点,将带宽填满,不填满中间设备的缓存

四、套接字socket

1. 概述

Socket参数(对TCP/IP协议的封装):

  • AF_INET/AF_INET6:基于IPv4还是IPv6
  • SOCK_STREAM/SOCK_DGRAM:基于TCP还是UDP
  • ... ...

2. 基于 TCP 协议的 Socket 程序函数调用过程

Socket队列:

内核为Scoket维护两个队列,一个是已经建立连接的,三次握手已经完成,处于 established 状态;另一个是三次握手还没有完成,处于SYN_RCVD状态的

基于TCP的Socket的使用流程:

  1. 调用bind函数,监听一个端口,给socket赋予IP和端口号。(设置端口号:内核需要通过端口号来找到对应的进程;设置IP:一台机器有多个网卡,有多个IP地址,需要选择监听某一些网卡,或全部网卡)
  2. 调用isten函数监听,此时处于TCP状态图的listen状态
  3. 服务端调用 accept 函数,从完成了三次握手的队列中取出一个连接处理。若无连接,则等待
  4. 服务端等待时,客户端可以通过connect函数发起连接。调用函数时,需要设置服务端IP地址和端口号,发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 Socket。(监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket)

socket在linux中的实现:

  • Socket 在 Linux 中就是以文件的形式存在的,存在文件描述符,写入和读出也是通过文件描述符。 
  • 每一个进程都有一个数据结构 task_struct,里面指向一个文件描述符数组,表示这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。数组中的内容是一个指针,指向内核中所有打开的文件的列表。
  • Socket 对应的 inode 存储在内存中的。在这个 inode 中,指向了 Socket 在内核中的 Socket 结构。
  • Socket结构中包含两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整的包的结构。

3. 基于 UDP 协议的 Socket 程序函数调用过程

  • UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是,UDP 的交互仍然需要 IP 和端口号,因而也需要 bind。
  • UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信
  • 因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。

4. 服务器如何处理多个连接

a. 概述

TCP最大连接数:

  • 理论上,tcp最大连接数=客户端 IP 数(2^32)×客户端端口数(2^16)
  • 文件描述符数量会限制TCP连接数(ulimit 配置文件描述符的数目)
  • 每个 TCP 连接都要占用一定内存,操作系统是有限的

b. 多进程方式

流程:

  1. 当一个连接创建后,fork创建一个子进程。
  2. fork子进程后,会复制文件描述符的列表,也会复制内存空间(仅复制页表,修改后才会开辟新的地址空间),还会复制一条记录当前执行到了哪一行程序(fork的创建可见操作系统copy on write)
  3. 因为复制了文件描述符列表,而文件描述符都是指向整个内核统一的打开文件列表,因而父进程刚才因为 accept 创建的已连接 Socket 也是一个文件描述符,同样也会被子进程获得。子进程就可以通过这个已连接 Socket 和客户端进行交互
  4. 父进程可以通过fork时返回的子进程ID查看子进程是否完成,子进程完成,父进程帮忙回收子进程内核数据。

c. 多线程方式

流程:

  1. 当一个连接创建后,在 Linux 下,通过 pthread_create 创建一个线程,也是调用do_fork创建子线程,文件描述符列表、进程空间,是共享的。
  2. 通过子线程也可以处理已经连接的Socket

d. IO 多路复用,一个线程维护多个 Socket

流程:

  1. Socket是文件描述符,用一个线程轮询所有Socket文件描述符集合,调用select函数来判断文件描述符是否有变化,若有变化,文件描述符集合fd_set对应的位会置1
  2. 将位为1(可读可写)的Socket交由别的线程处理,并开始下一轮轮询

select原理:

  1. 调用select时,socket文件描述符从用户态拷贝到内核态
  2. 内核通过读/写缓存区判断是否可读可写
  3. 内核监测到可读可写时,产生中断,通知select,select被内核触发之后,就返回可读可写的文件句柄的总数;
  4. select会将之前传递给内核的文件句柄再次从内核传到用户态,select在用户态通过FD_ISSET宏函数检测哪些文件I/O可读可写

e. IO 多路复用,基于事件驱动

IO多路复用(select)的问题:每次轮询需要遍历所有Socket文件描述符,select可操作的文件描述符总数受到FD_SETSIZE限制(select声明在一个进程中select所能操作的文件描述符的最大数目),具体机制见27、fd_set与FD_SETSIZE详解_w00347190的博客-CSDN博客

epoll原理:注册callback 函数,当某个文件描述符发送变化时,主动通知

流程:

  1. epoll_create在内核创建一个专属于epoll的缓冲区,缓存区中有红黑树和就绪链表,红黑树中保存所有epoll要监听的Socket
  2. 内核针对读缓冲区和写缓冲区来判断是否可读可写
  3. epoll_ctl执行add动作时,将文件句柄放到红黑树上,并向内核注册了该文件句柄的回调函数。
  4. 内核在检测到某句柄可读可写时,通过红黑树得到epoll对象,并调用回调函数,将文件句柄放到就绪链表。
  5. epoll_wait监控就绪队列,如果就绪链表有文件句柄,则表示该文件句柄可读可写,并返回到用户态
  6. epoll_ctl传入一次文件描述符,就可以重复监控,直到删除epoll_ctl

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值