Linux TCP/UDP 网络编程完全指南:从基础到实践

引言

在 Linux 网络编程中,传输层提供两种核心协议:TCP(传输控制协议)UDP(用户数据报协议)。它们各有特点,适用于不同的应用场景。

特性 TCP UDP
连接性 面向连接(三次握手) 无连接
可靠性 可靠(确认重传) 不可靠(尽最大努力)
数据边界 流式服务(无边界) 数据报服务(有边界)
传输效率 较低 较高
适用场景 文件传输、网页访问 实时音视频、DNS查询

今天,我们将深入学习 TCP 和 UDP 的编程模型,理解它们的核心差异,并通过完整的代码示例掌握两种协议的使用方法。


第一部分:TCP 编程回顾

一、TCP 服务端完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 绑定 IP 和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        exit(1);
    }
    
    // 3. 创建监听队列
    if (listen(listen_fd, 5) == -1) {
        perror("listen error");
        exit(1);
    }
    
    printf("TCP 服务器启动成功,端口:%d\n", PORT);
    
    while (1) {
        // 4. 接受客户端连接
        client_len = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept error");
            continue;
        }
        
        printf("客户端连接:%s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        
        // 5. 循环接收数据
        while (1) {
            memset(buffer, 0, BUFFER_SIZE);
            int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
            
            if (n == 0) {
                printf("客户端已断开\n");
                break;
            }
            
            if (n == -1) {
                perror("recv error");
                break;
            }
            
            printf("收到数据:%s\n", buffer);
            send(client_fd, "OK", 2, 0);
        }
        
        close(client_fd);
    }
    
    close(listen_fd);
    return 0;
}

二、TCP 客户端完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    // 3. 连接服务器
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect error");
        exit(1);
    }
    
    printf("连接服务器成功\n");
    
    // 4. 循环收发数据
    while (1) {
        printf("请输入消息(输入end退出):");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strlen(buffer) - 1] = '\0';
        
        if (strcmp(buffer, "end") == 0) {
            break;
        }
        
        send(sock_fd, buffer, strlen(buffer), 0);
        
        memset(buffer, 0, BUFFER_SIZE);
        recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
        printf("服务器响应:%s\n", buffer);
    }
    
    close(sock_fd);
    return 0;
}

三、netstat 命令使用

查看所有 TCP 连接

netstat -natp

查看特定端口

netstat -natp | grep 6000

查看 UDP 服务

netstat -naupt

netstat 输出字段说明:

字段 含义
Proto 协议类型(TCP/UDP)
Recv-Q 接收缓冲区待处理数据量
Send-Q 发送缓冲区待处理数据量
Local Address 本地 IP:端口
Foreign Address 对端 IP:端口
State 连接状态(TCP)
PID/Program name 进程ID/程序名

第二部分:TCP 协议栈深入理解

一、监听套接字与连接套接字

套接字类型 功能 生命周期
监听套接字 接收客户端连接请求 整个服务周期
连接套接字 与特定客户端通信 单次会话周期

二、TCP 缓冲区机制

TCP 是流式服务,数据在发送方和接收方都有缓冲区:

重要特性:

  • send() 成功只表示数据已写入发送缓冲区,不代表对方已接收

  • recv() 从接收缓冲区读取数据,缓冲区为空时阻塞

  • TCP 允许分次读取(如 recv 1字节可逐个接收)

cpp 复制代码
// 实验:修改接收长度为1字节,观察现象
// 客户端发送 "hello" 需要5次 recv 才能读完
int n = recv(client_fd, buffer, 1, 0);  // 每次只读1字节

三、TCP vs UDP 数据接收对比

场景 TCP UDP
发送端 多次 send 可能合并 每次 sendto 独立报文
接收端 可分批读取 必须单次完整读取
数据边界 无边界(流) 有边界(数据报)

第三部分:UDP 编程

一、UDP 服务端完整代码

UDP 无需建立连接,使用 recvfrom()sendto() 收发数据。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int sock_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字(注意:SOCK_DGRAM)
    sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 绑定 IP 和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        exit(1);
    }
    
    printf("UDP 服务器启动成功,端口:%d\n", PORT);
    
    while (1) {
        client_len = sizeof(client_addr);
        memset(buffer, 0, BUFFER_SIZE);
        
        // 3. 接收数据(同时获取客户端地址)
        int n = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0,
                         (struct sockaddr*)&client_addr, &client_len);
        
        if (n == -1) {
            perror("recvfrom error");
            continue;
        }
        
        printf("收到来自 %s:%d 的数据:%s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        
        // 4. 回复数据(需要指定客户端地址)
        sendto(sock_fd, "OK", 2, 0,
               (struct sockaddr*)&client_addr, client_len);
    }
    
    close(sock_fd);
    return 0;
}

二、UDP 客户端完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int sock_fd;
    struct sockaddr_in server_addr;
    socklen_t server_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_len = sizeof(server_addr);
    
    while (1) {
        printf("请输入消息(输入end退出):");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strlen(buffer) - 1] = '\0';
        
        if (strcmp(buffer, "end") == 0) {
            break;
        }
        
        // 3. 发送数据(需指定目标地址)
        sendto(sock_fd, buffer, strlen(buffer), 0,
               (struct sockaddr*)&server_addr, server_len);
        
        memset(buffer, 0, BUFFER_SIZE);
        
        // 4. 接收回复
        recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0, NULL, NULL);
        printf("服务器响应:%s\n", buffer);
    }
    
    close(sock_fd);
    return 0;
}

三、UDP 核心函数详解

recvfrom 函数
cpp 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数 说明
sockfd 套接字描述符
buf 接收数据缓冲区
len 缓冲区大小
flags 标志位(通常为0)
src_addr 输出参数,存储发送方地址
addrlen 输入输出参数,地址结构大小
sendto 函数
cpp 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数 说明
sockfd 套接字描述符
buf 发送数据缓冲区
len 数据长度
flags 标志位(通常为0)
dest_addr 目标地址
addrlen 地址结构大小

第四部分:TCP vs UDP 核心差异

一、协议特性对比

特性 TCP UDP
连接性 面向连接(三次握手) 无连接
可靠性 确认重传、顺序保证 尽最大努力,可能丢包
数据边界 流式(无边界) 数据报(有边界)
拥塞控制
传输效率 较低 较高
编程复杂度 较高 较低

二、编程模型对比

操作 TCP UDP
创建套接字 socket(AF_INET, SOCK_STREAM, 0) socket(AF_INET, SOCK_DGRAM, 0)
绑定地址 bind() bind()
建立连接(服务端) listen() + accept() 不需要
建立连接(客户端) connect() 不需要
发送数据 send() / write() sendto()
接收数据 recv() / read() recvfrom()
获取对端地址 accept() 返回 recvfrom() 返回

三、数据接收特性对比

TCP 流式服务:

cpp 复制代码
// 发送端:多次 send
send(fd, "hello", 5, 0);
send(fd, "world", 5, 0);

// 接收端:可能一次收到 "helloworld",也可能分次收到
// 数据没有边界

UDP 数据报服务:

cpp 复制代码
// 发送端:每次 sendto 独立
sendto(fd, "hello", 5, 0, ...);
sendto(fd, "world", 5, 0, ...);

// 接收端:每次 recvfrom 对应一次 sendto
// 数据有边界,必须单次完整读取
// 如果缓冲区太小,剩余数据会被丢弃!

第五部分:端口复用与并发

一、端口复用规则

场景 是否可复用 说明
TCP + TCP 同一端口 ❌ 不可 端口已被占用
UDP + UDP 同一端口 ❌ 不可 端口已被占用
TCP + UDP 同一端口 ✅ 可 不同协议,互不冲突

验证:TCP 6000 和 UDP 6000 可同时存在

netstat -naupt | grep 6000

二、UDP 的并发特性

UDP 是无连接的,单线程即可处理多个客户端:

总结

一、TCP vs UDP 速查表

对比项 TCP UDP
套接字类型 SOCK_STREAM SOCK_DGRAM
服务端流程 socket→bind→listen→accept→recv/send→close socket→bind→recvfrom→sendto→close
客户端流程 socket→connect→send/recv→close socket→sendto→recvfrom→close
数据边界 无(流) 有(数据报)
并发实现 需要多进程/多线程 单线程即可

二、代码运行测试

编译 UDP 程序

gcc udp_server.c -o udp_server

gcc udp_client.c -o udp_client

先启动服务器

./udp_server

另一终端启动客户端(可多个)

./udp_client

三、面试高频考点

  1. TCP 三次握手:SYN → SYN+ACK → ACK

  2. TCP 四次挥手:FIN → ACK → FIN → ACK

  3. TCP vs UDP 区别:连接性、可靠性、数据边界

  4. 端口复用:不同协议可绑定同一端口

  5. UDP 数据报边界:必须单次完整读取

本文详细介绍了 TCP 和 UDP 网络编程,包括:

  1. TCP 服务端/客户端完整实现:理解面向连接的通信模型

  2. UDP 服务端/客户端完整实现:理解数据报服务的特点

  3. 核心差异分析:连接性、可靠性、数据边界

  4. 端口复用与并发:UDP 天然支持多客户端

课后任务:

  1. 整理 TCP 和 UDP 的对比笔记

  2. 动手运行两种协议的代码

  3. 观察 UDP 数据报截断现象(减小 recvfrom 缓冲区)

相关推荐
嵌入式×边缘AI:打怪升级日志1 小时前
嵌入式 Linux V4L2 摄像头采集编程(五):MMAP + 亮度实时控制(附完整代码与面试题)
linux·运维·服务器
2301_789015622 小时前
Linux基础指令(一)
linux·运维·服务器·c语言·开发语言·c++·linux指令
晚风予卿云月2 小时前
【linux】进程优先级
linux·运维·服务器
wangl_922 小时前
Modbus RTU 与 Modbus TCP 深入指南-总览对比
网络·网络协议·tcp/ip·tcp·modbus·rtu
@insist1232 小时前
信息安全工程师-现代物理隔离三大核心技术与产品体系全解析
网络·软考·信息安全工程师·软件水平考试
一拳一个娘娘腔2 小时前
从sudo配置到Root Shell:Linux Sudo提权全景深度解析与防御指南
linux·网络·安全
万法若空3 小时前
Cortex-A7的运行模式
linux·arm开发
yyuuuzz3 小时前
aws注册过程中的常见问题梳理
运维·服务器·网络·云计算·github·aws
wangl_923 小时前
Modbus RTU 与 Modbus TCP 深入指南-CRC校验完全解析
网络·网络协议·tcp/ip·tcp·modbus·rtu