Linux 入门十一:Linux 网络编程

发布于:2025-04-22 ⋅ 阅读:(77) ⋅ 点赞:(0)

一、概述

1. 网络编程基础

网络编程是通过网络应用编程接口(API)编写程序,实现不同主机上进程间的信息交互。它解决的核心问题是:如何让不同主机上的程序进行通信

2. 网络模型:从 OSI 到 TCP/IP

  • OSI 七层模型(理论模型):
    物理层(传输比特流)→ 数据链路层(组帧、差错控制)→ 网络层(路由选择,IP 协议)→ 传输层(端到端通信,TCP/UDP)→ 会话层(建立会话)→ 表示层(数据格式转换)→ 应用层(HTTP、FTP 等具体应用)。
    特点:层次清晰,适合理论分析,但实际开发中较少直接使用。

  • TCP/IP 四层模型(实用模型):
    网络接口层(对应 OSI 下两层,处理硬件通信)→ 网络层(IP 协议,寻址和路由)→ 传输层(TCP/UDP,端到端数据传输)→ 应用层(HTTP、FTP、SMTP 等,具体业务逻辑)。
    特点:简化层次,广泛应用于实际开发。

3. 常用网络协议速查表

协议名称 英文全称 核心功能 典型场景
TCP 传输控制协议 面向连接、可靠传输 网页浏览(HTTP)、文件传输(FTP)
UDP 用户数据报协议 无连接、不可靠传输 视频直播、DNS 查询
IP 网际协议 网络层寻址与路由 所有网络通信的基础
ICMP 互联网控制消息协议 网络状态检测(如 ping) 故障排查(ping、traceroute)
FTP 文件传输协议 高效传输文件 服务器文件共享
SMTP 简单邮件传输协议 发送电子邮件 邮件服务器通信

二、网络通信三要素:IP、端口、套接字

1. IP 地址:主机的 “门牌号”

  • 定义:32 位(IPv4)或 128 位(IPv6)的二进制数,唯一标识网络中的主机。
    • IPv4 示例:192.168.1.1(点分十进制)
    • IPv6 示例:2001:0db8:85a3:0000:0000:8a2e:0370:7334(十六进制)
  • 查看本机 IP:终端输入 ifconfig(Linux)或 ipconfig(Windows)。
  • 特殊 IP
    • 127.0.0.1:本地回环地址,用于测试本机网络程序。
    • 0.0.0.0:监听所有可用网络接口。
    • 255.255.255.255:广播地址,向同一网络内所有主机发送数据。

2. 端口号:程序的 “房间号”

  • 定义:16 位无符号整数(0-65535),标识同一主机上的不同进程。
  • 分类
    • 保留端口(0-1023):系统专用(如 80 端口用于 HTTP,22 端口用于 SSH)。
    • 注册端口(1024-49151):分配给特定服务(如 3306 端口用于 MySQL)。
    • 动态端口(49152-65535):程序运行时动态申请,避免冲突。
  • 注意:编程时避免使用保留端口,可选择 1024 以上未被占用的端口(如 8888、3333)。

3. 套接字(Socket):通信的 “通道”

  • 定义:一种特殊的文件描述符,用于跨网络或本地进程通信。

  • 三要素:IP 地址 + 端口号 + 传输层协议(TCP/UDP)。

  • 类型

    1. 流式套接字(SOCK_STREAM):基于 TCP,可靠、面向连接(如打电话,需先接通)。
    2. 数据报套接字(SOCK_DGRAM):基于 UDP,无连接、不可靠(如发短信,无需确认对方是否在线)。
    3. 原始套接字(SOCK_RAW):直接访问底层协议(如 IP/ICMP),用于网络开发或抓包工具。
  • 地址结构体

    // IPv4 地址结构体(常用)
    struct sockaddr_in {
        sa_family_t sin_family;   // 地址族,固定为 AF_INET(IPv4)或 AF_INET6(IPv6)
        in_port_t sin_port;       // 端口号(网络字节序,需用 htons 转换)
        struct in_addr sin_addr;  // IP 地址(网络字节序,可用 inet_addr 转换字符串)
    };
    
    // 通用地址结构体(需强制转换使用)
    struct sockaddr {
        sa_family_t sa_family;   // 地址族
        char sa_data[14];        // 具体地址数据(不同协议族格式不同)
    };
    

三、TCP 编程:可靠的 “快递服务”

TCP 协议是 Linux 网络编程中实现可靠数据传输的核心协议,其核心思想是通过 “三次握手” 建立连接,“四次挥手” 释放连接,确保数据有序、无丢失地传输。以下从核心特点到具体开发步骤,结合实际代码示例,为新手提供详细的学习指南。

1. TCP 核心特点(面向连接的可靠通信)

(1)三次握手建立连接(确保双方 “准备就绪”)
  1. 客户端发起 SYN 同步请求:客户端向服务器发送带有 SYN 标志的数据包,请求建立连接,同时携带初始序列号(如 seq=100)。
  2. 服务器回复 SYN+ACK 确认:服务器收到后,返回 SYN+ACK 包,其中 SYN 标志表示同意连接,ACK 标志确认客户端序列号(ack=101),并携带自己的初始序列号(如 seq=200)。
  3. 客户端回复 ACK 确认:客户端收到后,发送 ACK 包确认服务器序列号(ack=201),至此连接建立完成。
(2)可靠传输的 “三重保障”
  • 确认机制:接收方收到数据后,必须发送 ACK 确认报文,发送方未在超时时间内收到 ACK 则重传数据(类似快递 “签收反馈”)。
  • 流量控制:通过滑动窗口(Sliding Window)动态调整发送速率,避免接收方缓冲区溢出(如接收方缓冲区剩余 1000 字节,则告知发送方最多发送 1000 字节)。
  • 拥塞控制:根据网络拥堵情况自动调整发送速率,常用算法包括慢启动、拥塞避免、快速重传等,防止网络拥塞(如发现丢包,立即降低发送速率)。
(3)流式传输与 “粘包问题”
  • TCP 数据传输无边界,多次发送的小数据可能被合并接收(如发送 “Hello” 和 “World”,接收方可能一次性收到 “HelloWorld”)。
  • 解决方案:在应用层自定义协议,例如在数据前添加 4 字节表示数据长度(如先发送 0x00000005 表示后续有 5 字节数据,再发送实际内容)。

2. TCP 服务器开发步骤(逐行代码解析)

步骤 1:引入必要头文件并定义常量
#include <sys/socket.h>   // 套接字相关函数  
#include <netinet/in.h>   // IPv4 地址结构体  
#include <arpa/inet.h>    // IP 地址转换函数  
#include <unistd.h>       // close 函数  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  

#define PORT 8888         // 服务器端口号(建议 1024+,避免系统保留端口)  
#define MAX_BUFFER_SIZE 1024  // 数据缓冲区大小  
步骤 2:创建套接字(socket)—— 打开 “网络通信通道”
int server_fd = socket(AF_INET, SOCK_STREAM, 0);  

  • 函数原型int socket(int domain, int type, int protocol);
  • 参数详解
    • domain:协议族,AF_INET 表示 IPv4 协议(最常用),AF_INET6 表示 IPv6,AF_UNIX 用于本地进程通信。
    • type:套接字类型,SOCK_STREAM 表示 TCP 流式套接字(可靠连接),SOCK_DGRAM 表示 UDP 数据报套接字(无连接)。
    • protocol:具体协议,通常填 0(自动选择对应 type 的默认协议,TCP 对应 IPPROTO_TCP)。
  • 返回值:成功返回非负套接字描述符(文件描述符,如 3),失败返回 -1
  • 错误处理
    if (server_fd == -1) {  
        perror("socket 创建失败");  
        exit(EXIT_FAILURE);  
    }  
    
步骤 3:填充服务器地址结构体(sockaddr_in
struct sockaddr_in server_addr;  
memset(&server_addr, 0, sizeof(server_addr));  // 初始化结构体为 0  
server_addr.sin_family = AF_INET;               // 使用 IPv4 协议  
server_addr.sin_port = htons(PORT);             // 端口号转换为网络字节序(大端模式)  
server_addr.sin_addr.s_addr = INADDR_ANY;        // 监听所有本地 IP(0.0.0.0),接受任意客户端连接  

  • 关键点
    • htons 函数:将主机字节序(小端,如 x86 架构)的端口号转换为网络字节序(大端),例如主机端口 8888(小端 0x22b8)转换后为 0xb822
    • INADDR_ANY:表示服务器绑定到所有本地网络接口(如同时支持有线和无线连接),若需指定固定 IP,可使用 inet_addr("192.168.1.100")
步骤 4:绑定套接字与地址(bind)—— 告诉网络 “我在这里”
int bind_result = bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));  

  • 函数原型int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:将套接字 server_fd 与本地 IP 地址和端口号绑定,使客户端能够通过该地址连接。
  • 参数
    • sockfd:步骤 2 创建的套接字描述符。
    • addr:指向地址结构体的指针,需将 sockaddr_in 强制转换为 sockaddr(通用地址结构体)。
    • addrlen:地址结构体的长度,即 sizeof(struct sockaddr_in)
  • 错误处理
    if (bind_result == -1) {  
        perror("bind 绑定失败");  
        close(server_fd);  // 释放资源  
        exit(EXIT_FAILURE);  
    }  
    
  • 常见错误:端口被占用时,可通过 netstat -tunlp | grep 8888 查看占用进程,或更换端口。
步骤 5:设置监听状态(listen)—— 准备接受连接
int listen_result = listen(server_fd, 5);  

  • 函数原型int listen(int sockfd, int backlog);
  • 作用:将套接字转为被动监听模式,创建连接队列存储未处理的客户端请求。
  • 参数
    • backlog:队列最大长度(如 5 表示最多缓存 5 个连接请求,超过则客户端收到 ECONNREFUSED 错误)。
  • 示例
    if (listen_result == -1) {  
        perror("listen 监听失败");  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("服务器启动,监听端口 %d...\n", PORT);  
    
步骤 6:接受客户端连接(accept)—— 处理单个客户端请求
struct sockaddr_in client_addr;  
socklen_t client_addr_len = sizeof(client_addr);  
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);  

  • 函数原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 作用:阻塞等待客户端连接,成功后返回新的套接字描述符(client_fd),用于与该客户端单独通信,原 server_fd 继续监听其他连接。
  • 参数
    • addr:输出参数,存储客户端地址(填充 client_addr.sin_addr 和 client_addr.sin_port)。
    • addrlen:传入时为 sizeof(client_addr),传出时自动更新为实际地址长度。
  • 解析客户端信息
    if (client_fd == -1) {  
        perror("accept 接受连接失败");  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    // 转换客户端 IP 地址(网络字节序转字符串)  
    char *client_ip = inet_ntoa(client_addr.sin_addr);  
    // 转换客户端端口号(网络字节序转主机字节序)  
    int client_port = ntohs(client_addr.sin_port);  
    printf("客户端连接:IP %s,端口 %d\n", client_ip, client_port);  
    
  • 核心逻辑:每个客户端连接对应一个独立的 client_fd,后续数据收发通过该描述符进行。
步骤 7:数据收发(send/recv)—— 实现双向通信
发送数据到客户端(send
char send_buffer[MAX_BUFFER_SIZE] = "Hello, Client! This is TCP server.";  
ssize_t send_bytes = send(client_fd, send_buffer, strlen(send_buffer), 0);  

  • 函数原型ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 参数
    • flags:通常为 0(默认模式,支持 MSG_NOSIGNAL 等高级标志,避免发送失败时程序终止)。
  • 错误处理
    if (send_bytes == -1) {  
        perror("send 发送数据失败");  
        close(client_fd);  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("发送数据成功,字节数:%ld\n", send_bytes);  
    
接收客户端数据(recv
char recv_buffer[MAX_BUFFER_SIZE] = {0};  // 初始化缓冲区  
ssize_t recv_bytes = recv(client_fd, recv_buffer, MAX_BUFFER_SIZE, 0);  

  • 函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 返回值
    • >0:成功接收的字节数。
    • 0:对方关闭连接(TCP 半关闭状态)。
    • -1:接收失败(需调用 perror 查看错误原因,如 EAGAIN 表示非阻塞模式下无数据)。
  • 示例处理
    if (recv_bytes > 0) {  
        printf("接收客户端数据:%s(字节数:%ld)\n", recv_buffer, recv_bytes);  
    } else if (recv_bytes == 0) {  
        printf("客户端断开连接\n");  
    } else {  
        perror("recv 接收数据失败");  
    }  
    
步骤 8:关闭连接(close)—— 释放资源并断开连接
close(client_fd);  // 关闭与当前客户端的通信套接字  
close(server_fd);  // 关闭服务器监听套接字  

  • 底层操作:触发 TCP 四次挥手:
    1. 客户端或服务器调用 close,发送 FIN 包请求断开。
    2. 对方回复 ACK 确认,进入半关闭状态(仍可接收数据)。
    3. 对方处理完剩余数据后,发送 FIN 包。
    4. 最初关闭方回复 ACK 确认,连接彻底断开。
  • 注意:多次调用 close 不会出错,但建议在断开后将描述符置为 -1 避免误操作。

3. TCP 客户端开发步骤(快速连接服务器)

步骤 1:创建客户端套接字(同服务器)
int client_fd = socket(AF_INET, SOCK_STREAM, 0);  
if (client_fd == -1) {  
    perror("客户端 socket 创建失败");  
    exit(EXIT_FAILURE);  
}  
步骤 2:填充服务器地址结构体(目标地址)
struct sockaddr_in server_addr;  
memset(&server_addr, 0, sizeof(server_addr));  
server_addr.sin_family = AF_INET;  
server_addr.sin_port = htons(PORT);  // 服务器端口(需与服务器端一致)  
// 服务器 IP 地址(字符串转网络字节序,如 "192.168.1.100")  
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");  
步骤 3:连接服务器(connect)—— 主动发起三次握手
int connect_result = connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));  
if (connect_result == -1) {  
    perror("连接服务器失败");  
    close(client_fd);  
    exit(EXIT_FAILURE);  
}  
printf("成功连接到服务器!\n");  
步骤 4:数据交互(同服务器,调用 send/recv)
  • 发送数据
    char msg[] = "Hello from client!";  
    send(client_fd, msg, strlen(msg), 0);  
    
  • 接收数据
    char buf[MAX_BUFFER_SIZE] = {0};  
    recv(client_fd, buf, MAX_BUFFER_SIZE, 0);  
    printf("服务器回复:%s\n", buf);  
    
步骤 5:关闭客户端连接
close(client_fd);  

4. 关键函数与易错点总结

函数 核心作用 必学参数解释 易错点提醒
socket 创建套接字 domain=AF_INETtype=SOCK_STREAM 忽略错误处理,导致程序崩溃
bind 绑定 IP 和端口 sin_port 需 htons 转换 未转换字节序,端口号无效
listen 设置监听队列 backlog 建议 5-10 过大的 backlog 可能占用过多资源
accept 接受客户端连接 返回新套接字 client_fd 未使用新套接字通信,导致数据混乱
send/recv 数据收发 处理 recv_bytes=0(连接关闭) 忽略 “粘包” 问题,数据解析错误
close 释放连接 触发四次挥手 未及时关闭,导致端口占用(TIME_WAIT)

通过以上步骤,新手可完整掌握 TCP 服务器与客户端的开发流程。实际项目中,需结合多线程(处理并发连接)或 IO 多路复用(如 select 函数)提升性能,后续章节将深入讲解高级编程技巧。

四、UDP 编程:轻量的 “明信片” 式数据传输

1. UDP 核心特性:无拘无束的 “快递员”

UDP(User Datagram Protocol,用户数据报协议)是一种轻量级的网络传输协议,与 TCP 的 “可靠快递” 模式不同,它更像是 “明信片” 传输,具有以下特点:

(1)无连接通信:说发就发,无需 “预约”
  • 核心机制:发送数据前无需建立连接(如 TCP 的三次握手),直接将数据封装成独立的数据报(Datagram)发送,接收方无需确认连接状态。
  • 类比场景:类似发送短信,无需等待对方 “在线确认”,直接发送内容,对方可能收到也可能收不到。
(2)不可靠传输:允许 “丢包” 的效率优先
  • 数据保障:不保证数据一定到达、不保证顺序、不处理重复包,完全依赖上层应用处理可靠性(如重传、排序)。
  • 适用场景:适合实时性要求高但允许少量丢包的场景,例如视频通话(丢几帧不影响观看)、DNS 查询(响应快更重要)、直播流传输。
(3)高效轻量:省去连接开销
  • 协议优势:没有连接建立和释放的开销,头部仅 8 字节(相比 TCP 的 20 字节),传输效率更高,适合小数据量、低延迟场景。

2. UDP 服务器开发:从 “监听” 到 “响应” 的三步曲

步骤 1:创建套接字(socket)—— 打开通信通道
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

  • 参数解析
    • AF_INET:指定协议族(IPv4),若用 IPv6 则为 AF_INET6
    • SOCK_DGRAM:指定 socket 类型为 UDP 数据报套接字(TCP 对应 SOCK_STREAM)。
    • 0:自动选择 UDP 协议(对应 IPPROTO_UDP),无需手动指定。
  • 返回值:成功返回文件描述符(非负整数),失败返回 -1,需用 perror 打印错误。
步骤 2:绑定地址(bind)—— 告诉系统 “我在这儿”
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr)); // 初始化结构体为 0
server_addr.sin_family = AF_INET;         // IPv4 协议族
server_addr.sin_port = htons(8888);       // 端口号(网络字节序,htons 转换主机字节序)
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地 IP 地址(0.0.0.0)

int bind_ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (bind_ret < 0) {
    perror("bind failed");
    close(sockfd);
    exit(1);
}

  • 关键细节
    • INADDR_ANY:表示绑定到所有网卡接口(如服务器有多个 IP,无需逐个指定)。
    • 端口号选择:建议使用 1024~65535 的非特权端口(1~1023 为系统保留,需管理员权限)。
    • htons 函数:将主机的 16 位端口号(小端或大端)转换为网络字节序(大端),确保跨平台兼容。
步骤 3:数据交互(sendto/recvfrom)—— 收发 “明信片”

UDP 没有连接概念,每次收发数据都需明确目标地址(服务器需记录客户端地址以便回复)。

接收数据:recvfrom 函数
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

  • 参数解析
    • buf:接收数据的缓冲区。
    • len:缓冲区大小。
    • flags:通常设为 0(默认阻塞模式,无特殊标志)。
    • src_addr:存储发送方地址(客户端地址)。
    • addrlen:传入 src_addr 的长度,返回时更新为实际地址长度。
  • 示例代码
char buffer[1024] = {0};
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);

ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                            (struct sockaddr*)&client_addr, &client_addr_len);
if (recv_len > 0) {
    // 转换 IP 和端口为可读格式
    char *client_ip = inet_ntoa(client_addr.sin_addr); // 将网络字节序 IP 转为字符串(如 "192.168.1.1")
    int client_port = ntohs(client_addr.sin_port);      // 将网络字节序端口转为主机字节序(整数)
    printf("Received from %s:%d: %s\n", client_ip, client_port, buffer);
} else if (recv_len == 0) {
    printf("Client disconnected\n");
} else {
    perror("recvfrom failed");
}
发送数据:sendto 函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

  • 参数解析
    • dest_addr:目标地址(如客户端地址,从 recvfrom 获取)。
    • addrlen:目标地址长度。
  • 示例代码

const char *response = "Hello from UDP server!";
sendto(sockfd, response, strlen(response), 0,
       (struct sockaddr*)&client_addr, client_addr_len);
步骤 4:关闭套接字(close)—— 结束通信
close(sockfd); // 直接关闭,无需释放连接(无连接状态)

3. UDP 客户端开发:主动 “投递” 数据

客户端无需监听端口(可选绑定,若不绑定系统自动分配临时端口),核心是指定服务器地址进行数据发送。

步骤 1:创建套接字(同服务器)
int client_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
步骤 2:指定服务器地址(无需绑定,直接构造目标地址)
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 服务器端口
// 转换服务器 IP 地址(字符串转网络字节序)
if (inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr) <= 0) {
    perror("inet_pton failed");
    exit(1);
}

  • inet_pton 函数:将点分十进制 IP 字符串转为网络字节序二进制,支持 IPv4 和 IPv6(inet_addr 仅支持 IPv4,已过时)。
步骤 3:发送 / 接收数据(主动发送,被动接收)
char send_buf[1024] = "Hello UDP Server!";
// 发送数据到服务器
sendto(client_sockfd, send_buf, strlen(send_buf), 0,
       (struct sockaddr*)&server_addr, sizeof(server_addr));

// 接收服务器回复(需提前分配客户端地址结构体)
char recv_buf[1024] = {0};
struct sockaddr_in server_reply_addr;
socklen_t reply_addr_len = sizeof(server_reply_addr);
recvfrom(client_sockfd, recv_buf, sizeof(recv_buf), 0,
         (struct sockaddr*)&server_reply_addr, &reply_addr_len);
printf("Server reply: %s\n", recv_buf);
步骤 4:关闭套接字
close(client_sockfd);

4. 实战:简单 UDP 双向通信程序(带错误处理)

服务器端完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8888
#define BUF_SIZE 1024

int main() {
    // 1. 创建 socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("UDP server listening on port %d...\n", PORT);

    // 3. 数据交互循环
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUF_SIZE] = {0};

    while (1) {
        // 接收客户端数据
        ssize_t recv_len = recvfrom(sockfd, buffer, BUF_SIZE, 0,
                                    (struct sockaddr*)&client_addr, &client_addr_len);
        if (recv_len < 0) {
            perror("recvfrom failed");
            continue;
        }

        // 转换客户端地址
        char *client_ip = inet_ntoa(client_addr.sin_addr);
        int client_port = ntohs(client_addr.sin_port);
        printf("Received from %s:%d: %s\n", client_ip, client_port, buffer);

        // 回复客户端
        const char *response = "Message received by server!";
        sendto(sockfd, response, strlen(response), 0,
               (struct sockaddr*)&client_addr, client_addr_len);
        printf("Response sent to client\n");

        // 清空缓冲区
        memset(buffer, 0, BUF_SIZE);
    }

    // 4. 关闭 socket
    close(sockfd);
    return 0;
}
客户端端完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUF_SIZE 1024

int main() {
    // 1. 创建 socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 指定服务器地址
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);

    // 转换 IP 地址(支持 IPv4 和 IPv6,比 inet_addr 更安全)
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char send_buf[BUF_SIZE] = {0};
    char recv_buf[BUF_SIZE] = {0};

    printf("Enter message to send (type 'exit' to quit):\n");
    while (1) {
        // 3. 输入数据并发送
        fgets(send_buf, BUF_SIZE, stdin);
        send_buf[strcspn(send_buf, "\n")] = '\0'; // 去除换行符

        if (strcmp(send_buf, "exit") == 0) {
            break;
        }

        sendto(sockfd, send_buf, strlen(send_buf), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));

        // 4. 接收服务器回复
        struct sockaddr_in server_reply_addr;
        socklen_t reply_addr_len = sizeof(server_reply_addr);
        ssize_t recv_len = recvfrom(sockfd, recv_buf, BUF_SIZE, 0,
                                    (struct sockaddr*)&server_reply_addr, &reply_addr_len);
        if (recv_len > 0) {
            printf("Server response: %s\n", recv_buf);
        } else {
            perror("recvfrom failed");
        }

        // 清空缓冲区
        memset(send_buf, 0, BUF_SIZE);
        memset(recv_buf, 0, BUF_SIZE);
    }

    // 5. 关闭 socket
    close(sockfd);
    return 0;
}

5. 新手常见问题与最佳实践

(1)UDP 数据报大小限制
  • 底层限制:IP 层最大传输单元(MTU)通常为 1500 字节,UDP 数据报建议不超过 1472 字节(预留 28 字节 IP+UDP 头部),否则可能分片,增加丢包风险。
  • 代码处理:发送前检查数据长度,超过限制时拆分为多个包,接收时重组(需上层实现)。
(2)不可靠性应对
  • 应用层重传:记录未确认的包,超时后重新发送(类似 TCP 的确认机制,但需手动实现)。
  • 序列号标记:给每个包添加序列号,接收方去重、排序。
(3)端口冲突处理
  • 绑定端口时若提示 Address already in use,可通过 netstat -anu | grep 端口号 查看占用进程,或更换端口。
(4)测试工具推荐
  • netcat:简单 UDP 测试工具。
    • 服务器端:nc -ul 8888(监听 UDP 端口 8888)。
    • 客户端:echo "test" | nc -u 服务器 IP 8888
  • Wireshark:抓包分析 UDP 数据格式,验证协议交互过程。

6. UDP vs TCP:如何选择?

场景 UDP TCP
实时性要求 高(如视频通话、直播) 低(需连接建立,延迟较高)
数据可靠性 不保证(需应用层处理) 保证(自动重传、排序)
数据量 小数据报(如 DNS 查询) 大数据流(如文件传输、HTTP)
连接状态维护 无连接,资源消耗低 维护连接状态,资源消耗高

总结:UDP 的 “快” 与 “简”

UDP 以牺牲可靠性换取高效传输,适合对实时性敏感的场景。掌握 UDP 编程的关键在于理解无连接模型、手动处理地址信息,以及在应用层补充可靠性逻辑。通过分步实践服务器和客户端代码,结合错误处理和工具测试,新手可逐步掌握这一轻量级网络编程技术。

五、高级编程:处理多连接与性能优化

在嵌入式和网络开发中,单线程单连接的编程模型难以应对并发场景(如多个客户端同时连接服务器)。本节介绍 IO 多路复用 和 非阻塞 IO 技术,帮助开发者高效处理多连接,提升程序性能。

1. IO 多路复用核心:select 函数(单线程监听多套接字)

核心作用

select 函数允许单线程同时监听多个文件描述符(如套接字),当任意一个描述符就绪(可读、可写或发生异常)时,程序能立即感知并处理,避免阻塞在单一操作上。
典型场景:聊天服务器、日志服务器(客户端多但实时活动少)。

函数原型与参数解析
#include <sys/select.h>  
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);  

参数 解释
maxfd 监听的最大文件描述符值 + 1(确保包含所有监听的 fd,例如有 fd 3、5,则 maxfd 为 6)。
readfds 可读事件集合(监听哪些 fd 有数据可读,用 FD_SET 添加 fd)。
writefds 可写事件集合(监听哪些 fd 可以无阻塞地写入数据,较少用,通常设为 NULL)。
exceptfds 异常事件集合(如带外数据到达,一般设为 NULL)。
timeout 超时时间:
NULL:永久阻塞,直到任意 fd 就绪
{0, 0}:立即返回,不等待
{2, 0}:最多等待 2 秒(2 秒超时)
返回值 就绪的 fd 数量;0 表示超时;-1 表示错误(需用 perror 打印原因)。
使用步骤(以 UDP 服务器同时监听多个客户端为例)
步骤 1:初始化事件集合
fd_set read_fds;  
FD_ZERO(&read_fds);  // 清空集合(必须先调用,避免脏数据)  
FD_SET(sockfd, &read_fds);  // 将服务器套接字添加到可读集合(监听客户端数据到达)  

  • FD_ZERO:重置集合,确保集合为空。
  • FD_SET:将目标 fd(如服务器套接字 sockfd)加入集合,监听其可读事件。
步骤 2:计算 maxfd
int maxfd = sockfd;  // 若有多个 fd(如客户端连接 fd),取最大值  

  • 若同时监听服务器套接字(fd=3)和两个客户端套接字(fd=5、fd=6),则 maxfd = 6
步骤 3:调用 select 阻塞等待
struct timeval timeout = {2, 0};  // 超时时间 2 秒(2 秒内无事件则返回)  
int ready_count = select(maxfd + 1, &read_fds, NULL, NULL, &timeout);  

  • 关键逻辑
    • 若 ready_count > 0:至少有一个 fd 就绪。
    • 若 ready_count == 0:超时,无事件发生(可继续循环或执行其他任务)。
    • 若 ready_count == -1:发生错误(如被信号中断),需重新调用或退出。
步骤 4:检查就绪的 fd 并处理
if (ready_count > 0) {  
    // 检查服务器套接字是否可读(有客户端发送数据)  
    if (FD_ISSET(sockfd, &read_fds)) {  
        struct sockaddr_in client_addr;  
        socklen_t client_len = sizeof(client_addr);  
        char buffer[1024] = {0};  
        ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0,  
                                    (struct sockaddr*)&client_addr, &client_len);  
        if (recv_len > 0) {  
            printf("Received from client: %s\n", buffer);  
            // 回复客户端(示例:原样返回)  
            sendto(sockfd, buffer, recv_len, 0,  
                   (struct sockaddr*)&client_addr, client_len);  
        }  
    }  
}  

  • FD_ISSET(fd, &set):判断 fd 是否在 set 集合中且就绪。
完整示例:带 select 的 UDP 服务器
#include <sys/select.h>  
// ...(其他头文件和初始化代码)  

int main() {  
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);  
    bind(sockfd, &server_addr, sizeof(server_addr));  
    fd_set read_fds;  

    while (1) {  
        FD_ZERO(&read_fds);  
        FD_SET(sockfd, &read_fds);  // 每次循环重新添加 fd(select 会修改集合内容)  
        struct timeval tv = {5, 0};  // 5 秒超时  
        int ready = select(sockfd + 1, &read_fds, NULL, NULL, &tv);  

        if (ready < 0) {  
            perror("select error");  
            break;  
        } else if (ready == 0) {  
            printf("No data received in 5 seconds\n");  
            continue;  
        }  

        if (FD_ISSET(sockfd, &read_fds)) {  
            // 处理数据接收和回复(同步骤 4 代码)  
        }  
    }  
    close(sockfd);  
    return 0;  
}  
注意事项
  • 集合重置:每次调用 select 前需用 FD_ZERO 和 FD_SET 重新初始化集合(内核会修改集合内容,移除未就绪的 fd)。
  • FD_SETSIZE 限制:默认最多监听 1024 个 fd(由系统宏 FD_SETSIZE 决定,如需监听更多,需改用 poll 或 epoll)。

2. 非阻塞 IO:fcntl 函数(让操作 “不等不靠”)

核心作用

将套接字设置为 非阻塞模式,使 recvaccept 等函数在无数据时立即返回(而非阻塞等待),配合轮询或事件驱动,实现单线程处理多任务。
典型场景

  • 客户端需要同时发送数据和接收服务器回复(如聊天程序边输入边接收消息)。
  • 服务器处理大量并发连接,避免单个慢连接阻塞整个程序。
函数原型与参数解析
#include <fcntl.h>  
int fcntl(int fd, int cmd, ... /* 可选参数 */);  

参数 解释
fd 文件描述符(如套接字 fd)。
cmd 操作类型:
F_GETFL:获取文件状态标志(返回值为标志位)
F_SETFL:设置文件状态标志(第三个参数为标志位)
... 当 cmd 为 F_SETFL 时,需传入标志位(如 O_NONBLOCK)。
设置非阻塞模式步骤
步骤 1:获取当前文件状态标志
int flags = fcntl(sockfd, F_GETFL);  // 获取套接字当前标志位  
if (flags == -1) {  
    perror("fcntl F_GETFL failed");  
    exit(EXIT_FAILURE);  
}  
步骤 2:添加非阻塞标志
flags |= O_NONBLOCK;  // 在原有标志位基础上,按位或非阻塞标志  
int ret = fcntl(sockfd, F_SETFL, flags);  // 设置新的标志位  
if (ret == -1) {  
    perror("fcntl F_SETFL failed");  
    exit(EXIT_FAILURE);  
}  

  • O_NONBLOCK:使 recvsendaccept 等操作在无数据或不可写时立即返回,错误码通常为 EAGAIN 或 EWOULDBLOCK
非阻塞模式下的读写处理
接收数据(非阻塞模式)
char buffer[1024];  
ssize_t recv_len = recv(sockfd, buffer, sizeof(buffer), 0);  
if (recv_len == -1) {  
    if (errno == EAGAIN || errno == EWOULDBLOCK) {  
        // 无数据可读,继续执行其他任务(如处理发送队列)  
        printf("No data available yet\n");  
    } else {  
        perror("recv error");  
    }  
} else if (recv_len > 0) {  
    // 处理接收的数据  
}  
发送数据(非阻塞模式)
const char *msg = "Hello from non-blocking client!";  
ssize_t send_len = send(sockfd, msg, strlen(msg), 0);  
if (send_len == -1) {  
    if (errno == EAGAIN || errno == EWOULDBLOCK) {  
        // 套接字不可写(如对方缓冲区满),稍后重试  
    } else {  
        perror("send error");  
    }  
}  
应用示例:非阻塞客户端(边输入边接收)
#include <fcntl.h>  
// ...(其他头文件和初始化代码)  

int main() {  
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);  
    connect(sockfd, &server_addr, sizeof(server_addr));  

    // 设置非阻塞模式  
    int flags = fcntl(sockfd, F_GETFL);  
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);  

    char send_buf[1024], recv_buf[1024];  
    while (1) {  
        // 处理用户输入(阻塞式,也可改用非阻塞输入)  
        fgets(send_buf, sizeof(send_buf), stdin);  
        send(sockfd, send_buf, strlen(send_buf), 0);  

        // 非阻塞接收服务器回复  
        ssize_t recv_len = recv(sockfd, recv_buf, sizeof(recv_buf), 0);  
        if (recv_len > 0) {  
            printf("Server reply: %s\n", recv_buf);  
        } else if (recv_len == -1 && errno != EAGAIN) {  
            perror("recv error");  
            break;  
        }  
    }  
    close(sockfd);  
    return 0;  
}  
注意事项
  • 错误码处理:非阻塞操作返回 -1 时,需判断错误码是否为 EAGAIN/EWOULDBLOCK,避免误判错误。
  • 轮询开销:需配合定时器或事件驱动(如结合 select),避免空转浪费 CPU 资源。

3. 技术对比与选择建议

技术 优势 劣势 适用场景
select 跨平台支持好,简单易用 监听 fd 数量有限(FD_SETSIZE) 小规模并发(<100 个连接)
非阻塞 IO 避免单个操作阻塞,灵活度高 需要手动处理错误码和轮询逻辑 需细粒度控制 IO 的场景
epoll(Linux) 高性能,支持大量 fd(>1000 个) 仅 Linux 支持,接口较复杂 高并发服务器(如 Web 服务器)

通过 select 和 fcntl 结合使用,开发者能在单线程内高效处理多连接,避免资源浪费。实际项目中,可根据并发规模和平台特性选择合适的技术(如 Linux 下优先使用 epoll 提升性能)。后续章节将深入讲解网络编程中的错误处理、协议设计等进阶内容,帮助读者构建健壮的网络应用。

六、实战:简单 TCP 服务器与客户端(完整代码)

通过前面的学习,我们已经掌握了 TCP 编程的核心原理和关键函数。本节将通过完整的代码示例,手把手教你实现一个可实际运行的 TCP 服务器与客户端,包含详细的错误处理和注释,帮助新手快速上手并理解网络编程的全流程。

一、TCP 服务器:从监听 to 响应(带完整注释)

核心功能
  • 监听指定端口,接受客户端连接
  • 与客户端双向通信(服务器接收数据后,原样回复客户端)
  • 支持多个客户端轮流连接(单线程,处理完一个再处理下一个)
完整代码(server.c)
#include <stdio.h>          // 标准输入输出头文件  
#include <stdlib.h>         // 标准库头文件(含 exit 函数)  
#include <string.h>         // 字符串操作头文件(如 memset)  
#include <unistd.h>         // UNIX 系统调用头文件(含 close 函数)  
#include <arpa/inet.h>      // IP 地址转换头文件(如 inet_ntoa)  
#include <sys/socket.h>     // 套接字相关头文件  
#include <netinet/in.h>     // IPv4 地址结构体头文件  

#define PORT 8888            // 服务器端口号(建议 1024+,避免系统保留端口)  
#define MAX_BUFFER_SIZE 1024 // 数据缓冲区大小  

int main() {  
    // 步骤 1:创建套接字(socket)—— 打开网络通信通道  
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);  
    if (server_fd == -1) {  
        perror("socket 创建失败");  // 打印错误信息(如 "socket: 没有那个文件或目录")  
        exit(EXIT_FAILURE);         // 退出程序,错误码 1  
    }  
    printf("服务器套接字创建成功,描述符:%d\n", server_fd);  

    // 步骤 2:填充服务器地址结构体(sockaddr_in)  
    struct sockaddr_in server_addr;  
    memset(&server_addr, 0, sizeof(server_addr));  // 初始化结构体为 0(重要!避免随机值)  
    server_addr.sin_family = AF_INET;               // 使用 IPv4 协议  
    server_addr.sin_port = htons(PORT);             // 端口号转换为网络字节序(大端模式)  
    server_addr.sin_addr.s_addr = INADDR_ANY;        // 监听所有本地 IP 地址(0.0.0.0)  

    // 步骤 3:绑定地址(bind)—— 告诉网络“我在这里”  
    int bind_result = bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));  
    if (bind_result == -1) {  
        perror("bind 绑定失败");  
        close(server_fd);  // 释放套接字资源  
        exit(EXIT_FAILURE);  
    }  
    printf("服务器绑定端口 %d 成功\n", PORT);  

    // 步骤 4:设置监听状态(listen)—— 准备接受连接  
    int listen_result = listen(server_fd, 5);  
    if (listen_result == -1) {  
        perror("listen 监听失败");  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("服务器启动,监听端口 %d,最大排队连接数:5\n", PORT);  

    // 步骤 5:接受客户端连接(accept)—— 阻塞等待连接  
    struct sockaddr_in client_addr;  
    socklen_t client_addr_len = sizeof(client_addr);  
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);  
    if (client_fd == -1) {  
        perror("accept 接受连接失败");  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    // 解析客户端地址(网络字节序转可读格式)  
    char* client_ip = inet_ntoa(client_addr.sin_addr);  // IP 地址转字符串(如 "192.168.1.100")  
    int client_port = ntohs(client_addr.sin_port);       // 端口号转主机字节序(整数)  
    printf("客户端连接成功:IP %s,端口 %d\n", client_ip, client_port);  

    // 步骤 6:数据交互(recv/send)—— 双向通信  
    char buffer[MAX_BUFFER_SIZE] = {0};  // 初始化接收缓冲区  

    // 接收客户端数据  
    ssize_t recv_len = recv(client_fd, buffer, MAX_BUFFER_SIZE, 0);  
    if (recv_len > 0) {  
        printf("接收客户端数据:%s(字节数:%ld)\n", buffer, recv_len);  
    } else if (recv_len == 0) {  
        printf("客户端关闭连接\n");  
    } else {  
        perror("recv 接收数据失败");  
        close(client_fd);  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  

    // 回复客户端(原样返回数据)  
    const char* response = "服务器已收到:";  
    char send_buffer[MAX_BUFFER_SIZE] = {0};  
    strcat(send_buffer, response);  
    strcat(send_buffer, buffer);  
    ssize_t send_len = send(client_fd, send_buffer, strlen(send_buffer), 0);  
    if (send_len == -1) {  
        perror("send 发送数据失败");  
        close(client_fd);  
        close(server_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("回复客户端成功,发送字节数:%ld\n", send_len);  

    // 步骤 7:关闭连接(close)—— 释放资源  
    close(client_fd);   // 关闭与当前客户端的通信套接字  
    close(server_fd);   // 关闭服务器监听套接字  
    printf("服务器关闭,连接释放完成\n");  

    return 0;  
}  

二、TCP 客户端:主动连接服务器(带完整注释)

核心功能
  • 主动连接服务器
  • 发送自定义数据并接收服务器回复
  • 简单的命令行交互(输入数据后按回车发送)
完整代码(client.c)
#include <stdio.h>          // 标准输入输出头文件  
#include <stdlib.h>         // 标准库头文件(含 exit 函数)  
#include <string.h>         // 字符串操作头文件(如 fgets)  
#include <unistd.h>         // UNIX 系统调用头文件(含 close 函数)  
#include <arpa/inet.h>      // IP 地址转换头文件(如 inet_addr)  
#include <sys/socket.h>     // 套接字相关头文件  
#include <netinet/in.h>     // IPv4 地址结构体头文件  

#define SERVER_IP "127.0.0.1"  // 服务器 IP 地址(本地回环地址,可改为实际服务器 IP)  
#define PORT 8888              // 服务器端口号(需与服务器代码一致)  
#define MAX_BUFFER_SIZE 1024   // 数据缓冲区大小  

int main() {  
    // 步骤 1:创建套接字(socket)—— 打开网络通信通道  
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);  
    if (client_fd == -1) {  
        perror("客户端 socket 创建失败");  
        exit(EXIT_FAILURE);  
    }  
    printf("客户端套接字创建成功,描述符:%d\n", client_fd);  

    // 步骤 2:填充服务器地址结构体(sockaddr_in)  
    struct sockaddr_in server_addr;  
    memset(&server_addr, 0, sizeof(server_addr));  
    server_addr.sin_family = AF_INET;                // 使用 IPv4 协议  
    server_addr.sin_port = htons(PORT);              // 服务器端口号(网络字节序)  
    // 将 IP 地址字符串转为网络字节序(如 "192.168.1.100" 转为 32 位整数)  
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {  
        perror("inet_pton 转换 IP 失败");  
        close(client_fd);  
        exit(EXIT_FAILURE);  
    }  

    // 步骤 3:连接服务器(connect)—— 发起三次握手  
    int connect_result = connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));  
    if (connect_result == -1) {  
        perror("客户端连接服务器失败");  
        close(client_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("成功连接到服务器:%s:%d\n", SERVER_IP, PORT);  

    // 步骤 4:数据交互(send/recv)—— 发送数据并接收回复  
    char send_buffer[MAX_BUFFER_SIZE] = {0};  // 发送缓冲区  
    char recv_buffer[MAX_BUFFER_SIZE] = {0};  // 接收缓冲区  

    printf("请输入要发送的数据(按回车发送,长度不超过 %d 字节):\n", MAX_BUFFER_SIZE - 1);  
    fgets(send_buffer, MAX_BUFFER_SIZE, stdin);  // 从标准输入读取数据(含换行符)  
    send_buffer[strcspn(send_buffer, "\n")] = '\0';  // 去除换行符(保留有效数据)  

    // 发送数据到服务器  
    ssize_t send_len = send(client_fd, send_buffer, strlen(send_buffer), 0);  
    if (send_len == -1) {  
        perror("客户端发送数据失败");  
        close(client_fd);  
        exit(EXIT_FAILURE);  
    }  
    printf("数据发送成功,字节数:%ld\n", send_len);  

    // 接收服务器回复  
    ssize_t recv_len = recv(client_fd, recv_buffer, MAX_BUFFER_SIZE, 0);  
    if (recv_len > 0) {  
        printf("接收服务器回复:%s(字节数:%ld)\n", recv_buffer, recv_len);  
    } else if (recv_len == 0) {  
        printf("服务器关闭连接\n");  
    } else {  
        perror("客户端接收数据失败");  
        close(client_fd);  
        exit(EXIT_FAILURE);  
    }  

    // 步骤 5:关闭连接(close)—— 释放资源  
    close(client_fd);  
    printf("客户端连接关闭,资源释放完成\n");  

    return 0;  
}  

三、代码解析与关键知识点回顾

1. 必学函数与参数对比
函数 服务器端作用 客户端端作用 核心参数解释
socket 创建监听套接字 创建通信套接字 AF_INET(IPv4)、SOCK_STREAM(TCP)
bind 绑定本地 IP 和端口 可选(系统自动分配端口) INADDR_ANY(监听所有 IP)、htons(端口转换)
listen 设置连接队列长度 无需调用 backlog(最大排队连接数,如 5)
accept 阻塞接受客户端连接 无需调用 返回新套接字 client_fd(与客户端通信)
connect 无需调用 主动连接服务器 服务器地址结构体(IP + 端口)
recv/send 接收 / 回复客户端数据 发送数据 / 接收服务器回复 缓冲区、数据长度、标志位(通常为 0)
2. 字节序转换:为什么必须用 htons/ntohs
  • 主机字节序:x86 架构为小端模式(低位字节存低地址,如端口 8888 存为 0x22b8)。
  • 网络字节序:大端模式(高位字节存低地址,如 0xb822)。
  • 后果:若不转换,服务器无法正确解析端口号,导致连接失败(客户端端口号同理)。
3. 错误处理:为什么每个系统调用都要检查返回值?
  • 网络编程中,套接字操作可能因网络波动、端口被占用、对方关闭连接等原因失败,不处理错误会导致程序崩溃或资源泄漏。
  • 示例:accept 失败时,必须关闭监听套接字并退出,避免僵尸进程。

四、编译与测试步骤

1. 编译代码
# 服务器端(需在终端 1 运行)  
gcc server.c -o server  
# 客户端(需在终端 2 运行)  
gcc client.c -o client  
2. 运行程序
  • 服务器端
    ./server  
    # 输出示例:  
    # 服务器套接字创建成功,描述符:3  
    # 服务器绑定端口 8888 成功  
    # 服务器启动,监听端口 8888,最大排队连接数:5  
    
  • 客户端
    ./client  
    # 输出示例:  
    # 客户端套接字创建成功,描述符:3  
    # 成功连接到服务器:127.0.0.1:8888  
    # 请输入要发送的数据(按回车发送,长度不超过 1023 字节):  
    # Hello, TCP server!  
    # 数据发送成功,字节数:16  
    # 接收服务器回复:服务器已收到:Hello, TCP server!(字节数:24)  
    
3. 进阶测试:使用 netcat 替代客户端
  • 服务器运行后,可通过 netcat 快速测试:
    # 客户端用 netcat 连接服务器  
    nc 127.0.0.1 8888  
    # 输入数据并回车,查看服务器回复  
    

五、新手常见问题与解决方案

1. 端口被占用(bind: Address already in use
  • 原因:端口已被其他程序占用(如之前运行的服务器未正确关闭)。
  • 解决
    1. 通过 netstat -tunlp | grep 8888 查看占用进程。
    2. 杀死进程:kill -9 <进程号>
    3. 更换端口(修改代码中的 PORT 为 8889 等未被占用的端口)。
2. 连接被拒绝(connect: Connection refused
  • 原因:服务器未启动,或防火墙阻止端口。
  • 解决
    1. 确保服务器已运行(在终端 1 先启动服务器)。
    2. 关闭防火墙(测试环境):systemctl stop firewalld
3. 数据接收不完整(粘包问题)
  • 原因:TCP 流式传输无边界,多次发送的数据可能被合并接收。
  • 解决:在应用层添加协议头(如先发送 4 字节表示数据长度,再发送实际内容),后续章节将详细讲解。

总结:从代码到实战的核心收获

通过本实战案例,你将掌握:

  1. TCP 服务器与客户端的完整开发流程。
  2. 关键函数的正确使用及错误处理。
  3. 网络字节序转换的必要性和方法。
  4. 基本的程序编译、运行和调试技巧。

后续可尝试扩展功能(如多客户端并发处理、数据加密传输),或结合 select 函数实现 IO 多路复用,提升程序性能。网络编程的核心在于理论与实践结合,多写代码、多调试,逐步积累经验。

1. 字节序转换:必须使用 htons/htonl/ntohs/ntohl

  • 原因:不同主机可能采用小端(x86)或大端(ARM)字节序,网络协议规定使用大端(网络字节序)。
  • 错误示例:直接赋值端口号 server_addr.sin_port = 8888;(未用 htons 转换,导致端口错误)。
  • 正确做法server_addr.sin_port = htons(8888);

2. 端口冲突:绑定前检查端口是否被占用

  • 检查命令netstat -tunlp | grep 端口号(查看端口占用情况)。
  • 解决方案:更换端口号,或确保上次运行的程序已正确关闭(避免 TIME_WAIT 状态残留)。

3. IP 地址转换:inet_addr 与 inet_pton 的区别

  • inet_addr:将点分十进制字符串转换为网络字节序(IPv4 专用,过时函数,建议用 inet_pton)。
  • inet_pton:支持 IPv4 和 IPv6,返回值更安全(成功返回 1,无效地址返回 0,错误返回 -1)。

4. 缓冲区溢出:固定缓冲区大小需谨慎

  • 风险:接收数据时未限制长度可能导致缓冲区溢出(如 recv(client_fd, buffer, sizeof(buffer), 0); 是安全的,而 recv(client_fd, buffer, 1024, 0); 若缓冲区不足 1024 字节则危险)。
  • 最佳实践:缓冲区大小固定为已知值,或使用动态内存分配(如 malloc)。

八、拓展学习:从入门到进阶

1. 必学工具

  • Wireshark:网络抓包工具,分析 TCP 三次握手、UDP 数据报格式。
  • netstat / ss:查看网络连接、端口状态(如 netstat -an 显示所有连接)。
  • telnet / nc:测试端口连通性(如 telnet 127.0.0.1 8888 检查服务器是否运行)。

2. 进阶知识点

  • HTTP 协议解析:基于 TCP 实现简单 Web 服务器(处理 GET/POST 请求)。
  • 多线程 / 多进程服务器:使用 pthread 或 fork 处理并发连接(解决 select 处理海量连接的性能瓶颈)。
  • IPv6 支持:修改地址结构体为 sockaddr_in6,协议族用 AF_INET6,实现跨 IPv4/IPv6 的兼容性。

3. 学习资源

  • 《UNIX 网络编程》:经典教材,深入理解套接字编程与协议细节。
  • Linux 官方文档man 2 socket 查看系统调用手册,man 7 ip 了解 IP 协议细节。

总结

Linux 网络编程是实现跨主机通信的核心技术,从基础的 TCP/UDP 套接字编程,到处理并发的 select/fcntl 高级技巧,需要逐步实践和调试。初学者应先掌握 TCP 服务器 / 客户端的基本流程,理解字节序、地址绑定等核心概念,再通过实战项目(如简易聊天室、文件传输工具)巩固知识。记住,网络编程的关键在于理解协议原理处理边界条件(如连接中断、数据丢失),多写代码、多抓包分析,才能真正掌握这门技术。


网站公告

今日签到

点亮在社区的每一天
去签到