跳转至

Transport Layer

概述和传输层服务

传输层的位置

  • 传输层位于应用层和网络层之间:基于网络层提供的服务,向分布式应用程序提供通信服务
  • 按照因特网的“端到端”设计原则:应用进程之间的通信。应用程序只运行在终端上,即不需要为网络设备编写程序
  • 传输层应提供进程之间本地通信的抽象:即运行在不同终端上的应用进程仿佛是直接连在一起的

传输层提供什么服务?

  • 因特网的网络层提供“尽力而为”的服务:
  • 传输层的有所为、有所不为:
    1. 传输层可以通过差错恢复、重排序等手段提供可靠、按序的交付服务
    2. 但传输层无法提供延迟保证、带宽保证等服务

套接字编程

  • 如何创建一个网络应用:编写一个分布式程序,使其运行在不同的端系统上,并通过网络通信
  • 选择一种应用程序体系结构:
    1. 传统及主流的是客户-服务器体系结构(C/S)
    1. 有一台总是在线的主机,运行一个服务器程序(server),服务器主机具有永久的、众所周知的地址
    2. 用户终端上运行一个客户程序(client),需要时主动与服务器程序通信,请求服务
    3. 客户只与服务器通信,客户之间不通信

在网络中通过IP地址来标识和区别不同的主机,通过端口号来标识和区分一台主机中的不同应用进程,端口号拼接到IP地址即构成套接字(Socket)。在网络中采用发送方和接收方的套接字来识别端点。

  • 套接字地址:通用地址:
    1. 套接字API接口的地址类型,作为不同类型套接字地址的“虚父类”
    2. 在套接字API的定义中使用
    3. 调用时为防止编译错误需要做强制类型转换
    struct sockaddr{/* struct to hold an address*/
        u_char sa_len;/*total length*/
        u_short sa_family;/*type of address*/
        char sa_data[14];/*value of address*/
    }
    
  • 套接字地址:IPv4地址:
    1. sin_family:AF_INET(IPv4),编程时实际使用地址类型
    2. sin_port:存放端口号(按照网络字节序存储)
    3. sin_addr:存放32位IP地址(无符号整数)
    4. sin_zero:为与sockaddr大小兼容而保留的空字节
    struct sockaddr_in { //struct to hold an address
        sa_family_t sin_family; //always AF_INET
        in_port_t sin_port; //protocol port number: uint16_t
        struct in_addr sin_addr; //IP address
        char sin_zero[8];//unused(set to zero)
        };
    struct in_addr {
        in_addr_t s_addr; //IPv4 address (uint32_t)
    };
    
  • 创建套接字:socket():
    1. 客户或服务器调用socket()创建本地套接字,返回套接字描述符
    2. domain指明网络层地址类型
    3. type指明传输层协议:
    1. SOCK_STREAM代表TCP字节流
    2. SOCK_DGRAM代表UDP数据报
    #include <sys/socket.h>
    int socket(
        int domain, /* AF_UNIX, AF_INET, etc. */
        int type, /* SOCK_STREAM, SOCK_DGRAM */
        int protocol); /* usually zero */
    /* Returns file descriptor or-1 on error (sets errno) */
    
  • 绑定套接字地址:bind():
    1. 用来将服务器本地套接字地址sa与描述符socket_fd绑定
    2. 客户程序不需要调用bind(),操作系统将为其在1024~5000之间分配一个端口号
    int bind(
        int socket_fd,
        const struct sockaddr *sa,
        socklen_t sa_len);
        /* Returns 0 if OK or-1 on error (sets errno) */
    
  • 关闭套接字:close():
    1. 客户和服务器调用close()关闭一个套接字
    2. 当实参fd为TCP套接字描述符时:调用close()会引起本地进程向远程进程发送关闭连接的消息
    3. 当实参fd为UDP套接字描述符时:调用close()会引起为此描述符分配的资源被内核释放
    int close(int fd);
    /* Returns 0 if OK or-1 on error */s
    
  • 基于UDP的套接字通信流程:
  • 使用TCP套接字实现回音服务:使用TCP套接字编程时,可将TCP连接想像成一对套接字之间的一条封闭管道:
    1. 发送端TCP将要发送的字节序列从管道的一端(套接字)送入
    2. 接收端TCP从管道的另一端(套接字)取出字节序列
    3. 在管道中传输的字节不丢失,并保持顺序
  • TCP服务器监听套接字:listen()
    1. 将主动套接字转变为被动套接字,进入监听状态
    2. backlog设定了监听队列长度
    int listen(
        int socket_fd;
        int backlog;) //maximum connection queue length
        /* Returns 0 if OK or-1 on error */
    
  • TCP套接字系统调用:accept():
    1. 服务端从已建立连接的队列中出队列一个客户端
    2. 返回值是用来和客户端交换数据的套接字描述符,需要在通信完毕后调用close关闭
    3. 返回时客户端的IP地址和端口可在cliaddr中访问
     int accept(
        int socket_fd,
        struct sockaddre *cliaddr, /* client address */
        socklen_t *addrlen;) /* address struct length */
        /* Returns file descriptor or-1 on error */
    
  • TCP套接字系统调用:connect()
    1. 客户端调用connect向服务器发起建立连接的请求
    2. 服务器的IP地址和端口信息在servaddr中
    3. 三次握手的过程导致connect的调用是阻塞的
     int connect(
        int socket_fd,
        const struct sockaddr *servaddr, /* server address */
        socklen_t *addrlen); /* address struct length */
        /* Returns 0 or-1 on error */
    
  • 基于TCP的套接字通信流程:

传输层复用和分用

复用是指发送方不同的应用进程都可以使用同一个传输层协议传送数据。分用是指接收方的传输层在剥去报文的首部后能够把这些数据正确交付到目的应用进程。

  • 套接字标识与端口号:
    1. 端口号是套接字标识的一部分:
    1. 每个套接字在本地关联一个端口号
    2. 端口号是一个16比特的数
    2. 端口号的分类:
    1. 熟知端口:0~1023,由公共域协议使用
    2. 注册端口:1024~49151,需要向IANA注册才能使用
    3. 动态和/或私有端口:49152~65535,一般程序使用
    3. 报文段中有两个字段携带端口号
    1. 源端口号:与发送进程关联的本地端口号
    2. 目的端口号:与接收进程关联的本地端口号

  • 套接字标识与端口号:
    1. 服务器端使用的端口号。分为两类,一类是熟知端口号,数值为01023,IANA(互联网地址指派机构)把这些端口号指派给了TCP/IP最重要的一些应用程序,让所有的用户都知道。另一类称为登记端口号,数值为102449151,它是供没有数值端口号的应用程序使用的,使用这类端口号必须在IANA登记,以防止重复。
    2. 客户端使用的端口号,数值为49152~65535。因为这类端口号仅在客户进程运行时才动态地选择,所以又称短暂端口号。党服务器进程收到客户进程地报文时,就知道了客户进程所使用的端口号,因为可以把数据发送给客户进程。通信结束后,刚用过的客户端口号就不复存在,这个端口号就可以供其他客户进程使用。
  • 套接字端口号的分配
    1. 自动分配:
    1. 创建套接字时不指定端口号
    2. 由操作系统从49152~65535中分配
    3. 客户端通常使用这种方法
    2. 使用指定端口号创建套接字:
    1. 创建套接字时指定端口号
    2. 实现公共域协议的服务器应分配众所周知的端口号(0~1023)
    3. 服务器通常采用这种方法
  • UDP分用的方法:UDP套接字使用二元组进行标识
    * 接收方传输层收到一个UDP报文段后:
    1. 检查报文段中的目的端口号,将UDP报文段交付到具有该端口号的套接字
    2. <目的IP地址,目的端口号> 相同的UDP报文段被交付给同一个套接字,与<源IP地址,源端口号> 无关
    3. 报文段中的<源IP地址,源端口号> 被接收进程用来发送响应报文
  • TCP服务器使用的套接字:
    1. 监听套接字:
    1. 服务器平时在监听套接字上等待客户的连接请求,该套接字具有众所周知的端口号
    2. 连接套接字:
    1. 服务器在收到客户的连接请求后,创建一个连接套接字,使用临时分配的端口号
    2. 服务器同时创建一个新的进程,在该连接套接字上服务该客户
    3. 每个连接套接字只与一个客户通信,即只接收具有以下四元组的报文段:
    * 源IP地址 = 客户IP地址,源端口号 = 客户套接字端口号
    * 目的IP地址 = 服务器IP地址,目的端口号 = 服务器监听套接字的端口号

    * 连接套接字需要使用<源IP地址,目的IP地址,源端口号,目的端口号>四元组进行标识,服务器使用该四元组将TCP报文段交付到正确的连接套接字。

无连接传输:UDP

  • 网络层提供的服务(best-effort service):
    1. 尽最大努力将数据包交付到目的主机
    2. 不保证投递的可靠性和顺序
    3. 不保证带宽及延迟要求
  • UDP提供的服务:
    1. 进程到进程之间的报文交付
    2. 报文完整性检查(可选):检测并丢弃出错的报文
  • UDP需要实现的功能:
    1. 复用和分用
    2. 报文检错
  • UDP报文段结构:
    1. UDP报文:
    1. 报头:携带协议处理需要的信息
    2. 载荷(payload):携带上层数据
    2. 用于复用和分用的字段:
    1. 源端口号
    2. 目的端口号
    3. 用于检测报文错误的字段:
    1. 报文总长度
    2. 校验和(checksum)

  • UDP校验和计算:
    1. 计算UDP校验和时,要包括伪头、UDP头和数据三个部分
    2. UDP伪头信息取自IP报头,包括:
    1. 源IP地址,目的IP地址
    2. UDP的协议号
    3. UDP报文段总长度
    3. UDP校验和的使用是可选的,若不计算校验和,该字段填入0

面向连接的传输:TCP

TCP报文段结构

  • 重要的TCP选项:
    1. 最大段长度(MSS):
    1. TCP段中可以携带的最大数据字节数
    2. 建立连接时,每个主机可声明自己能够接受的MSS,缺省为536字节
    2. 窗口比例因子(window scale):
    1. 建立连接时,双方可以协商一个窗口比例因子
    2. 实际接收窗口大小= window size * 2^window scale
    3. 选择确认(SACK):
    1. 最初的TCP协议只使用累积确认
    2. 改进的TCP协议引入选择确认,允许接收端指出缺失的数据字节
  • 发送序号和确认序号的含义:

TCP可靠数据传输

  • TCP 在不可靠的IP服务上建立可靠的数据传输
  • 基本机制:
    1. 发送端:流水线式发送数据、等待确认、超时重传
    2. 接收端:进行差错检测,采用累积确认机制
  • 乱序段处理:协议没有明确规定
    1. 接收端不缓存:可以正常工作,处理简单,但效率低
    2. 接收端缓存:效率高,但处理复杂
  • 一个高度简化的TCP协议:仅考虑可靠传输机制,且数据仅在一个方向上传输
    * 接收方:
    1. 确认方式:采用累积确认,仅在正确、按序收到报文段后,更新确认序号;其余情况,重复前一次的确认序号(与GBN类似)
    2. 失序报文段处理:缓存失序的报文段(与SR类似)
    * 发送方:
    1. 发送策略:流水线式发送报文段
    2. 定时器的使用:仅对最早未确认的报文段使用一个重传定时器(与GBN类似)
    3. 重发策略:仅在超时后重发最早未确认的报文段(与SR类似,因为接收端缓存了失序的报文段)
  • TCP发送方要处理的事件:
    1. 收到应用数据:
    1. 创建并发送TCP报文段
    2. 若当前没有定时器在运行(没有已发送、未确认的报文段),启动定时器
    2. 超时:
    1. 重传包含最小序号的、未确认的报文段
    2. 重启定时器
    3. 收到ACK:
    1. 如果确认序号大于基序号(已发送未确认的最小序号):
    1. 推进发送窗口(更新基序号)
    2. 如果发送窗口中还有未确认的报文段,启动定时器,否则终止定时器
  • 如何设置超时值:
    * 为什么设置合理的超时值很重要:
    1. 若超时值太小,容易产生不必要的重传
    2. 若超时值太大,则丢包恢复的时间太长
    * 直观上,超时值应大于RTT,但RTT是变化的
    * 问题转化为如何估计RTT:
    1. RTT是变化的,需要实时测量从发出某个报文段到收到其确认报文段之间经过的时间(称SampleRTT)
    2. 由于SampleRTT波动很大,更有意义的是计算其平均值(称EstimatedRTT)
    3. 平均RTT的估算方法(指数加权移动平均):
    * \(EstimatedRTT = (1- α)*EstimatedRTT + α*SampleRTT\)
    * 典型地,α=0.125
    4. 估算SampleRTT 与 EstimatedRTT的偏差(称DevRTT):
    * \(DevRTT = (1-β)*DevRTT + β*|SampleRTT - EstimatedRTT|\)
    * 典型地,β= 0.25
    5. 设置重传定时器的超时值:\(TimeoutInterval = EstimatedRTT + 4*DevRTT\)
  • TCP确认的二义性:
    1. TCP确认的二义性问题:
    1. 重传的TCP报文段使用与原报文段相同的序号
    2. 发送端收到确认后,无法得知是对哪个报文段进行的确认
    2. 二义性确认带来的问题:
    1. 对重传报文段测量的SampleRTT,可能不准确
    3. 解决方法:
    1. 忽略有二义性的确认,只对一次发送成功的报文段测量SampleRTT ,并更新EstimtedRTT
    2. 当TCP重传一个段时,停止测量SampleRT

简单忽略重传报文段的问题:重传意味着超时值可能偏小了,需要增大;若简单忽略重传报文段(不更新EstimtedRTT),则超时值也不会更新,超时设置过小的问题没有解决。

  • 定时器补偿:采用定时器补偿策略,发送方每重传一个报文段,就直接将超时值增大一倍(不依赖于RTT的更新);若连续发生超时事件,超时值呈指数增长(至一个设定的上限值)
    * Karn算法:结合使用RTT估计值和定时器补偿策略确定超时值
  • TCP的接收端:
    * 为减小通信量,TCP允许接收端推迟确认:接收端可以在收到若干个报文段后,发送一个累积确认的报文段
    * 推迟确认带来的问题:若延迟太大,会导致不必要的重传;推迟确认造成RTT估计不准确
    * TCP协议规定:
    1. 推迟确认的时间最多为500ms
    2. 接收方至少每隔一个报文段使用正常方式进行确认
  • TCP接收端的事件和处理:

仅靠超时重发丢失的报文段,恢复太慢!

  • 快速重传:
    * 发送方可利用重复ACK检测报文段丢失:
    1. 发送方通常连续发送许多报文段
    2. 若仅有个别报文段丢失,发送方将收到多个重复序号的ACK
    3. 多数情况下IP按序交付分组,重复ACK极有可能因丢包产生

    例如:TCP协议规定:
    当发送方收到对同一序号的3次重复确认时,立即重发

    event:ACK received, with ACK field value of y 
        if (y > SendBase) { //收到更新的确认号
            SendBase= y
            if (there are currently not-yet-acknowledged segments)
                start timer 
        } 
        else { //收到重复序号的ACK
            increment count of dup ACKs received for y
            if (count of dup ACKs received for y = 3) {
                resend segment with sequence number y //快速重传
            }
        }
        ![](./img/157.png){width="400"}      
        ![](./img/158.png){width="400"}      
    

TCP流量控制

流量控制的功能就是让发送方的发送速率不要太快,以便让接收方来得及接收,因此可以说流量控制是一个速度匹配服务(匹配发送方的发送速率与接收方的读取速率)。

  • TCP的接收端:接收缓存
    1. 接收端TCP将收到的数据放入接收缓存
    2. 应用进程从接收缓存中读数据
    3. 进入接收缓存的数据不一定被立即取走、取完
    4. 如果接收缓存中的数据未及时取走,后续到达的数据可能会因缓存溢出而丢失
  • 流量控制:发送端TCP通过调节发送速率,不使接收端缓存溢出

UDP不保证交付:当应用进程消费数据不够快时,接收缓存溢出,报文数据丢失,UDP不负责任

  • TCP如何进行流量控制:
    1. 接收缓存中的可用空间称为接收窗口:RcvWindow = RcvBuffer-[LastByteRcvd - LastByteRead]
    2. 接收方将RcvWindow放在报头中,向发送方通告接收缓存的可用空间
    3. 发送方限制未确认的字节数不超过接收窗口的大小,即:LastByteSent-LastByteAcked ≤ RcvWindow

    * 特别是,当接收方通告接收窗口为0时,发送方必须停止发送

  • 非零窗口通告:
    * 发送方/接收方对零窗口的处理:
    1. 发送方:当接收窗口为0时,发送方必须停止发送
    2. 接收方:当接收窗口变为非0时,接收方应通告增大的接收窗口
    * 当发送方停止发送后,接收方不再收到数据,如何触发接收端发送“非零窗口通告”呢?
    * TCP协议规定:
    1. 发送方收到“零窗口通告”后,可以发送“零窗口探测”报文段
    2. 从而接收方可以发送包含接收窗口的响应报文段
  • 零窗口探测的实现:
    1. 发送端收到零窗口通告时,启动一个持续计时器
    2. 计时器超时后,发送端发送一个零窗口探测报文段(序号为上一个段中最后一个字节的序号)
    3. 接收端在响应的报文段中通告当前接收窗口的大小
    4. 若发送端仍收到零窗口通告,重新启动持续计时器

TCP连接管理

  • 建立一条TCP连接需要确定两件事:
    1. 双方都同意建立连接(知晓另一方想建立连接)
    2. 初始化连接参数(序号,MSS等)

TCP两次握手建立连接

  • Solution 1:使用seq来标识
    packet arrival:
        if (packet.seq see before)
            discard packet
    
  • Solution2:使用seq的同时,再添加time的信息
    packet arrival:
        if (packet.seq see before and time is short (<T))
            discard packet
        if (packet.seq see before and time is large (>T))
            accept packet
    
  • 实现方法:
  • Forbidden region:如果前一个包进入了Forbidden region,后续就会出现duplicate

从更宏观的角度来看:

TCP三次握手建立连接

  • 流程如下:
    1. 客户TCP发送SYN 报文段(SYN=1, ACK=0)
    * 给出客户选择的起始序号
    * 不包含数据
    2. 服务器TCP发送SYNACK报文段(SYN=ACK=1)(服务器端分配缓存和变量)
    * 给出服务器选择的起始序号
    * 确认客户的起始序号
    * 不包含数据
    3. 客户发送ACK报文段(SYN=0,ACK=1)(客户端分配缓存和变量)
    * 确认服务器的起始序号
    * 可能包含数据
  • 关闭TCP连接:非对称释放和对称释放
    1. 不对称释放 Asymmetric:当一部分挂起时,另一部分挂起时,连接中断。会导致一些数据丢失:

    2. 对称释放 symmetric:将连接视为两个单独的单向连接和要求,每一个都要单独释放。
      * 先关闭从左往右的连接(CLOSE_WAIT状态),然后再发送FIN(LAST_ACK),关闭从右往左的连接。
    
  • 客户/服务器经历的TCP状态序列:

安全问题

  • SYN洪泛攻击:服务器收到SYN段后,会发送SYNACK段,会分配资源,超时后还会重发。而发送方发送SYN是低代价的,如果攻击者采用伪造的源IP地址,向服务器发送大量的SYN段,却不发送ACK段,服务器就会崩掉。
  • TCP端口扫描:(攻击的前置)
    1. 若收到SYNACK段,表明目标端口上有服务在运行
    2. 若收到RST段,表明目标端口上没有服务在运行
    3. 若什么也没收到,表明路径上有防火墙,有些防火墙会丢弃来自外网的SYN报文段

TCP拥塞控制

  • 流量控制与拥塞控制的异同:
    1. 流量控制:限制发送速度,使不超过接收端的处理能力
    2. 拥塞控制:限制发送速度,使不超过网络的处理能力
  • 网络拥塞造成:
    1. 丢包:由路由器缓存溢出造成
    2. 分组延迟增大:链路接近满载造成
  • 大量网络资源用于:
    1. 重传丢失的分组
    2. (不必要地)重传延迟过大的分组
    3. 转发最终被丢弃的分组
  • 发送方采用什么机制限制发送速率?
    发送方使用拥塞窗口cwnd限制已发送未确认的数据量: (cwnd随发送方感知的网络拥塞程度而变化)
    * LastByteSent-LastByteAcked ≤ cwnd
    * rate = cwnd/RTT Bytes/sec
  • 拥塞窗口的调节策略:AIMD:
    1. 乘性减(Multiplicative Decrease)
    * 发送方检测到丢包后,将cwnd的大小减半(但不能小于一个MSS(最大报文段大小))
    * 目的:迅速减小发送速率,缓解拥塞
    2. 加性增(Additive Increase)
    * 若无丢包,每经过一个RTT,将cwnd增大一个MSS,直到检测到丢包
    * 目的:缓慢增大发送速率,避免振荡

  • TCP慢启动:
    * 采用“加性增”增大发送窗口,太慢!
    1. 在新建连接上,令cwnd= 1 MSS,起始速度= MSS/RTT
    2. 然而,网络中的可用带宽可能远大于MSS/RTT
    * 慢启动的基本思想:
    1. 在新建连接上指数增大cwnd,直至检测到丢包(此时终止慢启动)
    2. 希望迅速增大cwnd至可用的发送速度
    * 慢启动的实施:
    1. 每收到一个ACK段,cwnd增加一个MSS
    2. 只要发送窗口允许,发送端可以立即发送下一个报文段
    * 特点:以一个很低的速率开始,按指数增大发送速率
  • 发送方维护变量ssthresh:
    * 发生丢包时,ssthresh=cwnd/2
    * ssthresh是从慢启动转为拥塞避免的分水岭:
    1. cwnd低于门限时,执行慢启动
    2. cwnd高于门限:执行拥塞避免
  • 拥塞避免阶段,拥塞窗口线性增长:
    * 每当收到ACK,cwnd=cwnd + MSS*(MSS/cwnd)

    * 超时:cwnd回到1
    * 检测到3个重复的ACK后:(如果是reno,回到ssthresh附近)
    1. TCP Reno实现:cwnd= ssthresh+3,线性增长
    2. TCP Tahoe实现:cwnd=1 MSS,慢启动

  • TCP发送端的事件与动作:
  • TCP的公平性:
    * 公平性目标: 如果K条TCP连接共享某条带宽为R的瓶颈链路,每条连接应具有平均速度R/K
    * 为什么TCP是公平的:
    * 考虑两条竞争的连接(各种参数相同)共享带宽为R的链路:斜率为-1的线是充分利用的线,斜率为1的线是充分公平的线
    1. 加性增:连接1和连接2按照相同的速率增大各自的拥塞窗口,得到斜率为1的直线
    2. 乘性减:连接1和连接2将各自的拥塞窗口减半
      ![](./img/176.png){width="240"}
    
      * A->B,A的利用不充分,通过加性增,沿着斜率为1的线进行增长,到B后发生了丢包。
      * B->C,乘性减,会往原点方向减少。
      * 不断迭代后,会不断趋向两条线相交的地方(也就是充分公平充分利用)
    
  • TCP公平性更复杂的情形
    * 若相互竞争的TCP连接具有不同的参数(RTT、MSS等),不能保证公平性
    * 若应用(如web)可以建立多条并行TCP连接,不能保证带宽在应用之间公平分配,比如:
    1. 一条速率为R的链路上有9条连接
    2. 若新应用建立一条TCP连接,获得速率R/10
    3. 若新应用建立11条TCP,可以获得速率R/2

DCCP

DCCP = UDP + 拥塞控制