运输层
# 传输层
运输层位于应用层和网络层之间,为运行在不同主机上的应用进程之间提供了逻辑通信(logic communication)。
# 运输层服务
运输层协议为运行在不同的主机上的应用进程之间提供了**逻辑通信(logic communication)**功能。
运输层报文段(segment):在发送到,运输层将从发送应用程序进程接收到的报文转换成运输层分组,用因特网术语来讲该分组称为运输层报文段。
# 运输层和网络层的关系
网络层:提供了主机之间的逻辑通信。
运输层:为运行在不同主机上的进程之间提供了逻辑通信。
运输层协议只工作在端系统中,能为应用程序提供可靠的数据传输服务。
# 因特网运输层概述
因特网运输层有两种协议:TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)。
TCP是一种面向连接、可靠的服务,UDP是一种无连接、不可靠的服务。
UDP和TCP最基本的任务是,将两个端系统间IP
的交付服务扩展为运行在端系统上的两个进程之间的交付服务。
网络层的IP(Internet Protocol,网际协议)协议:IP
的服务模型是尽力而为交付服务(best-effort delivery service),是一个不可靠服务(unreliable service)。它不确保报文段的交付,不保证报文段的按序交付,不保证报文段中数据的完整性。
# 多路复用和多路分解
将主机间交付扩展到进程间的交付被称为运输层的多路复用(transport-layer multiplexing)与多路分解(demultiplexing)。
多路复用(multiplexing):在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层,所有这些工作称为多路复用。
多路分解(demultiplexing):将运输层报文段中的数据交付到正确的套接字的工作称为多 路分解。
运输层多路复用的要求:
- 套接字有唯一标识符
- 每个报文有特殊字段来指示该报文段所要交付到的套接字。
- 这些特殊字段是源端口号字段(source port number field)和目的端口号字段(destination port number field)
- 端口号是一个16比特的数,其大小在
0~65535
之间。0~1024
范围的端口号称为周知端口号(well-known port number),是受限制的,在RFC 1700和RFC 3232中给出。
# 无连接(UDP)的多路复用和多路分解
一个UDP套接字是一个由一个二元组全面标识的,该二元组包含一个目的IP地址和一个目的端口号。因此,如果两个UDP报文段有不同的源IP地址或源端口号,但具有相同的目的IP地址
和目的端口号
,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程。
# 面向连接(TCP)的多路复用和多路分解
TCP套接字是一个由一个四元组(源IP地址,源端口号,目的IP地址,目的端口号)来标识的。因此,当一个TCP报文段从网络到达一台主机时,该主机使用全部4个值来将报文定向(分解)到相应的套接字。
# 无连接运输:UDP
运输层最低限度必须提供一种复用/分解服务,以便在网络层与正确的应用级进程之间传递数据。
由RFC 768 (opens new window)定义的UDP只是做了运输协议能够做的最少工作。处理复用/分解功能及少量的差错检测外,它几乎没有对IP增加别的东西。
# UDP的特点
- “no frills,” “bare bones” Internet传输协议
- “尽力而为”的服务,报文段可能丢失,送到应用进程的报文段可能乱序
- 无连接
- UDP发送端和接收端之间没有握手
- 每个UDP报文段都被独立地处理
- UDP被用于
- 流媒体(丢失不敏感,速率敏感、应用可控制传输速度)
- DNS
- SNMP
- 在UDP上实现可靠传输
- 在应用层增加可靠性
- 应用特定的差错恢复
# 为什么要有UDP
- 关于发送什么数据以及何时发送的应用层控制更为精细。
- 无拥塞控制和流量控制,UDP可以尽可能快的发送报文段。
- 无须建立连接。
- TCP在开始数据传输之前要经过三次握手,而UDP却不需要任何准备即可进行数据传输,可以减少建立连接的时延。
- 无连接状态。
- TCP需要在端系统中维护连接状态,而UDP不维护连接状态,也不跟踪这些状态参数。
- 分组首部开销小。
- 每个TCP报文段都有20字节的首部开销,而UDP仅有8字节的开销。
# UDP报文段结构
UDP报文段结构如图所示,它由RFC 768 (opens new window)定义。应用层数据占用UDP报文段的数据字段(application data)。
- 源端口号(source port):这个字段占据UDP报文头的前16位。源端口是一个可选字段,如果有意义,它指示发送进程的端口,并且可以假设是在没有任何其他信息的情况下应答应寻址的端口。如果未使用,则插入零值。这样,接收端的应用程序就不能发送响应了。
- 目的端口号(dest port):目的地端口在特定互联网目的地地址的上下文中具有含义,占据16位。
- 长度(length):该字段占据16位,长度字段指示了在UDP报文段中的字节数(首部加数据)。因为UDP报文头长度是8个字节,所以这个值最小为8。
- 校验和(checksum):校验和是来自IP报头、UDP报头和数据的伪报头信息的补和中的16位1的补和,在末尾用零个八位字节填充(如有必要)以形成两个八位比特的倍数。占据16位。
# UDP校验和
目标: 检测在被传输报文段中的差错 (如比特反转)。
发送方:
- 将报文段的内容视为16比特的整数
- 校验和:报文段的加法和 (1的补运算)
- 发送方将校验和放在UDP的校验和字段
接收方:
- 计算接收到的报文段的校验和
- 检查计算出的校验和与校验和字段的内容是否相等:
- 不相等–--检测到差错
- 相等–--没有检测到差错,但也许还是有差错
- 残存错误
# 分析UDP通信
- 客户端(linux):192.168.122.131
- 服务器(linux):192.168.122.130
通信过程:
UDP Client
向Server发送字符串hi
UDP Server
向Client返回HI
# 面向连接的运输:TCP
为了提供可靠数据传输,TCP依赖了许多原理,其中包括差错检测、重传、累积确认、定时器以及用于序号和确认号的首部字段。TCP定义在 RFC 793 (opens new window)、RFC 1122 (opens new window)、RFC 1323、RFC 2018 以及 RFC 2581 中。
# TCP报文段结构
TCP
报文段由首部字段和一个数字字段组成。数字字段包含一块应用数据。MMS
(MSS
见下文)限制了报文段数据字段的最大长度。当TCP
发送一个大文件时,TCP
通常是将该文件划分成长度为MMS
的若干(最后一块除外,它通常小于MMS
)。交互式应用通常传送长度小于MMS
的数据块。
TCP
的首部一般是20字节(比UDP
首部多12字节)。
- 源端口号和目的端口号
- TCP源端口(Source Port):源计算机上的应用程序端口号,占16位。
- TCP目的端口(Destination Port):目标计算机的应用程序端口号,占16位。
- 序列号字段和确认号字段
- TCP序列号(Sequence Number):占32位。它表示本报文所发送数据的第一个字节的编号。在
TCP
连接中,所传送的字节流的每一个字节都会按顺序编号。当SYN
标记不为1时,这是当前数据分段第一个字母的序列号;如果SYN
的值是1时,这个字段的值就是初始序列值(ISN
),用于对序列号进行同步。这时,第一个字节的序列号比这个字段的值大1,也就是ISN
加1。 - TCP确认号(Acknowledgment Number,ACK Number):占32位。它表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。其值是接收计算机即将接收到的下一个序列号,也就是下一个接收到的字节的序列号加1。
- TCP序列号(Sequence Number):占32位。它表示本报文所发送数据的第一个字节的编号。在
- 数据偏移字段
- TCP首部长度(Header Length):数据偏移是指数据段中的“数据”部分起始处距离
TCP
数据段起始处的字节偏移量,占4位。其实这里的“数据偏移”也是在确定TCP数据段头部分的长度,告诉接收端的应用程序,数据从何处开始。
- TCP首部长度(Header Length):数据偏移是指数据段中的“数据”部分起始处距离
- 保留字段
- 保留(Reserved):占4位。为TCP将来的发展预留空间,目前必须全部为0。
- 标志位字段
- CWR(Congestion Window Reduce):拥塞窗口减少标志,用来表明它接收到了设置
ECE
标志的TCP
包。并且,发送方收到消息之后,通过减小发送窗口的大小来降低发送速率。 - ECE(ECN Echo):用来在TCP三次握手时表明一个TCP端是具备ECN功能的。在数据传输过程中,它也用来表明接收到的
TCP
包的IP
头部的ECN
被设置为11,即网络线路拥堵。 - URG(Urgent):表示本报文段中发送的数据是否包含紧急数据。
URG=1
时表示有紧急数据。当URG=1
时,后面的紧急指针字段才有效。 - ACK:表示前面的确认号字段是否有效。
ACK=1
时表示有效。只有当ACK=1
时,前面的确认号字段才有效。TCP
规定,连接建立后,ACK
必须为1。 - PSH(Push):告诉对方收到该报文段后是否立即把数据推送给上层。如果值为1,表示应当立即把数据提交给上层,而不是缓存起来。
- RST:表示是否重置连接。如果
RST=1
,说明TCP连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。 - SYN:在建立连接时使用,用来同步序号。当
SYN=1
,ACK=0
时,表示这是一个请求建立连接的报文段;当SYN=1
,ACK=1
时,表示对方同意建立连接。SYN=1
时,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中SYN
才为1。 - FIN:标记数据是否发送完毕。如果
FIN=1
,表示数据已经发送完成,可以释放连接。
- CWR(Congestion Window Reduce):拥塞窗口减少标志,用来表明它接收到了设置
- 窗口大小字段
- 窗口大小(Window Size):占16位。它表示从
Ack Number
开始还可以接收多少字节的数据量,也表示当前接收端的接收窗口还有多少剩余空间。该字段可以用于TCP
的流量控制。
- 窗口大小(Window Size):占16位。它表示从
- TCP校验和字段
- 校验位(TCP Checksum):占16位。它用于确认传输的数据是否有损坏。发送端基于数据内容校验生成一个数值,接收端根据接收的数据校验生成一个值。两个值必须相同,才能证明数据是有效的。如果两个值不同,则丢掉这个数据包。
Checksum
是根据伪头+TCP头+TCP数据三部分进行计算的。 - 紧急指针(Urgent Pointer):仅当前面的URG控制位为1时才有意义。它指出本数据段中为紧急数据的字节数,占16位。当所有紧急数据处理完后,TCP就会告诉应用程序恢复到正常操作。即使当前窗口大小为0,也是可以发送紧急数据的,因为紧急数据无须缓存。
- 校验位(TCP Checksum):占16位。它用于确认传输的数据是否有损坏。发送端基于数据内容校验生成一个数值,接收端根据接收的数据校验生成一个值。两个值必须相同,才能证明数据是有效的。如果两个值不同,则丢掉这个数据包。
- 可选项(Options):该字段用于发送方与接收方协商最大报文长度(
MSS
),或在高速网络环境下用作窗口调节因子时使用。长度不定,但长度必须是32bits的整数倍。可参见RFC 854
和RFC 1323
了解更多细节。
# TCP连接
- TCP的“连接”是一条逻辑连接,其共同状态仅保留在两个通信端系统的TCP程序中。
- TCP连接提供的是全双工服务(full-duplex service)。
- TCP连接也总是点对点(point-to-point)的,即在单个发送方与单个接收方之间的连接。
# TCP连接的建立(三次握手)
发起连接的进程被称为客户进程,而另一个接受连接的进程被称为服务器进程。
clientSocket.connect((serverName, serverPort))
建立连接三次握手(three-way handshake)的过程:
- 客户首先发送一个特殊的TCP报文段。
- 服务器用另一个特殊的TCP报文段来响应。
- 最后,客户再用第三个特殊报文段作为响应。
前两个报文段不承受“有效载荷”,也就是不包含应用层数据;而第三个报文段可以承载有效载荷。
# 三次握手的过程分析
三次握手实验,使用wireshark
抓包分析。
- 客户端(linux):192.168.122.131
- 服务器(linux):192.168.122.130
服务器启动端口号为1000的TCP服务,客户端向服务器发起TCP请求。可以使用Telnet
或nc
等方式模拟。
1 0.000000000 192.168.122.131 192.168.122.130 TCP 74 46020 → 1000 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=167606712 TSecr=0 WS=128
2 0.001003923 192.168.122.130 192.168.122.131 TCP 74 1000 → 46020 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM=1 TSval=3903728312 TSecr=167606712 WS=128
3 0.001065679 192.168.122.131 192.168.122.130 TCP 66 46020 → 1000 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=167606713 TSecr=3903728312
2
3
第一次握手
第1次握手建立连接时,客户端向服务器发送SYN报文(SEQ=x,SYN=1),并进入SYN_SENT
状态,等待服务器确认。
Src: 192.168.122.131, Dst: 192.168.122.130
,源IP地址为192.168.122.131
,目标IP地址为192.168.122.130
。
在Transmission Control Protocol
中可以看到,Flags
为SYN
,并且Syn
的值设置为1(客户端SYNbit=1
),表示该数据包是192.168.122.131
向主机192.168.122.130
发起的请求,希望建立TCP连接。
Sequence Number
表示请求序列化Seq
,值为0(客户端Seq=x
,此时x
的值为0),是由主机192.168.122.131
随机生成的。
第2次握手
第2次握手实际上是分两部分来完成的,即SYN
+ACK
(请求和确认)报文。
- 服务器收到客户端的请求,向客户端回复一个确认信息(
ACKnum=x+1
) - 服务器再向客户端发送一个
SYN
包(Seq=y
)建立连接的请求,此时服务器进入SYN_RCVD
状态。
Src: 192.168.122.130, Dst: 192.168.122.131
,源IP地址为192.168.122.130
,目标IP地址为192.168.122.131
。
Flags
为(SYN, ACK)
,将SYN
置为1(服务器SYNbit=1
),并且将Ack
置为1(服务器ACKbit=1
),表示该数据包是主机192.168.122.130
用来回复主机192.168.122.131
发来的TCP连接请求。
Acknowledgment Number
表示ACK
,值为1(服务器ACKnum=x+1
,即ACKnum=0+1=1
)。该值是回复主机192.168.122.131
发来的连接请求SEQ
,因此在SEQ
的基础上加1,以代表确认。
Sequence Number
值为0(服务器Seq=y
,此时y
的值为0),该值时由主机192.168.122.130
生成的,是向主机192.168.122.131
发送的SYN
,表示同意该主机发来的连接请求。
第3次握手
第3次握手,是客户端收到服务器的回复(SYN
+ACK
报文)。此时,客户端也要向服务器发送确认包(ACK
)。此包发送完毕客户端和服务器进入ESTABLISHED
状态,完成3次握手。
Src: 192.168.122.131, Dst: 192.168.122.130
,源IP地址为192.168.122.131
,目标IP地址为192.168.122.130
。
在Transmission Control Protocol
中可以看到,Flags
为ACK
,Ack
的值置为1(客户端ACKbit=1
)。表示该数据包是主机192.168.122.131
对主机192.168.122.130
发来的同意连接数据包后做出的确认回复。
Acknowledgment number
的值为1(客户端ACKnum=y+1
,即ACKnum=0+1=1
),该值是在主机192.168.122.130
发来的SEQ
的基础上加1得到的。
Sequence Number
的值为1,表示收到主机192.168.122.130
发来的同意连接数据包后,再次向该主机发送连接请求,表示要连接了。
# TCP连接
建立TCP连接之后,两个应用进程之间就可以互相发送数据了。客户进程向服务器进程发送数据需要经过多个流程:
- 客户进程通过套接字(该进程之门)传递数据流。数据一旦通过该门,它就由客户运行的TCP控制了。
- TCP将这些数据引导到该连接的发送缓存(send buffer)里,发送缓存是三次握手期间设置的缓存之一。
- 接下来TCP就会不时从发送缓存里取出一块数据,然后为每块客户数据配上一个TCP首部,从而形成多个TCP报文段(TCP segment)。
- 这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中。
- 然后这些IP数据报被发送给网络中。
- 当TCP在另一端(服务器进程TCP)接收到一个报文段后,该报文段的数据就被放入该TCP连接的接收缓存中。
- 最后应用程序从此缓存中读取数据流。
TCP连接的组成包括:一台主机上的缓存、变量和与进程理解的套接字,以及另一个主机上的另一组缓存、变量和与进程连接的套接字。
# MSS
与MTU
TCP可以从缓存中取出并放入报文段中的数据量受限于最大报文段长度(Maximum Segment Size, MSS)。
MSS
通过根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元(Maximum Transmission Unit,MTU))来设置。
MSS
与MTU
的关系:
MSS
就是TCP
数据包每次能够传输的最大数据分段。为了达到最佳的传输效能TCP
协议在建立连接的时候通常要协商双方的MSS
值,这个值TCP
协议在实现的时候往往用MTU
值代替(需要减去IP
数据包包头的大小20Bytes
和TCP
数据段的包头20Bytes
)所以往往MSS
为1460
。通讯双方会根据双方提供的MSS
值得最小值确定为这次连接的最大MSS
值。
MSS
是指在报文段里应用层数据的最大长度,而不是包括首部的TCP报文段的最大长度。MTU
即物理接口(数据链路层)提供给其上层(通常是IP层)最大一次传输数据的大小。- 当
MSS
加上TCP/IP
首部长度(通常40字节)后的长度就MTU
的大小了。 - 以太网和
PPP
链路层协议都具有1500字节的MTU
,因此MSS
的典型值为1460字节。
TCP
在三次握手建立连接过程中,会在SYN
报文中使用MSS(Maximum Segment Size)
选项功能,协商交互双方能够接收的最大段长MSS
值。
# TCP数据传输
TCP通过使用肯定确认与定时器来提供可靠数据传输。TCP也使用流水线,使得发送方在任意时刻都可以有多个已发出但还未被确认的的报文段存在。一个发送方能够具有的未被确认报文段的具体数量是由TCP的流量控制和拥塞控制机制决定的。
以telnet
为例,Host A
向Host B
发起数据传输。Host A
向Host C
发送一个字符C
,Host B
收到后返回一个字符C
。
Host A
向Host B
发送字符C
:序列号Seq
的值为42;ACK=79
,表示Host B
的序列号Seq
从79开始;data='C'
表示字符C
。Host B
向Host A
返回字符C
:序列化Seq
的值为79;ACK=43
,表示确认收到了42及其之前的报文段,希望Host A
从43开始传;data='C'
表示传送给Host A
的数据。对客户到服务器的数据的确认被装载在一个承载服务器到客户的数据的报文段中;这种确认被称为是捎带(piggybacked)在服务器到客户的数据报文段中的。
Host A
向Host B
返回确认ACK
:Seq=43
,ACK=80
,表示收到了Host B
发过来的字符。
# 可靠数据传输
IP
服务是不可靠的,而TCP
在IP
不可靠的尽力而为服务之上创建了一种可靠数据传输服务(reliable data transfer service)。
# 面向拥塞控制原理
# TCP拥塞控制
# 附录
# UDP测试程序
udpClient.py
import sys
from socket import *
server_name = sys.argv[1]
server_port = int(sys.argv[2])
# 创建UDP Socket
client_socket = socket(AF_INET, SOCK_DGRAM)
# 输入需要传输的信息
message = input(">")
# 将服务器名称、端口附加到消息;发送到socket
client_socket.sendto(message.encode(), (server_name, server_port))
# 将套接字中的应答字符读到字符串
modified_message, server_address = client_socket.recvfrom(2048)
# 打印结果
print(modified_message.decode())
client_socket.close
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在192.168.122.130
(服务端)上启动UDP Server
:
python3 udpServer.py 1700
udpServer.py
import sys
from socket import *
server_port = int(sys.argv[1])
# 创建UDP Socket
server_socket = socket(AF_INET, SOCK_DGRAM)
# 绑定本地端口号12000
server_socket.bind(("", server_port))
print("The server is ready to receive")
while True:
# 从socket中接收消息和客户端地址(IP和端口号)
message, client_address = server_socket.recvfrom(2048)
modified_message = message.decode().upper()
server_socket.sendto(modified_message.encode(), client_address)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后在192.168.122.131
(客户端)上启动UDP Client
,并向Server发送字符串hi
:
# python3 udpClient.py 192.168.122.130 1700
>hi # 发送信息:hi
HI # server的回包
2
3
# TCP测试程序
tcpServer.py
import sys
from socket import *
server_port = int(sys.argv[1])
# 创建TCP Welcome Socket
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.bind(('', server_port))
# server begins listening for incoming TCP requests
server_socket.listen(1)
print('The server is ready to receive')
while True:
# server waits on accept() for incoming requests, new socket created on return
connection_socket, addr = server_socket.accept()
# read bytes from socket (but not address as in UDP)
sentence = connection_socket.recv(1024).decode()
capitalized_sentence = sentence.upper()
# close connection to this client (but not welcoming socket)
connection_socket.send(capitalized_sentence.encode())
connection_socket.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在192.168.122.130
(服务端)上启动TCP Server
:
python3 tcpServer.py 1000
tcpClient.py
import sys
from socket import *
server_name = sys.argv[1]
server_port = int(sys.argv[2])
# 为远程服务器创建TCP Socket
client_socket = socket(AF_INET, SOCK_STREAM)
client_socket.connect((server_name, server_port))
sentence = input(">")
# No need to attach server name, port
client_socket.send(sentence.encode())
modified_sentence = client_socket.recv(1048)
print('From Server:', modified_sentence.decode())
client_socket.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后在192.168.122.131
(客户端)上启动TCP Client
,并向Server发送字符串hello
:
$ python3 tcpClient.py 192.168.122.130 1000
>hello
From Server: HELLO
2
3