一、 网络编程概述
1.1 概述
在现代软件开发与系统交互场景里,基于 Socket 的网络多进程通信占据核心地位,其适用场景广泛且深入到各类数字化交互中:
- 直播场景:主播端通过 Socket 建立的网络连接,将音视频流以数据包形式,依托 TCP 或 UDP 协议(依据场景选择,如低延迟需求可能用 UDP 结合纠错策略 ),实时传输到平台服务器,再由服务器通过多进程通信分发给百万级观众端,实现直播内容的跨地域传播。
- 文件发送与资源下载:发送端把文件拆分为网络数据包,借由 TCP 可靠传输(保障文件完整性 ),按序发送给接收端;资源下载同理,客户端与服务器通过 Socket 握手建立连接,以流式传输逐步获取资源,像浏览器下载软件安装包,背后就是基于 Socket 网络通信的文件传输逻辑。
- 即时聊天:聊天消息(文字、表情、语音片段 )作为字符串或二进制数据,通过网络载体发送,例如 QQ、微信等应用,利用自定义协议封装消息,基于 TCP 保证消息不丢包、不乱序,让双方主机能解析还原对话内容,实现实时交互。
对于 IOT(物联网) ,智能家居是典型场景:智能空调基于 TCP/UDP 协议,与家庭网关建立连接,采集室内温度(数据上发 ),同时接收手机 APP 发送的 MQTT 协议指令(如下发 "设置 26℃" ),完成制冷温度调节(下发控制 ),依托现有网络实现设备互联与智能控制。本质上,就是把物理设备数据与控制指令,通过网络协议转化为可传输、解析的信息,打通 "设备 - 网络 - 控制端" 链路 。
1.2 计算机网络发展
起源背景与 ARPA 成立
1957 年苏联发射 Sputnik(斯普特尼克)人造卫星,这一事件触动美国对军事、科技主导权的担忧 ------ 若苏联凭借卫星实现通信、情报优势,美国将陷入被动。因此,1958 年 1 月 7 日,美国紧急拨款成立 ARPA(Advanced Research Projects Agency,美国高级研究计划署 ) ,核心目标是突破苏联可能的技术封锁,研发先进网络、通信技术,为军事及科研提供支撑,后续 ARPA 推动的网络研究,成为现代计算机网络的重要起源 。
计算机网络核心需求(区别传统通信 )
- 非语音优先 :传统电话网聚焦语音通话,而计算机网络设计初衷是 数据传输与多设备协同 ,比如科研计算数据共享、军事信息交互,不局限于 "通话" 单一功能,更强调多类型数据(文件、指令、传感器数据等 )的传递 。
- 简单可靠 :早期网络用于科研、军事,需在复杂环境(如跨地域、恶劣条件 )下稳定运行,所以追求 结构简洁、传输容错性强 。例如,即便部分节点故障,仍能通过冗余路由传递数据,保障整体网络不瘫痪 。
- 异构设备兼容:要连接不同厂商、架构的计算机(如早期大型机、小型机 ),让 IBM 主机与 DEC 终端能互传数据,就得突破硬件、系统差异,制定通用通信规则,这也是网络协议诞生的驱动力之一 。
- 节点平等与路由冗余 :网络里没有 "中心主机优先",所有节点(如实验室的计算终端、军事监测设备 )地位等同 ,保障数据交互的去中心化;同时,"冗余路由" 是为了避免单点故障,像军事网络,若一条通信链路被破坏,能自动切换到备用链路,确保信息持续传输 。
- 唯一标识(IP 地址 ) :网络里每台设备必须有 独一无二的 "身份" ,才能精准收发数据,这就是 IP 地址的核心作用。
IP 地址体系(IPv4 & IPv6 )
- IPv4 :采用 32 位二进制 表示地址(如
192.168.1.1
,转换为二进制是 32 个 0/1 组合 ),分成 4 个 8 位段(即 "点分十进制" ),每个段取值0 - 255
。但由于互联网早期分配策略与地址消耗速度,IPv4 资源极度稀缺 ------ 中国仅拿到156.x.x.x
、188.x.x.x
等少量 "高级资源段"(实际是 A 类、B 类地址中的部分分配 ),导致局域网常用 NAT(网络地址转换 ) 技术,让多设备共享一个公网 IPv4,通过 "区域唯一 IP + 多级路由"(如家庭路由器分配192.168.xxx
内网地址,再映射到公网 IP )缓解地址不足问题 。 - IPv6 :为解决 IPv4 枯竭,推出 128 位二进制 地址(格式如
2001:0db8:85a3:0000:0000:8a2e:0370:7334
),地址空间从 IPv4 的约 43 亿个,暴增至近乎 "无限"(理论上约 \(2^{128}\) 个 ),能为全球设备(包括未来海量物联网终端 )分配独立公网地址,彻底解决地址短缺,推动万物互联更高效落地 。
1.3 TCP/IP & UDP 协议
协议本质与应用逻辑
基于 Socket(套接字 ) ,TCP/IP 和 UDP 是网络数据传输的 "规则框架",让不同设备(如手机与服务器、智能手表与云端 )能跨网络通信。两者都以 "报文头 + 数据体" 形式封装信息:
- "报文头" 类似快递面单:包含 "消息类型"(如 TCP 的连接标识、UDP 的端口号 )、源 IP、目的 IP 等,告诉网络 "数据从哪来、到哪去、按什么规则传" ;
- "数据体" 是实际内容:可以是聊天消息、文件片段、传感器数值等,依托 "面单(报文头 )" 的指引,在网络中流转 。
TCP/IP vs UDP 核心差异(以传输文件、直播为例 )
- TCP(传输控制协议 ) :
- 可靠传输 :通过 "三次握手" 建立连接(确保双方收发能力正常 )、"确认应答"(发出去的数据,对方必须回传确认,丢包就重发 )、"拥塞控制"(网络拥堵时自动降低发送速度 ),像 传输重要文件(如合同、安装包 ) ,必须保证数据完整,就依赖 TCP 。缺点是 传输延迟高 ,不适合对实时性要求极致的场景 。
- UDP(用户数据报协议 ) :
- 无连接、低延迟 :无需提前握手,直接发数据,省去连接耗时,像 直播推流、游戏实时交互 (如 FPS 游戏里的子弹射击指令 ),追求 "瞬间响应" ,哪怕偶尔丢包(画面卡顿一下、游戏子弹偶尔没同步 ),也优先保证实时性。但缺点是 不可靠 ,数据发出去不保证对方收到,需应用层自己处理丢包、乱序问题 。
网络分层模型(OSI 七层 & TCP/IP 四层 )
- OSI 七层结构 (理论参考 ):从下到上为 物理层(网线、光纤传输电信号 / 光信号 )→ 数据链路层(以太网协议,处理网卡收发包 )→ 网络层(IP 协议,负责寻址、路由 )→ 传输层(TCP/UDP,负责端到端数据可靠 / 快速传输 )→ 会话层(管理应用程序间的连接会话 )→ 表示层(数据加密、压缩、格式转换 )→ 应用层(HTTP、FTP 等具体应用协议 ) 。它是理想的 "标准化分层",便于理解网络各环节职责,但实际应用中较难严格分层实现 。
- TCP/IP 四层结构 (实际主流 ):简化为 网络接口层(对应 OSI 物理层 + 数据链路层,处理硬件、链路通信 )→ 网络层(IP 协议,管理地址、路由 )→ 传输层(TCP/UDP,负责端到端传输 )→ 应用层(HTTP、MQTT 等应用协议,直接对应用户功能 ) 。更贴合实际开发与协议设计,比如我们写代码调用 HTTP(应用层 ),底层自动通过 TCP(传输层 )、IP(网络层 )等完成通信,无需关注七层细节 。

组包和解包:

1.4 小端字节序和大选字节序

二、UDP编程
1. UDP 协议概述
- 无连接特性:UDP 协议面向【无连接】,通信前无需像 TCP 那样建立连接,直接发送数据报,降低通信初始开销 。
- 角色平等性:不区分客户端和服务器,仅有发送端和接收端概念,双方都可主动发数据,灵活适用于广播、组播场景 。
- 传输可靠性:因无连接,数据传输无确认、重传机制,存在丢包、乱序风险,数据安全性依赖应用层保障 。
- 传输效率:无需连接管理、确认等额外流程,数据传递速度较快 。
- 典型应用场景:适用于广播、组播,以及对数据完整性要求不绝对严格的场景,如直播(容忍偶尔丢包不影响观看流畅性 )、网络游戏(优先保证实时交互,轻微数据丢失可通过游戏逻辑弥补 )。
2. 本地数据转网络相关函数
2.1 htons
:本地 16 位数据转网络数据【小转大】
-
函数原型 :
cpp#include <arpa/inet.h> uint16_t htons(uint16_t hostshort);
-
功能 :将无符号
short
类型本地小端字节序数据(如主机存储的端口号 ),转换为网络大端字节序,适配网络传输要求(网络协议通常以大端序解析数据 ),常用于【端口号小转大】 。 -
参数 :
uint16_t hostshort
,uint16_t
是无符号short
类型(16 位二进制位 ),参数内容为本地主机 16 位二进制数据(如端口号实际值 )。 -
返回值 :转换后符合网络大端字节序的 16 位无符号
short
类型数据 。
2.2 htonl
:本地 32 位数据转网络数据【小转大】
-
函数原型 :
cpp#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong);
-
功能 :把无符号
int
类型本地小端字节序数据(如 32 位 IP 地址 ),转为网络大端字节序,常用于【32 位int
类型 IP 地址小转大】 。 -
参数 :
uint32_t hostlong
,uint32_t
是无符号int
类型(32 位二进制位 ),参数为本地主机 32 位二进制数据(如 IP 地址实际值 )。 -
返回值 :转换后符合网络大端字节序的 32 位无符号
int
类型数据 。
自定义实现示例(补充代码逻辑 )
以下是手动实现 my_htons
、my_htonl
及测试函数的代码,展示字节序转换逻辑:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
uint16_t my_htons(uint16_t host_value) {
uint16_t net_value = 0;
// 小端转大端:交换高低 8 位
net_value |= (host_value << 8) | (host_value >> 8);
return net_value;
}
uint32_t my_htonl(uint32_t host_value) {
uint32_t net_value = 0;
// 小端转大端:重新排列 4 个字节顺序
net_value |= (host_value & 0xFF) << 24;
net_value |= ((host_value >> 8) & 0xFF) << 16;
net_value |= ((host_value >> 16) & 0xFF) << 8;
net_value |= (host_value >> 24);
return net_value;
}
void test_htons(void) {
uint16_t host_value = 7385;
uint16_t net_value = htons(host_value);
printf("htons net_value : %d\n", net_value);
net_value = my_htons(host_value);
printf("my_htons net_value : %d\n", net_value);
}
void test_htonl(void) {
uint32_t host_value = 0xC0A80D48; // 示例 IP 地址转换前值
uint32_t net_value = htonl(host_value);
printf("htonl net_value : %d\n", net_value);
net_value = my_htonl(host_value);
printf("my_htonl net_value : %d\n", net_value);
}
int main(int argc, char const *argv[]) {
test_htons();
test_htonl();
return 0;
}
- 逻辑说明 :
my_htons
通过位移操作,交换 16 位数据的高低 8 位,实现小端到网络大端(大端通常等价于 "高字节在前" )转换 。my_htonl
拆分 32 位数据的 4 个字节,重新按大端序(高字节对应高位 )拼接,完成小端到网络大端转换 。
2.3 ntohs
:网络 16 位数据转本地数据【大转小】
-
函数原型 :
cpp#include <arpa/inet.h> uint16_t ntohs(uint16_t netshort);
-
功能 :将无符号
short
类型网络大端字节序数据(如网络接收的端口号 ),转换为本地小端字节序,适配主机存储解析,常用于【端口号大转小】 。 -
参数 :
uint16_t netshort
,表示网络大端序的 16 位无符号short
类型数据(如网络传来的端口号 )。 -
返回值 :转换后符合本地小端字节序的 16 位无符号
short
类型数据 。
2.4 ntohl
:网络 32 位数据转本地数据【大转小】
-
函数原型 :
cpp#include <arpa/inet.h> uint32_t ntohl(uint32_t netlong);
-
功能 :把无符号
int
类型网络大端字节序数据(如网络接收的 32 位 IP 地址 ),转为本地小端字节序,常用于【32 位int
类型 IP 地址大转小】 。 -
参数 :
uint32_t netlong
,表示网络大端序的 32 位无符号int
类型数据(如网络传来的 IP 地址 )。 -
返回值 :转换后符合本地小端字节序的 32 位无符号
int
类型数据 。
3. IPv4 地址相关操作
3.1 IPv4 地址描述
IPv4 地址有两种表示形式:
- 点分十进制字符串 :如
192.168.13.72
,直观易读,要求字符串长度最小 16 字节(保障数据完整性 ),用于人机交互展示 。 - 4 字节
int
类型数值:网络传输、程序内部操作时常用,以紧凑二进制形式存储,减少数据量,提升传输、处理效率 。
3.2 点分十进制 IP 转网络 4 字节 int
类型 IP 地址
- 函数
inet_pton
:-
原型 :
cpp#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst);
-
功能 :将【点分十进制】IPv4/IPv6 字符串(如
192.168.13.20
),转换为对应协议的 32 位(IPv4 )或 128 位(IPv6 )整数 IP 描述形式,存入指定内存 。 -
参数 :
int af
:协议族,AF_INET
对应 IPv4,AF_INET6
对应 IPv6 。const char *src
:点分十进制格式的 IP 地址字符串(如192.168.13.20
)。void *dst
:存储转换后 IP 地址二进制数据的内存地址(如uint32_t
变量地址,用于存 IPv4 转换结果 )。
-
返回值 :转换成功返回
1
;地址族不支持返回0
;输入无效(非合法 IP 格式 )返回-1
,并设置errno
为EINVAL
。
-
代码示例(inet_pton
应用 )
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[]) {
// 点分十进制 IPv4 地址
char *str = "192.168.13.72";
// 存储转换后的 4 字节 int 类型 IP 数据
uint32_t ip_value = 0;
// 调用 inet_pton 转换,AF_INET 表示 IPv4 协议
int ret = inet_pton(AF_INET, str, &ip_value);
if (ret != 1) {
perror("inet_pton error!");
exit(EXIT_FAILURE);
}
// 转换后为网络大端序 4 字节 IP 数据,符合网络传输要求
printf("ip_value : %d\n", ip_value);
return 0;
}
- 说明 :
192.168.13.72
转换后,ip_value
存储大端序二进制数据(如示例中对应1208854720
),可用于网络通信的 IP 地址设置 。
3.3 网络 4 字节 int
类型 IP 地址转点分十进制 IP 地址
- 函数
inet_ntop
:-
原型 :
cpp#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
功能:将 32 位(IPv4 )或 128 位(IPv6 )整数形式的 IP 地址,按协议要求转换为【点分十进制】(IPv4 )或对应格式(IPv6 )的字符串,支持 IPv4、IPv6 。
-
参数 :
int af
:协议族(AF_INET
对应 IPv4,AF_INET6
对应 IPv6 )。const void *src
:存储 IP 地址二进制数据的内存地址(如uint32_t
变量地址,存 IPv4 大端序数据 )。char *dst
:存储转换后点分十进制字符串的缓冲区,IPv4 要求最小 16 字节空间 。socklen_t size
:dst
缓冲区的字节长度 。
-
返回值 :转换成功返回指向
dst
的指针(即点分十进制字符串首地址 );失败返回NULL
。
-
代码示例(inet_ntop
应用 )
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[]) {
// 网络大端序 4 字节 IP 数据(对应 192.168.13.72 转换后的值 )
uint32_t net_ip = 1208854720;
// 存储点分十进制 IP 的缓冲区
char ip_addr[16] = "";
// 调用 inet_ntop 转换,AF_INET 表示 IPv4 协议
const char *ip = inet_ntop(AF_INET, &net_ip, ip_addr, 16);
if (NULL == ip) {
perror("inet_ntop error!");
exit(EXIT_FAILURE);
}
printf("ip_addr : %s\n", ip_addr);
printf("ip : %s\n", ip);
return 0;
}
- 说明 :
net_ip
是网络大端序的 4 字节 IP 数据,经inet_ntop
转换后,ip_addr
存储点分十进制字符串(如192.168.13.72
),实现 "网络二进制 IP" 到 "可读字符串" 的转换 。
4. UDP 编程
4.1 UDP 编程流程

-
UDP 发送端 :
socket()
→sendto()
→close()
socket()
:创建网络通信套接字,建立通信 "通道" 。sendto()
:指定目标地址、端口,发送 UDP 数据报 。close()
:关闭套接字,释放资源 。
-
UDP 接收端 :
socket()
→bind()
→recvfrom()
→close()
socket()
:创建套接字 。bind()
:绑定本地 IP、端口,让套接字监听指定地址的数据 。recvfrom()
:阻塞等待接收 UDP 数据报,同时获取发送端地址信息 。close()
:关闭套接字 。
4.2 UDP 编程相关函数
4.2.1 socket
:申请创建 Socket 套接字
-
函数原型 :
cpp#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
-
功能:创建网络通信套接字,返回对应文件描述符,作为后续网络操作(收发数据等 )的标识 。
-
参数 :
int domain
:协议族,如AF_INET
(IPv4 协议 )、AF_INET6
(IPv6 协议 )。int type
:套接字类型,SOCK_DGRAM
表示 UDP 套接字(无连接、基于数据报 );SOCK_STREAM
表示 TCP 套接字(面向连接、基于流 );SOCK_RAW
表示原始套接字(可直接操作网络层协议 )。int protocol
:协议类型,IPPROTO_UDP
表示 UDP 协议;IPPROTO_TCP
表示 TCP 协议 。
-
返回值 :成功返回套接字文件描述符(非负整数 );失败返回
-1
。
代码示例(socket
创建 UDP 套接字 )
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char const *argv[]) {
// 存储套接字文件描述符
int socket_fd = -1;
// 创建 UDP 套接字:AF_INET(IPv4 ) + SOCK_DGRAM(UDP 类型 ) + IPPROTO_UDP(UDP 协议 )
socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (-1 == socket_fd) {
perror("socket error!");
exit(EXIT_FAILURE);
}
printf("socket_fd : %d\n", socket_fd);
getchar();
close(socket_fd);
return 0;
}
补充说明:struct sockaddr
和 struct sockaddr_in
结构体
-
struct sockaddr
:通用套接字地址结构体,系统定义的标准格式,用于兼容不同协议地址:cppstruct sockaddr { sa_family_t sa_family; // 地址族(如 AF_INET ) char sa_data[14]; // 协议地址(14 字节,存具体地址、端口等 ) };
-
struct sockaddr_in
:针对 IPv4 协议的地址结构体,更直观描述 IPv4 通信地址:cppstruct sockaddr_in { sa_family_t sin_family; // 地址族(固定为 AF_INET ) in_port_t sin_port; // 16 位端口号(网络字节序 ) struct in_addr sin_addr; // 32 位 IPv4 地址(网络字节序 ) char sin_zero[8]; // 填充字段,使结构体长度与 sockaddr 一致 };
cpp
struct in_addr {
uint32_t s_addr; // 32位无符号整数,存储IPv4地址(网络字节序)
};
- 该结构体专门用于存储 IPv4 地址的 32 位二进制表示,
s_addr
字段的值必须是网络字节序 (大端序),通常通过inet_pton
等函数转换得到。
结构体使用说明
sockaddr
与sockaddr_in
的关系 :
两者内存大小相同(均为 16 字节),sockaddr_in
是AF_INET
协议族的专用结构体,字段更清晰;而sockaddr
是通用结构体,作为系统调用(如bind
、sendto
)的参数类型。使用时需将sockaddr_in*
强制转换为sockaddr*
。- 初始化注意事项 :
sin_zero
字段需用bzero
或memset
清零,保证与sockaddr
结构体大小一致;sin_family
必须设为AF_INET
;sin_port
和sin_addr.s_addr
必须转换为网络字节序。
4.2.2 bind
:绑定套接字到本地地址和端口
-
函数原型 :
cpp#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:将创建的套接字与本地 IP 地址和端口绑定,确保数据能通过该地址和端口收发(服务器必须绑定,客户端可选)。
-
参数 :
sockfd
:socket
函数返回的套接字文件描述符。addr
:指向sockaddr
(或sockaddr_in
)结构体的指针,包含本地 IP 和端口信息(需转换为网络字节序)。addrlen
:addr
结构体的字节长度(如sizeof(struct sockaddr_in)
)。
-
返回值 :成功返回
0
;失败返回-1
。
4.2.3 sendto
:发送 UDP 数据报
-
函数原型 :
cpp#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);
-
功能:向指定目标地址发送 UDP 数据报(无需提前建立连接)。
-
参数 :
sockfd
:套接字文件描述符。buf
:待发送数据的缓冲区地址。len
:待发送数据的字节长度。flags
:发送标志(通常为0
,表示默认行为)。dest_addr
:指向目标地址结构体(sockaddr_in
)的指针,包含对方 IP 和端口。addrlen
:目标地址结构体的字节长度。
-
返回值 :成功返回发送的字节数;失败返回
-1
。
4.2.4 recvfrom
:接收 UDP 数据报
-
函数原型 :
cpp#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
-
功能:接收 UDP 数据报,并获取发送方的地址信息。
-
参数 :
sockfd
:套接字文件描述符。buf
:存储接收数据的缓冲区地址。len
:缓冲区的最大容量(字节)。flags
:接收标志(通常为0
)。src_addr
:输出参数,用于存储发送方的地址信息(需提前分配内存)。addrlen
:输入输出参数,输入时为src_addr
的容量,输出时为实际地址长度。
-
返回值 :成功返回接收的字节数;失败返回
-1
。
4.2.5 close
:关闭套接字
-
函数原型 :
cpp#include <unistd.h> int close(int fd);
-
功能:关闭套接字,释放相关资源。
-
参数 :
fd
为套接字文件描述符。 -
返回值 :成功返回
0
;失败返回-1
。
三、UDP 客户端与服务器完整通信示例
服务器代码(udp_server.c
)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8888 // 服务器端口号
#define BUFFER_SIZE 1024 // 缓冲区大小
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 1. 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 初始化服务器地址结构体
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地IP
server_addr.sin_port = htons(PORT); // 端口转换为网络字节序
// 3. 绑定套接字到本地地址和端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP server started on port %d...\n", PORT);
while (1) {
// 4. 接收客户端数据
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_addr, &client_addr_len);
if (recv_len == -1) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0'; // 手动添加字符串结束符
// 打印客户端信息和接收的数据
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Received from %s:%d: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);
// 若收到"exit",则退出服务器
if (strcmp(buffer, "exit") == 0) {
printf("Server exiting...\n");
break;
}
// 5. 回复客户端(echo功能)
const char* reply = "Message received";
if (sendto(sockfd, reply, strlen(reply), 0,
(struct sockaddr*)&client_addr, client_addr_len) == -1) {
perror("sendto failed");
}
}
// 6. 关闭套接字
close(sockfd);
return 0;
}
客户端代码(udp_client.c
)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERVER_IP "127.0.0.1" // 服务器IP(本地回环地址)
#define SERVER_PORT 8888 // 服务器端口号
#define BUFFER_SIZE 1024 // 缓冲区大小
int main() {
int sockfd;
struct sockaddr_in server_addr;
socklen_t server_addr_len = sizeof(server_addr);
char buffer[BUFFER_SIZE];
// 1. 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 初始化服务器地址结构体
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
// 转换服务器IP为网络字节序
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) != 1) {
perror("invalid server IP");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Enter message to send (type 'exit' to quit):\n");
while (1) {
// 3. 从标准输入获取数据
printf("> ");
fgets(buffer, BUFFER_SIZE, stdin);
// 移除fgets添加的换行符
buffer[strcspn(buffer, "\n")] = '\0';
// 4. 发送数据到服务器
if (sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, server_addr_len) == -1) {
perror("sendto failed");
continue;
}
// 若发送"exit",则退出客户端
if (strcmp(buffer, "exit") == 0) {
printf("Client exiting...\n");
break;
}
// 5. 接收服务器回复
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
NULL, NULL); // 不关心服务器地址,设为NULL
if (recv_len == -1) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0';
printf("Server reply: %s\n", buffer);
}
// 6. 关闭套接字
close(sockfd);
return 0;
}