车载以太网最少必要姿势之传输层 车载以太网1000base

佚名 2024-02-17

车载以太网最少必要知识之传输层

这是《车载以太网最少必要知识》系列之传输层(Transport Layer),对应OSI七层模型的Transport Layer。

传输层作为上层应用软件(Application Layer)与下层网络层(Network Layer)之间的接口,弥补了网络层传输不可靠、不对数据流进行控制等缺失。传输层的协议主要有两个:TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。虽然它们的工作原理、数据格式各不相同,但都需要用到一个共同的概念Socket

通过阅读该篇文章,我们将对Socket、UDP、TCP等概念建立最少的且必要的了解。

1. Socket

Socket指的是IP地址和端口号(Port Number) 的组合,即IP Address:Port Number,比如127.0.0.1:65432。它是对某个设备上的某个应用进程(Process)的标记,设备对应IP地址,进程对应端口号。

Socket可以看作是更深一个层级的地址(an additional level of addressing)。之所以要这么做,是因为通讯的双方可能同时有多个应用进程在交互,仅仅通过IP地址是不足以区分,哪些数据要具体给到哪个应用来处理的。甚至同一个应用软件也可能有多个实例(Instance),比如在浏览器里打开多个窗口(Tab)。

如上图所示,也正是因为多了一个层级的地址,在设备A与服务器之间,多个进程(或服务,SMTP、HTTP、DNS、DHCP)才可以同时通讯。把不同的应用下发过来的数据,放到同样的IP数据报里的操作(对于IP层来说,它反正也看不见里边的payload部分),叫做Multiplexing;相反,接收方从同样的IP数据报里拆解出数据分发给不同的进程去处理,叫做Demultiplexing

Many application processes run simultaneously and have their data multiplexed for transmission. It is the impetus for why higher-level addressing is a necessity in TCP/IP.

1.1 端口

前边我们提到,Socket是由IP地址和端口号组成的。在通讯的双方看来,某条信息的发送方需指定源端口(Source Port),信息的接收方需指定目标端口(Destination Port)。

端口的标识采用16个bits,故端口号的取值范围0至65535。具体哪个端口号与哪一类进程所绑定,是由IANA(Internet Assigned Numbers Authority)【1】统一管理的。

对于服务端(Server),端口号的分类方式有三种,

  • • 约定熟成的(Well-Known),取值范围从0至1023,也叫System Port Number,属于IANA预留的;

  • • 注册的(Registered),取值范围从1024至49151,也叫User Port Number,需要向IANA申请并获得审批;

  • • 私有的(Private),取值范围49152至65535,用于私有协议。

服务端之所以要使用约定的或者注册的端口号,是因为请求服务的一方,必须知道服务器上确切的端口号。但是对于发起请求的客户端(Client)来说,它的身份可以随意变动,就好像火警电话119一旦设定就不能随意更改,但是打电话的人就很随机。

为了防止出错,客户端甚至是不能够使用约定的或者注册的端口号的,因为很可能某些客户端也会是服务端,这就造成了混乱。这种随意给客户端进程分配的端口号叫做临时端口号(Ephemeral Port Number)。临时端口号的取值范围是1024至4999,需要注意,这是传统的做法,来自BSD(Berkeley Standard Distribution)。当前更推荐的取值范围是49152至65535【2】。

所有约定的和注册的端口号,可以通过该链接【3】查询。比较常用的端口号有:HTTPS(443)、HTTP(80)、SMTP(25)、Telnet(23)、FTP(21)、Echo(7)。

1.2 Socket库

Python提供了官方库socket【4】,这里我们参考realpython的一个示例【5】简单做个了解。

Client端的代码如下,连接成功后,Client向Server端的Socket 127.0.0.1:65432发送数据"Hello, world"。

# echo-client.py

import socket

HOST = "127.0.0.1"  # The server's hostname or IP address
PORT = 65432  # The port used by the server

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b"Hello, world")
    data = s.recv(1024)

print(f"Received {data!r}")

Server端的代码如下,收到Client端的数据后,添加前缀"Loopback from Server Port "并返回数据。

# echo-server.py

import socket

HOST = "127.0.0.1"  # Standard loopback interface address (localhost)
PORT = 54321  # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(b"Loopback from Server Port " + data)

Server的执行结果如下,可以看到Client使用的Socket为127.0.0.1:58117

C:\Users\zheng\Documents\Books\Book_AutomotiveEthernet>python echo-server.py
Connected by ('127.0.0.1', 58117)

Client的执行结果如下。

C:\Users\zheng\Documents\Books\Book_AutomotiveEthernet>python echo-client.py
Received b'Loopback from Server Port Hello, world'


2. UDP

在传输层的两个协议里,UDP相对简单,因为它不需要建立连接,也没有错误检测和流量控制的机制。这里我们将从UDP的信息格式开始介绍,并结合CANoe的实例加深理解。

2.1 UDP Message Format

对于IPV4和IPV6,UDP的信息格式是不一样的。这里我们仅讨论IPV4

由上图可知,UDP信息由以下几个部分构成,其中UDP报头(UDP Header)占8个字节:

  1. 1. Source Port:源端口号, 占2个字节;

  2. 2. Destination Port:目标端口号,占2个字节;

  3. 3. Length:指的是整个UDP信息的长度,包括UDP报头和数据(Data Fields),占2个字节;

  4. 4. Checksum:对于IPV4,Checksum是可选的,如果不使用该区域都用0填充;具体的计算方式参加2.3章节;

  5. 5. Data:应用层发过来的数据。

2.2 CANoe实例

借用CANoe自带的Sample Configuration,“Basic UDP Sender and Receiver (CAPL)”来看下实例。

  1. 1. 源端口号18651;

  2. 2. 目标端口号40001;

  3. 3. 数据长度20个字节,含8个字节的UDP报头,和12个字节的数据:"Hello World!",即第5部分payload;

  4. 4. checksum计算值为17766。

2.3 UDP Checksum

计算checksum时,需要先创建一个伪报头(pseudo header),仅作为计算用。伪报头的格式如下,凑了1个字节的0(Zeros)为了组成12个字节;加上8个字节的UDP报头,共20个字节。另外UDP Length实际上等于UDP报头里的Length。

这里我们借助chatGPT生成一段python代码,实现16-bits checksum的计算。

def caculate_udp_checksum(data):
    # 将数据按16位划分成块
    words = [int(data[i:i+4], 16for i in range(0len(data), 4)]

    # 初始化校验和
    checksum = 0

    # 对每个16位块进行求和
    for word in words:
        checksum += word

        # 如果和的高16位不为零,则将其加到和的低16位上
        checksum = (checksum & 0xFFFF) + (checksum >> 16)

    # 对和取反得到最终校验和
    checksum = ~checksum & 0xFFFF

    return checksum

# 定义IPV4伪头部字段
source_address = 0xc0a80101  # 源地址
destination_address = 0xc0a80102  # 目标地址
zeros = 0x00 #填充1个字节的0
protocol = 0x11     # 协议(UDP)
udp_length_in_ipv4 = 0x0000 # UDP长度(初始值,后续计算)

# 定义UDP头部字段
source_port = 0x48db  # 源端口
destination_port = 0x9c41  # 目标端口
udp_length = 0x0000  # UDP长度(初始值,后续计算)
udp_checksum = 0x0000  # UDP校验和(初始值,后续计算)

# 定义UDP数据
udp_data = "48656c6c6f20576f726c6421"  # 内容为"Hello World!"

# 计算UDP长度
udp_length = int(len(udp_data)/2 + 8)  # UDP头部长度为8字节,加上数据长度
udp_length_in_ipv4 = udp_length

# 构建IPv4伪头部
ipv4_pseudo_header = f"{source_address:08x}{destination_address:08x}{zeros:02x}{protocol:02x}{udp_length_in_ipv4:04x}"

# 构建UDP头部
udp_header = f"{source_port:04x}{destination_port:04x}{udp_length:04x}{udp_checksum:04x}"

# 计算UDP校验和
checksum_data = ipv4_pseudo_header + udp_header + udp_data
print("Checksum Data", checksum_data)
udp_checksum_result = caculate_udp_checksum(checksum_data)

# 更新UDP头部的校验和字段
udp_header = f"{source_port:04x}{destination_port:04x}{udp_length:04x}{udp_checksum_result:04x}"

# 更新IPV4伪头部
ipv4_pseudo_header = f"{source_address:08x}{destination_address:08x}{zeros:02x}{protocol:02x}{udp_length_in_ipv4:04x}"

# 打印结果
print("IPv4伪头部:", ipv4_pseudo_header)
print("UDP头部:", udp_header)
print("UDP数据:", udp_data)
print("UDP Checksum:", udp_checksum_result)

输入的数据与2.2章节里的实例一致,可以看到计算的结果也一致。

Checksum Data c0a80101c0a801020011001448db9c410014000048656c6c6f20576f726c6421
IPv4伪头部: c0a80101c0a8010200110014
UDP头部: 48db9c4100144566
UDP数据: 48656c6c6f20576f726c6421
UDP Checksum: 17766

需要注意的是,存在某种情况下checksum计算为0。前边我们说到Checksum字段是可选的,如果不使用该区域用0填充。因此当计算得到的checksum值为0时,需要刻意都修改为用1填充。


3. TCP

UDP的机制相对简单,它是基于Message的,也叫message-oriented;但是TCP是基于数据流(Data Stream)的,它的机制更为复杂,也叫stream-oriented。上层应用传过来的数据,并不是立即发送出去的。而是需要先存到buffer里,具体什么时候发送,发送多少个字节取决于Sliding Window的机制。

接下来我们将分五个部分展开讨论,分别是:TCP的数据格式,TCP的状态机,Sliding Window机制,TCP的可靠性和流量控制。

3.1 TCP Segment Format

从上层应用传过来的数据流,被TCP拆成若干个小段再传递给IP层,这样的数据小段叫做TCP Segment

由上图可知,TCP Segment由以下几个部分构成,其中TCP报头(TCP Header)至少占20个字节:

  1. 1. Source Port:源端口号, 占2个字节;

  2. 2. Destination Port:目标端口号,占2个字节;

  3. 3. Sequence Number:指的是在该Segment里数据(Data)的第一个字节,在整个数据流里的序号,占4个字节;

  4. 4. Acknowledge Number:指的是期望下一次接收到的Segment里数据的第一个字节,相当于做应答,表示之前的数据都已经接收到了,占4个字节;

  5. 5. Data Offset:表示TCP报头的长度,单位是words(32-bits,也就是4个字节);最小值是5,表示20字节;最大值是15,表示60字节;本身占4个bit;

  6. 6. Reserved:预留,占6个bit;

  7. 7. Control Bits:指的是六种flag,占6个bit;

    • • URG,urgent bit

    • • ACK,acknowledgement bit

    • • PSH,push bit

    • • RST,reset bit

    • • SYN,synchronize bit

    • • FIN, finish bit

  8. 8. Window,指的是发出该TCP Segment的一方,期望对方最多能够发送的数据大小,即是自己的Receive Window,也是对端设备的Send Window;占2个字节;

  9. 9. Checksum:同UDP一样,也是采用的16-bit Checksum算法,也需要构建12个字节的伪报头用于计算;

  10. 10. Urgent Pointer:与Control Bit里的URG联动,当URG置1时,这个数据表示urgent data最后一个字节的Sequence Number;

  11. 11. Options:可选字段;

  12. 12. Paddings:使用0进行填充,使得Options+Paddings构成32 bits的整数倍;

  13. 13. Data:应用层发过来的数据。

不难看出,TCP Segment的数据格式比UDP复杂很多。这里我们结合CANoe的实例,就一些重要的概念再深入地探讨下。

3.1.1 Sequence Number

为了跟踪记录数据流里每个字节的状态,比如是否被发送,是否被应答,TCP数据格式里引入了序列号(Sequence Number)的概念。

当Control Bit里SYN=1时,表明这是连接请求报文(Connection Request Message, SYN),此时的序列号指的是初始序列号(ISN,Initial Sequence Number)。序列号的取值范围是0至4294967295,但是初始序列号并不是从0开始。

简单来说,当TCP进程启动时,有一个计数器也一起启动,并且每隔4ms自增1。任意一个TCP连接建立时,取当时的计数器的值作为初始序列号。如此,需要间隔大概四个小时才可能出现相同的ISN,进而可以避免出错。示例中,从第2条可以看到Seq=2892544100。小编认为,还有随机数(random number)的方式给ISN赋值。

而当Control Bit里SYN=0时,表明这是正常通讯报文(normal transmission message),此时的序列号指的是在当前这个Segment里,数据的第一个字节在整个数据流里的位置。示例中,从第4条可以看到Seq=2892544101,在之前的基础上增加了1,表明第一份数据"Hello World!"在数据流里的起始位置;到了第6条可以看到Seq=2892544113,在之前的基础上增加了12("Hello World!"共12个字节),表明第二份数据的起始位置。

3.1.2 Acknowledgement Number

当Control Bit里ACK=1时,表明这是应答(Acknowledgment)。应答是接收方告诉发送方,自己成功接收了此前的数据。此前指的是数据流里Sequence Number小于Acknowledgement Number的所有数据。示例中,从第5条可以看到Ack=2892544113,表示此前,即ISN至2892544112的TCP Segment都已经被接收到了;下一次期望接收到的数据起始位置应该是2892544113。

3.1.3 Control Bits

在Control Bit的6个flag里,我们已经认识了SYN和ACK。RST用于,当一方觉得出现了问题,通知另一方进行重启。FIN用于终端连接,我们将在3.2章节展开。

示例中我们还可以看到PSH,即push bit。当PSH=1时,表示TCP需要立即清空自己的buffer,将数据发送出去。怎么理解呢?我们说过TCP是基于数据流的,它在接收到上层应用发过来的数据时,会先存到自己的buffer里。假设上层应用某一次只下发了一个字节给TCP,很可能TCP会继续等待更多的数据到来,再一起发送。示例中,从第5条我们可以看到PSH被置位。

When an application has data that it needs to have sent across the internetwork immediately, it sends the data to TCP, and then uses the TCP push function. This tells the sending TCP to immediately “push” all the data to the recipient’s TCP as soon as it is able to do so, without waiting for more data.

而有些时候,应用层最新下发的数据需要立刻送达。但TCP处理的是数据流,first in first out,先来的先走。因此TCP设计了URG这个flag,使得紧急的数据可以插队到前排。相应地,紧急指针(Urgent Potiner)表明最后一个字节的紧急数据的位置(第一个字节的紧急数据插队到最前边了)。

3.1.4 Window

接收到对端的数据后,通常是先存放到buffer里的。但不同的设备,在不同时段给应用分配的buffer不一样。我们当然不想看到数据接收的速度超出被处理的速度,因为这会造成数据丢失。为此TCP设计了Window字段,用于通讯双方告知对方自己的Receive Window,也是对方的Send Window。示例中,我们可以看到Client和Server的window值始终保持65535。

3.2 TCP Finite State Machine

TCP是面向连接(Connection-Oriented)的,通讯的双方需要通过约定的机制建立连接(Connection Establishment),之后才可以收发数据;且需要按照既定的流程终止通讯(Connection Termination)。

3.2.1 建立连接

建立连接需要经历所谓的三次握手(Three-Way Handshake)。以Client-Server模型为例,实际上就是需要Client发出SYN并收到Server的ACK;并且Server也需要发出SYN并收到Client的ACK。如此,双方都知道自己的信息能够成功地被对方接收,完成通讯的建立。

这里我们结合CANoe的实例来看看。Client的IP地址192.168.1.2,Server的IP地址为192.168.1.1。下图中步骤1、2、3分别对应上图里的#1、#2、#3。

  1. 1. 起初Server处于Closed状态,随后切换至Listen状态,监听潜在的请求;

  2. 2. Client从Closed状态,主动发起连接(Active Open)并发送SYN消息,并切换至SYN-SENT状态;

  3. 3. 随后Server收到了SYN消息,它针对性地回复了ACK,发出了自己的SYN消息,并切换至SYN-Received状态;

  4. 4. 而后Client收到了Server回复的ACK,所以它知道通讯正常;同时它回复ACK给Server,至此Client切换至Established状态;

  5. 5. 最后Server也收到了Client回复的ACK,它也切换至Established状态。

3.2.2 终止通讯

建立通讯时需要双方认可,终止通讯也是一样,只不过SYN消息换成了FIN消息,即finish。

依旧拿上边的CANoe实例来看。通讯建立后,Server端向Client发送了2次Hello World。之后Client请求中断连接,对应下图中的8、9、10、11。

  1. 8. Client请求终止通讯,故发送了FIN-client消息,并切换至FIN-WAIT-1状态;

  2. 9. Server收到了Client的FIN-client,针对性地回复了ACK,通知应用层准备终止通讯,并切换至CLOSE-WAIT状态;之后,Client收到了ACK,切换至FIN-WAIT-2的状态;此时它在等待Server端发出的FIN-server;

  3. 10. 经过一段时间,Server端的APP已经做好关闭的准备,因此它发出了FIN-server,并切换至LAST-ACK

  4. 11. Client收到了FIN-server,针对性地回复了ACK,并切换至TIME-WAIT状态;等待是为了给它的ACK传递到Server端预留时间;等待时间结束后,Client立即切换至CLOSED状态;

  5. 12. Server端收到ACK后,立马切换至CLOSED状态。

3.3 Sliding Window System

TCP的状态机确立了开启通讯和关闭通讯的规则,它们相对来说还是比较简单的,而更为复杂的是数据传输的过程。这就引出了TCP里非常关键的机制Sliding Window System

详细解释这个机制需要很长的篇幅,这里我们借用书里【6】的示例演练一遍。在这之前我们先来了解下指针(Pointers)的概念。

3.3.1 指针

从发送方的视角来看,数据流可以分成四个部分:

  1. 1. 已发送并且被应答的字节(Sent and Acknowledged);

  2. 2. 已发送但还未被应答的字节(Sent but not Acknowledged);

  3. 3. 未发送但是处于可用窗口里的字节(Not yet Sent for which Recipient is Ready);

  4. 4. 未发生但是处于可用窗口之外的字节(Not yet Sent for which Recipient is not Ready)。

它们可以用三个指针来区分:

  • • SND.UNA(Send Unacknowledged Pointer)用来界定,已发送的数据里,被应答和未被应答的部分;当接收到对端发来的ACK,那么相应地要更新指针SND.UNA;

  • • SND.NXT(Send Next Pointer)用来界定“未发送但是处于可用窗口里的字节”;当成功发送了信息,那么相应地要更新指针SND.NXT;

  • • SND.WND(Send Window)表示整个window的大小。整个window减去“已发送但还未被应答的字节”,就得到“未发送但是处于可用窗口里的字节”。即,可用窗口(usable window)=SND.UNA + SND.WND - SND.NXT。usable window决定了当前最多有多少字节能够被发送出去。

从接收方的视角来看,数据流可以分成三个部分,用到两个指针。

  • • RCV.NXT(Receive Next Pointer)用来界定哪些数据已经接收到了(也被应答了)。当收到对端发来的数据,那么相应地要更新指针RCV.NXT,记录下一次预期接收的数据起始位;发送出去的Acknowledge Number取值于RCV.NXT;

  • • RCV.WND(Receive Window)等效于SND.WND。

3.3.2 示例

知道了指针的概念,我们就可以通过示例来看看窗口是如何滑动的(Window Sliding)。为了简化,这里其实假定了Client的Send Window保持360个字节不变,而Server的Send Window保持200个字节不变。

从Client的视角看,是这样子的:

从Server的视角看,是这样子的:

下面是整个交互的过程,跟着步骤推演一遍,进而加深理解。

  1. 0. 建立连接

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Client初始值1初始值1360360241
    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
  2. 1. Client向Server发送Request(共140 bytes)。发送数据使得Client的指针SND.NXT和Usable Window更新。该TCP Segment里的Sequence Number = 1。对于Server端,没有更新。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Client初始值1初始值1360360241
    Client11+140=141360360-140=220241

    由此发送的数据为:

    Request
    Length = 140;
    Seq Num = 1;

  3. 2. Server收到了Client的Request(共140 bytes)并做出Ack应答,同时回复Reply(共80 bytes)。接收到Request使得Server端的指针RCV.NXT更新,最新的值将作为Ack Number发送给Client。回复Reply使得Server的指针SND.NXT和Usable Window更新。该TCP Segment里的Sequence Number = 241。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141

    由此发送的数据为:

    Reply
    Lenght = 80;
    Seq Num = 241;
    Ack Num = 141

  4. 3. Client收到了Server发出的Ack(Ack Num=141),以及Reply(共80 bytes)并做出Ack应答。接收到Ack(Ack Num=141)将使得指针SND.UNA和Usable Window更新。接收到Reply将使得指针RCV.NXT更新,最新的值将作为Ack Number发送给Server。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Client初始值1初始值1360360241
    Client11+140=141360360-140=220241
    Client141141360220+140=360241+80=321

    由此发送的数据为:

    Acknowledgement
    Ack Num = 321

    4. Server发送数据part1(共120 bytes)。发送数据part1使得Server的指针SND.NXT和Usable Window更新。该TCP Segment里的Sequence Number =321。至此Server共发送了两次数据,一次80byte,一次120byte,把Send Window耗尽。这两次数据的Ack应答均未收到,故SND.UNA保持初始值241。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141
    Server维持241321+120=441200120-120=0维持141

    由此发送的数据为:

    File (part1)
    Length = 120
    Seq Num = 321

    5. Client收到了Server发出的part1数据并作出Ack应答。接收到数据将使得指针RCV.NXT更新,最新的值将作为Ack Number发送给Server。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Client初始值1初始值1360360241
    Client11+140=141360360-140=220241
    Client141141360220+140=360241+80=321
    Client141141360360321+120=441

    由此发送的数据为:

    Acknowledgment
    Ack Num = 441

    6. Server收到了Client在step3里回复的Ack(Ack Num = 321, 对应Server发出的80 bytes的Reply)。接收到Ack将使得Server的指针SND.UNA和Usable Window更新。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141
    Server维持241321+120=441200120-120=0141
    Server321441200321+200-441=80141

    7. Server收到了Client针对step5回复的Ack(Ack Num = 441, 对应Server发出的120 bytes)。接收到Ack将使得Server的指针SND.UNA和Usable Window更新。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141
    Server维持241321+120=441200120-120=0141
    Server321441200321+200-441=80141
    Server441441200441+200-441=200141

    8. Server发送数据part2(共160 bytes),发送数据part2使得Server的指针SND.NXT和Usable Window更新。该TCP Segment里的Sequence Number =441。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141
    Server维持241321+120=441200120-120=0141
    Server321441200321+200-441=80141
    Server441441200441+200-441=200141
    Server441441+160=601200441+200-601=40141

    由此发送的数据为:

    File (part2)
    Length = 160
    Seq Num = 441


  5. 9. Client收到了Server发出的part2数据(共160 bytes),并作出Ack应答。接收到数据将使得指针RCV.NXT更新,最新的值将作为Ack Number发送给Server。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Client初始值1初始值1360360241
    Client11+140=141360360-140=220241
    Client141141360220+140=360241+80=321
    Client141141360360321+120=441
    Client141141360360441+160=601

    由此发送的数据为:

    Acknowledgement
    Ack Num = 601

  6. 10. Server收到了Client针对step9回复的Ack(Ack Num = 601,对应Server发出的160 bytes),接收到Ack将使得Server的指针SND.UNA和Usable Window更新。

    RoleSND.UNASND.NXTSND.WNDUsable WindowRCV.NXT
    Server初始值241初始值2412002001
    Server维持241241+80=321200200-80=1201+140=141
    Server维持241321+120=441200120-120=0141
    Server321441200321+200-441=80141
    Server441441200441+200-441=200141
    Server441441+160=601200441+200-601=40141
    Server601601200601+200-601=200141

前边我们讲TCP是面向连接的,并且是基于数据流的。小编认为,TCP还有可靠的、控制数据流等特性。接下来我们就依次看下TCP是如何做到可靠(Reliability)和控制数据流的(Flow Control)。

3.4 TCP Reliability

可靠意味着需要监测数据是否丢失(detect lost segments),丢失后需要重发(retransmit)。每当TCP发送了一个Segment,就会开启一个重发定时器(Retransmission Timer),并拷贝一份数据放到重发队列(Retransmission Queue)里。如果在定时器超时之前,接收到了对方的ACK,则该备份数据会被移除出重发队列;反之,定时器超时后也没能收到ACK,则会自动触发Segment再次发送。之后重发定时器被重置,重新计时。当然,重发的次数是限定的。

我们知道TCP传输的是连续的数据流。假设相邻的两个数据流Segment A和B,A先发送但超时未能收到ACK,B后发送但收到了ACK。那么重发时,是仅再次发送A呢?还是把A+B都依次发送一次呢?

这就引出了两种解决方案,Non-Continous AcknowledgementSlective Acknowledgment。Non-Continous Acknowledgement会把自丢失位置起始的数据全部重发,也叫go-back-to-n protocol。而Slective Acknowledgment仅针对丢失的部分重发,也叫selective repeat protocol,它的缩写是SACK,在RFC1072和RFC2024里定义。关于这两种方式的比较可以参考这篇文章【7】。

另外关于Timer时长的设定也很关键,这里不做展开。

3.5 TCP Flow Control

所谓控制数据流,实际上是通讯的双方通过协商,找到最合适的传输速率,背后的原理是调整window size。

By reducing or increasing window size, the server and client ensure that the other device sends data just as fast as the recipient can deal with it.

这里我们借用书里【6】的示例来看一下,双方是如何协商的。注意,示例中我们仅从Client的视角去看send window的调节。

  1. 0. 起初send window = 360;

  2. 1. Client发送140个字节的数据,此时usable window = 360-140 =220;

  3. 2. Server降低100个字节的receive window,即360-100 = 260,并告知Client;

  4. 3. Client收到了ACK,usable window是满的;又因为收到通知缩减send window, 故send window = usable window = 260;

  5. 4. Client发送180个字节的数据,此时usable window = 260-180 = 80;

  6. 5. Server进一步降低180个字节的receive window,即260-180 = 80,并告知Client;

  7. 6. Client缩减send window至80;

  8. 7. Client发送80个字节的数据,此时usable window = 0;

  9. 8. Server进一步降低receive window至0,来到了所谓的窗口关闭(Closing the Receive Window)。

上述示例中,我们可以看到send window是如何一步步从360降低至0的。实际运行中,窗口大小的变化可增可减。窗口管理(Window Management)也会遇到一些问题,比如窗口收缩(Shrinking the Window)和Silly Window Syndrome(SWS)

窗口收缩指的是,窗口调整不协调,导致可用窗口变为负数。而Silly Window Syndrome指的是窗口先是缩减得很小,再逐步放开的过程中,节奏太慢,比如一次仅放开一个字节。如果一个TCP Segment只发送一个字节的数据(或者很少的数据),是极其低效的,数据占比1/21(1个字节的数据+20字节的TCP报头)。不同的症状有不同的对策,这里就不展开说明。


4. 比较TCP和UDP

至此,我们对传输层的两大协议TCP和UDP有了比较基础且全面的了解。最后,我们来对比下这两种协议的差异作为收尾。

特性UDPTCP
连接无需连接需要连接,并有状态机约束建立和中断通讯的过程
接口基于信息的,message-oriented基于数据流的,stream-oriented
可靠性不可靠可靠传输
ACK应答
重发机制
数据流控制
适用场景更注重数据传输的速度,不十分关心数据的完整性看重数据的完整性
熟知的协议多媒体应用(如视频流)FTP,Telnet,SMTP,HTTP...

以上内容,大部分是基于本书【6】的学习和思考,感兴趣的同学可以去美国亚马逊官网购买。

[1]: "IANA"

com.org/
[2]: "ephemeral_ports"
https://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html 
[3]: "Service Name and Transport Protocol Port Number Registry" 

com.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml 
[4]"socket library" 

org/3/library/socket.html#socket-objects 
[5]: "real python示例
"

ysjwg.com.com/Automotive-Ethernet-Definitive-Colt-Correa-ebook/dp/B0BTDW52PF/ref=sr_1_1?crid=9POXLBSX9RYF&keywords=automotive+ethernet+the+definitive+guide&qid=1708079773&sprefix=automotive+ether%2Caps%2C470&sr=8-1
[7]: "difference between GBN and SACK"

com.org/difference-between-go-back-n-and-selective-repeat-protocol/ 

本文转载自网络,版权归原作者所有,如侵犯您的权益请联系3810298020#qq.com,我们将第一时间删除。

上一篇:长安反恐精英75PLUS荣登中国市场乘用车销量第一 反恐精英1.7
下一篇: