MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TCP/IP协议栈中的传输层协议分析

2022-01-182.9k 阅读

传输层协议概述

在TCP/IP协议栈中,传输层起着至关重要的作用。它位于网络层之上,应用层之下,主要负责为应用层提供端到端的可靠通信或高效的数据传输服务。传输层协议主要有两个:传输控制协议(TCP)和用户数据报协议(UDP),它们各自有着不同的特点和适用场景。

TCP与UDP的基本差异

TCP是一种面向连接的、可靠的传输协议。在数据传输之前,需要在发送端和接收端之间建立一条连接,就像打电话一样,要先拨通号码建立连接后才能进行通话。连接建立后,数据会按照顺序发送,并且接收方会对每一个接收到的数据包进行确认,发送方只有收到确认信息后才会继续发送下一个数据包,以此保证数据的可靠传输。

UDP则是一种无连接的、不可靠的传输协议。它不需要建立连接,就像写信一样,直接把信投进邮箱就不管了,不关心对方是否收到。UDP发送数据时,不会对数据进行排序,也不会要求接收方进行确认,因此传输速度相对较快,但可能会出现数据丢失或乱序的情况。

TCP协议深度分析

TCP连接的建立与终止

  1. 三次握手建立连接
    • 客户端发送一个带有SYN(同步序列号)标志的TCP报文段到服务器,这个报文段包含客户端初始序列号(ISN),假设为x。这一步可以看作是客户端向服务器发出连接请求,说:“我想和你建立连接,我的初始序列号是x”。
    • 服务器接收到客户端的SYN报文段后,会回复一个SYN + ACK报文段。其中,SYN标志表示服务器也同步一个序列号,假设为y,ACK标志是对客户端SYN报文段的确认,确认号为x + 1。这相当于服务器回应:“我收到你的连接请求了,我也有个初始序列号y,同时确认你发送的序列号为x的报文段我已收到,下一个我期望收到的序列号是x + 1”。
    • 客户端收到服务器的SYN + ACK报文段后,再发送一个ACK报文段给服务器,确认号为y + 1,序列号为x + 1。这一步是客户端告诉服务器:“我收到你回复的连接信息了,确认你发送的序列号为y的报文段我已收到,下一个我期望收到的序列号是y + 1”。至此,TCP连接建立完成。

以下是用Python的socket模块模拟TCP三次握手建立连接的简单代码示例:

import socket

# 创建TCP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 服务器地址和端口
server_address = ('localhost', 10000)
sock.connect(server_address)

try:
    # 发送数据
    message = 'This is the message.  It will be repeated.'
    sock.sendall(message.encode('utf - 8'))

    # 接收响应
    amount_received = 0
    amount_expected = len(message)

    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print('Received {!r}'.format(data))

finally:
    print('Closing socket')
    sock.close()
  1. 四次挥手终止连接
    • 主动关闭方(假设为客户端)发送一个FIN(结束标志)报文段到被动关闭方(服务器),表示客户端不再发送数据,但还可以接收数据。这就像是客户端说:“我数据发完了,准备关闭连接”。
    • 服务器接收到FIN报文段后,回复一个ACK报文段给客户端,确认号为客户端FIN报文段的序列号加1。这是服务器告诉客户端:“我知道你要关闭连接了,你发的FIN报文段我收到了”。此时,服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
    • 服务器处理完剩余的数据后,也发送一个FIN报文段给客户端,表明服务器也准备关闭连接。
    • 客户端收到服务器的FIN报文段后,回复一个ACK报文段给服务器,确认号为服务器FIN报文段的序列号加1。此时,客户端进入TIME_WAIT状态,服务器进入LAST_ACK状态。经过一段时间(2MSL,MSL是最大段生存期)后,客户端关闭连接,服务器在收到客户端的ACK后也关闭连接。

TCP的可靠传输机制

  1. 序列号与确认号
    • 序列号用于标识TCP发送的每一个字节的数据。在建立连接时,客户端和服务器各自生成初始序列号(ISN)。每次发送数据时,序列号会随着数据的发送而递增。例如,假设客户端发送的第一个报文段包含100字节的数据,序列号为x,那么下一个报文段的序列号就是x + 100。
    • 确认号是接收方告诉发送方期望接收的下一个序列号。当接收方成功接收数据后,会根据接收到的数据的序列号计算确认号并发送给发送方。比如接收方接收到序列号为x到x + 99的数据,那么确认号就是x + 100,发送方收到这个确认号后,就知道哪些数据已经被成功接收。
  2. 重传机制
    • 当发送方发送数据后,如果在一定时间内(超时时间)没有收到接收方的确认信息,就会认为数据传输失败,然后重传该数据。TCP的超时时间并不是固定的,它会根据网络状况动态调整。例如,在网络状况较好时,超时时间会相对较短;在网络拥塞时,超时时间会适当延长。发送方会不断重传数据,直到收到接收方的确认信息或者达到最大重传次数。

TCP的流量控制与拥塞控制

  1. 流量控制
    • 流量控制是为了防止发送方发送数据过快,导致接收方来不及处理而丢失数据。TCP通过接收方通告窗口(Advertised Window)来实现流量控制。接收方在发送确认报文段时,会在报文中告知发送方自己当前的接收缓冲区还能接收多少数据,这个值就是通告窗口大小。发送方根据接收方通告的窗口大小来调整自己的发送速率。例如,如果接收方通告窗口大小为1000字节,发送方就不会一次性发送超过1000字节的数据。
  2. 拥塞控制
    • 拥塞控制是为了防止网络出现拥塞,保证网络的稳定性和高效性。TCP拥塞控制主要通过四个算法来实现:慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快速重传(Fast Retransmit)和快速恢复(Fast Recovery)。
    • 慢启动:在连接建立初期,发送方的拥塞窗口(Congestion Window,cwnd)初始值通常为一个最大段大小(MSS)。每收到一个确认报文段,拥塞窗口就增加一个MSS大小。这样,拥塞窗口会呈指数增长,快速增加发送速率。
    • 拥塞避免:当拥塞窗口增长到慢启动门限(ssthresh)时,进入拥塞避免阶段。在这个阶段,每收到一个往返时间(RTT)内所有报文段的确认信息,拥塞窗口只增加一个MSS大小,即拥塞窗口呈线性增长,以避免网络拥塞。
    • 快速重传:当发送方连续收到三个相同的确认号时,就认为某个报文段丢失了,此时不会等待超时,而是立即重传该报文段。
    • 快速恢复:在快速重传后,进入快速恢复阶段。将慢启动门限设置为当前拥塞窗口的一半,然后将拥塞窗口设置为慢启动门限加上三个重复确认报文段所对应的数据量,继续进行拥塞避免阶段的线性增长。

以下是用C语言实现简单TCP拥塞控制模拟的代码示例(简化版,仅演示原理):

#include <stdio.h>
#include <stdlib.h>

#define MSS 1000 // 最大段大小
#define INITIAL_CWND 1 * MSS // 初始拥塞窗口
#define INITIAL_SSTHRESH 65535 // 初始慢启动门限

void slow_start(int *cwnd, int *ssthresh) {
    *cwnd += MSS;
    if (*cwnd >= *ssthresh) {
        *cwnd = *ssthresh;
    }
}

void congestion_avoidance(int *cwnd) {
    *cwnd += (MSS * MSS) / *cwnd;
}

void fast_retransmit(int *cwnd, int *ssthresh) {
    *ssthresh = *cwnd / 2;
    *cwnd = *ssthresh + 3 * MSS;
}

int main() {
    int cwnd = INITIAL_CWND;
    int ssthresh = INITIAL_SSTHRESH;
    int event; // 模拟事件,0表示正常接收ACK,1表示收到三个重复ACK

    while (1) {
        printf("Current cwnd: %d, ssthresh: %d\n", cwnd, ssthresh);
        printf("Enter event (0 for normal ACK, 1 for 3 duplicate ACKs): ");
        scanf("%d", &event);

        if (event == 0) {
            if (cwnd < ssthresh) {
                slow_start(&cwnd, &ssthresh);
            } else {
                congestion_avoidance(&cwnd);
            }
        } else if (event == 1) {
            fast_retransmit(&cwnd, &ssthresh);
        }
    }

    return 0;
}

UDP协议深度分析

UDP数据报结构

UDP数据报由首部和数据两部分组成。首部长度固定为8字节,包含源端口号(2字节)、目的端口号(2字节)、长度(2字节,包括首部和数据部分的总长度)和校验和(2字节)。源端口号和目的端口号用于标识发送端和接收端的应用程序。长度字段表示整个UDP数据报的长度,校验和用于检测数据在传输过程中是否发生错误。虽然UDP校验和是可选的,但在实际应用中,通常都会计算并使用校验和来提高数据传输的可靠性。

UDP的应用场景

  1. 实时性要求高的应用:如实时视频流、音频流传输。在视频会议、在线直播等场景中,偶尔丢失一些数据帧对整体的观看体验影响不大,更重要的是保证数据的实时传输,以避免音视频卡顿。UDP由于不需要建立连接,传输速度快,适合这类应用。
  2. 简单请求 - 响应模型的应用:如DNS(域名系统)查询。客户端向DNS服务器发送查询请求,DNS服务器返回查询结果。这种情况下,数据量通常较小,且对可靠性要求相对不是特别高,因为可以通过多次查询来获取正确结果。UDP的简单性和高效性使得它成为DNS查询的首选协议。

以下是用Python实现简单UDP通信的代码示例:

import socket

# 创建UDP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

server_address = ('localhost', 10000)
message = 'This is the message.  It will be repeated.'

try:
    # 发送数据
    sent = sock.sendto(message.encode('utf - 8'), server_address)

    # 接收响应
    data, server = sock.recvfrom(4096)
    print('Received {!r}'.format(data))

finally:
    print('Closing socket')
    sock.close()

UDP与TCP的性能对比

在网络状况良好的情况下,UDP的传输速度通常比TCP快,因为UDP不需要建立连接和进行复杂的确认机制。但是,当网络出现拥塞或丢包时,TCP的可靠传输机制会发挥作用,通过重传等方式保证数据的完整性,而UDP可能会丢失大量数据,导致应用层数据错误或不完整。例如,在一个稳定的局域网环境中,使用UDP进行文件传输可能会比TCP快很多;但在广域网环境中,特别是网络质量不稳定时,TCP更适合传输重要的、不允许丢失的数据,如文件下载、数据库备份传输等。

传输层协议的选择策略

在实际开发中,选择TCP还是UDP协议取决于具体的应用需求。如果应用对数据的可靠性要求极高,如金融交易数据传输、文件传输等,TCP是首选协议,因为它能够保证数据的准确无误传输。而对于实时性要求高、对少量数据丢失不太敏感的应用,如实时音视频通信、在线游戏等,UDP则更为合适,它可以提供较低的延迟和较高的传输效率。同时,在一些复杂的应用场景中,也可能会结合使用TCP和UDP。例如,在一个在线游戏中,游戏的登录认证过程对数据可靠性要求高,可以使用TCP;而游戏中的实时对战数据,如玩家的位置信息更新,对实时性要求高,可以使用UDP。

在选择传输层协议时,还需要考虑网络环境因素。如果网络带宽充足且稳定,UDP的优势可能更能体现;而在网络质量较差、容易出现拥塞和丢包的环境中,TCP的可靠性机制可以更好地保障数据传输。此外,应用的性能需求、数据量大小等也是选择协议时需要综合考虑的因素。

总结传输层协议在不同场景下的应用

通过对TCP和UDP协议的详细分析,我们了解到它们各自的特点和适用场景。在后端开发的网络编程中,深入理解这两种传输层协议,并根据具体应用需求准确选择合适的协议,对于构建高效、可靠的网络应用至关重要。无论是追求数据准确性的企业级应用,还是注重实时交互体验的互联网应用,正确运用TCP和UDP协议都能为用户带来更好的服务体验。同时,随着网络技术的不断发展,对传输层协议的优化和创新也在持续进行,开发人员需要不断关注新技术,以提升应用的性能和竞争力。