C高级:关于UDP和TCP的学习记录

在网络编程中,TCP 和 UDP 是两种最核心的传输层协议。它们为应用程序提供了不同的数据传输方式——TCP 提供可靠的、面向连接的服务,而 UDP 提供无连接的、尽最大努力交付的服务。本文将从概念原理到实际代码,系统讲解 UDP 和 TCP 的含义、作用和使用方法,最后做一个全面的对比。以下是我在职坐标的学习记录:

一、UDP 的概念及其作用

1.1 什么是 UDP

UDP(User Datagram Protocol,用户数据报协议)是 OSI 参考模型中传输层的一种协议。它是一种无连接的协议,发送方在发送数据之前不需要与接收方建立连接,直接把数据报"扔"出去就完事了。

可以这样理解:UDP 就像寄明信片——你写好内容、贴上邮票、写上地址,直接丢进邮筒就行了。你不需要提前和收件人"建立联系",邮局也不会给你回执确认对方是否收到。至于对方能不能收到、收到的顺序对不对,UDP 都不保证。

1.2 UDP 的核心特点

  • 无连接:发送数据前不需要建立连接,不需要三次握手。
  • 不可靠:不保证数据一定到达、不保证数据按序到达、不保证数据不重复。如果网络丢包,UDP 不会重传。
  • 面向报文:UDP 对应用层传下来的数据不做拆分也不做合并,而是直接封装成 UDP 数据报发送。接收方也是整块接收,保留了报文的边界。
  • 没有拥塞控制:不管网络多拥堵,UDP 都会按照应用层给的速度发送数据。
  • 头部开销小:UDP 头部只有 8 字节(源端口、目的端口、长度、校验和),相比 TCP 的 20~60 字节头部要轻量得多。
  • 支持一对一、一对多、多对多:UDP 支持单播、组播和广播。

1.3 UDP 的作用与适用场景

正因为 UDP 轻量、快速的特点,它特别适合以下场景:

  • 实时性要求高的应用:视频会议、在线直播、语音通话等。这些场景对延迟敏感,偶尔丢一帧画面比延迟几百毫秒更可接受。
  • 简单请求-响应模型:DNS 查询、NTP 时间同步等。一次请求一次响应,不需要建立连接的开销。
  • 游戏:多人在线游戏中玩家位置、操作的同步,丢一条旧的位置信息没关系,下一条马上就来了。
  • 广播/组播:局域网设备发现、流媒体分发等。

1.4 UDP 通信流程

客户端                              服务器
  │                                  │
  ├─ socket() 创建套接字              ├─ socket() 创建套接字
  │                                  ├─ bind() 绑定地址和端口
  │                                  │
  ├─ sendto() 发送数据 ──────────→   ├─ recvfrom() 接收数据
  │                                  │
  ├─ recvfrom() 接收响应  ←────────  ├─ sendto() 发送响应
  │                                  │
  ├─ close()                         ├─ close()

二、如何使用 UDP

2.1 核心函数

UDP 编程主要用到以下几个函数:

函数 作用
socket() 创建套接字
bind() 绑定本地地址和端口(通常只有服务器需要)
sendto() 向指定地址发送数据
recvfrom() 接收数据并获取发送方地址
close() 关闭套接字

2.2 socket() —— 创建套接字

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
参数 类型 说明
domain int 协议族(地址族)。常用 AF_INET(IPv4)或 AF_INET6(IPv6)。
type int 套接字类型。UDP 使用 SOCK_DGRAM(数据报套接字)。
protocol int 具体协议。通常传 0,表示自动选择默认协议(对于 SOCK_DGRAM 就是 UDP)。也可以显式传 IPPROTO_UDP

返回值: 成功返回套接字文件描述符(非负整数),失败返回 -1

// 创建 UDP 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
    perror("socket failed");
}

2.3 bind() —— 绑定地址和端口

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 类型 说明
sockfd int socket() 返回的套接字描述符。
addr const struct sockaddr * 指向地址结构体的指针,包含要绑定的 IP 地址和端口号。通常使用 struct sockaddr_in(IPv4),然后强转为 struct sockaddr *
addrlen socklen_t 地址结构体的大小,通常传 sizeof(struct sockaddr_in)sizeof(struct sockaddr)

struct sockaddr_in 结构体:

#include <netinet/in.h>

struct sockaddr_in {
    sa_family_t    sin_family;  // 协议族,固定为 AF_INET
    in_port_t      sin_port;    // 端口号(网络字节序,用 htons() 转换)
    struct in_addr sin_addr;    // IP 地址(网络字节序)
    // ...
};

常用 IP 地址设置方式:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));

servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);             // 端口 8080

// 方式 1:绑定所有网卡的 IP(服务器常用)
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 等价于 0.0.0.0

// 方式 2:绑定指定 IP
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);

// 方式 3:绑定回环地址(本机测试)
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);  // 等价于 127.0.0.1

2.4 sendto() —— 发送数据

#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数 类型 说明
sockfd int 套接字描述符。
buf const void * 要发送的数据缓冲区指针。
len size_t 要发送的数据长度(字节数)。
flags int 发送标志,通常传 0
dest_addr const struct sockaddr * 目标地址(对方的 IP + 端口)。
addrlen socklen_t 目标地址结构体的大小。

返回值: 成功返回实际发送的字节数,失败返回 -1

2.5 recvfrom() —— 接收数据

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数 类型 说明
sockfd int 套接字描述符。
buf void * 接收数据的缓冲区指针。
len size_t 缓冲区的最大容量(字节数)。
flags int 接收标志,通常传 0
src_addr struct sockaddr * 输出参数,用于获取发送方的地址信息(IP + 端口)。如果不需要可以传 NULL
addrlen socklen_t * 输入输出参数。输入时为 src_addr 缓冲区大小,输出时为实际地址长度。不需要时传 NULL

返回值: 成功返回实际接收到的字节数,对端关闭连接时返回 0,失败返回 -1

2.6 完整示例:UDP 服务器和客户端

UDP 服务器(udp_server.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUF_SIZE 1024

int main() {
    int sockfd;
    char buf[BUF_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;

    // 1. 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    // 3. 绑定地址和端口
    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("[服务器] UDP 服务器已启动,监听端口 %d...\n", PORT);

    // 4. 循环接收和响应
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        
        // 接收数据
        int n = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
                         (struct sockaddr *)&cliaddr, &cliaddr_len);
        if (n == -1) {
            perror("recvfrom failed");
            continue;
        }
        buf[n] = '\0';

        // 打印客户端信息
        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("[服务器] 收到来自 %s:%d 的消息: %s\n",
               cli_ip, ntohs(cliaddr.sin_port), buf);

        // 发送响应
        const char *response = "服务器已收到你的消息!";
        sendto(sockfd, response, strlen(response), 0,
               (struct sockaddr *)&cliaddr, cliaddr_len);
    }

    close(sockfd);
    return 0;
}

UDP 客户端(udp_client.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUF_SIZE 1024

int main() {
    int sockfd;
    char buf[BUF_SIZE];
    struct sockaddr_in servaddr;
    socklen_t servaddr_len = sizeof(servaddr);

    // 1. 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);

    // 3. 发送数据
    const char *msg = "Hello, UDP Server!";
    sendto(sockfd, msg, strlen(msg), 0,
           (struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("[客户端] 已发送: %s\n", msg);

    // 4. 接收响应
    int n = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
                     (struct sockaddr *)&servaddr, &servaddr_len);
    if (n > 0) {
        buf[n] = '\0';
        printf("[客户端] 收到响应: %s\n", buf);
    }

    // 5. 关闭套接字
    close(sockfd);
    return 0;
}

编译运行:

gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client

# 终端 1:运行服务器
./udp_server

# 终端 2:运行客户端
./udp_client

三、TCP 的概念及其作用

3.1 什么是 TCP

TCP(Transmission Control Protocol,传输控制协议)是 OSI 参考模型中传输层的另一种协议。它是一种面向连接的、可靠的基于字节流的传输协议。互联网上绝大多数需要可靠数据传输的应用(HTTP、HTTPS、FTP、SSH 等)都基于 TCP。

可以这样理解:TCP 就像打电话——你需要先拨号(三次握手建立连接),对方接听后才能通话(传输数据),通话结束后双方挂断(四次挥手断开连接)。整个过程中,双方都能确认对方说的话(确认机制),没听清还能让对方重复(重传机制)。

3.2 TCP 的核心特点

  • 面向连接:通信前必须通过"三次握手"建立连接,通信结束后通过"四次挥手"断开连接。
  • 可靠传输:通过序列号、确认应答(ACK)、超时重传、校验和等机制保证数据完整、按序、无差错地到达对方。
  • 面向字节流:TCP 把数据看成无结构的字节流,不保留消息边界。发送方发送了 100 字节,接收方可能分多次接收(如 30 + 70),也可能一次读完。
  • 流量控制:通过滑动窗口机制,接收方可以告诉发送方"我还能接收多少数据",防止发送方发太快把接收方淹没。
  • 拥塞控制:当网络拥堵时,TCP 会自动降低发送速率,避免加剧网络拥塞。
  • 全双工通信:连接建立后,双方可以同时发送和接收数据。
  • 点对点:TCP 连接只能是两个端点之间的(一对一),不支持广播和组播。

3.3 TCP 的三次握手(建立连接)

客户端                          服务器
  │                                │
  ├──── SYN (seq=x) ──────────→   │  第 1 次握手:客户端发起连接请求
  │                                │
  │   ←──── SYN+ACK (seq=y,        │  第 2 次握手:服务器同意并回复
  │         ack=x+1) ────────────  │
  │                                │
  ├──── ACK (ack=y+1) ────────→   │  第 3 次握手:客户端确认
  │                                │
  │      ===== 连接建立 =====       │

为什么是三次? 两次握手可能导致"已失效的连接请求"被服务器误认为新的请求,从而建立无效连接并浪费资源。第三次握手让服务器确认客户端确实收到了自己的回复。

3.4 TCP 的四次挥手(断开连接)

客户端                          服务器
  │                                │
  ├──── FIN (seq=u) ──────────→   │  第 1 次挥手:客户端请求断开
  │                                │
  │   ←──── ACK (ack=u+1) ──────  │  第 2 次挥手:服务器确认(但可能还有数据要发)
  │                                │
  │   ←──── FIN (seq=w) ────────  │  第 3 次挥手:服务器也请求断开
  │                                │
  ├──── ACK (ack=w+1) ────────→   │  第 4 次挥手:客户端确认
  │                                │
  │      ===== 连接断开 =====       │

3.5 TCP 的作用与适用场景

  • Web 浏览:HTTP/HTTPS 协议基于 TCP,确保网页内容完整加载。
  • 文件传输:FTP、SCP 等,文件数据不能有任何丢失。
  • 远程登录:SSH、Telnet,命令和响应必须可靠。
  • 电子邮件:SMTP、IMAP 等。
  • 数据库访问:MySQL、PostgreSQL 等的客户端连接。

3.6 TCP 通信流程

客户端                                服务器
  │                                     │
  ├─ socket()                           ├─ socket()
  │                                     ├─ bind()
  │                                     ├─ listen()
  ├─ connect() ───三次握手──────────→   ├─ accept() ←── 阻塞等待连接
  │                                     │
  ├─ send()/write() ──────────────→    ├─ recv()/read()
  ├─ recv()/read()  ←─────────────    ├─ send()/write()
  │                                     │
  ├─ close() ───四次挥手────────────→   ├─ close()

四、如何使用 TCP

4.1 核心函数

TCP 编程比 UDP 多了连接管理相关的函数:

函数 角色 作用
socket() 通用 创建套接字
bind() 服务器 绑定本地地址和端口
listen() 服务器 将套接字设为监听状态
accept() 服务器 接受客户端连接请求
connect() 客户端 向服务器发起连接请求
send() / write() 通用 发送数据
recv() / read() 通用 接收数据
close() 通用 关闭连接

4.2 socket() —— 创建套接字

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET:IPv4
// SOCK_STREAM:流式套接字(TCP)
// 0:自动选择协议

与 UDP 的唯一区别是第二个参数:TCP 使用 SOCK_STREAM(流式套接字),UDP 使用 SOCK_DGRAM(数据报套接字)。

4.3 bind() —— 绑定地址(服务器)

TCP 的 bind() 用法与 UDP 完全相同:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);

bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

4.4 listen() —— 开始监听(服务器)

#include <sys/socket.h>

int listen(int sockfd, int backlog);
参数 类型 说明
sockfd int 已经 bind() 过的套接字描述符。
backlog int 监听队列的最大长度,即允许多少个客户端连接排队等待 accept()。当队列满时,新的连接请求会被拒绝。常见取值为 510128 等。

返回值: 成功返回 0,失败返回 -1

listen(sockfd, 10);  // 最多允许 10 个连接排队

4.5 accept() —— 接受连接(服务器)

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 类型 说明
sockfd int 正在监听的套接字描述符(即 listen() 的那个)。
addr struct sockaddr * 输出参数,用于获取连接的客户端地址信息(IP + 端口)。不需要时传 NULL
addrlen socklen_t * 输入输出参数。输入时为 addr 缓冲区大小,输出时为实际地址长度。不需要时传 NULL

返回值: 成功返回一个新的套接字描述符(用于与该客户端通信),失败返回 -1

⚠️ 重要概念accept() 返回的新 fd 和原来的监听 fd 是两个不同的东西。监听 fd 继续监听新的连接请求,新的 fd 专门用来和这个客户端通信。服务器通常会为每个客户端连接创建新线程或新进程来处理。

4.6 connect() —— 发起连接(客户端)

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 类型 说明
sockfd int 客户端的套接字描述符。
addr const struct sockaddr * 服务器的地址(IP + 端口)。
addrlen socklen_t 地址结构体大小。

返回值: 成功返回 0,失败返回 -1

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

4.7 send() / recv() —— 收发数据

连接建立后,TCP 可以像操作普通文件一样用 send()/recv()write()/read() 来收发数据:

// 发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// sockfd:连接的套接字
// buf:数据缓冲区
// len:数据长度
// flags:标志,通常传 0

// 接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// sockfd:连接的套接字
// buf:接收缓冲区
// len:缓冲区大小
// flags:标志,通常传 0

返回值: 成功返回实际发送/接收的字节数。recv() 返回 0 表示对端已关闭连接,返回 -1 表示出错。

⚠️ TCP 粘包问题:由于 TCP 是面向字节流的,send() 发送的数据可能会被合并或拆分。例如发送两次各 50 字节,接收方可能一次 recv() 就收到 100 字节。解决粘包问题的常见方案包括:固定长度、特殊分隔符、消息头携带长度字段等。

4.8 close() —— 关闭连接

close(sockfd);  // 发起四次挥手,断开连接

4.9 完整示例:TCP 服务器和客户端

TCP 服务器(tcp_server.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUF_SIZE 1024
#define BACKLOG 10

int main() {
    int listenfd, connfd;
    char buf[BUF_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;

    // 1. 创建 TCP 套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 允许端口复用(服务器重启时不用等 TIME_WAIT)
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 2. 绑定地址和端口
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("bind failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    // 3. 开始监听
    if (listen(listenfd, BACKLOG) == -1) {
        perror("listen failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    printf("[服务器] TCP 服务器已启动,监听端口 %d...\n", PORT);

    // 4. 接受客户端连接
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if (connfd == -1) {
            perror("accept failed");
            continue;
        }

        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("[服务器] 客户端 %s:%d 已连接\n",
               cli_ip, ntohs(cliaddr.sin_port));

        // 5. 与该客户端通信
        while (1) {
            int n = recv(connfd, buf, BUF_SIZE - 1, 0);
            if (n <= 0) {
                // n == 0:客户端主动断开
                // n == -1:出错
                printf("[服务器] 客户端 %s:%d 已断开\n",
                       cli_ip, ntohs(cliaddr.sin_port));
                break;
            }
            buf[n] = '\0';
            printf("[服务器] 收到: %s\n", buf);

            // 回显(Echo):将收到的内容原样返回
            send(connfd, buf, n, 0);
        }

        close(connfd);  // 关闭与该客户端的连接
    }

    close(listenfd);  // 关闭监听套接字
    return 0;
}

TCP 客户端(tcp_client.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUF_SIZE 1024

int main() {
    int sockfd;
    char buf[BUF_SIZE], msg[BUF_SIZE];
    struct sockaddr_in servaddr;

    // 1. 创建 TCP 套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 连接服务器
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("[客户端] 已连接到服务器 %s:%d\n", SERVER_IP, SERVER_PORT);
    printf("输入消息(输入 quit 退出):\n");

    // 3. 发送和接收
    while (1) {
        printf("> ");
        fgets(msg, BUF_SIZE, stdin);

        // 去掉末尾换行符
        msg[strcspn(msg, "\n")] = '\0';

        if (strcmp(msg, "quit") == 0) break;

        // 发送
        send(sockfd, msg, strlen(msg), 0);

        // 接收回显
        int n = recv(sockfd, buf, BUF_SIZE - 1, 0);
        if (n <= 0) {
            printf("[客户端] 服务器已断开\n");
            break;
        }
        buf[n] = '\0';
        printf("[服务器回显] %s\n", buf);
    }

    // 4. 关闭连接
    close(sockfd);
    return 0;
}

编译运行:

gcc tcp_server.c -o tcp_server
gcc tcp_client.c -o tcp_client

# 终端 1:运行服务器
./tcp_server

# 终端 2:运行客户端
./tcp_client

五、TCP 和 UDP 的区别

5.1 核心对比

对比维度 TCP UDP
连接方式 面向连接(三次握手) 无连接
可靠性 可靠传输(确认、重传、排序) 不可靠(不保证到达、不保证顺序)
数据格式 面向字节流(无边界) 面向报文(保留边界)
传输速度 较慢(建立连接 + 确认机制开销) 较快(无连接开销)
头部大小 20~60 字节 8 字节
流量控制 有(滑动窗口)
拥塞控制
通信模式 点对点(一对一) 支持一对一、一对多、多对多
系统调用 socketbindlistenacceptconnectsend/recv socketbindsendto/recvfrom
套接字类型 SOCK_STREAM SOCK_DGRAM

5.2 编程方式对比

对比维度 TCP 编程 UDP 编程
socket() 的 type SOCK_STREAM SOCK_DGRAM
是否需要 listen() ✅ 服务器需要 ❌ 不需要
是否需要 accept() ✅ 服务器需要 ❌ 不需要
是否需要 connect() ✅ 客户端需要 ❌ 不需要
发送函数 send() / write() sendto()
接收函数 recv() / read() recvfrom()
发送时是否指定地址 ❌ 连接已确定 ✅ 每次都要指定目标地址
粘包问题 ⚠️ 存在(字节流) ❌ 不存在(报文边界清晰)

5.3 选择建议

应用场景 推荐协议 原因
Web 网页浏览(HTTP/HTTPS) TCP 网页内容不能丢失
文件传输(FTP) TCP 文件完整性要求高
远程登录(SSH) TCP 命令不能丢
视频通话/直播 UDP 实时性优先,偶尔卡顿比延迟更可接受
在线游戏 UDP 操作同步实时性高,丢旧数据无所谓
DNS 查询 UDP 请求简单,一次一问
语音通话(VoIP) UDP 延迟敏感
邮件收发(SMTP/IMAP) TCP 邮件内容不能丢

5.4 一张图看懂

              ┌──────────────────────────────────┐
              │          应用层                   │
              │   HTTP  FTP  SSH  DNS  视频  游戏 │
              └──────┬─────────────┬─────────────┘
                     │             │
              ┌──────▼─────┐ ┌────▼─────┐
              │    TCP     │ │   UDP    │
              │  可靠传输   │ │ 尽最大努力│
              │  面向连接   │ │  无连接   │
              │  字节流     │ │  报文     │
              │  流量控制   │ │  无控制   │
              │  拥塞控制   │ │  无控制   │
              └──────┬─────┘ └────┬─────┘
                     │             │
              ┌──────▼─────────────▼──────────────┐
              │               IP 层               │
              └───────────────┬───────────────────┘
                              │
              ┌───────────────▼───────────────────┐
              │             链路层/物理层           │
              └───────────────────────────────────┘

六、常见问题 FAQ

Q1:TCP 为什么比 UDP 慢?
A:TCP 在发送数据前需要三次握手建立连接,每次发送数据还需要等待 ACK 确认,超时还要重传。这些机制保证了可靠性,但也增加了延迟。UDP 直接发,不等确认,所以更快。

Q2:UDP 能实现可靠传输吗?
A:UDP 本身不可靠,但可以在应用层实现可靠性(如自定义确认、重传机制)。QUIC 协议(HTTP/3)就是基于 UDP 实现了类似 TCP 的可靠传输,同时减少了握手延迟。

Q3:TCP 的 recv() 返回 0 是什么意思?
A:返回 0 表示对端已经调用了 close() 关闭了连接(发送了 FIN),不是出错。这是正常的连接关闭信号。

Q4:一个 TCP 服务器能同时处理多少客户端?
A:取决于系统资源(文件描述符数量、内存等)。单线程的 TCP 服务器一次只能和一个客户端通信(accept → 通信 → accept 下一个)。要并发处理多个客户端,需要使用多线程、多进程或 I/O 多路复用(select/poll/epoll)。

Q5:TCP 粘包怎么解决?
A:常见方案有三种:① 固定长度(每条消息固定 N 字节);② 分隔符(用特殊字符如 \n 分隔消息);③ 消息头(在消息头部用一个字段标明消息体的长度,接收方先读头部再读对应长度的数据)。

七、总结

协议 一句话概括 典型应用
TCP 可靠、面向连接、字节流 Web、文件传输、远程登录、数据库
UDP 快速、无连接、报文 视频通话、在线游戏、DNS、语音

TCP 和 UDP 没有优劣之分,只有适用场景的不同。需要可靠性就选 TCP,需要速度和实时性就选 UDP(必要时在应用层补充可靠性)。理解它们的原理和特点,是进行网络编程的基础。

posted @ 2026-08-04 22:25  Solitude0420  阅读(0)  评论(0)    收藏  举报