当前位置: 首页 > news >正文

Linux 入门十一:Linux 网络编程

一、概述

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 1024int main() {// 1. 创建 socketint 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. 关闭 socketclose(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 1024int main() {// 1. 创建 socketint 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. 关闭 socketclose(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:如何选择?

场景UDPTCP
实时性要求高(如视频通话、直播)低(需连接建立,延迟较高)
数据可靠性不保证(需应用层处理)保证(自动重传、排序)
数据量小数据报(如 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 服务器 / 客户端的基本流程,理解字节序、地址绑定等核心概念,再通过实战项目(如简易聊天室、文件传输工具)巩固知识。记住,网络编程的关键在于理解协议原理处理边界条件(如连接中断、数据丢失),多写代码、多抓包分析,才能真正掌握这门技术。

相关文章:

Linux 入门十一:Linux 网络编程

一、概述 1. 网络编程基础 网络编程是通过网络应用编程接口&#xff08;API&#xff09;编写程序&#xff0c;实现不同主机上进程间的信息交互。它解决的核心问题是&#xff1a;如何让不同主机上的程序进行通信。 2. 网络模型&#xff1a;从 OSI 到 TCP/IP OSI 七层模型&…...

车载软件架构 --- 二级boot设计说明需求规范

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 周末洗了一个澡,换了一身衣服,出了门却不知道去哪儿,不知道去找谁,漫无目的走着,大概这就是成年人最深的孤独吧! 旧人不知我近况,新人不知我过…...

在Ubuntu下用Chrony做主从机时间同步

主机 下载chrony sudo apt install chrony修改配置文件&#xff1a; sudo gedit /etc/chrony/chrony.conf# Welcome to the chrony configuration file. See chrony.conf(5) for more # information about usuable directives.# This will use (up to): # - 4 sources fro…...

开箱即用:一款带世界时钟简约好用在线时间戳转换工具源码

这款工具简直是为“时间管理大师”和“国际化玩家”量身定制!它不仅支持全球十大热门语言,还能无缝切换多时区,帮你轻松搞定时间戳和日期的转换。重点是,它完全前端实现,无需复杂后端,部署起来比泡杯咖啡还简单!开发人员可以在本地电脑运行来进行时间戳装换,还可以加Ad…...

代码随想录第22天:回溯算法4

一、全排列&#xff08;Leetcode 46&#xff09; 与组合问题不同&#xff0c;排列问题要注意2个特点&#xff1a; 每层都是从0开始搜索而不是startIndex需要used数组记录path里都放了哪些元素 class Solution:def permute(self, nums):result [] # 存储所有的排列self.back…...

cdq 系列 题解

从二维数点&#xff08;二维偏序&#xff09;到三维偏序。 用 cdq 分治可以解决二维数点问题。 1.洛谷 P1908 逆序对 题意 求所有数对 ( i , j ) (i,j) (i,j) 的个数&#xff0c;满足 i < j i<j i<j 且 a i > a j a_i>a_j ai​>aj​。 1 ≤ n ≤ 5 1…...

稳压二极管详解:原理、作用、应用与选型要点

一、稳压二极管的基本定义 稳压二极管&#xff08;齐纳二极管&#xff0c;Zener Diode&#xff09; 是一种利用反向击穿特性实现电压稳定的半导体器件。其核心特性是&#xff1a;在反向击穿时&#xff0c;两端电压几乎恒定&#xff08;Vz&#xff09;&#xff0c;且不会因电流…...

如何在量子计算时代保障 Sui 的安全性

量子计算的出现对依赖加密机制的系统构成了重大威胁。区块链依赖加密技术来进行身份管理、安全交易和数据完整性保护&#xff0c;而量子计算具备打破传统加密模型的能力&#xff0c;因此区块链面临特别严峻的挑战。 然而&#xff0c;Sui 天生具备“加密灵活性”&#xff0c;可…...

linux sysfs使用cat无显示的原因:返回值未赋值

在Linux驱动中通过sysfs定义的文件使用cat命令无显示&#xff0c;通常由以下原因导致&#xff1a; 1. show函数未正确实现 原因&#xff1a;show函数&#xff08;如show_status&#xff09;未正确填充缓冲区或返回有效字节数。 排查&#xff1a; // 错误示例&#xff1a;未写…...

Discuz论坛网站忘记管理员密码进不去管理中心怎么办?怎么改管理员密码?

Discuz论坛网站忘记管理员密码进不去管理中心怎么办&#xff1f;怎么改管理员密码&#xff1f;今天驰网飞飞和你分享 首先我们需要用到Discuz&#xff01;急诊箱tools.php这个文件&#xff0c;可在下载中心搜索关键词下载&#xff0c;下载好后将tools.php文件放到网站根目录&a…...

基于LangChain与Neo4j构建企业关系图谱的金融风控实施方案,结合工商数据、供应链记录及舆情数据,实现隐性关联识别与动态风险评估

以下是基于LangChain与Neo4j构建企业关系图谱的金融风控实施方案&#xff0c;结合工商数据、供应链记录及舆情数据&#xff0c;实现隐性关联识别与动态风险评估&#xff1a; 一、数据整合与图谱构建 多源数据融合与清洗 • 数据源&#xff1a;整合企业工商数据&#xff08;股权…...

数据结构第六章(五)-拓扑排序、关键路径

数据结构第六章&#xff08;五&#xff09; 图的应用&#xff08;二&#xff09;一、有向无环图二、拓扑排序1. AOV网2. 拓扑排序3. 逆拓扑排序 三、关键路径1.AOE网2.关键路径2.1 介绍2.2 关键路径的求法 总结 图的应用&#xff08;二&#xff09; 一、有向无环图 首先我们得…...

stc32单片机实现串口2M波特率满带宽传输

我需要实现已极高的速度用串口往上位机发送数据, 并且还不能占用mcu资源, 使用的单片机位stc32g8K64 我的方法是串口接收采用中断接收, 发送采用dma自动发送, 预先初始化16个64字节的缓冲区, 每次通过串口发送时, 先找到当前的空闲缓冲区, 然后往缓冲区里填充数据, 在dma传输完…...

uni-app 状态管理深度解析:Vuex 与全局方案实战指南

uni-app 状态管理深度解析&#xff1a;Vuex 与全局方案实战指南 一、Vuex 使用示例 1. 基础 Vuex 配置 1.1 项目结构 src/ ├── store/ │ ├── index.js # 主入口文件 │ └── modules/ │ └── counter.js # 计数器模块 └── main.js …...

STM32之DHT11温湿度传感器---附代码

DHT11简介 DHT11的供电电压为 3&#xff0d;5.5V。 传感器上电后&#xff0c;要等待 1s 以越过不稳定状态在此期间无需发送任何指令。 电源引脚&#xff08;VDD&#xff0c;GND&#xff09;之间可增加一个100nF 的电容&#xff0c;用以去耦滤波。 DATA 用于微处理器与DHT11之间…...

Fluent 内置双向流固耦合FSI 液舱晃荡仿真计算

本案例利用Fluent 内置双向流固耦合FSI对液舱晃荡仿真展开了计算&#xff0c;提供了一种更为便捷快速的分析方法&#xff0c;对不同杨氏模量的液舱内部构件进行分析&#xff0c;后续可以通过该案例对不同的双向流固耦合模型展开计算分析。 1 SCDM 设置 1.1 导入几何 本案例根…...

嵌入式开发板调试方式完全指南:串口/SSH/Telnet及其他方式对比

文章目录 &#x1f4bb;嵌入式开发板调试方式完全指南&#xff1a;串口/SSH/Telnet及其他方式对比一、为什么需要连接嵌入式开发板❓二、串口调试&#xff1a;最古老的调试方式仍在发光&#x1f3db;️2.1 什么是串口调试&#xff1f; 三、SSH/Telnet&#xff1a;网络时代的调试…...

JavaScript数据结构与算法实战: 探秘Leetcode经典题目

# JavaScript数据结构与算法实战: 探秘Leetcode经典题目 第一章&#xff1a;掌握LeetCode经典题目 什么是LeetCode&#xff1f; 力扣&#xff09;是一个专门为程序员提供算法题目练习的平台&#xff0c;涵盖了广泛的题目类型&#xff0c;包括数据结构、算法、数据库等多个领域。…...

内网穿透实践:cpolar快速入门教程

最近有个朋友联系我&#xff0c;问我有没有方法将自己做的项目让别人也能访问到&#xff0c;我寻思这不就是外网映射的事情。于是我很愉快的和他说&#xff0c;你去买个云服务器就行&#xff0c;尽管我一再和他说&#xff0c;个人新用户能有免费试用期&#xff0c;但是本着又蠢…...

HAL库(STM32CubeMX)——高级ADC学习、HRTIM(STM32G474RBT6)

系列文章目录 文章目录 系列文章目录前言存在的问题HRTIMcubemx配置前言 对cubemx的ADC的设置进行补充 ADCs_Common_Settings Mode:ADC 模式 Independent mod 独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选 ADC_Se…...

Kafka 详细解读

1. Producer&#xff08;生产部卷王&#xff09; 职责&#xff1a;往 Kafka 里疯狂输出数据&#xff0c;KPI 是「日抛式消息海啸」 职场人设&#xff1a; 白天开会画饼&#xff0c;深夜写周报的奋斗逼&#xff0c;口头禅是「这个需求今晚必须上线&#xff01;」代码里的「福报…...

Python爬虫实战:获取高考网专业数据并分析,为志愿填报做参考

一、引言 高考志愿填报是考生人生的关键节点,合理的志愿填报能为其未来发展奠定良好基础。计算机类专业作为当下热门领域,相关信息对考生填报志愿至关重要。教育在线网站虽提供丰富的计算机类专业数据,但存在反爬机制,增加了数据获取难度。本研究借助 Scrapy 爬虫技术及多…...

Ubuntu下展锐刷机工具spd_dump使用说明

spd_dump使用说明 源码地址&#xff1a;https://github.com/ilyakurdyukov/spreadtrum_flash 编译环境准备&#xff1a; sudo apt update sudo apt install git sudo apt install build-essential sudo apt install libusb-1.0-0-devIf you create /etc/udev/rules.d/80-spd…...

配置 VS Code 使用 ESLint 格式化

1、在设置里面搜索Default Formatter&#xff0c;下拉框里选择eslint 2、并勾选Enables ESlint as a formatter 3、再在settings.json文件中添加配置代码&#xff0c;如下所示&#xff1a; 1&#xff09; 、打开 VS Code 设置 快捷键&#xff1a;Ctrl ,&#xff08;Mac: ⌘ ,…...

极刻云搜-专业的软件网址搜索引擎

软件名&#xff1a;极刻云搜 版本&#xff1a;v1.0 软件功能&#xff1a;搜索实用软件和网址 之前有个全网爆火的软件叫搜软 但是它满屏广告而且很久都没更新了 我看也有好多人在求这门类似的软件 我就按照它扒了一个一模一样的 软件丑是丑了点 但是这个功能确实简单粗暴 因为用…...

android Stagefright框架

作为Android音视频开发人员&#xff0c;学习Stagefright框架需要结合理论、源码分析和实践验证。以下是系统化的学习路径&#xff1a; 1. 基础准备 熟悉Android多媒体体系 掌握MediaPlayer、MediaCodec、MediaExtractor等核心API的用法。 理解Android的OpenMAX IL&#xff08…...

vscode 打开新页签

目录 vscode 打开新页签 完整settings.json内容&#xff1a; vscode 打开新页签 .vscode目录中 新建settings.json 在 settings.json 文件中&#xff0c;添加或修改以下行&#xff1a; json "workbench.editor.enablePreview": false 这将禁用预览模式&#xff0…...

【C++编程入门】:从零开始掌握基础语法

C语言是通过对C语言不足的地方进行优化创建的&#xff0c;C在C语言之上&#xff0c;C当然也兼容C语言&#xff0c; 在大部分地方使用C比C更方便&#xff0c;可能使用C需要一两百行代码&#xff0c;而C只需要五六十行。 目录 C关键字 命名空间 缺省参数 缺省参数分类 函数…...

Vue中如何优雅地阻止特定标签的移除并恢复其原始位置

Vue中如何优雅地阻止特定标签的移除并恢复其原始位置 在使用 Element Plus 或 Element UI 的 <el-select> 组件时,有时我们希望根据某些条件阻止用户移除特定的标签,并且在阻止移除后将该标签重新添加到其原始位置。这在处理与子项目关联的成员时特别有用。本文将详细…...

基于Arduino的ESP8266连接OneNET云平台(MQTT协议 物模型)(二)连接云平台

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、前期准备1.1 硬件配置1.2 软件环境配置 二、接线三、核心代码3.1 总代码 三、最终效果总结 前言 本系列将以0基础新手视角&#xff0c;完整演示ArduinoESP…...

空间注意力和通道注意力的区别

空间注意力和通道注意力是深度学习中两种常见的注意力机制。 1. ​​关注维度不同​​ ​​通道注意力​​&#xff08;Channel Attention&#xff09; 对特征图的每个通道分配不同的权重&#xff0c;强调“哪些通道更重要”。例如&#xff0c;在RGB图像中&#xff0c;可能红色…...

git 版本提交规范

Git 提交规范&#xff08;Git Commit Message Convention&#xff09;是为了让项目的提交历史更加清晰、可读、便于追踪和自动化工具解析。常见的规范之一是 Conventional Commits&#xff0c;下面是一个推荐的格式规范&#xff1a; &#x1f31f; 提交信息格式&#xff08;Con…...

Linux的基础的操作指令

一.目录文件&#xff1a;在Linux中的以d开头的文件&#xff0c;就叫做目录文件(Directory): 二.普通的文件&#xff1a;在Linux中的以-r开头的文件&#xff0c;就叫做普通的文件,他们通常以.txt .cpp .c为后缀: 三.pwd:查看当前目录的绝对路径:查看当前所在位置的目录的绝对路径…...

级联vs端到端、全双工、轮次检测、方言语种、商业模式…语音 AI 开发者都在关心什么?丨Voice Agent 学习笔记

编者按&#xff1a; A16Z在《AI Voice Agents: 2025 Update》中提到&#xff1a; 语音是 AI 应用公司最强大的突破之一。 它是人类沟通中最频繁&#xff08;也是信息密度最高的&#xff09;形式&#xff0c;AI 也让其首次变得“可编程”。 在13期Z沙龙&#xff0c;我们聚焦AI…...

使用Cloudflare加速网站的具体操作步骤

要通过Cloudflare加速网站&#xff0c;您需要按照以下步骤进行设置和配置。这些步骤包括域名设置、接入Cloudflare、配置缓存和其他设置&#xff0c;以及测试网站性能。 1. 注册Cloudflare账户 访问Cloudflare官网&#xff1a;前往 Cloudflare官网。创建账户&#xff1a;点击…...

编译原理实验(四)———— LR(1)分析法

一、实验目的 掌握LR(1)分析法的基本原理与实现流程。通过构造LR(1)分析表&#xff0c;验证符号串是否符合给定文法规则。理解LR(1)分析中向前搜索符&#xff08;Lookahead Symbol&#xff09;的作用&#xff0c;解决移进-归约冲突。 二、实验题目 1.对下列文法&#xff0c;用…...

【Redis】Jedis与Jedis连接池

目录 1. Jedis 单实例连接 2. Jedis 连接池&#xff08;JedisPool&#xff09; 3. JedisPool 与 Jedis 的区别 4. JedisPool 线程安全 6. 使用 JedisPool 的注意事项 1. Jedis 单实例连接 在最简单的用法中&#xff0c;Jedis 提供了直接与 Redis 服务器连接的方式。这适合…...

MongoDB数据库的安装到入门使用详细讲解

本篇文章主要讲解MongoDB的安装使用教程及基础的数据库管理和操作能力的讲解,通过本篇文章您可以快速的掌握对MongDB数据库的基本认识及,基础开发能力。 一、MongoDB介绍 MongoDB是一款免费开源的非关系型数据库,该数据库适应于复杂关系的存储和管理,非常适合数据结构复杂…...

Argo CD

文章目录 一、什么是 Argo CD二、为什么选择 Argo CD三、Argo CD 架构1、API服务器2、存储库服务器3、应用程序控制器 四、Argo CD 的使用1、要求2、安装 Argo CD2.1、创建 argocd 命名空间2.2、部署 Argo CD2.3、验证部署是否成功 3、下载 Argo CD CLI4、发布 Argo CD 服务器5…...

ElementUI中checkbox v-model绑定值为布尔、字符串或数字类型

这篇博客介绍了在Vue.js中使用El-Checkbox组件时&#xff0c;如何设置和处理v-model的布尔值和类型转换。通过示例代码展示了如何设置true-label和false-label属性来改变选中状态的值&#xff0c;适用于需要特定类型&#xff08;如字符串或整数&#xff09;的场景。v-model不能…...

Wasm Client SDK线上优化

前言 随着 WebAssembly&#xff08;Wasm&#xff09;在前端开发中的普及&#xff0c;越来越多的开源项目开始在浏览器端提供高性能的逻辑处理方案。OpenIM Wasm SDK 便是其中的代表&#xff1a;通过将 Go 语言编写的 OpenIMSDK 核心编译为 .wasm 文件&#xff0c;在前端即可完成…...

Spring(第一章)

一&#xff0c;Spring介绍 什么是Spring 1. 轻量级&#xff1a;Spring 是非侵入性的 - 基于 Spring 开发的应用中的对象可以不依赖于 Spring 的 API 2. 依赖注入(DI --- dependency injection、IOC) 3. 面向切面编程(AOP --- aspect oriented programming) 4. 容器: Spring 是…...

蓝桥杯 18.分考场

分考场 原题目链接 题目描述 有 n 个人参加某项特殊考试。 为了公平&#xff0c;要求任何两个认识的人不能分在同一个考场。 你的任务是求出最少需要分几个考场才能满足这个条件。 输入描述 第一行&#xff1a;一个整数 n&#xff0c;表示参加考试的人数&#xff08;1 ≤…...

大文件分片上传进阶版(新增md5校验、上传进度展示、并行控制,智能分片、加密上传、断点续传、自动重试),实现四位一体的网络感知型大文件传输系统‌

上篇文章我们总结了大文件分片上传的主要核心&#xff0c;但是我对md5校验和上传进度展示这块也比较感兴趣&#xff0c;所以在deepseek的帮助下&#xff0c;扩展了一下我们的代码&#xff0c;如果有任何问题和想法&#xff0c;非常欢迎大家在评论区与我交流&#xff0c;我需要学…...

【项目管理】成本类计算 笔记

项目管理-相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 &#xff08;一&#xff09;知识总览 项目管理知识域 知识点&#xff1a; &#xff08;项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域&#xff09; 对应&…...

Redis 事务

事务介绍 Redis 事务和 MySQL 事务在概念上类似的 把一些列的操作绑定成一组&#xff0c;让这一组能够批量执行 MySQL 事务 原子性&#xff1a;把多个操作打包成一个整体 一致性&#xff1a;事务执行前后数据合理 持久性&#xff1a;事务做出的操作都会修改硬盘 隔离性&#…...

JavaScript day5

立即执行函数 <script>(function(){ console.log//函数不需调用&#xff0c;立马执行 })() </script> //另外写法 <script> (function(){}()) </script> 常见的内置对象 Math console.dir()——打印对象的 使用Math中的属性——console.log(Math.…...

辛格迪客户案例 | 浙江高跖医药委托生产质量管理协同(OWL MAH)项目

一、案例概述 浙江高跖医药科技股份有限公司是一家集“研、产、销”为一体的专业化药品持证企业。高跖医药自成立之初就建立并运行着一套相对完善的质量管理体系&#xff0c;涵盖了药品的研发、生产监管及销售。高跖医药于2022年选择实施了辛格迪的“委托生产质量管理协同解决…...

科学养生指南:解锁健康生活新方式

在快节奏的现代生活中&#xff0c;健康养生已成为人们关注的焦点。科学合理的养生方式&#xff0c;能帮助我们增强体质、预防疾病&#xff0c;享受更优质的生活。​ 饮食是健康养生的基石。遵循 “均衡饮食” 原则&#xff0c;每日饮食需包含谷类、蔬菜水果、优质蛋白质和健康…...

FreeRTos学习记录--2.内存管理

后续的章节涉及这些内核对象&#xff1a;task、queue、semaphores和event group等。为了让FreeRTOS更容易使用&#xff0c;这些内核对象一般都是动态分配&#xff1a;用到时分配&#xff0c;不使用时释放。使用内存的动态管理功能&#xff0c;简化了程序设计&#xff1a;不再需…...