概述和运输层服务


0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| | |
| Length | Checksum |
+--------+--------+--------+--------+
|
| data octets ...
+---------------- ...
User Datagram Header Format

0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| source address |
+--------+--------+--------+--------+
| destination address |
+--------+--------+--------+--------+
| zero |protocol| UDP length |
+--------+--------+--------+--------+
Pseudo Header Format
准备数据:将 UDP 头部(除了校验和字段,该字段在计算时设为0)和数据载荷分为16位的字(2字节)。如果数据的字节总数不是偶数(即不能完全分成16位的字),则在最后添加一个额外的字节的填充(通常是0)以使之成对添加伪头部 :根据 UDP 协议,校验和的计算应包含一个"伪头部",它包括发送者和接收者的 IP 地址(各占4字节),一个8位的全0字段(用于占位,确保伪头部的格式与传输层数据段的其余部分保持一致),1字节的协议号(UDP 是17, 0x11),以及 UDP 数据报的长度反码求和:使用 16 位反码运算,将所有的 16 位字加到一起。在这种加法中,如果任何一次加法结果超过 16 位,即最左边产生了进位,那么进位会被加回到计算结果的最低位。这就是所谓的“环绕”或“端到端进位”求和的反码:将最终的求和结果进位的一部分和基本部分相加之后取反码,即将所有位从 0 变为 1 或从 1 变为 0,这就是要存储在校验和字段的值特殊情况:如果计算出的检验和为 0,为防止将合法的检验和值 0 解释为没有使用检验和,发送端将检验和字段为 0 的情况下置为全 1

rdt_send(data):从上层接收数据 (如果可靠数据传输实现在 Layer4,那么这些数据就是 Layer5 注入 socket 的报文 (message)(可以是 HTTP 报文,SMTP 报文...))make_pkt(packet, data):将上层数据(根据需要拆分)加上头部建立分组 (注意⚠️:这里的分组指的是封包,在不同网络层级中的名称不同)udt_send(packet):将此分组送入下层信道rdt_rcv(packet):从下层信道接收分组extract(packet,data):从分组中取出数据deliver_data(data):将数据交给上层
┌────────────┐ ┌────────────┐
│ │ │ │
│ sender │ │ receiver │
│ │ │ │
└────────────┘ └────────────┘
layer 5 layer 4 layer 3 layer 4 layer 5
│ │
┌────────────┐ waiting for │ │
│datadatadata│ layer 5 │ │
│datadata │ invocation │ │
└────────────┘ │ │
layer 5 call rdt_send(data) │ │ waiting for
───────────────────► │ │ layer 3
packet=make_pkt(data)│ │ invocation
udt_send(packet) │ │
┌────────────┐ │ │
│headdatadata│ │ layer 3 channel delivery │
│datadatadata│ │ reliable │
└────────────┘ │ ────────────────────────► │ ┌────────────┐
│ │ │headdatadata│
│ │ │datadatadata│
│ │ └────────────┘
│ layer 3 call │ rdt_rcv(packet)
waiting for │ │ ────────────────────►
layer 5 │ │ extract(packet,data)
invocation │ │ deliver_data(data) ┌────────────┐
│ │ │datadatadata│
│ │ │datadata │
│ │ └────────────┘
│ │
│ │
│ │ waiting for
│ │ layer 3
From: │ │ invocation
Chris White │ │
▼ ▼
差错检测:让接收端能够发现比特差错,UDP 使用反码求和的方式来检验接收端反馈:接收端反馈信息给发送端,ACK 代表成功,NAK 代表失败重传:分组有差错,就重传分组make_pkt(data,checksum):计算 data 的 checksum 并且将数据封包isNAK(rcvpkt):received packet为 NAKisACK(rcvpkt):received packet 为 ACKcorrupt(rcvpkt):rcvpkt 有比特错误notcorrupt(rcvpkt):rcvpkt 没有比特错误

has_seq0(rcvpkt):接收分组的序号字段为 0has_seq1(rcvpkt):接收分组的序号字段为 1



等待来自上层的调用0 状态,当发送方发送一个 seq=0 的分组后,进入 等待 ACK 或 NAK 0 状态。等待来自下层的0 的接收方出现两种情况,情况 1 接收方收到了无差错的分组返回一个 ACK 分组,并转换为 等待来自下层的1 状态,情况 2 接收方收到了有差错的分组返回一个 NAK 分组,并且保持当前状态等待 ACK 或 NAK 0 状态的发送方收到确认分组时,出现三种情况,情况 1 收到 ACK 分组,情况 2 收到 NAK 分组,情况 3 分组出现了差错,情况 1 时分组转换为 等待来自上层的调用1 状态,进入下半个周期,情况 2 与情况 3 时发送端重传 seq=0 的分组并保持当前状态等待确认分组等待来自下层的0 状态,第二种 2 中的报文已经被接收送往上层,转换为 等待来自下层的1 状态,第一种状态下跳转至 2,第二种状态存在两种情况,情况 1 重传的 seq=0 分组完好,此时接收方发送 ACK 分组,情况 2 重传的 seq=0 分组出现差错,此时接收方发送 NAK 分组,跳转至 3,直至发送端接收到 ACK 分组转换为 等待来自上层的调用1 状态



k,则该序号的范围是
从上层收到数据:若 超时:每个分组必须拥有自己的逻辑定时器,超时发生后只能发送一个分组 收到ACK:若分组序号在窗口内,则 SR 发送方将被确认的分组标记为已接收,若分组的序号等于 send_base 则将窗口基序号向前移动到最小的未确认分组处,如果窗口已到了有序号落在窗口内的未发送分组,则发送这些分组
序号在[rcv_base, rcv_base + N - 1] 内的分组被正确接收:回传选择 ACK (与当前收到分组 seq 对应) ,在接收端如果该分组以前没收到过,且序号大于接收窗口的基序号 (图中 rcv_base + 123的黑框框) 时,这些分组是失序分组,由接收端缓存,如果序号等于接收窗口的基序号 (rcv_base) 时,则将该分组及以前缓存的序号连续失序分组 (图中的 rcv_base + 1, rcv_base + 2, rcv_base + 3 的黑框框)交付给上层,接收窗口向前移动到下一个未交付的分组处 (图中 rcv_base + 4)序号在[rcv_base - N, rcv_base - 1] 内的分组被正确接收:必须产生一个 ACK (与分组 seq 对应),此分组出现的原因是接收端已经收到分组移动窗口,但返回的 ACK 损坏,发送端重传该分组,为了让发送端接收到 ACK 确认并让发送端窗口向前移动,所以要产生 ACK 其他情况:若出现分组损坏等情况则忽略该分组等待发送端重传

源端口号和目的端口号 (各 16 bits):用于多路复用 / 分解来自上层应用的数据序号字段 (sequence number field) 和 确认号字段 (acknowledgement number field)(各 32 bits):被 TCP 发送方和接收方用来实现可靠数据传输服务首部长度字段 (header length field)(4 bits):指示了以 32 bits 的字为单位的 TCP 首部长度 (最右边的括号里的 8)标志字段 (flag field) (最初定义 6 bits,现在存在 3 bits 的保留位与 9 bits 的控制位):
Accurate ECN:指示经过的路由器正在经历拥塞,被路由器置位 CWR (Congestion Window Reduced):发送方接收到了设置了 ECE 标志的 TCP 包,并已降低拥塞窗口的大小 ECE (ECN-Echo):这个标志在两种情况下会被设置。一种是作为对于收到设置了 CE(拥塞经历)标志的 IP 包的响应。另一种是在 TCP 三次握手中,用来指示 ECN(Explicit Congestion Notification,显式拥塞通告)的可用性 URG (Urgent):表示紧急指针字段有效ACK (Acknowledgement):表示确认字段有效PSH (Push):告诉接收方应该立即将接收到的数据传递给上层应用,而不是等待缓冲RST (Reset):用于重置一个错误的连接,或者拒绝非法的段或启动关闭连接SYN (Synchronize):在建立连接时使用,用于初始化序列号字段FIN (Finish):发送方完成发送任务,希望关闭连接紧急数据指针字段 (urgent data pointer field):紧急指针字段的值表示从当前序列号开始,紧急数据的序号数

import socket
import threading
def client_thread(conn, addr):
print(f"Connected to {addr}")
conn.send(b"Welcome to the Telnet server!\n")
while True:
try:
data = conn.recv(1024)
if not data:
break
conn.sendall(b"Echo: " + data)
except ConnectionResetError:
break
print(f"Connection with {addr} closed")
conn.close()
def start_telnet_server(host, port):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(5)
print(f"Telnet server started on {host}:{port}")
try:
while True:
conn, addr = server_socket.accept()
threading.Thread(target=client_thread, args=(conn, addr)).start()
except KeyboardInterrupt:
print("Shutting down the server...")
finally:
server_socket.close()
if __name__ == "__main__":
HOST = "127.0.0.1" # 绑定到所有网络接口
PORT = 23 # 标准Telnet端口号是23,但如果没有管理员权限,您可能需要选择1024以上的端口号
start_telnet_server(HOST, PORT)
accept() 方法是阻塞的,直到有新连接建立时 accept() 方法返回一个 (conn, addr) 对,conn 是一个新的套接字对象,用于在此连接上收发数据,address 是连接另一端的套接字所绑定的地址 (IP 和端口号),随后程序创建并启动新的线程调用 client_thread() 方法来处理这个连接,连接开始时服务端打印了客户端的地址,并调用 socket 对象发送欢迎文本,接着该子线程进入循环,recv 的参数 bufsize 指定 buffer 的大小 ( bufsize 参数指定了单次调用 recv方法时最多可以读取的字节数,这个大小是由程序员根据具体的网络应用程序而定的,它可以是任意非零的整数,告诉 recv 方法一次性从内部缓冲区中读取多少字节的数据),当 TCP 缓冲中没有数据可用时,recv() 会阻塞,如果 buffer 中有可用数据,但小于 bufsize,recv() 会返回实际可用的数据量,如果到达的数据量大于 bufsize,数据会存放在操作系统的缓冲区中,recv() 一次读取 bufsize 大小的数据,将这些数据加上 Echo: 字节串头部后使用 socket 对象发送回客户端lsof 命令查看 TCP 连接是否被建立,图中在回环地址的 56971 端口到 23 端口建立了一条 TCP 连接 

捎带 (piggybacked) :需要注意的是,在上图的情况中对于携带数据的报文段的确认 (ACK) 均是是一个单独的不携带数据的 ACK 报文段,在实际情况下,对携带数据报文的确认有可能被装载在返回的承载数据的报文段中,这种确认称为是被捎带在数据报文段中单个报文确认原则:为了避免对 RTT 估计引入额外的变化,通常不对重传的报文段进行 SampleRTT 的测量。换句话说,TCP 只测量对于那些被首次传输的报文段的确认,而不是对重传的报文段的确认Karn/Partridge 算法:如果一个报文必须重传,那么它的 SampleRTT 就不会被采用,因为这个 RTT 可能包含了重传的延迟,这不利于准确计算网络的真实 RTT避免并行测量:TCP 通常避免在同一时间测量多个报文段的 SampleRTT,通常是在任何给定的时间只对一个在传输路径上的数据段测量 SampleRTT。这样做是为了防止报文段的确认回应由于网络路径上的排队延迟而彼此影响,进而影响 RTT 的准确性样本选择:在某些实现中,TCP 可能会选择性地估算 RTT,例如只在特定间隔或者对特定的报文段测量 SampleRTTRFC 6298 定义了 RTT 偏差,用于估算 SampleRTT 会偏离 EstimatedRTT 的程度,快速重传(Fast Retransmit): 当接收方收到一个失序的数据包时,它会立即发送一个重复的 ACK(也就是隐式 NAK),这是当前已正确接收的最高序列号的数据包。如果发送方收到三个或者更多连续的重复 ACK,它将推测在传输中出现了数据包丢失,并且会在定时器到期之前立即重传那些丢失的数据包)TCP 可靠数据传输简化描述:假设发送方不受 TCP 流量和拥塞控制的限制的,来自上层数据的长度小于 MSS,且数据传送只在一个方向进行NextSeqNum = InitialSeqNumber
SendBase = InitialSeqNumber
while (1) {
switch (event) {
case 1: //从上层接收到数据e
sndpkt[NextSeqNum] = make_pkt(NextSeqNum, data, checksum);
if (isTimerInactive) //定时器当前没有运行
timer_start();
udt_send(sndpkt[NextSeqNum]);
NextSeqNum = NectSeqNum + length(data);
break;
case 2: //定时器超时
udt_snd(sndpkt[SendBase]); //重传具有最小序号但仍未应答的报文段
timer_start();
break;
case 3: //收到ACK,Ack = y
if (y > SendBase) {
SendBase = y; //累积确认
if (NextSeqNum > SendBase) //当前有未被确认的报文段
timer_start();
}
break;
}
/*没有事件到来时循环阻塞*/
}



快速重传(fast retransmit),即在该报文段的定时器过期前重传丢失的报文段if (y > SendBase) {
SendBase = y;
DupAck = 0;
if (NextSeqNum > SendBase)
timer_start();
} else { /*当收到对已经确认报文段的一个冗余ACK,此时 y = SendBase*/
DupAck++;
if (DupAck == 3) { //TCP快速重传
udt_snd(sndpkt[y]) //重新发送具有序号 y 的报文段
}
}
break;
已发送但未被确认的字节的最小序号(SendBase) 和 下一个要发送的字节的序号(NextSeqNum)RcvBuffer的接收缓存,主机 B 上的应用进程不时从该缓存中读取数据,定义 LastByteRead 为主机 B 的应用进程从缓存读出的数据流的最后一个字节的编号,LastByteRcvd 为从网络中到达的并且已放入主机 B 接收缓存中的数据流的最后一个字节的编号rwnd 表示 rwnd 值放入报文段接收窗口字段中,通知主机 A 在该连接的缓存中还有多少可用空间rwnd 内,即

RST 标志代表一个异常的终止,通常有以下几种情况:
主动拒绝连接建立:如果接收方收到 SYN 报文段,但在对应端口上没有运行服务,不希望建立连接,则可以发送 RST 报文段来拒绝连接,告诉该源"我没有那个报文段的套接字,请不要再发送该报文段了",如果接收的 UDP 分组目的端口与 UDP 套接字不匹配,主机会发送一个特殊的 ICMP 报文致命的协议错误:如果一个端点检测到对方违反了协议的规定时,发送 RST 报文段来关闭连接未经期望的数据到达:如果一个 TCP 端点接收到不存在的连接到达的数据,可以发送 RST 报文段回复

端到端拥塞控制:网络层没有为运输层拥塞控制提供显式支持,网络层对端系统来说是一个黑箱,需要通过对 IO 的观察 (分组丢失与时延) 来推断,TCP 采取端到端的拥塞控制方法网络辅助的拥塞控制 :在 ATM 可用比特率 (Available Bite Rate, ABR) 拥塞控制中,路由器显式地通知发送方能在输出链路上支持的最大主机发送速率
慢启动 (slow-start)
拥塞避免
快速恢复