前言:
- 概述网络通信中socket套接字的主要编程接口
- 针对UDP通信,对相关socket接口进行详细阐述
- 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 相关结构体的设计。
首先,我们回顾一个核心结论:网络通信的本质,其实就是跨主机的进程间通信。而进程间通信主要分为两大类别,对应不同的标准与实现:
- System V 标准 :主要针对本地进程间通信(同一主机内的不同进程),核心实现包括共享内存、消息队列、信号量等,仅适用于本机内的进程交互,无法支持跨网络通信。
- 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_in和struct 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_UNIX(AF_LOCAL):本地进程间通信(对应sockaddr_un)。
- 核心意义:告诉操作系统如何解析后续的
sa_data字段。
- 类型:
char sa_data[14]:- 类型:固定长度为 14 的字符数组。
- 作用:存储该地址族对应的「原始地址数据」,包含端口号、IP 地址等信息,但无固定结构,直接以二进制字节流存储。
- 局限性:无法直接通过字段名访问端口、IP 等具体信息,只能通过专用函数解析,无法手动赋值或修改。
😂核心特点
- 统一性 :POSIX 标准定义的通用接口,所有网络地址相关的 API 函数(
bind()、connect()、accept()等)均以struct sockaddr*作为参数类型,兼容不同协议的地址结构体。 - 抽象性:不区分具体网络协议,仅提供「地址族标识 + 原始地址数据」的通用框架,无法直接操作具体地址字段。
- 固定大小 :总大小为 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大小完全匹配。
😆核心特点
- 专用性 :仅支持 IPv4 协议,是 IPv4 地址的结构化载体,对应 IPv6 的专用结构体为
sockaddr_in6。 - 结构化:将 IPv4 地址信息拆分为「地址族、端口号、IP 地址」等独立字段,可直接通过字段名访问和修改,方便开发者操作。
- 内存兼容性 :总大小 16 字节,与
sockaddr内存布局对齐,支持安全的强制类型转换。 - 实用性 :是网络编程中实际用于填充、修改、解析 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 服务端实现
- 创建套接字socket
- 创建套接字地址sockaddr_in,
- bind绑定套接字与套接字地址
- 发送/接收消息数据
- 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 客户端实现
- 创建套接字socket
- 创建套接字地址sockaddr_in,填写目标服务器信息
- 发送/接收信息
- 客户端不需要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.100、10.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之后,两个终端即可通过网络进行通信。
