网络编程
咱们之前所学的进程间通信(IPC)有很多种方式,管道(匿名管道,命名管道,标准流管道),消息队列,共享内存,信号量,信号等基本上都是在同一台PC 上进行的。
那么在不同PC 的进程间该如何通信呢?一般的我们是有两种方法,一种是借助硬件的通讯端口(比方说串口),但是由于这些端口一般上都存在传输距离的限制,而且也不太方便多机相互通讯,二一种就是使用网线,借助网络来实现不同计算机之间的数据传输,今天我们就来一起学习网络通信。
OSI
OSI 系统模型是国际化标准组织(ISO)为了实现计算机网络标准化颁布的参考模型,根据网络中数据传输的过程,将该模式分为7个层,每一层都向上一层提供服务,同时使用下层提供的服务。
层次 | 名称 | 功能 |
7 | 应用层 | 计算机网络和最终用户的接口 |
6 | 表示层 | 数据编码,数据压缩,数据加密 |
5 | 会话层 | 提供面向用户的连接服务,并为会话活动提供有效组织和同步的手段,为数据传输提供控制和管理。 |
4 | 传输层 | 实现通讯子网端到端的可靠传输,传输层下面的三层属于通讯子网。 |
3 | 网络层 | 实现分别位于不同网络的源节点与目的节点的数据包传输。 |
2 | 数据链路层 | 通过物理层提供的BIT 流服务,在相邻节点之间建立链路,对传输中的差错进行检错和纠错,向网络层提供无差错的传输 |
1 | 物理层 | 提供建立,维护和释放物理连接的方法,实现物理通道上的BIT 流传输。 |
TCP/IP
TCP/IP 协议的四层结构模型获得了更广泛的使用。TCP/IP 协议是Internet 事实上的工业标准。
TCP三次握手(建立)和四次挥手(断开)
优点
可靠传输:采用 “三次握手” 建立连接、“四次挥手” 释放连接,确保通信双方状态同步;同时通过确认应答(ACK) 、重传机制(超时重传、快速重传),保障数据无丢失交付。
有序交付:为每个数据段分配序号,接收端按序号重组数据,避免因网络延迟导致的乱序问题。
流量控制:基于滑动窗口机制,接收端根据自身缓存能力动态告知发送端可接收的数据量,防止发送端因速率过快导致接收端数据溢出。
拥塞控制:通过慢启动、拥塞避免、快重传、快恢复等算法,实时感知网络拥塞状态并调整发送速率,减少网络拥塞对整体传输的影响。
缺点
传输延迟较高:连接建立 / 释放的握手 / 挥手过程、确认应答、重传等机制会增加额外的网络开销,导致传输延迟高于 UDP,不适合对实时性要求严苛的场景。
资源占用大:需维护连接状态(如序号、窗口大小、拥塞窗口等),且每个连接需独立的资源管理,在高并发场景下会消耗更多的服务器 CPU 与内存资源。
灵活性低:固定的可靠传输机制无法按需简化,对于可容忍少量数据丢失的场景,会造成资源浪费。
TCP的连接建立过程又称为三次握手。
我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。
(1)TCP服务器准备好接受连接,进入LISTEN状态,这一过程称为被动打开。
(2)第一次握手:客户端发送SYN标志为1(表示这是一个同步报文段),且seq随机的报文段,请求建立连接。此时的seq记为ISN(c)(Initial Sequence Number,初始序列号),括号中的c表示这是和客户端的序列号。客户端发送后变为SYN-SENT状态。
(3)第二次握手:服务端收到客户端的第一次握手信号,变为SYN-RCVD状态。随即确认客户端的SYN报文段,发送一个ACK和SYN标志均为1的报文段。该报文段中ack=ISN(c)+1,seq随机,标记为ISN(s),此处的s表示这是服务端的序列号。服务端变为SYN-RCVD状态。
(4)第三次握手:客户端收到服务端的第二次握手信号,变为ESTABLISHED状态,随即确认服务端的报文段,发送ACK标志为1的报文段。该报文段中ack=ISN(s)+1,seq=ISN(c)+1。服务端收到客户端的第三次握手信号之后变为ESTABLISHED状态。
TCP连接释放过程
TCP的连接释放过程又称为四次挥手。
四次挥手可以由客户端发起,也可以由服务端发起。此处假设连接请求的断开操作由客户端发起。连接断开前,双方都处于ESTABLISHED状态。需要注意的是,连接建立后,即客户端和服务端处于ESTABLISHED时,双方发送的报文段ACK标志均为1。
我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。
(1)第一次挥手:客户端发送FIN标志为1(即FINISH,表示通信结束)的报文段,请求断开连接,执行主动关闭(active close)。此时,报文段中包含对于服务端数据的确认,ACK为 1,假设ack=V。连接断开前已经历了一系列的数据传输,seq取决于之前已发送的报文段,假设seq=U。客户端状态变为FIN-WAIT-1。
(2)第二次挥手:服务端接收到第一次挥手信息,切换为CLOSE-WAIT状态,随即发送ACK标志为1,ack=U+1的报文段,此时seq=V。客户端接收到服务端的第二次挥手信号,变为FIN-WAIT-2状态。第二次挥手后,服务端仍可发送数据,客户端仍可接收。
(3)第三次挥手:服务端完成数据传送后,发送FIN标志和ACK标志均为1的报文段,ack=U+1,seq大于V,假设为W,请求断开连接,这一过程称为被动关闭。服务端发送第三次挥手信号后,变为LAST-ACK状态。
(4)第四次挥手:客户端收到第三次挥手信号,随即发送ACK标志为1,seq=U+1,ack=W+1的报文段,变为TIME-WAIT状态。服务端收到第四次挥手信号,变为CLOSED状态。客户端从变为TIME-WAIT状态开始计时,等待2MSL(2倍最大报文时长,约定值)后进入CLOSED状态。四次挥手结束。
套接字描述符 | 名称(功能) | 核心作用 | 生命周期 |
server_fd | 监听套接字(被动) | 只负责 “等待客户端连接”,不参与实际数据传输 | 从 socket() 创建到 close(server_fd) 销毁,贯穿服务器整个运行过程 |
new_socket | 通信套接字(主动) | 专门和 “已连接的客户端” 收发数据 | 从 accept() 创建到 close(new_socket) 销毁,仅对应一次客户端连接 |
案例
在Linux C编程中,socket编程是实现网络通信的核心技术。下面详细介绍socket编程中常用的函数及其相关数据类型:
### 1. socket() - 创建套接字
`socket()`函数用于创建一个套接字描述符,是网络通信的起点。
```c
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
```
- **参数说明**:
- `domain`:协议族,常用的有`AF_INET`(IPv4)、`AF_INET6`(IPv6)
- `type`:套接字类型,`SOCK_STREAM`(TCP)、`SOCK_DGRAM`(UDP)
- `protocol`:协议,通常为0(自动选择对应类型的默认协议)
- **返回值**:成功返回套接字描述符(非负整数),失败返回-1
### 2. 地址相关数据类型(用于bind等函数)
```c
// IPv4地址结构
struct in_addr {
in_addr_t s_addr; 32位IPv4地址(网络字节序)
};
struct sockaddr_in {
sa_family_t sin_family; 地址族,AF_INET
in_port_t sin_port; 16位端口号(网络字节序)
struct in_addr sin_addr; IPv4地址
unsigned char sin_zero[8]; 填充字段,通常设为0
};
// 通用地址结构(用于函数参数)
struct sockaddr {
sa_family_t sa_family; 地址族
char sa_data[14]; 地址数据
};
// 字节序转换函数
uint32_t htonl(uint32_t hostlong); 主机字节序转网络字节序(32位)
uint16_t htons(uint16_t hostshort); 主机字节序转网络字节序(16位)
uint32_t ntohl(uint32_t netlong); 网络字节序转主机字节序(32位)
uint16_t ntohs(uint16_t netshort); 网络字节序转主机字节序(16位)
IP地址转换函数
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr)
这句代码的作用,就是将 SERVER_IP(字符串)转换为 server_addr.sin_addr(二进制)
int inet_pton(int af, const char *src, void *dst); 字符串转网络字节序
af
(address family,地址族):
指定要转换的 IP 地址类型,常用值:
AF_INET:表示转换 IPv4 地址(32 位);
AF_INET6:表示转换 IPv6 地址(128 位)。
作用:告诉函数要处理的 IP 地址版本,因为 IPv4 和 IPv6 的字符串格式和二进制长度不同。
src
(source,源):
指向一个字符串,存储待转换的 IP 地址(如 "127.0.0.1" 或 "::1")。
这是人类可读的形式,例如 IPv4 的点分十进制(a.b.c.d)或 IPv6 的冒分十六进制。
dst
(destination,目标):
指向一块内存,用于存储转换后的二进制 IP 地址(网络字节序)。
对于 IPv4(AF_INET):dst 通常指向 struct in_addr 类型的变量(内部是 32 位整数);
对于 IPv6(AF_INET6):dst 通常指向 struct in6_addr 类型的变量(内部是 128 位整数数组)。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序转字符串
```
### 3. bind() - 绑定地址和端口
`bind()`函数将套接字与特定的IP地址和端口号绑定。
```c
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
```
- **参数说明**:
- `sockfd`:socket()返回的套接字描述符
- `addr`:指向`sockaddr`结构的指针,包含要绑定的地址信息
- `addrlen`:地址结构的长度
- **返回值**:成功返回0,失败返回-1
### 4. listen() - 监听连接(TCP服务器)
`listen()`函数将套接字设为监听状态,准备接收客户端连接。
```c
#include <sys/socket.h>
int listen(int sockfd, int backlog);
```
- **参数说明**:
- `sockfd`:绑定后的套接字描述符
- `backlog`:等待连接队列的最大长度
- **返回值**:成功返回0,失败返回-1
### 5. accept() - 接受连接(TCP服务器)
`accept()`函数从连接队列中取出一个连接请求,创建新的套接字用于与客户端通信。
```c
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
```
- **参数说明**:
- `sockfd`:监听状态的套接字描述符
- `addr`:用于存储客户端地址信息的结构体指针
- `addrlen`:地址结构长度的指针
- **返回值**:成功返回新的套接字描述符,失败返回-1
### 6. connect() - 建立连接(TCP客户端)
`connect()`函数用于客户端与服务器建立连接。
```c
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
```
- **参数说明**:
- `sockfd`:客户端套接字描述符
- `addr`:指向服务器地址结构的指针
- `addrlen`:地址结构的长度
- **返回值**:成功返回0,失败返回-1
### 7. send() - 发送数据
`send()`函数用于发送数据。
```c
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
```
- **参数说明**:
- `sockfd`:已连接的套接字描述符
- `buf`:指向要发送数据的缓冲区
- `len`:要发送的数据长度
- `flags`:发送标志,通常为0
- **返回值**:成功返回实际发送的字节数,失败返回-1
### 8. recv() - 接收数据
`recv()`函数用于接收数据。
```c
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
```
- **参数说明**:
- `sockfd`:已连接的套接字描述符
- `buf`:用于存储接收数据的缓冲区
- `len`:缓冲区的最大长度
- `flags`:接收标志,通常为0
- **返回值**:成功返回实际接收的字节数,0表示连接关闭,失败返回-1
### 9. shutdown() - 关闭连接
`shutdown()`函数用于关闭套接字的读、写或两者都关闭。
```c
#include <sys/socket.h>
int shutdown(int sockfd, int how);
```
- **参数说明**:
- `sockfd`:套接字描述符
- `how`:关闭方式,`SHUT_RD`(关闭读)、`SHUT_WR`(关闭写)、`SHUT_RDWR`(关闭读写)
- **返回值**:成功返回0,失败返回-1
### 10. close() - 关闭套接字
`close()`函数用于关闭套接字描述符,释放相关资源。
```c
#include <unistd.h>
int close(int fd);
```
- **参数说明**:
- `fd`:要关闭的套接字描述符
- **返回值**:成功返回0,失败返回-1
这些函数是Linux C中实现TCP/IP网络通信的基础,通常TCP服务器的流程是:
socket()→bind()→listen()→accept()→recv()/send()→close(),
而TCP客户端的流程是:socket()→connect()→send()/recv()→close()。
服务端
#include <stdio.h> // 标准输入输出函数(printf, perror等)
#include <stdlib.h> // 标准库函数(exit等)
#include <string.h> // 字符串处理函数(memset, strlen等)
#include <unistd.h> // Unix标准函数(close, shutdown等)
#include <sys/socket.h> // 套接字相关函数(socket, bind等)
#include <netinet/in.h> // 定义网络地址结构(sockaddr_in等)
#include <arpa/inet.h> // IP地址转换函数(inet_ntoa等)
宏定义 - 常量参数
#define PORT 8080 服务器监听端口
#define BUFFER_SIZE 1024 数据缓冲区大小
#define BACKLOG 5 最大等待连接队列长度
int main() {
// 声明变量
int server_fd; 服务器监听套接字描述符
int new_socket; 与客户端通信的新套接字描述符
struct sockaddr_in address; 存储IP地址和端口的结构
int addrlen = sizeof(address); 地址结构的长度
char buffer[BUFFER_SIZE] = {0}; 数据缓冲区,初始化为0
const char *response = "Hello from server"; 要发送给客户端的响应信息
1. 创建套接字
// AF_INET: 使用IPv4协议
// SOCK_STREAM: 使用TCP协议(面向连接的可靠传输)
0: 自动选择对应的默认协议
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed"); // 输出错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 设置服务器地址结构
address.sin_family = AF_INET; 使用IPv4地址族
address.sin_addr.s_addr = INADDR_ANY; 绑定到所有可用的网络接口
address.sin_port = htons(PORT); 将端口号转换为网络字节序
2. 将套接字绑定到指定的端口和地址
// server_fd: 要绑定的套接字
// (struct sockaddr *)&address: 转换为通用地址结构的指针
// sizeof(address): 地址结构的大小
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed"); // 输出绑定失败的错误信息
exit(EXIT_FAILURE); // 退出程序
}
3. 开始监听连接请求
// server_fd: 要监听的套接字
// BACKLOG: 最大等待连接队列长度
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed"); // 输出监听失败的错误信息
exit(EXIT_FAILURE); // 退出程序
}
printf("Server listening on port %d...\n", PORT); // 显示服务器开始监听
4. 接受客户端的连接请求
server_fd: 监听套接字
(struct sockaddr *)&address: 用于存储客户端地址信息
// (socklen_t*)&addrlen: 地址长度的指针
// 注意:accept会阻塞程序,直到有客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed"); // 输出接受连接失败的错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 显示客户端连接信息
// inet_ntoa: 将网络字节序IP地址转换为字符串
// ntohs: 将网络字节序端口号转换为主机字节序
printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
5. 接收客户端发送的数据
new_socket: 与客户端通信的套接字
buffer: 存储接收数据的缓冲区
BUFFER_SIZE: 缓冲区大小
0: 无特殊标志
ssize_t valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
printf("Received from client: %s\n", buffer); // 显示接收到的数据
6. 向客户端发送响应
new_socket: 与客户端通信的套接字
response: 要发送的数据
strlen(response): 数据长度
// 0: 无特殊标志
send(new_socket, response, strlen(response), 0);
printf("Response sent to client\n"); // 显示响应已发送
7. 关闭连接
shutdown(new_socket, SHUT_RDWR); 关闭读写方向的连接
close(new_socket); 关闭与客户端通信的套接字
close(server_fd); 关闭服务器监听套接字
return 0;
}
服务器工作流程:
- 使用socket()创建一个 TCP 套接字
- 使用bind()将套接字绑定到指定端口 (8080)
- 使用listen()进入监听状态,等待客户端连接
- 使用accept()接受客户端的连接请求
- 使用recv()接收客户端发送的数据
- 使用send()向客户端发送响应数据
- 使用shutdown()和close()关闭连接
客户端
#include <stdio.h> // 标准输入输出库,提供printf、perror等函数
#include <stdlib.h> // 标准库,提供exit等进程控制函数
#include <string.h> // 字符串处理库,提供memset、strlen等函数
#include <unistd.h> // Unix系统调用库,提供close、shutdown等函数
#include <sys/socket.h> // 套接字核心库,提供socket、connect等网络函数
#include <netinet/in.h> // 网络地址结构定义,提供sockaddr_in等结构体
#include <arpa/inet.h> // IP地址转换库,提供inet_pton、htons等函数
// 宏定义常量(便于统一修改和维护)
#define PORT 8080 服务器监听的端口号
#define BUFFER_SIZE 1024 数据缓冲区大小(1KB)
#define SERVER_IP "127.0.0.1" 服务器IP地址(本地回环地址,用于本地测试)
int main() {
int sock = 0; // 客户端套接字描述符(类似文件句柄)
struct sockaddr_in serv_addr; // 存储服务器地址信息的结构体
char buffer[BUFFER_SIZE] = {0}; // 接收数据的缓冲区,初始化为0
const char *message = "Hello from client"; // 要发送给服务器的消息
1. 创建客户端套接字
// 参数说明:
// AF_INET:使用IPv4协议
// SOCK_STREAM:使用TCP协议(面向连接的可靠传输)
// 0:自动选择对应类型的默认协议(此处为TCP)
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation error"); // 输出错误信息(如"socket creation error: Permission denied")
exit(EXIT_FAILURE); // 发生错误时退出程序
}
// 将服务器地址结构体清零初始化(避免内存中残留的垃圾数据影响)
memset(&serv_addr, '0', sizeof(serv_addr));
配置服务器地址信息
serv_addr.sin_family = AF_INET; // 使用IPv4地址族
serv_addr.sin_port = htons(PORT); // 将端口号从主机字节序转为网络字节序
// (网络中统一使用大端字节序,不同主机可能有差异)
将服务器IP地址从字符串形式(如"127.0.0.1")转换为网络字节序的二进制形式
// 参数说明:
AF_INET:地址族(IPv4)
SERVER_IP:字符串形式的IP地址
&serv_addr.sin_addr:存储转换结果的地址
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("invalid address/address not supported"); // 地址无效或不支持时的错误提示
exit(EXIT_FAILURE); // 退出程序
}
2. 尝试与服务器建立TCP连接
参数说明:
sock:客户端套接字描述符
(struct sockaddr *)&serv_addr:转换为通用地址结构的服务器地址
// sizeof(serv_addr):服务器地址结构体的大小
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed"); // 连接失败时的错误提示(如服务器未启动)
exit(EXIT_FAILURE); // 退出程序
}
// 连接成功后,打印服务器信息
printf("Connected to server %s:%d\n", SERVER_IP, PORT);
3. 向服务器发送数据
// 参数说明:
sock:已连接的套接字描述符
// message:要发送的数据
// strlen(message):数据长度(不包含字符串结束符'\0')
// 0:无特殊标志(默认阻塞发送)
send(sock, message, strlen(message), 0);
printf("Message sent to server\n"); // 提示数据已发送
4. 接收服务器返回的响应数据
// 参数说明:
// sock:已连接的套接字描述符
// buffer:存储接收数据的缓冲区
// BUFFER_SIZE:缓冲区最大容量(避免溢出)
// 0:无特殊标志(默认阻塞接收)
ssize_t valread = recv(sock, buffer, BUFFER_SIZE, 0);
printf("Received from server: %s\n", buffer); // 打印接收到的服务器响应
5. 关闭连接释放资源
shutdown(sock, SHUT_RDWR); 关闭套接字的读写通道(通知对方连接即将关闭)
close(sock); 彻底关闭套接字描述符,释放系统资源
return 0; // 程序正常结束
}
客户端工作流程:
- 使用socket()创建一个 TCP 套接字
- 使用connect()连接到服务器
- 使用send()向服务器发送数据
- 使用recv()接收服务器的响应
- 使用shutdown()和close()关闭连接
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
int main(int argc, char const *argv[])
{
int client_fd = 0;
char message[100] = "你好我是客户端"; // 要发送给服务器的消息,修改为数组形式
// 移除对常量字符串的memset操作,避免错误
struct sockaddr_in server;//存储服务端的信息
memset(&server,0,sizeof(server));// 服务端地址清零初始化
client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd< 0)
{
perror("套接字创建失败");
exit(EXIT_FAILURE); // 添加退出机制,避免错误后继续执行
}
//用于连接服务端的信息
server.sin_family=AF_INET;
server.sin_port=htons(8084);
// 将服务器IP地址从字符串形式转换为网络字节序的二进制形式
if (inet_pton(AF_INET, SERVER_IP, &server.sin_addr) <= 0) {
perror("invalid address/address not supported");
exit(EXIT_FAILURE);
}
int connect_res=connect(client_fd, (struct sockaddr *)&server, sizeof(server));
if (connect_res<0)
{
perror("连接服务端失败"); // 改用perror输出具体错误原因
exit(EXIT_FAILURE);
}
//向服务端发送数据
send(client_fd, message, strlen(message), 0);
printf("已向服务端发送消息: %s\n", message);
// 补充:接收服务端响应
char buffer[100] = {0};
ssize_t valread = recv(client_fd, buffer, sizeof(buffer), 0);
if (valread > 0)
{
printf("收到服务端响应: %s\n", buffer);
}
// 补充:关闭连接
shutdown(client_fd, SHUT_RDWR);
close(client_fd);
return 0;
}
TCP多个客服端对应一个服务端
服务端
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_CLIENTS 5
void *client_read_write(void *arg)
{
ssize_t count = 0, send_count = 0;
// 修复1:用临时变量接收参数,避免主线程clinet_fd覆盖(原代码传地址会导致多线程共享同一个变量)
int client_fd = *(int *)arg;
free(arg); // 主线程malloc的内存,这里要释放
char *read_buf = malloc(1024 * sizeof(char));
char *write_buf = malloc(1024 * sizeof(char));
if (!read_buf) {
printf("内存申请失败\n");
close(client_fd);
pthread_exit(NULL);
}
if (!write_buf) {
printf("内存申请失败\n");
free(read_buf);
close(client_fd);
pthread_exit(NULL);
}
while ((count = recv(client_fd, read_buf, 1023, 0)) > 0) { // 留1字节存字符串结束符
// 给接收的字符串加结束符,避免乱码
read_buf[count] = '\0';
printf("receive message from client_fd: %d: %s\n", client_fd, read_buf);
strcpy(write_buf, "received~\n");
send_count = send(client_fd, write_buf, strlen(write_buf), 0);
if (send_count < 0) {
perror("send");
break;
}
}
if (count < 0) perror("recv"); // 接收错误时打印
shutdown(client_fd, SHUT_WR);
close(client_fd);
free(read_buf);
free(write_buf);
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
// 修复2:server_listen未赋值(原代码socket创建后没存结果,导致后续bind失败)
int server_listen = socket(AF_INET, SOCK_STREAM, 0);
int clinet_fd = 0;
struct sockaddr_in server_adder, clinet_adder;
memset(&server_adder, 0, sizeof(server_adder));
memset(&clinet_adder, 0, sizeof(clinet_adder));
// 修复3:addrlen应该是客户端地址长度(原代码用server_adder的长度,不影响但逻辑更严谨)
socklen_t addrlen = sizeof(clinet_adder);
pthread_t client_threads[MAX_CLIENTS];
int client_count = 0;
if (server_listen < 0) { // 检查socket创建结果
perror("socket failed");
exit(EXIT_FAILURE);
}
server_adder.sin_addr.s_addr = INADDR_ANY;
// 修复4:端口转换用htons(原代码用htonl,会导致端口错误,客户端连不上)
server_adder.sin_port = htons(8081);
server_adder.sin_family = AF_INET;
if (bind(server_listen, (struct sockaddr *)&server_adder, sizeof(server_adder)) < 0) {
perror("绑定失败"); // 用perror显示具体错误(如端口被占用)
close(server_listen);
exit(EXIT_FAILURE);
}
if (listen(server_listen, 6) < 0) {
perror("监听失败");
close(server_listen);
exit(EXIT_FAILURE);
}
printf("服务端启动成功,监听端口8081...\n"); // 增加启动提示
while (1)
{
if (client_count >= MAX_CLIENTS) {
printf("已超出最大客户端数量(%d),等待中...\n", MAX_CLIENTS);
sleep(1);
continue;
}
// 修复5:accept的第三个参数必须是socklen_t*类型(原代码直接传sizeof,会编译警告+运行错误)
clinet_fd = accept(server_listen, (struct sockaddr *)&clinet_adder, &addrlen);
if (clinet_fd < 0) {
perror("新的请求socket创建失败");
continue;
}
printf("客户端连接成功:\n");
printf(" IP地址: %s\n", inet_ntoa(clinet_adder.sin_addr));
printf(" 端口: %d\n", ntohs(clinet_adder.sin_port));
client_count++;
printf("当前客户端数量:%d\n", client_count);
// 修复6:用malloc分配客户端fd(原代码传&clinet_fd地址,多线程会共享同一个变量,导致fd混乱)
int *p_clinet_fd = malloc(sizeof(int));
*p_clinet_fd = clinet_fd;
if (pthread_create(&client_threads[client_count-1], NULL, client_read_write, (void *)p_clinet_fd) != 0) {
perror("线程创建失败");
close(clinet_fd);
free(p_clinet_fd);
client_count--;
continue;
}
pthread_detach(client_threads[client_count-1]);
}
close(server_listen); // 理论上不会执行,但加上更规范
return 0;
}
客户端
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SERVER_PORT 8081
#define BUFFER_SIZE 1024
int main(int argc, char const *argv[])
{
int client_socket;
struct sockaddr_in server_addr;
char send_buf[BUFFER_SIZE];
char recv_buf[BUFFER_SIZE];
ssize_t send_len, recv_len;
// 创建客户端套接字
if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket创建失败");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
// 转换服务器IP地址(默认为本地回环地址,可修改为实际服务器IP)
if (argc > 1)
{
// 如果提供了命令行参数,则使用该参数作为服务器IP
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0)
{
perror("无效的服务器IP地址");
close(client_socket);
exit(EXIT_FAILURE);
}
}
else
{
// 否则使用本地回环地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
}
// 连接到服务器
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("连接服务器失败");
close(client_socket);
exit(EXIT_FAILURE);
}
printf("成功连接到服务器,端口: %d\n", SERVER_PORT);
printf("请输入要发送的消息(输入exit退出):\n");
// 循环发送和接收数据
while (1)
{
// 读取用户输入
if (fgets(send_buf, BUFFER_SIZE, stdin) == NULL)
{
perror("读取输入失败");
break;
}
// 检查是否要退出
if (strncmp(send_buf, "exit", 4) == 0)
{
printf("准备断开连接...\n");
break;
}
// 发送数据到服务器
send_len = send(client_socket, send_buf, strlen(send_buf), 0);
if (send_len < 0)
{
perror("发送数据失败");
break;
}
// 接收服务器响应
recv_len = recv(client_socket, recv_buf, BUFFER_SIZE, 0);
if (recv_len < 0)
{
perror("接收响应失败");
break;
}
else if (recv_len == 0)
{
printf("服务器已断开连接\n");
break;
}
// 打印服务器响应
recv_buf[recv_len] = '\0'; // 确保字符串结束
printf("服务器响应: %s", recv_buf);
}
// 关闭连接
shutdown(client_socket, SHUT_WR);
close(client_socket);
printf("已断开与服务器的连接\n");
return 0;
}
运行结果
setsockopt()
在 Linux C 编程中,setsockopt( )是一个非常重要的系统调用,用于设置套接字(socket)的各种选项。它允许开发者在创建套接字后,对其行为进行精细化控制,以适应不同的网络通信需求。
函数原型
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- **参数说明**:
- `sockfd`:套接字描述符,即通过 `socket( )` 函数创建的文件描述符
- `level`:选项所在的协议层(如 SOL_SOCKET、IPPROTO_TCP 等)
- `optname`:要设置的具体选项名称
- `optval`:指向存放选项值的缓冲区指针
- `optlen`:缓冲区的长度(以字节为单位)
- **返回值**:成功返回 0,失败返回 -1 并设置 errno
主要功能
`setsockopt()` 用于配置套接字的各种属性,这些属性控制着套接字的行为,包括:
- 数据包的发送和接收方式
- 连接超时设置
- 地址重用
- 缓冲区大小调整
- 广播和多播设置
- TCP 协议特定选项(如是否启用 Nagle 算法)
常见使用场景
1. **地址和端口重用**
当服务器程序关闭后,通常需要等待一段时间才能重新使用相同的端口。使用 `SO_REUSEADDR` 选项可以避免这个问题:
int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
2. **设置发送/接收缓冲区大小**
可以调整套接字的发送和接收缓冲区大小以优化性能:
int bufsize = 65536; 64KB
设置接收缓冲区
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
设置发送缓冲区
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
3. **设置连接超时**
结合 `connect()` 使用,可以设置连接的超时时间:
struct timeval timeout;
timeout.tv_sec = 10; 10秒
timeout.tv_usec = 0;
设置连接超时
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
4. **禁用 Nagle 算法**
在某些实时性要求高的场景(如游戏),可以禁用 Nagle 算法减少延迟:
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
5. **允许广播**
要发送广播数据包,需要先设置 `SO_BROADCAST` 选项:
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
6. 设置 IP 包的 TTL(生存时间)
控制数据包在网络中能够经过的路由器数量:
int ttl = 64; 常见的TTL值
setsockopt(sockfd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
使用注意事项
1. 不同的选项需要在特定的协议层(level)设置
2. 有些选项需要在 `bind()` 或 `connect()` 之前设置才有效
3. 选项值的类型可能是整数、结构体等,需要根据具体选项确定
4. 对同一个选项的设置可能会有系统限制(如缓冲区大小的最大值)
案例
通过setsockopt ( )函数实现伪超时
伪超时的运行结果意义在于:
- 提供即时准确的错误信息
(不是模糊的"超时")
- 实现快速失败机制,提高系统响应性
- 优化资源利用率,避免不必要的等待
- 支持高效的服务发现和故障处理
服务端
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int opt = 1;
char buffer[1024] = {0};
const char *response = "我是服务端 ";
// 服务端监听套接字描述符
int server_listen = socket(AF_INET, SOCK_STREAM, 0);
if (server_listen < 0)
{
printf("socket创建失败");
exit(EXIT_FAILURE); // 添加退出机制
}
struct sockaddr_in server_bind;
int addrlen = sizeof(server_bind);
server_bind.sin_addr.s_addr = INADDR_ANY;
server_bind.sin_family = AF_INET;
server_bind.sin_port = htons(8084); // 修改为与客户端一致的端口
// 设置套接字选项,允许重用端口和地址(修正重复的SO_REUSEADDR)
if (setsockopt(server_listen, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
{
perror("setsockopt 创建失败");
exit(EXIT_FAILURE); // 添加退出机制
}
// 绑定地址
if (bind(server_listen, (struct sockaddr *)&server_bind, sizeof(server_bind)) < 0)
{
perror("绑定失败");
exit(EXIT_FAILURE); // 添加退出机制
}
// 监听连接请求,最大等待队列长度为5
if (listen(server_listen, 5) < 0)
{
perror("listen");
exit(EXIT_FAILURE); // 添加退出机制
}
printf("服务端启动,监听端口 8084...\n");
while (1)
{
// 等待客户端申请连接服务端
int newsocket = accept(server_listen, (struct sockaddr *)&server_bind, (socklen_t *)&addrlen);
if (newsocket < 0)
{
perror("连接服务端失败");
continue; // 继续等待下一个连接
}
// 读取客户端发送的数据
ssize_t valread = read(newsocket, buffer, 1024);
if (valread > 0)
{
printf("收到客户端消息:%s\n", buffer);
// 向客户端发送消息
send(newsocket, response, strlen(response), 0);
printf("已发送响应给客户端\n");
}
else if (valread == 0)
{
printf("客户端正常关闭连接\n");
}
else
{
perror("读取数据失败");
}
// 关闭客户端连接
close(newsocket);
printf("客户端连接已关闭\n");
}
// 实际不会执行到这里,可用于后续扩展
close(server_listen);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/time.h>
#define SERVER_IP "127.0.0.1" //服务端IP地址
#define SERVER_PORT 8084 //服务端端口号(与服务端保持一致)
#define TIMEOUT_SEC 5 // 连接超时时间
#define BUFFER_SIZE 1024 // 添加缓冲区定义
int main(int argc, char const *argv[])
{
int client = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_bind; //客户端接受绑定服务端的协议和端口
if (client < 0)
{
perror("客户端套接字创建失败");
exit(EXIT_FAILURE); // 添加退出机制
}
//设置服务器地址结构
memset(&server_bind, 0, sizeof(server_bind));
server_bind.sin_family = AF_INET;
server_bind.sin_port = htons(SERVER_PORT);
//将IP转化为网络字节序
if (inet_pton(AF_INET, SERVER_IP, &server_bind.sin_addr) <= 0)
{
perror("网络字节序转换失败");
close(client);
exit(EXIT_FAILURE);
}
//设置连接超时时间
struct timeval timeout;
timeout.tv_sec = TIMEOUT_SEC; //5秒
timeout.tv_usec = 0; // 微秒
//使用SO_SNDTIMEO选项设置发送超时(影响connect)
if (setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0)
{
perror("setsockopt 设置失败");
close(client);
exit(EXIT_FAILURE);
}
//尝试链接服务器
printf("尝试连接到 %s:%d, 超时时间 %d 秒...\n", SERVER_IP, SERVER_PORT, TIMEOUT_SEC);
// 尝试连接服务器
int ret = connect(client, (struct sockaddr *)&server_bind, sizeof(server_bind));
if (ret < 0) {
// 检查错误是否为超时
if (errno == EINPROGRESS || errno == EWOULDBLOCK || errno == ETIMEDOUT) {
fprintf(stderr, "连接超时! 超过 %d 秒未响应\n", TIMEOUT_SEC);
} else {
perror("连接失败");
}
close(client);
exit(EXIT_FAILURE);
}
printf("成功连接到服务器!\n");
// 添加发送消息的功能
const char *message = "你好,服务端!";
send(client, message, strlen(message), 0);
printf("已向服务端发送消息: %s\n", message);
// 接收服务端响应
char buffer[BUFFER_SIZE] = {0};
ssize_t valread = read(client, buffer, BUFFER_SIZE);
if (valread > 0) {
printf("收到服务端响应: %s\n", buffer);
} else if (valread == 0) {
printf("服务端关闭了连接\n");
} else {
perror("读取响应失败");
}
// 关闭套接字
close(client);
return 0;
}
服务端不开启,只开启客户端(体现了伪超时)
UDP
在 Linux 系统中,UDP 协议的开发主要基于 Socket 编程接口,核心函数围绕“创建套接字、绑定地址、发送数据、接收数据”四个核心操作展开。以下是 UDP 开发中最常用的函数及其用法:
1. 创建 UDP 套接字:`socket()`
- **功能**:创建一个 UDP 类型的套接字(文件描述符),作为网络通信的端点。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- **参数**:
- `domain`:协议族,UDP 用 `AF_INET`(IPv4)或 `AF_INET6`(IPv6)。
- `type`:套接字类型,UDP 必须指定为 `SOCK_DGRAM`(数据报套接字)。
- `protocol`:具体协议,UDP 填 `0`(自动匹配 `SOCK_DGRAM` 对应的 UDP 协议)。
- **返回值**:成功返回套接字描述符(非负整数),失败返回 `-1`(需检查 `errno`)。
- **示例**:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket 创建失败");
exit(1);
}
2. 绑定地址与端口:`bind()`
- **功能**:将套接字与本地 IP 地址和端口绑定(服务器必用,客户端可选)。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- **参数**:
- `sockfd`:`socket()` 返回的套接字描述符。
- `addr`:指向本地地址结构的指针(需用 `struct sockaddr_in` 填充 IPv4 信息)。
- `addrlen`:地址结构的长度(`sizeof(struct sockaddr_in)`)。
- **返回值**:成功返回 `0`,失败返回 `-1`。
- **示例**(绑定 IPv4 地址):
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET; // IPv4
local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地网卡
local_addr.sin_port = htons(8080); // 端口 8080(需转换为网络字节序)
if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) == -1) {
perror("bind 失败");
close(sockfd);
exit(1);
}
```
> 注:`htonl()`/`htons()` 用于将主机字节序转换为网络字节序(大端序),必须使用。
3. 发送 UDP 数据:`sendto()`
- **功能**:向指定的目标地址发送 UDP 数据报(无需建立连接)。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
```
- **参数**:
- `sockfd`:套接字描述符。
- `buf`:待发送的数据缓冲区。
- `len`:数据长度(字节数)。
- `flags`:发送标志,通常填 `0`(阻塞发送)。
- `dest_addr`:目标主机的地址结构(含 IP 和端口)。
- `addrlen`:目标地址结构的长度。
- **返回值**:成功返回发送的字节数,失败返回 `-1`。
- **示例**:
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 目标 IP
dest_addr.sin_port = htons(8080); // 目标端口
char *msg = "Hello UDP";
ssize_t send_len = sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
if (send_len == -1) {
perror("sendto 失败");
}
4. 接收 UDP 数据:`recvfrom()`
- **功能**:从套接字接收 UDP 数据报,并获取发送方的地址信息。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
```
- **参数**:
- `sockfd`:套接字描述符。
- `buf`:接收数据的缓冲区。
- `len`:缓冲区最大长度(避免溢出)。
- `flags`:接收标志,`0` 表示阻塞等待数据。
- `src_addr`:输出参数,存储发送方的地址信息(可填 `NULL` 忽略)。
- `addrlen`:输入输出参数,传入 `src_addr` 缓冲区大小,返回实际地址长度。
- **返回值**:成功返回接收的字节数,失败返回 `-1`,连接关闭返回 `0`(UDP 一般不涉及)。
- **示例**:
char buf[1024];
struct sockaddr_in src_addr;
socklen_t src_len = sizeof(src_addr);
ssize_t recv_len = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
(struct sockaddr*)&src_addr, &src_len);
if (recv_len == -1) {
perror("recvfrom 失败");
} else {
buf[recv_len] = '\0'; // 确保字符串结束
printf("收到来自 %s:%d 的数据:%s\n",
inet_ntoa(src_addr.sin_addr), // 转换 IP 为字符串
ntohs(src_addr.sin_port), // 转换端口为本地字节序
buf);
}
5. 关闭套接字:`close()`
- **功能**:释放套接字资源,终止网络通信。
- **原型**:
#include <unistd.h>
int close(int fd);
- **示例**:
close(sockfd); // 关闭 UDP 套接字
辅助函数(地址转换)
- `inet_addr(const char *cp)`:将点分十进制 IP 字符串(如 `"192.168.1.1"`)转换为网络字节序的 32 位整数(IPv4)。
- `inet_ntoa(struct in_addr in)`:将网络字节序的 32 位整数转换为点分十进制 IP 字符串(IPv4)。
- `htons(uint16_t hostshort)`/`htonl(uint32_t hostlong)`:将 16 位/32 位主机字节序转换为网络字节序。
- `ntohs(uint16_t netshort)`/`ntohl(uint32_t netlong)`:将网络字节序转换为主机字节序(常用于解析接收的端口/IP)。
案例
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8085
#define BUFFER_SIZE 1024
int main(int argc, char const *argv[])
{
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[BUFFER_SIZE];
socklen_t client_len = sizeof(client_addr);
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("套接字创建失败");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有可用的接口
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 绑定套接字到端口
if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("绑定失败");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP服务器启动,监听端口 %d...\n", PORT);
while (1)
{
// 接受客服端的消息
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0, (struct sockaddr *)&client_addr, &client_len);
if (recv_len < 0)
{
perror("接收失败");
continue;
}
// 准备回复消息
char reply[BUFFER_SIZE];
snprintf(reply, BUFFER_SIZE, "服务器已收到: %s", buffer);
// 回复客户端
ssize_t send_len = sendto(sockfd, reply, strlen(reply), 0,
(const struct sockaddr *)&client_addr, client_len);
if (send_len < 0)
{
perror("发送回复失败");
}
// 如果收到"exit",服务器退出
if (strcmp(buffer, "exit") == 0)
{
printf("收到退出命令,服务器关闭...\n");
break;
}
}
// 关闭套接字
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8085
#define BUFFER_SIZE 1024
#define SERVER_IP "127.0.0.1" // 服务器IP地址,本地测试用回环地址
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
socklen_t server_len = sizeof(server_addr);
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("套接字创建失败");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 转换服务器IP地址
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("无效的服务器IP地址");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP客户端启动,连接到 %s:%d\n", SERVER_IP, PORT);
printf("请输入消息(输入exit退出):\n");
while (1) {
// 读取用户输入
printf("> ");
fflush(stdout);
if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
perror("读取输入失败");
break;
}
// 移除换行符
buffer[strcspn(buffer, "\n")] = '\0';
// 发送消息到服务器
ssize_t send_len = sendto(sockfd, buffer, strlen(buffer), 0,
(const struct sockaddr *)&server_addr, server_len);
if (send_len < 0) {
perror("发送消息失败");
continue;
}
// 如果输入exit,客户端退出
if (strcmp(buffer, "exit") == 0) {
printf("客户端退出...\n");
break;
}
// 接收服务器回复
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr *)&server_addr, &server_len);
if (recv_len < 0) {
perror("接收回复失败");
continue;
}
// 确保字符串以null结尾
buffer[recv_len] = '\0';
printf("服务器回复: %s\n", buffer);
}
// 关闭套接字
close(sockfd);
return 0;
}
核心流程总结
1. **服务器**:`socket()` 创建 UDP 套接字 → `bind()` 绑定端口 → `recvfrom()` 接收数据 → `sendto()` 回复数据 → `close()` 关闭。
2. **客户端**:`socket()` 创建 UDP 套接字 → `sendto()` 发送数据 → `recvfrom()` 接收回复(可选)→ `close()` 关闭。