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
(十六进制)
- IPv4 示例:
- 查看本机 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)。
-
类型:
- 流式套接字(SOCK_STREAM):基于 TCP,可靠、面向连接(如打电话,需先接通)。
- 数据报套接字(SOCK_DGRAM):基于 UDP,无连接、不可靠(如发短信,无需确认对方是否在线)。
- 原始套接字(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)三次握手建立连接(确保双方 “准备就绪”)
- 客户端发起 SYN 同步请求:客户端向服务器发送带有 SYN 标志的数据包,请求建立连接,同时携带初始序列号(如
seq=100
)。 - 服务器回复 SYN+ACK 确认:服务器收到后,返回 SYN+ACK 包,其中 SYN 标志表示同意连接,ACK 标志确认客户端序列号(
ack=101
),并携带自己的初始序列号(如seq=200
)。 - 客户端回复 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 四次挥手:
- 客户端或服务器调用
close
,发送 FIN 包请求断开。 - 对方回复 ACK 确认,进入半关闭状态(仍可接收数据)。
- 对方处理完剩余数据后,发送 FIN 包。
- 最初关闭方回复 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_INET , type=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:如何选择?
场景 | 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 函数(让操作 “不等不靠”)
核心作用
将套接字设置为 非阻塞模式,使 recv
、accept
等函数在无数据时立即返回(而非阻塞等待),配合轮询或事件驱动,实现单线程处理多任务。
典型场景:
- 客户端需要同时发送数据和接收服务器回复(如聊天程序边输入边接收消息)。
- 服务器处理大量并发连接,避免单个慢连接阻塞整个程序。
函数原型与参数解析
#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
:使recv
、send
、accept
等操作在无数据或不可写时立即返回,错误码通常为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
)
- 原因:端口已被其他程序占用(如之前运行的服务器未正确关闭)。
- 解决:
- 通过
netstat -tunlp | grep 8888
查看占用进程。 - 杀死进程:
kill -9 <进程号>
。 - 更换端口(修改代码中的
PORT
为 8889 等未被占用的端口)。
- 通过
2. 连接被拒绝(connect: Connection refused
)
- 原因:服务器未启动,或防火墙阻止端口。
- 解决:
- 确保服务器已运行(在终端 1 先启动服务器)。
- 关闭防火墙(测试环境):
systemctl stop firewalld
。
3. 数据接收不完整(粘包问题)
- 原因:TCP 流式传输无边界,多次发送的数据可能被合并接收。
- 解决:在应用层添加协议头(如先发送 4 字节表示数据长度,再发送实际内容),后续章节将详细讲解。
总结:从代码到实战的核心收获
通过本实战案例,你将掌握:
- TCP 服务器与客户端的完整开发流程。
- 关键函数的正确使用及错误处理。
- 网络字节序转换的必要性和方法。
- 基本的程序编译、运行和调试技巧。
后续可尝试扩展功能(如多客户端并发处理、数据加密传输),或结合 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. 网络编程基础 网络编程是通过网络应用编程接口(API)编写程序,实现不同主机上进程间的信息交互。它解决的核心问题是:如何让不同主机上的程序进行通信。 2. 网络模型:从 OSI 到 TCP/IP OSI 七层模型&…...
车载软件架构 --- 二级boot设计说明需求规范
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 周末洗了一个澡,换了一身衣服,出了门却不知道去哪儿,不知道去找谁,漫无目的走着,大概这就是成年人最深的孤独吧! 旧人不知我近况,新人不知我过…...
在Ubuntu下用Chrony做主从机时间同步
主机 下载chrony sudo apt install chrony修改配置文件: 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
一、全排列(Leetcode 46) 与组合问题不同,排列问题要注意2个特点: 每层都是从0开始搜索而不是startIndex需要used数组记录path里都放了哪些元素 class Solution:def permute(self, nums):result [] # 存储所有的排列self.back…...
cdq 系列 题解
从二维数点(二维偏序)到三维偏序。 用 cdq 分治可以解决二维数点问题。 1.洛谷 P1908 逆序对 题意 求所有数对 ( i , j ) (i,j) (i,j) 的个数,满足 i < j i<j i<j 且 a i > a j a_i>a_j ai>aj。 1 ≤ n ≤ 5 1…...
稳压二极管详解:原理、作用、应用与选型要点
一、稳压二极管的基本定义 稳压二极管(齐纳二极管,Zener Diode) 是一种利用反向击穿特性实现电压稳定的半导体器件。其核心特性是:在反向击穿时,两端电压几乎恒定(Vz),且不会因电流…...
如何在量子计算时代保障 Sui 的安全性
量子计算的出现对依赖加密机制的系统构成了重大威胁。区块链依赖加密技术来进行身份管理、安全交易和数据完整性保护,而量子计算具备打破传统加密模型的能力,因此区块链面临特别严峻的挑战。 然而,Sui 天生具备“加密灵活性”,可…...
linux sysfs使用cat无显示的原因:返回值未赋值
在Linux驱动中通过sysfs定义的文件使用cat命令无显示,通常由以下原因导致: 1. show函数未正确实现 原因:show函数(如show_status)未正确填充缓冲区或返回有效字节数。 排查: // 错误示例:未写…...
Discuz论坛网站忘记管理员密码进不去管理中心怎么办?怎么改管理员密码?
Discuz论坛网站忘记管理员密码进不去管理中心怎么办?怎么改管理员密码?今天驰网飞飞和你分享 首先我们需要用到Discuz!急诊箱tools.php这个文件,可在下载中心搜索关键词下载,下载好后将tools.php文件放到网站根目录&a…...
基于LangChain与Neo4j构建企业关系图谱的金融风控实施方案,结合工商数据、供应链记录及舆情数据,实现隐性关联识别与动态风险评估
以下是基于LangChain与Neo4j构建企业关系图谱的金融风控实施方案,结合工商数据、供应链记录及舆情数据,实现隐性关联识别与动态风险评估: 一、数据整合与图谱构建 多源数据融合与清洗 • 数据源:整合企业工商数据(股权…...
数据结构第六章(五)-拓扑排序、关键路径
数据结构第六章(五) 图的应用(二)一、有向无环图二、拓扑排序1. AOV网2. 拓扑排序3. 逆拓扑排序 三、关键路径1.AOE网2.关键路径2.1 介绍2.2 关键路径的求法 总结 图的应用(二) 一、有向无环图 首先我们得…...
stc32单片机实现串口2M波特率满带宽传输
我需要实现已极高的速度用串口往上位机发送数据, 并且还不能占用mcu资源, 使用的单片机位stc32g8K64 我的方法是串口接收采用中断接收, 发送采用dma自动发送, 预先初始化16个64字节的缓冲区, 每次通过串口发送时, 先找到当前的空闲缓冲区, 然后往缓冲区里填充数据, 在dma传输完…...
uni-app 状态管理深度解析:Vuex 与全局方案实战指南
uni-app 状态管理深度解析:Vuex 与全局方案实战指南 一、Vuex 使用示例 1. 基础 Vuex 配置 1.1 项目结构 src/ ├── store/ │ ├── index.js # 主入口文件 │ └── modules/ │ └── counter.js # 计数器模块 └── main.js …...
STM32之DHT11温湿度传感器---附代码
DHT11简介 DHT11的供电电压为 3-5.5V。 传感器上电后,要等待 1s 以越过不稳定状态在此期间无需发送任何指令。 电源引脚(VDD,GND)之间可增加一个100nF 的电容,用以去耦滤波。 DATA 用于微处理器与DHT11之间…...
Fluent 内置双向流固耦合FSI 液舱晃荡仿真计算
本案例利用Fluent 内置双向流固耦合FSI对液舱晃荡仿真展开了计算,提供了一种更为便捷快速的分析方法,对不同杨氏模量的液舱内部构件进行分析,后续可以通过该案例对不同的双向流固耦合模型展开计算分析。 1 SCDM 设置 1.1 导入几何 本案例根…...
嵌入式开发板调试方式完全指南:串口/SSH/Telnet及其他方式对比
文章目录 💻嵌入式开发板调试方式完全指南:串口/SSH/Telnet及其他方式对比一、为什么需要连接嵌入式开发板❓二、串口调试:最古老的调试方式仍在发光🏛️2.1 什么是串口调试? 三、SSH/Telnet:网络时代的调试…...
JavaScript数据结构与算法实战: 探秘Leetcode经典题目
# JavaScript数据结构与算法实战: 探秘Leetcode经典题目 第一章:掌握LeetCode经典题目 什么是LeetCode? 力扣)是一个专门为程序员提供算法题目练习的平台,涵盖了广泛的题目类型,包括数据结构、算法、数据库等多个领域。…...
内网穿透实践:cpolar快速入门教程
最近有个朋友联系我,问我有没有方法将自己做的项目让别人也能访问到,我寻思这不就是外网映射的事情。于是我很愉快的和他说,你去买个云服务器就行,尽管我一再和他说,个人新用户能有免费试用期,但是本着又蠢…...
HAL库(STM32CubeMX)——高级ADC学习、HRTIM(STM32G474RBT6)
系列文章目录 文章目录 系列文章目录前言存在的问题HRTIMcubemx配置前言 对cubemx的ADC的设置进行补充 ADCs_Common_Settings Mode:ADC 模式 Independent mod 独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选 ADC_Se…...
Kafka 详细解读
1. Producer(生产部卷王) 职责:往 Kafka 里疯狂输出数据,KPI 是「日抛式消息海啸」 职场人设: 白天开会画饼,深夜写周报的奋斗逼,口头禅是「这个需求今晚必须上线!」代码里的「福报…...
Python爬虫实战:获取高考网专业数据并分析,为志愿填报做参考
一、引言 高考志愿填报是考生人生的关键节点,合理的志愿填报能为其未来发展奠定良好基础。计算机类专业作为当下热门领域,相关信息对考生填报志愿至关重要。教育在线网站虽提供丰富的计算机类专业数据,但存在反爬机制,增加了数据获取难度。本研究借助 Scrapy 爬虫技术及多…...
Ubuntu下展锐刷机工具spd_dump使用说明
spd_dump使用说明 源码地址:https://github.com/ilyakurdyukov/spreadtrum_flash 编译环境准备: 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,下拉框里选择eslint 2、并勾选Enables ESlint as a formatter 3、再在settings.json文件中添加配置代码,如下所示: 1) 、打开 VS Code 设置 快捷键:Ctrl ,(Mac: ⌘ ,…...
极刻云搜-专业的软件网址搜索引擎
软件名:极刻云搜 版本:v1.0 软件功能:搜索实用软件和网址 之前有个全网爆火的软件叫搜软 但是它满屏广告而且很久都没更新了 我看也有好多人在求这门类似的软件 我就按照它扒了一个一模一样的 软件丑是丑了点 但是这个功能确实简单粗暴 因为用…...
android Stagefright框架
作为Android音视频开发人员,学习Stagefright框架需要结合理论、源码分析和实践验证。以下是系统化的学习路径: 1. 基础准备 熟悉Android多媒体体系 掌握MediaPlayer、MediaCodec、MediaExtractor等核心API的用法。 理解Android的OpenMAX IL(…...
vscode 打开新页签
目录 vscode 打开新页签 完整settings.json内容: vscode 打开新页签 .vscode目录中 新建settings.json 在 settings.json 文件中,添加或修改以下行: json "workbench.editor.enablePreview": false 这将禁用预览模式࿰…...
【C++编程入门】:从零开始掌握基础语法
C语言是通过对C语言不足的地方进行优化创建的,C在C语言之上,C当然也兼容C语言, 在大部分地方使用C比C更方便,可能使用C需要一两百行代码,而C只需要五六十行。 目录 C关键字 命名空间 缺省参数 缺省参数分类 函数…...
Vue中如何优雅地阻止特定标签的移除并恢复其原始位置
Vue中如何优雅地阻止特定标签的移除并恢复其原始位置 在使用 Element Plus 或 Element UI 的 <el-select> 组件时,有时我们希望根据某些条件阻止用户移除特定的标签,并且在阻止移除后将该标签重新添加到其原始位置。这在处理与子项目关联的成员时特别有用。本文将详细…...
基于Arduino的ESP8266连接OneNET云平台(MQTT协议 物模型)(二)连接云平台
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、前期准备1.1 硬件配置1.2 软件环境配置 二、接线三、核心代码3.1 总代码 三、最终效果总结 前言 本系列将以0基础新手视角,完整演示ArduinoESP…...
空间注意力和通道注意力的区别
空间注意力和通道注意力是深度学习中两种常见的注意力机制。 1. 关注维度不同 通道注意力(Channel Attention) 对特征图的每个通道分配不同的权重,强调“哪些通道更重要”。例如,在RGB图像中,可能红色…...
git 版本提交规范
Git 提交规范(Git Commit Message Convention)是为了让项目的提交历史更加清晰、可读、便于追踪和自动化工具解析。常见的规范之一是 Conventional Commits,下面是一个推荐的格式规范: 🌟 提交信息格式(Con…...
Linux的基础的操作指令
一.目录文件:在Linux中的以d开头的文件,就叫做目录文件(Directory): 二.普通的文件:在Linux中的以-r开头的文件,就叫做普通的文件,他们通常以.txt .cpp .c为后缀: 三.pwd:查看当前目录的绝对路径:查看当前所在位置的目录的绝对路径…...
级联vs端到端、全双工、轮次检测、方言语种、商业模式…语音 AI 开发者都在关心什么?丨Voice Agent 学习笔记
编者按: A16Z在《AI Voice Agents: 2025 Update》中提到: 语音是 AI 应用公司最强大的突破之一。 它是人类沟通中最频繁(也是信息密度最高的)形式,AI 也让其首次变得“可编程”。 在13期Z沙龙,我们聚焦AI…...
使用Cloudflare加速网站的具体操作步骤
要通过Cloudflare加速网站,您需要按照以下步骤进行设置和配置。这些步骤包括域名设置、接入Cloudflare、配置缓存和其他设置,以及测试网站性能。 1. 注册Cloudflare账户 访问Cloudflare官网:前往 Cloudflare官网。创建账户:点击…...
编译原理实验(四)———— LR(1)分析法
一、实验目的 掌握LR(1)分析法的基本原理与实现流程。通过构造LR(1)分析表,验证符号串是否符合给定文法规则。理解LR(1)分析中向前搜索符(Lookahead Symbol)的作用,解决移进-归约冲突。 二、实验题目 1.对下列文法,用…...
【Redis】Jedis与Jedis连接池
目录 1. Jedis 单实例连接 2. Jedis 连接池(JedisPool) 3. JedisPool 与 Jedis 的区别 4. JedisPool 线程安全 6. 使用 JedisPool 的注意事项 1. Jedis 单实例连接 在最简单的用法中,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组件时,如何设置和处理v-model的布尔值和类型转换。通过示例代码展示了如何设置true-label和false-label属性来改变选中状态的值,适用于需要特定类型(如字符串或整数)的场景。v-model不能…...
Wasm Client SDK线上优化
前言 随着 WebAssembly(Wasm)在前端开发中的普及,越来越多的开源项目开始在浏览器端提供高性能的逻辑处理方案。OpenIM Wasm SDK 便是其中的代表:通过将 Go 语言编写的 OpenIMSDK 核心编译为 .wasm 文件,在前端即可完成…...
Spring(第一章)
一,Spring介绍 什么是Spring 1. 轻量级:Spring 是非侵入性的 - 基于 Spring 开发的应用中的对象可以不依赖于 Spring 的 API 2. 依赖注入(DI --- dependency injection、IOC) 3. 面向切面编程(AOP --- aspect oriented programming) 4. 容器: Spring 是…...
蓝桥杯 18.分考场
分考场 原题目链接 题目描述 有 n 个人参加某项特殊考试。 为了公平,要求任何两个认识的人不能分在同一个考场。 你的任务是求出最少需要分几个考场才能满足这个条件。 输入描述 第一行:一个整数 n,表示参加考试的人数(1 ≤…...
大文件分片上传进阶版(新增md5校验、上传进度展示、并行控制,智能分片、加密上传、断点续传、自动重试),实现四位一体的网络感知型大文件传输系统
上篇文章我们总结了大文件分片上传的主要核心,但是我对md5校验和上传进度展示这块也比较感兴趣,所以在deepseek的帮助下,扩展了一下我们的代码,如果有任何问题和想法,非常欢迎大家在评论区与我交流,我需要学…...
【项目管理】成本类计算 笔记
项目管理-相关文档,希望互相学习,共同进步 风123456789~-CSDN博客 (一)知识总览 项目管理知识域 知识点: (项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域) 对应&…...
Redis 事务
事务介绍 Redis 事务和 MySQL 事务在概念上类似的 把一些列的操作绑定成一组,让这一组能够批量执行 MySQL 事务 原子性:把多个操作打包成一个整体 一致性:事务执行前后数据合理 持久性:事务做出的操作都会修改硬盘 隔离性&#…...
JavaScript day5
立即执行函数 <script>(function(){ console.log//函数不需调用,立马执行 })() </script> //另外写法 <script> (function(){}()) </script> 常见的内置对象 Math console.dir()——打印对象的 使用Math中的属性——console.log(Math.…...
辛格迪客户案例 | 浙江高跖医药委托生产质量管理协同(OWL MAH)项目
一、案例概述 浙江高跖医药科技股份有限公司是一家集“研、产、销”为一体的专业化药品持证企业。高跖医药自成立之初就建立并运行着一套相对完善的质量管理体系,涵盖了药品的研发、生产监管及销售。高跖医药于2022年选择实施了辛格迪的“委托生产质量管理协同解决…...
科学养生指南:解锁健康生活新方式
在快节奏的现代生活中,健康养生已成为人们关注的焦点。科学合理的养生方式,能帮助我们增强体质、预防疾病,享受更优质的生活。 饮食是健康养生的基石。遵循 “均衡饮食” 原则,每日饮食需包含谷类、蔬菜水果、优质蛋白质和健康…...
FreeRTos学习记录--2.内存管理
后续的章节涉及这些内核对象:task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需…...