「Linux 网络编程」Socket 构建 UDP 本地通信

1 UDP 协议基础

1.1 什么是 UDP?

UDP(User Datagram Protocol,用户数据报协议) 是 OSI 七层模型中传输层的核心协议之一,它的核心特点就是 “简单直接”——不建立连接、不保证可靠传输,但速度快、开销小,特别适合对 延迟敏感、能接受少量数据丢失 的场景。


1.2 UDP 的核心特性

  • 无连接:通信前不需要像 TCP “三次握手” 建立连接,不需要发送类似 ACK 的应答确认消息,也不需要 SEQ 分配数据包序列号。UDP 直接发送数据,省时省力。​
  • 不可靠:不确认接收方是否收到数据,不重传丢失的数据包,也不保证数据按序到达。​
  • 面向数据报:数据以 数据报 为单位传输,每个数据报都是独立的,包含完整的源地址和目的地址。​
  • 开销小:首部结构简单,仅 8 字节(TCP 首部至少 20 字节),传输效率高。

1.3 UDP 首部结构

UDP 首部非常简洁,共 4 个字段,每个字段 2 字节(16 位),总长度 8 字节:

字段 长度(位) 作用
源端口号 16 标识发送方的应用程序端口,若不需要回复,可设为 0
目的端口号 16 标识接收方的应用程序端口,是数据报的 “收件人门牌号”
UDP 长度 16 表示整个 UDP 数据报(首部 + 数据)的字节数,最小值为 8(仅首部)
校验和 16 用于校验 UDP 数据报是否损坏(IPv4 中可选,IPv6 中必选),若校验失败则丢弃


2 UDP 内部工作原理

UDP 的工作流程非常直观,没有 TCP 的 “连接管理”“流量控制”“拥塞控制” 等复杂逻辑,核心就是 封装 - 传输 - 解封装 三步。


2.1 UDP 发送方流程

  1. 应用层程序(如游戏客户端)将数据交给 UDP 协议;

  2. UDP 给数据添加 首部(源端口、目的端口、长度、校验和),形成 UDP 数据报

  3. UDP 将数据报交给网络层(IP 协议),IP 再添加 IP 首部(源 IP、目的 IP 等),形成 IP 数据报

  4. IP 数据报通过网络(如路由器、交换机)传输到接收方。


2.2 UDP 接收方流程

  1. 网络层收到 IP 数据报后,解封装取出 UDP 数据报,交给传输层的 UDP 协议;

  2. UDP 验证校验和。若校验失败,直接丢弃数据报;若成功,提取出应用层数据;

  3. UDP 通过 目的端口号 找到对应的应用程序,将数据交给应用层。



3 Linux UDP 服务端和客户端实现

UDP 不同于 TCP,不存在请求连接和请求受理的过程,实际上无法从协议层面明确区分服务端和客户端。

所谓的 服务端,本质是应用层约定的 提前绑定固定端口、被动接收数据并回复 的一方;客户端主动向固定端口发送数据 的一方 —— 但这种角色划分完全是人为约定,而非协议强制。

例如,若 UDP 客户端也绑定固定端口,服务端同样可以主动向客户端的固定端口发送数据;在 P2P 通信场景中(如局域网文件互传),双方既可以发送数据也可以接收数据,此时根本无法界定谁是 “服务端”、谁是 “客户端”,只能根据实际功能(如谁先发起请求)临时划分角色。

然而 TCP 不一样,一旦通过三次握手建立连接,主动发起连接的客户端 和 被动接受连接的服务端 角色便固定下来,无法随意切换。


3.1 UDP 服务端和客户端只需 1 个套接字

在 Linux 网络编程中,套接字 是应用层与内核网络协议栈交互的 “桥梁”,每个套接字对应一个 通信端点,包含协议类型(TCP/UDP)、IP 地址、端口号等核心信息。

UDP 与 TCP 对套接字的使用逻辑对比

  • TCP 因 面向连接 的特性,服务端需维护 监听套接字连接套接字 两类套接字(监听套接字仅用于接收连接请求每建立一个客户端连接就会生成一个独立的连接套接字,用于与该客户端的专属通信,即 一个连接对应一个套接字),客户端虽仅需 1 个套接字,但服务端的套接字数量会随客户端连接数增加而增长;

  • UDP 因 无连接 的特性,无需为每个通信对象新建套接字,服务端和客户端仅靠 1 个套接字就能完成所有通信操作。


为什么 UDP 只需 1 个套接字

UDP 通过 数据报携带地址信息 + 套接字绑定固定端口 实现了 “一对多” 的通信能力。

UDP 客户端只需通过 socket() 创建 1 个 UDP 套接字,即可直接调用 sendto() 向服务端的 IP + 固定端口发送数据(此时内核会自动为该套接字分配一个临时端口)。

服务端通过客户端的临时端口回复数据时,内核会将这些数据交付给客户端的这 1 个套接字,客户端通过同一个套接字调用 recvfrom() 即可接收响应。

即便客户端需要与多个服务端通信(如同时向两个不同的 UDP 服务器发送数据),无需创建多个套接字

  • sendto() 中指定不同服务端的 IP + 端口,即可通过同一个套接字向不同目标发送数据;

  • 接收不同服务端的回复时,同样通过 recvfrom() 获取发送方地址,就能区分是哪个服务端的响应。


3.2 UDP 相关 I/O 函数

3.2.1 sendto 和 recvfrom

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

参数说明

  • sendto 参数

    • sockfd: socket 文件描述符
    • buf: 要发送的数据缓冲区
    • len: 要发送的数据长度
    • flags: 发送标志(通常为0)
    • dest_addr: 目标地址结构体指针
    • addrlen: 目标地址结构体长度
  • recvfrom 参数

    • sockfd: socket 文件描述符
    • buf: 接收数据缓冲区
    • len: 缓冲区最大长度
    • flags: 接收标志(通常为0)
    • src_addr: 来源地址结构体指针
    • addrlen: 来源地址结构体长度指针

函数作用

  • sendto向指定地址发送数据

    sockfd 指定的 UDP 套接字,将 buf 中的数据发送到 dest_addr 指定的目标地址(IP + 端口),addrlen 指定目标地址结构体的长度。

    UDP 无连接,内核不会像 TCP 那样维护连接表,每次发送都需明确告知 数据要发给谁

  • recvfrom接收数据获取发送方地址

    sockfd 指定的 UDP 套接字接收数据,存入 buf 缓冲区,并将发送方的地址信息(IP、端口)写入 src_addr 结构体,同时通过 addrlen 返回地址结构体的实际长度。

    UDP 是无连接协议,服务端需知道客户端的地址才能回复数据。


3.2.2 函数参数详细解析

  1. 共性参数(sockfd、buf、len、flags)

    参数名 类型 作用与注意事项
    sockfd int UDP 套接字的文件描述符,由socket(AF_INET, SOCK_DGRAM, 0)创建,必须是有效的(未关闭、绑定成功)。
    buf void*/const void* recvfrom中是 接收缓冲区(非 const,需提前分配内存,如char buf[1024]);
    sendto中是 发送缓冲区(const,存储要发送的数据,如字符串、二进制数据)。
    len size_t recvfrom中是接收缓冲区的最大容量(避免数据溢出,通常设为缓冲区数组长度 - 1,留 1 字节存字符串结束符\0);
    sendto中是要发送的数据的实际长度(如字符串用strlen(buf),二进制数据需明确字节数)。
    flags int 控制函数行为的标志,通常设为0默认阻塞模式),常用特殊标志如下:
    MSG_DONTWAIT非阻塞模式(若暂无数据 / 无法发送,立即返回,不阻塞进程);
    MSG_PEEK:仅查看数据(不从内核缓冲区移除数据,下次调用仍能接收该数据,多用于 “预览数据长度” 场景);
    MSG_OOB:发送 / 接收带外数据(UDP 极少用,TCP 紧急数据场景更常见)。
  2. 差异参数(地址相关参数)

    recvfrom需获取发送方地址,sendto需指定目标地址,这是二者最核心的差异,也是 UDP 无连接特性的直接体现。

    函数 地址参数 1 地址参数 2 作用与注意事项
    recvfrom src_addrstruct sockaddr* addrlensocklen_t* src_addr:存储 发送方地址(通常用 struct sockaddr_in 强制转换,因 struct sockaddr 是通用地址结构体,需结合具体协议(IPv4/IPv6)使用);
    addrlen输入输出参数
    - 输入src_addr 结构体的初始长度(如 sizeof(struct sockaddr_in));
    - 输出:实际存储的地址长度(因不同协议地址长度可能不同,UDP IPv4 场景通常与输入一致)。
    sendto dest_addrconst struct sockaddr* addrlensocklen_t dest_addr:指定 目标地址(同样用 struct sockaddr_in 强制转换,需提前初始化 IP 和端口,且端口需转为网络字节序 htons());
    addrlendest_addr 结构体的 固定长度(如 sizeof(struct sockaddr_in),纯输入参数,无需指针)。

3.2.3 返回值和错误处理

两个函数的返回值类型均为 ssize_t(带符号的大小类型,可表示 “成功接收 / 发送的字节数” 或 “负数值表示错误”),必须通过返回值判断执行结果,否则会遗漏数据丢失、地址错误等问题。

  • 返回成功

    • recvfrom:返回 实际接收的字节数(注意:不包含字符串结束符 \0,需手动添加,如 buf[recv_len] = '\0');
    • sendto:返回 实际发送的字节数(正常情况下与 len 参数相等,若网络拥堵可能小于 len,需循环发送补全)。
  • 返回失败

    返回 -1,同时设置全局变量 errno 表示错误类型,需用 perror()strerror(errno) 打印错误信息。


3.2.4 使用注意事项

  1. 缓冲区大小与数据截断

    • 接收端recvfromlen参数(缓冲区最大容量)必须大于等于可能接收的最大 UDP 数据报长度(UDP 数据报最大长度为 65507 字节,因 UDP 首部占 8 字节,IP 首部最小 20 字节,总长度不超过 IP 最大值 65535),建议设为 65536(留有余量),避免数据截断;

    • 发送端sendtolen参数(数据长度)不能超过 65507 字节,否则内核返回EMSGSIZE错误(数据报过大,无法封装)。

  2. 地址结构体初始化与字节序转换

    • 初始化地址结构体:必须用 memset 清空地址结构体(如 struct sockaddr_in),避免结构体中的随机垃圾数据导致地址错误;

    • 字节序转换:IP 和端口需转为 网络字节序(大端序)

      • 端口:用 htons()(host to network short)转换(如 server_addr.sin_port = htons(8888));
      • IP(IPv4):用 inet_pton()(presentation to network)转换字符串 IP(如 inet_pton(AF_INET, "``127.0.0.1``", &server_addr.sin_addr)),禁止直接赋值(如 server_addr.sin_addr.s_addr = 0x7F000001,虽 127.0.0.1 的网络字节序是 0x0100007F,但跨平台兼容性差)。
  3. 服务端绑定与客户端地址

    • 服务端必须绑定端口recvfrom 依赖绑定的端口监听数据,若服务端未调用 bind(),内核会自动分配临时端口,但客户端无法知道该端口,导致无法通信;

    • 客户端无需绑定端口:客户端调用 sendto 时,若未绑定端口,内核会自动分配一个临时端口(1024-65535)。而且后续 recvfrom 会通过该端口接收服务端的回复,无需手动绑定。


3.3 Linux UDP 服务端构建

核心流程

  1. socket 创建 UDP 套接字 → 2. bind 绑定 IP 和端口 → 3. recvfrom 接收客户端数据 → 4. sendto 发送响应 → 5. close 关闭套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// udp_server.c
#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 8080 // 服务端端口
#define BUF_SIZE 1024 // 缓冲区大小

int main() {
int sockfd; // 套接字文件描述符
struct sockaddr_in server_addr; // 服务端地址
struct sockaddr_in client_addr; // 获取客户端地址
char buf[BUF_SIZE]; // 缓冲区
socklen_t client_addr_len = sizeof(client_addr);

// 1. 创建 UDP 套接字, 采用 AF_INET(IPv4),SOCK_DGRAM 数据报格式
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}

// 2. 初始化服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(PORT); // 端口转换为网络字节序(大端)
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡IP

// 3. 绑定服务端 IP 和端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP server started, listening on port %d...\n", PORT);

// 4. 循环接收客户端数据并响应
while (1) {
// 接收客户端发送过来的数据,并获取发送方(客户端)的地址信息
ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr*)&client_addr, &client_addr_len);
if (recv_len == -1) {
perror("recvfrom failed");
continue;
}
buf[recv_len] = '\0'; // 手动加字符串结束符
printf("Received from %s:%d: %s\n",
inet_ntoa(client_addr.sin_addr), // 转换IP为字符串
ntohs(client_addr.sin_port), // 转换端口为本地字节序
buf);

// 服务端发送响应给客户端
const char* resp = "Server received the message!";
if (sendto(sockfd, resp, strlen(resp), 0,
(struct sockaddr*)&client_addr, client_addr_len) == -1) {
perror("sendto failed");
continue;
}
}

// 5. 关闭套接字(循环不会退出,实际开发需加退出逻辑)
close(sockfd);

return 0;
}

3.4 Linux UDP 客户端构建

核心流程

  1. socket 创建 UDP 套接字 → 2. 初始化服务端地址 → 3. sendto 发送数据给服务端 → 4. recvfrom 接收服务端响应 → 5. close 关闭套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// udp_client.c
#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" // 服务端IP(本地测试用本地回环地址127.0.0.1)
#define PORT 8080 // 服务端端口
#define BUF_SIZE 1024 // 缓冲区大小

int main() {
int sockfd; // 客户端套接字文件描述符
struct sockaddr_in server_addr; // 获取服务端地址
char buf[BUF_SIZE]; // 缓冲区
socklen_t server_addr_len = sizeof(server_addr);

// 1. 创建UDP套接字(和服务端一样)
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}

// 2. 初始化服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 将字符串IP转换为网络字节序IP(大端序)
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid server IP");
close(sockfd);
exit(EXIT_FAILURE);
}

// 3. 输入要发送的消息
printf("Please enter message to send: ");
fgets(buf, BUF_SIZE - 1, stdin);
// 去掉fgets读取的换行符
buf[strcspn(buf, "\n")] = '\0';

// 并发送数据给服务端
if (sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr*)&server_addr, server_addr_len) == -1) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}

// 4. 接收服务端响应 并 获取服务端的地址信息
ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr*)&server_addr, &server_addr_len);
if (recv_len == -1) {
perror("recvfrom failed");
close(sockfd);
exit(EXIT_FAILURE);
}
buf[recv_len] = '\0'; // 手动添加结束符
printf("Server response: %s\n", buf);

// 5. 关闭套接字
close(sockfd);

return 0;
}

3.5 UDP 服务端和客户端编译运行测试

编译 udp_server.c(服务端代码)和 udp_client.c(客户端代码)。

1
2
3
4
# 编译服务端
gcc udp_server.c -o udp_server
# 编译客户端
gcc udp_client.c -o udp_client

测试运行:


3.6 UDP 和 TCP 的对比

对比维度 UDP TCP
连接性 无连接 面向连接(三次握手建立,四次挥手关闭)
可靠性 不可靠(不确认、不重传、不排序) 可靠(确认、重传、排序、流量控制、拥塞控制)
传输效率 高(首部小、无额外逻辑) 低(首部大、有连接、重传等开销)
数据边界 有(数据报独立,一次接收一个) 无(字节流,需应用层自己处理边界)
适用场景 实时通信(直播、游戏、VOIP)、DNS 查询、物联网 可靠传输(文件下载、网页浏览、聊天消息)


4 在 UDP 中调用 connect 函数

UDP 虽然是无连接的,但并不是不能调用 connect 函数

UDP 的 connect 并非建立像 TCP 那样的双向连接,而是告知内核 “该套接字后续只与指定的 IP 和端口通信”,本质是对套接字的 目的地址绑定。这种绑定能带来 4 个核心好处。

4.1 简化发送逻辑:无需重复指定目的地址

未调用 connect 时,每次发送数据必须用 sendto() 并传入目的地址(struct sockaddr_in),否则内核不知道数据要发给谁;

调用 connect 后,内核已记录目的地址,后续可直接用 send()(无需传地址),或继续用 sendto()(但地址参数可忽略),减少代码冗余和参数错误风险

在客户端代码中可进行如下修改:

1
2
3
4
5
6
7
8
9
// 创建套接字后调用connect绑定服务端地址
if (connect(sockfd, (struct sockaddr*)&server_addr, server_addr_len) == -1) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}

// 后续发送可直接用send(),无需传地址
send(sockfd, buf, strlen(buf), 0); // 替代原来的sendto(..., &server_addr, ...)

4.2 降低内核开销:减少重复路由查找

未调用 connect 时,每次 sendto() 内核都要重新解析目的 IP、查找路由表(确定从哪个网卡发送);​

调用 connect 后,内核会 缓存路由信息,后续发送直接复用缓存,尤其在 频繁与同一服务器通信(如游戏客户端、实时监控)场景下,能显著减少内核耗时。


4.3 支持错误通知:感知 “对方不可达”

UDP 的一大痛点是 丢包无反馈—— 若对方主机宕机或端口未监听,未 connect 的套接字会直接丢弃数据包,应用层完全不知道错误;​

调用 connect 后,内核会监听 ICMP 错误(如 “目标不可达”),并将错误通过 recvfrom()/recv() 返回给应用层(表现为返回 -1,errno设为 ECONNREFUSED等),方便排查问题。

错误处理示例

1
2
3
4
5
6
7
8
9
10
ssize_t recv_len = recv(sockfd, buf, BUF_SIZE-1, 0);
if (recv_len == -1) {
if (errno == ECONNREFUSED) {
printf("Error: Server is unreachable (port not listening)\n");
} else {
perror("recv failed");
}
close(sockfd);
exit(EXIT_FAILURE);
}

4.4 支持 “单播定向”:避免误收其他地址数据

未调用 connect 的 UDP 套接字 会接收所有发送到该端口的 UDP 数据报(无论来源 IP);

调用 connect 后,套接字仅接收 来自绑定目的地址 的数据报,相当于给数据接收加了过滤规则,适合需要 一对一专属通信 的场景。



5 UDP 服务端与客户端轮流收发消息

5.1 核心设计思路

  • 客户端:循环 “输入消息→发送→接收响应”,直到输入 “exit” 退出;​
  • 服务端:循环 “接收消息→判断是否退出→发送响应”,若收到 “exit” 则退出;
  • 双方都需处理 消息边界缓冲区清空,避免脏数据干扰。

5.2 服务端代码(轮流转发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#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>

#define PORT 8080
#define BUF_SIZE 1024

int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buf[BUF_SIZE];
socklen_t client_addr_len = sizeof(client_addr);

// 1. 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}

// 2. 初始化地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// 3. 绑定服务端地址
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP server started (port %d), enter 'exit' in client to quit...\n", PORT);

// 4. 循环轮流收发(增加循环与退出判断)
while (1) {
// 接收客户端消息
memset(buf, 0, BUF_SIZE); // 清空缓冲区,避免脏数据
ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE-1, 0,
(struct sockaddr*)&client_addr, &client_addr_len);
if (recv_len == -1) {
perror("recvfrom failed");
continue;
}
buf[recv_len] = '\0';
printf("Received from %s:%d: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buf);

// 判断是否退出(客户端发送"exit")
if (strcmp(buf, "exit") == 0) {
const char* exit_resp = "Server received exit request, closing...";
sendto(sockfd, exit_resp, strlen(exit_resp), 0,
(struct sockaddr*)&client_addr, client_addr_len);
printf("Client exited, server shutting down...\n");
break; // 退出循环,关闭套接字
}

// 发送响应(支持自定义回复,而非固定内容)
char resp_buf[BUF_SIZE];
snprintf(resp_buf, BUF_SIZE, "Server reply: %s", buf); // 拼接收到的消息
if (sendto(sockfd, resp_buf, strlen(resp_buf), 0,
(struct sockaddr*)&client_addr, client_addr_len) == -1) {
perror("sendto failed");
continue;
}
}

// 5. 关闭套接字
close(sockfd);

return 0;
}

5.3 客户端代码(轮流转发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#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>

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

int main() {
int sockfd;
struct sockaddr_in server_addr;
char buf[BUF_SIZE];
socklen_t server_addr_len = sizeof(server_addr);

// 1. 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket create failed");
exit(EXIT_FAILURE);
}

// 2. 初始化服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid server IP");
close(sockfd);
exit(EXIT_FAILURE);
}

// (可选)调用connect优化发送逻辑
if (connect(sockfd, (struct sockaddr*)&server_addr, server_addr_len) == -1) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to server %s:%d, enter messages (type 'exit' to quit)\n",
SERVER_IP, PORT);

// 3. 循环轮流收发(核心修改:增加循环与退出判断)
while (1) {
// 输入要发送的消息
memset(buf, 0, BUF_SIZE); // 清空缓冲区
printf("Your message: ");
if (fgets(buf, BUF_SIZE - 1, stdin) == NULL) { // 处理输入错误
perror("fgets failed");
continue;
}
// 去掉fgets自带的换行符(避免消息带换行)
buf[strcspn(buf, "\n")] = '\0';

// 判断是否退出
if (strcmp(buf, "exit") == 0) {
send(sockfd, buf, strlen(buf), 0); // 用send(之前用了connect)
printf("Sending exit request to server...\n");
// 接收服务端的退出确认
memset(buf, 0, BUF_SIZE);
recv(sockfd, buf, BUF_SIZE - 1, 0);
printf("Server response: %s\n", buf);
break; // 退出循环
}

// 发送消息(已connect,用send更简洁)
if (send(sockfd, buf, strlen(buf), 0) == -1) {
perror("send failed");
continue;
}

// 接收服务端响应(加入丢包错误处理)
memset(buf, 0, BUF_SIZE);
ssize_t recv_len = recv(sockfd, buf, BUF_SIZE-1, 0);
if (recv_len == -1) {
if (errno == ECONNREFUSED) {
printf("Error: Server is unreachable\n");
break;
}
perror("recv failed");
continue;
}
buf[recv_len] = '\0';
printf("Server response: %s\n", buf);
}

// 4. 关闭套接字
close(sockfd);
printf("Client closed\n");

return 0;
}

5.4 编译与运行测试

编译代码

1
2
3
4
# 编译修改后的服务端
gcc udp_server_loop.c -o udp_server_loop
# 编译修改后的客户端
gcc udp_client_loop.c -o udp_client_loop

启动服务端

1
2
./udp_server_loop
# 输出:UDP server started (port 8888), enter 'exit' in client to quit...

启动客户端并测试轮流收发

1
2
3
4
5
6
7
8
9
10
./udp_client_loop
# 输出:Connected to server 127.0.0.1:8888, enter messages (type 'exit' to quit)
# 输入第一条消息:Hello, this is round 1
# 接收响应:Server response: Server reply: Hello, this is round 1
# 输入第二条消息:How are you?
# 接收响应:Server response: Server reply: How are you?
# 输入exit退出:
# 输出:Sending exit request to server...
# Server response: Server received exit request, closing...
# Client closed

运行结果:


「Linux 网络编程」Socket 构建 UDP 本地通信
https://marisamagic.github.io/2025/10/08/20251008/
作者
MarisaMagic
发布于
2025年10月8日
许可协议