UDP 通信接口全维度解析:API 设计原理、调用规范与应用实战

前言:

  1. 概述网络通信中socket套接字的主要编程接口
  2. 针对UDP通信,对相关socket接口进行详细阐述
  3. UDP无连接网络通信:服务-客户端实现

目录

  • [1. socket编程接口](#1. socket编程接口)
  • [2. UDP通信相关接口详解](#2. UDP通信相关接口详解)
    • [2.1 创建套接字socket](#2.1 创建套接字socket)
    • [2.2 绑定端口号bind](#2.2 绑定端口号bind)
    • [2.3 sockaddr数据类型(IPv4)struct sockaddr_in类型(套接字地址)](#2.3 sockaddr数据类型(IPv4)struct sockaddr_in类型(套接字地址))
      • [2.3.1 sockaddr类型及参数详解](#2.3.1 sockaddr类型及参数详解)
      • [2.3.2 sockaddr_in类型及参数详解](#2.3.2 sockaddr_in类型及参数详解)
    • [2.4 bzero(内存清0)](#2.4 bzero(内存清0))
    • [2.5 点分十进制IP与网络字节序相互转换(仅IPv4)](#2.5 点分十进制IP与网络字节序相互转换(仅IPv4))
      • [1. inet_addr(点分十进制IP转网络字节序)](#1. inet_addr(点分十进制IP转网络字节序))
      • [2. inet_ntoa(网络字节序转点分十进制IP)](#2. inet_ntoa(网络字节序转点分十进制IP))
    • [2.6 port端口号与网络字节序相互转换](#2.6 port端口号与网络字节序相互转换)
      • [1. htons 函数:16位主机字节序 → 16位网络字节序](#1. htons 函数:16位主机字节序 → 16位网络字节序)
      • [2. ntohs 函数:16位网络字节序 → 16位主机字节序](#2. ntohs 函数:16位网络字节序 → 16位主机字节序)
      • [3. htonl 函数:32位主机字节序 → 32位网络字节序](#3. htonl 函数:32位主机字节序 → 32位网络字节序)
      • [4. ntohl 函数:32位网络字节序 → 32位主机字节序](#4. ntohl 函数:32位网络字节序 → 32位主机字节序)
    • [2.7 收发信息recvfrom & sendto](#2.7 收发信息recvfrom & sendto)
      • [1. 接收消息](#1. 接收消息)
      • [2. 发送消息](#2. 发送消息)
  • [3. 网络通信 服务-客户端实现(UDP)](#3. 网络通信 服务-客户端实现(UDP))
    • [3.1 锁&日志信息打印](#3.1 锁&日志信息打印)
    • [3.2 服务端实现](#3.2 服务端实现)
    • [3.3 客户端实现](#3.3 客户端实现)
    • [3.4 Makefile实现](#3.4 Makefile实现)
    • [3.5 Linux系统查看本机IP地址](#3.5 Linux系统查看本机IP地址)
    • [3.6 功能验证](#3.6 功能验证)
  • [4. 完整版代码](#4. 完整版代码)

1. socket编程接口

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

socket API 是一层抽象的网络编程接口,具有良好的兼容性和通用性,适用于各种底层网络协议,如 IPv4、IPv6,以及后续要讲解的 UNIX Domain Socket(本地域套接字)。然而,不同底层网络协议的地址格式并不相同,为了统一接口并支持多种协议,就有了 sockaddr 相关结构体的设计。

首先,我们回顾一个核心结论:网络通信的本质,其实就是跨主机的进程间通信。而进程间通信主要分为两大类别,对应不同的标准与实现:

  1. System V 标准 :主要针对本地进程间通信(同一主机内的不同进程),核心实现包括共享内存、消息队列、信号量等,仅适用于本机内的进程交互,无法支持跨网络通信。
  2. POSIX 标准 :兼容性更强,既支持网络进程间通信 (跨主机进程交互),也支持本地进程间通信 ,socket 通信就是 POSIX 标准下的核心实现,对应两种核心 socket 类型:网络 socket (用于跨网络通信)与本地 socket(也称为 UNIX Domain Socket,用于本机内高效进程通信)。

关于 socket 通信的接口设计,有几个关键的设计思路与实现细节:

socket 存在多种类型(如流式 socket、数据报式 socket),用于满足不同的应用场景(如可靠传输、高效实时传输)。

socket 后续的各类通信接口(如 bind()connect()),本可以针对不同通信场景(网络、本地)设计不同的接口规范。

但 socket 的设计者更希望提供一套统一、通用的通信接口,简化编程难度,提升接口的可扩展性与兼容性。

因此,在数据传输相关的 socket 接口调用中,地址参数只能使用 const struct sockaddr * 这一通用类型;而在实际定义与构建地址结构体时,则根据通信场景的不同选择对应的专用结构体:网络通信(IPv4)使用 struct sockaddr_in,本地通信使用 struct sockaddr_un

这种设计思路可以类比编程语言中的继承与多态struct sockaddr 如同基类(提供统一的接口抽象),struct sockaddr_instruct sockaddr_un 如同派生类(针对具体场景实现具体的数据结构),通过强制类型转换实现接口的统一调用与数据的差异化 存储。

  • IPv4 和 IPv6 的地址格式均定义在头文件 netinet/in.h 中,其中 IPv4 地址专门使用 sockaddr_in 结构体(互联网地址结构体)来表示,该结构体核心包含三个部分:16 位地址类型字段、16 位端口号字段、32 位 IPv4 地址字段(此外还包含预留填充字段,用于保证结构体大小与通用 sockaddr 结构体一致)。
  • IPv4、IPv6 对应的地址类型分别被定义为常量 AF_INET(Address Family: Internet,互联网协议族)、AF_INET6。基于这个设计,只要获取到某种 sockaddr 系列结构体的首地址,即使不需要知道具体是哪种协议的地址结构体,也可以通过解析开头的 16 位地址类型字段,确定该结构体的具体类型和后续内容格式。
  • socket API 的各类函数(如 bind()connect()accept())均使用通用的 struct sockaddr * 类型作为地址参数,在实际使用针对 IPv4 协议编程时,需要将 sockaddr_in 结构体指针强制转换为 struct sockaddr * 类型传入。这样设计的核心好处是保证了程序的通用性和可扩展性,使得 socket API 能够统一接收 IPv4、IPv6 以及 UNIX Domain Socket 等多种类型的 sockaddr 结构体指针作为参数,无需为每种协议单独设计一套 API 接口。

2. UDP通信相关接口详解

2.1 创建套接字socket

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 参数:
//	  domain:指定套接字的地址族,决定通信的地址格式和范围,常用AF_INET(IPv4 网络通信)、AF_INET6(IPv6 网络通信)、AF_UNIX(本地进程间通信)。
//    type:指定套接字的通信类型,决定数据传输特性,常用SOCK_STREAM(面向连接、可靠流式传输,对应 TCP)、SOCK_DGRAM(无连接、不可靠数据报传输,对应 UDP)、SOCK_RAW(原始套接字,可访问底层协议)。
//	  protocol:指定具体通信协议,最常用设为0,让系统根据domain和type自动选择默认协议(如 TCP/UDP),也可手动指定IPPROTO_TCP、IPPROTO_UDP等,需与前两个参数兼容。

// 返回值:如果成功,则返回新套接字的文件描述符。如果出现错误,则返回-1,并适当地设置errno。

2.2 绑定端口号bind

cpp 复制代码
// 绑定端口号 (TCP/UDP, 服务器)
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 功能:将指定的本地网络地址(IP 地址 + 端口号)与套接字描述符绑定,建立套接字与本地地址的关联,让客户端能够通过该地址定位并连接到服务器。

// 参数:
// 		sockfd:即 socket() 函数返回的套接字描述符,用于指定要绑定地址信息的目标套接字,是后续操作套接字的唯一标识。
// 		addr:通用地址结构体指针,实际传入对应地址族的具体地址结构体(如 AF_INET 对应 sockaddr_in),存放要绑定的本地 IP 地址和端口号等信息。
// 		addrlen:指定 addr 指向的地址结构体的实际字节长度,让操作系统能够正确解析地址信息,避免解析异常。

// 返回值:调用成功返回 0,表示地址已成功绑定到套接字;调用失败返回-1,具体错误信息存入全局变量errno可供查询。

关于bind绑定本机IP地址:

  • 云服务器场景下,不允许直接绑定公有 IP 地址 (公有 IP 通常由云服务商的网关 / 弹性网卡进行转发,并非主机本地网卡直接配置的有效本地 IP,直接绑定会导致调用失败);同时,在编写服务器程序时,我们也不推荐绑定某一个明确的具体 IP 地址 ,更推荐将绑定的 IP 地址参数设置为INADDR_ANY(对应 IPv4 的 0.0.0.0),以提升服务的灵活性和兼容性。
  • 在网络编程中,当服务器进程调用bind函数绑定端口以监听网络请求时,使用INADDR_ANY作为 IP 地址参数,其核心含义是:让该绑定的端口监听本机所有网络接口(网卡)的 IP 地址 ,能够接受来自任意来源 IP 地址的连接请求(包括本地回环地址 127.0.0.1、本机其他网卡的私有 IP、远程主机的公网 IP 等)。例如,若服务器主机配备了多个网卡(每个网卡对应不同的 IP 地址,如内网 IP、外网 IP),使用INADDR_ANY无需单独为每个网卡 / IP 配置端口监听,即可统一接收发送到本机任意 IP、对应端口的网络数据,省去了区分具体网卡和 IP 的繁琐操作,同时也能适配主机 IP 地址变更、网卡增减的场景,大幅简化服务器部署与维护成本。

2.3 sockaddr数据类型(IPv4)struct sockaddr_in类型(套接字地址)

1.sockaddr:通用套接字地址结构体,提供统一 API 接口,不直接操作。

2.sockaddr_in:IPv4 专用套接字地址结构体,实际用于填充 / 操作 IPv4 地址信息,使用时需强制转换为sockaddr*

2.3.1 sockaddr类型及参数详解

cpp 复制代码
#include <netinet/in.h>

struct sockaddr {
    sa_family_t sa_family;  // 地址族/协议族标识(2字节)
    char        sa_data[14]; // 无结构的原始地址数据(14字节)
};

😊成员解析

  • sa_family_t sa_family
    • 类型:sa_family_t(本质是无符号短整数,对应uint16_t)。
    • 作用:标识当前结构体对应的地址族 / 协议族 ,取值为预定义常量,例如:
      • AF_INET:IPv4 协议(对应sockaddr_in)。
      • AF_INET6:IPv6 协议(对应sockaddr_in6)。
      • AF_UNIXAF_LOCAL):本地进程间通信(对应sockaddr_un)。
    • 核心意义:告诉操作系统如何解析后续的sa_data字段。
  • char sa_data[14]
    • 类型:固定长度为 14 的字符数组。
    • 作用:存储该地址族对应的「原始地址数据」,包含端口号、IP 地址等信息,但无固定结构,直接以二进制字节流存储。
    • 局限性:无法直接通过字段名访问端口、IP 等具体信息,只能通过专用函数解析,无法手动赋值或修改。

😂核心特点

  1. 统一性 :POSIX 标准定义的通用接口,所有网络地址相关的 API 函数(bind()connect()accept()等)均以struct sockaddr*作为参数类型,兼容不同协议的地址结构体。
  2. 抽象性:不区分具体网络协议,仅提供「地址族标识 + 原始地址数据」的通用框架,无法直接操作具体地址字段。
  3. 固定大小 :总大小为 2+14=16 字节,为了与专用地址结构体(如sockaddr_in)进行内存对齐,保证强制类型转换的安全性。

😁核心用途与使用限制

  • 核心用途:作为网络 API 的统一参数类型,实现「一个接口兼容多种协议地址」,避免为每种协议单独定义 API 函数。
  • 使用限制:几乎从不直接定义和使用,仅作为强制类型转换的「目标类型」,无法直接填充或解析地址数据。

2.3.2 sockaddr_in类型及参数详解

😒成员解析

  • sa_family_t sin_family
    • sockaddr.sa_family对应,必须且只能赋值为AF_INET(标识当前为 IPv4 协议地址)。
    • 若赋值为其他地址族常量(如AF_INET6),会导致后续网络 API 调用失败(返回错误码EINVAL)。
  • in_port_t sin_port
    • 类型:in_port_t(本质是uint16_t,无符号 16 位整数)。
    • 作用:存储 IPv4 通信的 TCP/UDP 端口号,必须是网络字节序(大端序) ,需通过htons()函数将主机字节序的端口号转换后赋值。
    • 示例:serv_addr.sin_port = htons(8080);(将 8080 端口转换为网络字节序)。
  • struct in_addr sin_addr
    • 类型:专用in_addr结构体,核心成员为in_addr_t s_addr(本质是uint32_t,无符号 32 位整数)。
    • 作用:存储 IPv4 地址,必须是网络字节序(大端序) ,可通过inet_addr()inet_pton()等函数转换后赋值。
    • 示例:inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);(将点分十进制 IP 转换为网络字节序并赋值)。
  • unsigned char sin_zero[8]
    • 类型:固定长度为 8 的无符号字符数组。
    • 核心作用:填充内存,保证sockaddr_in的总大小与sockaddr一致(均为 16 字节),实现内存布局兼容。
    • 使用规范:通常通过memset()函数将其置为 0,无需手动赋值其他数据,填充后不影响地址解析。
    • 计算验证:2(sin_family)+2(sin_port)+4(sin_addr)+8(sin_zero)=16 字节,与sockaddr大小完全匹配。

😆核心特点

  1. 专用性 :仅支持 IPv4 协议,是 IPv4 地址的结构化载体,对应 IPv6 的专用结构体为sockaddr_in6
  2. 结构化:将 IPv4 地址信息拆分为「地址族、端口号、IP 地址」等独立字段,可直接通过字段名访问和修改,方便开发者操作。
  3. 内存兼容性 :总大小 16 字节,与sockaddr内存布局对齐,支持安全的强制类型转换。
  4. 实用性 :是网络编程中实际用于填充、修改、解析 IPv4 地址信息的核心结构体,开发者日常操作的均是该结构体。

😃核心用途

  • 存储 IPv4 通信所需的完整地址信息(端口 + IP)。
  • bind()connect()等函数提供结构化的 IPv4 地址数据,使用前需强制转换为struct sockaddr*类型。
  • 解析从网络中接收的 IPv4 地址信息(如accept()函数返回的客户端地址)。
cpp 复制代码
/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port;			/* Port number.  */
struct in_addr sin_addr;		/* Internet address.  */

/* Pad to size of `struct sockaddr'.  */
unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };


#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

// 作用:定义了三个变量
// sa_family_t sin_family
// in_port_t   sin_port;
// struct in_addr sin_addr;

2.4 bzero(内存清0)

功能:将指定起始地址的连续 n 个字节内存区域,全部置为二进制 0(即内存清零)

cpp 复制代码
#include <string.h>
void bzero(void *s, size_t n);
// 参数:
//		void *s指向需要进行内存置零操作的起始地址,void* 类型支持接收任意数据类型的内存指针(如套接字结构体、数组、普通变量的地址等),确保函数的通用性。
//  	size_t n指定需要置零的内存字节数,size_t 是无符号整数类型,通常传入 sizeof() 计算目标结构体 / 变量的长度,确保对应内存区域全部被置为 0。

// 返回值:类型为 void,无任何返回值,调用后直接完成指定内存区域的置零操作,没有成功或失败的返回状态反馈。

2.5 点分十进制IP与网络字节序相互转换(仅IPv4)

1. inet_addr(点分十进制IP转网络字节序)

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
// 功能:将点分十进制格式的 IPv4 字符串(如 "192.168.1.100"),转换为网络字节序(大端序)的 32 位无符号整数,供套接字地址结构体(如 sockaddr_in)使用。
// 参数: const char *cp 指向点分十进制 IPv4 地址字符串的常量指针,要求字符串格式合法(如四段 0-255 的数字用点分隔),否则函数转换失败。

// 返回值:
// 		转换成功:返回 in_addr_t 类型的网络字节序 32 位整数,对应传入的 IPv4 地址,可直接赋值给 sockaddr_in 结构体的 sin_addr.s_addr 成员。
// 		转换失败:返回常量 INADDR_NONE(对应十六进制 0xFFFFFFFF,即点分十进制的 "255.255.255.255"),无法区分该合法地址与转换错误,这是该函数的局限性。

2. inet_ntoa(网络字节序转点分十进制IP)

IP到字符串转换 inet_ntoa // 4字节网络风格的IP -> 点分十进制的字符串风格的IP

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);
// 功能:将封装在 in_addr 结构体中的网络字节序(大端序)32 位无符号整数,转换为以 null 结尾的点分十进制格式 IPv4 字符串(如 "192.168.1.100"),供人工阅读、打印输出等场景使用。
// 参数: struct in_addr in  存储着网络字节序 IPv4 地址的 in_addr 结构体实例,该结构体的核心成员为 s_addr(用于存放 32 位网络字节序整数),要求传入的 in.s_addr 为合法的网络字节序 IPv4 地址,否则转换结果不可预期。该参数为值传递,可直接传入 sockaddr_in 结构体的 sin_addr 成员。

// 返回值:
// 		转换成功:返回 char* 类型的指针,指向函数内部维护的静态内存缓冲区,缓冲区中存放着转换后的 null 结尾点分十进制 IPv4 字符串。注意:该静态缓冲区会被后续调用的 inet_ntoa 函数覆盖,且无需手动释放该指针指向的内存,同时该函数不具备线程安全性。
// 		转换失败:返回 NULL 指针(部分系统环境下,无效输入可能导致返回不可预期的非法字符串),相较于 inet_addr,其失败标识更为明确,但仍存在静态缓冲区覆盖的固有局限性。

2.6 port端口号与网络字节序相互转换

以下四个函数的头文件相同

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

1. htons 函数:16位主机字节序 → 16位网络字节序

cpp 复制代码
// 1. htons 函数:16位主机字节序 → 16位网络字节序 host to network short
uint16_t htons(uint16_t hostshort);
// 功能:将16位主机字节序的无符号整数,转换为16位网络字节序(大端序)的无符号整数,解决跨主机架构的字节序差异,供网络通信中16位数据(如TCP/UDP端口号)的传输与存储使用。
// 参数: uint16_t hostshort  需转换的16位主机字节序无符号整数,无格式合法性强制要求,可直接传入本地表示的TCP/UDP端口号等数据,该参数为值传递。
// 返回值:
// 		转换成功:返回 uint16_t 类型的16位网络字节序无符号整数,可直接用于填充 sockaddr_in 结构体的 sin_port 成员。注意:若当前主机字节序已是大端序,函数原样返回参数值,保证跨平台兼容性。
// 		转换失败:该函数无明确的失败返回值,无论输入何种16位无符号整数,均会返回一个对应的16位无符号整数(仅做字节序排列调整或原样返回),无错误判定逻辑。

2. ntohs 函数:16位网络字节序 → 16位主机字节序

cpp 复制代码
// 2. ntohs 函数:16位网络字节序 → 16位主机字节序 network to host short
uint16_t ntohs(uint16_t netshort);
// 功能:将16位网络字节序(大端序)的无符号整数,转换为16位主机字节序的无符号整数,是 htons 函数的反向转换,供本地CPU解析、处理、打印从网络中接收的16位数据(如TCP/UDP端口号)使用。
// 参数: uint16_t netshort  需转换的16位网络字节序无符号整数,通常是从网络通信中接收的原始数据(如 sockaddr_in 结构体的 sin_port 成员),该参数为值传递。
// 返回值:
// 		转换成功:返回 uint16_t 类型的16位主机字节序无符号整数,可供本地程序直接运算、打印展示。注意:若当前主机字节序已是大端序,函数原样返回参数值,保证跨平台兼容性。
// 		转换失败:该函数无明确的失败返回值,无论输入何种16位无符号整数,均会返回一个对应的16位无符号整数(仅做字节序排列调整或原样返回),无错误判定逻辑。

3. htonl 函数:32位主机字节序 → 32位网络字节序

cpp 复制代码
// 3. htonl 函数:32位主机字节序 → 32位网络字节序 host to network long
uint32_t htonl(uint32_t hostlong);
// 功能:将32位主机字节序的无符号整数,转换为32位网络字节序(大端序)的无符号整数,解决跨主机架构的字节序差异,供网络通信中32位数据(如IPv4地址)的传输与存储使用。
// 参数: uint32_t hostlong  需转换的32位主机字节序无符号整数,无格式合法性强制要求,可直接传入本地表示的32位IPv4地址等数据,该参数为值传递。
// 返回值:
// 		转换成功:返回 uint32_t 类型的32位网络字节序无符号整数,可直接用于填充 sockaddr_in 结构体的 sin_addr.s_addr 成员。注意:若当前主机字节序已是大端序,函数原样返回参数值,保证跨平台兼容性。
// 		转换失败:该函数无明确的失败返回值,无论输入何种32位无符号整数,均会返回一个对应的32位无符号整数(仅做字节序排列调整或原样返回),无错误判定逻辑。

4. ntohl 函数:32位网络字节序 → 32位主机字节序

cpp 复制代码
// 4. ntohl 函数:32位网络字节序 → 32位主机字节序 network to host long
uint32_t ntohl(uint32_t netlong);
// 功能:将32位网络字节序(大端序)的无符号整数,转换为32位主机字节序的无符号整数,是 htonl 函数的反向转换,供本地CPU解析、处理、打印从网络中接收的32位数据(如IPv4地址)使用。
// 参数: uint32_t netlong  需转换的32位网络字节序无符号整数,通常是从网络通信中接收的原始数据(如 sockaddr_in 结构体的 sin_addr.s_addr 成员),该参数为值传递。
// 返回值:
// 		转换成功:返回 uint32_t 类型的32位主机字节序无符号整数,可供本地程序直接运算、打印展示。注意:若当前主机字节序已是大端序,函数原样返回参数值,保证跨平台兼容性。
// 		转换失败:该函数无明确的失败返回值,无论输入何种32位无符号整数,均会返回一个对应的32位无符号整数(仅做字节序排列调整或原样返回),无错误判定逻辑。

2.7 收发信息recvfrom & sendto

1. 接收消息

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

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

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

// (recvfrom)功能: 从指定套接字接收数据,同时获取发送方的网络地址信息(IP + 端口),无需提前建立连接,是 UDP 通信中接收数据的核心函数,也可兼容接收 TCP 数据。
// 参数含义
//	int sockfd:已创建(UDP)或已连接(TCP)的套接字描述符,指定要从中接收数据的目标套接字。
//	void *buf:指向接收数据的缓冲区起始地址,用于存放接收到的实际数据,void* 支持接收任意类型数据。
//	size_t len:指定接收缓冲区的最大字节长度,防止数据溢出缓冲区,通常传入 sizeof(buf) 计算的值。
//	int flags:接收操作的选项标志,通常设为 0(默认阻塞接收),也可指定 MSG_DONTWAIT(非阻塞接收)等特殊选项。
//	struct sockaddr *src_addr:指向通用地址结构体的指针,用于存放数据发送方的网络地址信息(IP + 端口),无需获取时可设为 NULL。
//	socklen_t *addrlen:地址结构体长度的指针,传入时是 src_addr 结构体的实际长度,返回时是发送方地址的真实长度,src_addr 非 NULL 时不可设为 NULL。

// 返回值含义调用成功:返回 接收到的实际字节数,若为 UDP 则对应一个完整数据报的有效长度,若为 TCP 则对应实际接收的字节数。调用失败:返回 -1,具体错误信息存入全局变量 errno,可通过 perror("recvfrom") 查询错误原因。特殊情况(仅 TCP):返回 0,表示对方套接字已正常关闭连接,无更多数据可接收。

2. 发送消息

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

// (sendto)功能: 向指定的目标网络地址发送数据,无需提前建立连接,是 UDP 通信中发送数据的核心函数,也可用于已连

// 参数含义
//		int sockfd:已创建(UDP)或已连接(TCP)的套接字描述符,指定要用于发送数据的目标套接字。
//		const void *buf:指向要发送数据缓冲区的常量指针,存放待发送的原始数据,void* 支持发送任意类型数据,const 保证缓冲区数据不被函数修改。
//		size_t len:指定待发送数据的实际字节长度,即要从 buf 中读取并发送的数据大小。
//		int flags:发送操作的选项标志,通常设为 0(默认阻塞发送),也可指定 MSG_DONTWAIT(非阻塞发送)等特殊选项。
//		const struct sockaddr *dest_addr:指向目标接收方网络地址结构体的常量指针,存放接收方的 IP 地址和端口号,UDP 必须指定,TCP 已连接时可设为 NULL。
//		socklen_t addrlen:指定 dest_addr 指向的地址结构体的实际字节长度,UDP 需传入对应地址结构体的 sizeof() 值,TCP 已连接时可设为 0。

//返回值含义:调用成功:返回 实际成功发送的字节数,UDP 通常等于传入的 len(数据报完整发送),TCP 可能小于 len(受网络缓冲区限制)。调用失败:返回 -1,具体错误信息存入全局变量 errno,可通过 perror("sendto") 查询错误原因。特殊情况(仅 TCP):返回 0 表示对方套接字已正常关闭,无法继续发送数据。

3. 网络通信 服务-客户端实现(UDP)

3.1 锁&日志信息打印

  • 复用直线系统写好的代码

3.2 服务端实现

  1. 创建套接字socket
  2. 创建套接字地址sockaddr_in,
  3. bind绑定套接字与套接字地址
  4. 发送/接收消息数据
  • IP地址的绑定local.sin_addr.s_addr = INADDR_ANY;要使用宏定义INADDR_ANY,因为服务端需要接收发送到本机的所有IP地址的数据,而本机的IP地址不止有1个!!!

UdpServer.hpp

cpp 复制代码
#pragma

#include <iostream>
#include <string>
#include <functional>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string&)>;

const int defaultfd = -1;

// 网络通信服务端
// 1.创建套接字socket
// 2.创建套接字地址sockaddr_in,
// 3.bind绑定套接字与套接字地址
// 4.发送/接收消息数据

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
    :_sockfd(defaultfd),
    _port(port),
    _isrunning(false),
    _func(func)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;

        // 2.创建套接字地址sockaddr_in,填充协议sin_famil、ip地址(sin_addr)(网络序列)、sin_port端口号
        struct sockaddr_in local;
        bzero(&local, sizeof(local));   // 内存清0

        local.sin_family = AF_INET;		 	// IPv4协议是AF_INET
        local.sin_port = htons(_port);  	// 端口号 //本地格式 -> 网络序列 htons()函数转换
        local.sin_addr.s_addr = INADDR_ANY; // 但是实际使用Server服务端要接收发送到本机所有IP的信息。IP不止1个。所以IP地址要使用INADDR_ANY宏定义来赋值

        // 3.bind绑定套接字与套接字地址
        // 那么为什么服务器端要显式的bind呢?IP和端口必须2是众所周知不能轻易改变的!
        // 此处如果绑定云服务器的具体IP地址会绑定失败:原因是公网 IP 未配置到本地,无法直接 bind,解决方案是 bind 0.0.0.0 或内网 IP。
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
    // 4.发送/接收消息数据
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //  1. 收消息,client为什么要向服务器发送消息?目的就是要服务器端处理数据
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if(s > 0)
            {
                // 从网络中拿到client的网络序列
                int peer_port = ntohs(peer.sin_port);   
                std::string peer_ip = inet_ntoa(peer.sin_addr); //4字节网络风格的IP->点分十进制的字符串风格的IP

                buffer[s] = 0;
                std::string result = _func(buffer); // 从client客户端拿到的数据

                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]#" << buffer;   //打印client的IP+port+消息内容

                // 2.发消息
                std::string echo_string = "server echo@ ";
                echo_string += buffer;
                // 将接收到的数据再返回client客户端
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip;     // 用的是字符串风格,点分十进制,"192.168.1.1"
    bool _isrunning;

    func_t _func;   // 服务器的回调 函数,对数据进行处理
};

UdpServer.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"

// 仅仅是用来测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]); // 字符串转整型

    Enable_Console_Log_Strategy();

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
    
    usvr->Init();
    usvr->Start();
    return 0;
}

3.3 客户端实现

  1. 创建套接字socket
  2. 创建套接字地址sockaddr_in,填写目标服务器信息
  3. 发送/接收信息
  • 客户端不需要bind:因为首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式。一个端口号只能被一个进程bind,OS采用随机端口号是为了避免client端口冲突。

UdpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

// ./udpclient server_ip server_port
// 1.创建套接字socket
// 2.创建套接字地址sockaddr_in,填写目标服务器信息
// 3.发送/接收信息

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1.创建套接字socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    // 关于bind:本地的IP和端口是什么?需不需要和套接字关联呢?
    // 问题:client要不要bind?
    // 答:不需要,因为首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随即端口号的方式
    // 为什么?
    // 答:一个端口号只能被一个进程bind,为了避免client端口冲突
    
    // 2.创建套接字地址sockaddr_in,填写目标服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 3.发送/接收消息
    while(true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 3.1 发送消息
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        char buffer[1024];
        // 3.2 创建套接字地址,用于接收信息来源主机的套接字地址(IP和port端口号)
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        // 3.3 接受消息
        int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

3.4 Makefile实现

Makefile

makefile 复制代码
.PHONY:all
all:udpclient udpserver

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f udpclient udpserver

3.5 Linux系统查看本机IP地址

lo:本地回环接口(Loopback):

  • 全称:Loopback Interface(回环接口),lo 是缩写(源于 "loop" 的缩写)。

  • 本质:虚拟网络接口(无对应物理硬件,由内核软件模拟实现),不依赖网卡、网线等物理设备。

特性 具体说明
IP 地址 默认绑定 127.0.0.1(IPv4: inet)和 ::1(IPv6: inet6),整个 127.0.0.0/8 网段均为回环地址(如 127.0.0.2、127.255.255.255)。
子网掩码 IPv4 默认 255.0.0.0,IPv6 默认 ::1/128
通信范围 仅局限于本机,数据不会离开主机,也不经过物理网卡或网络链路。
传输效率 极高(内核直接转发数据,无物理层延迟),无需网络协议栈的复杂处理。

ens:以太网接口(Ethernet),物理 / 虚拟、对外通信专用,用于本机与外部网络交互,IP 为局域网 / 公网地址。

  • 全称:Ethernet Interface(以太网接口),ens 是 Linux 系统的现代网卡命名规则 (取代传统的 eth0)。
  • 本质:物理 / 虚拟以太网接口(对应真实网卡或虚拟机虚拟网卡),是本机与外部网络通信的 "物理通道"。
特性 具体说明
IP 地址 通常由 DHCP 服务器自动分配(如路由器分配的局域网 IP:192.168.1.10010.0.0.5),也可手动配置静态 IP。(IPv4: inet)(IPv6: inet6)
子网掩码 与局域网匹配(如 255.255.255.0),用于区分本机与外部网络的地址范围。
通信范围 可与局域网内其他主机、互联网主机通信(数据经过物理网卡、网线 / 无线信号传输)。
依赖条件 需启用物理网卡(或虚拟网卡),且正确配置 IP、网关、DNS(否则无法访问外部网络)。

3.6 功能验证

bash 复制代码
# 终端1
make
./udpserver 8080
bash 复制代码
# 终端2
./udpclient 127.0.0.1 8080

开启server和client之后,两个终端即可通过网络进行通信。

4. 完整版代码

网络通信-UDP-UDP实现服务-客户端通信

相关推荐
阿巴~阿巴~14 小时前
TCP可靠传输双引擎:确认应答与超时重传的精妙协同
运维·服务器·网络·网络协议·tcp·超时重传·确认应答
航Hang*14 小时前
第八章:综合布线技术 —— 进线间和建筑群子系统设计
网络·笔记·学习·设计·期末·光纤
运维有小邓@1 天前
Active Directory服务账户是什么?
运维·服务器·网络
计算机网恋1 天前
Ubuntu22.04Server虚拟机网络配置
网络·数据库·postgresql
航Hang*1 天前
第十五章:网络系统建设与运维(高级)—— 总复习
网络·华为·ensp·期末·复习
塔能物联运维1 天前
Zigbee自适应信道选择提升网络稳定性
网络
chenyuhao20241 天前
Linux网络编程:数据链路层
linux·运维·网络
北邮刘老师1 天前
【智能体互联协议解析】AIP/ACPs如何实现“自主互联,协商互通,独立自治”
网络·人工智能·大模型·智能体·智能体互联网
西敏寺的乐章1 天前
ThreadLocal / InheritableThreadLocal / TransmittableThreadLocal(TTL)学习总结
java·开发语言·网络