目录
5.dest_addr (const struct sockaddr *)
[1. INADDR_ANY的技术原理](#1. INADDR_ANY的技术原理)
[2. 正确绑定代码实现](#2. 正确绑定代码实现)
[3. 配套配置要点](#3. 配套配置要点)
[1. 多IP服务器场景](#1. 多IP服务器场景)
[2. IPv6环境配置](#2. IPv6环境配置)
[1、私有IP vs 公网IP vs INADDR_ANY](#1、私有IP vs 公网IP vs INADDR_ANY)
[(1) 私有IP(如 172.31.9.74)](#(1) 私有IP(如 172.31.9.74))
[(2) 公网IP](#(2) 公网IP)
[(3) INADDR_ANY(0.0.0.0)](#(3) INADDR_ANY(0.0.0.0))
[(1) 推荐 INADDR_ANY 的情况](#(1) 推荐 INADDR_ANY 的情况)
[(2) 绑定特定IP的情况](#(2) 绑定特定IP的情况)
[(3) 代码改进建议](#(3) 代码改进建议)
[十五、补充:UDP 的 Socket 是全双工的](#十五、补充:UDP 的 Socket 是全双工的)
[2、UDP 的 Socket 是全双工的](#2、UDP 的 Socket 是全双工的)
[3、为什么 UDP 是全双工的?](#3、为什么 UDP 是全双工的?)
[4、对比 TCP](#4、对比 TCP)
[5、示例代码(UDP 全双工)](#5、示例代码(UDP 全双工))
[1、客户端必须知道服务器的 2 个关键信息](#1、客户端必须知道服务器的 2 个关键信息)
[2、如果客户端和服务器是同一家公司写的,怎么获取 IP 和端口?](#2、如果客户端和服务器是同一家公司写的,怎么获取 IP 和端口?)
[情况 1:服务器和客户端在同一台机器(本地测试)](#情况 1:服务器和客户端在同一台机器(本地测试))
[情况 2:服务器和客户端在不同机器(公司内网)](#情况 2:服务器和客户端在不同机器(公司内网))
[情况 3:服务器在公网(对外提供服务)](#情况 3:服务器在公网(对外提供服务))
[(1) 硬编码(适合简单项目)](#(1) 硬编码(适合简单项目))
[(2) 配置文件(适合生产环境)](#(2) 配置文件(适合生产环境))
[(3) 环境变量(适合容器化部署)](#(3) 环境变量(适合容器化部署))
[(4) 服务发现(适合微服务架构)](#(4) 服务发现(适合微服务架构))
[1. ifconfig lo](#1. ifconfig lo)
[2. ip addr show lo](#2. ip addr show lo)
[3. 主要区别](#3. 主要区别)
[4. 如何选择?](#4. 如何选择?)
[(1) 绑定 127.0.0.1(仅限本地访问)](#(1) 绑定 127.0.0.1(仅限本地访问))
[(2) 绑定内网 IP(如 192.168.1.100)](#(2) 绑定内网 IP(如 192.168.1.100))
[(3) 绑定 0.0.0.0(监听所有网卡)](#(3) 绑定 0.0.0.0(监听所有网卡))
[(4) 绑定公网 IP(通常不行!)](#(4) 绑定公网 IP(通常不行!))
[十八、补充:bzero 函数](#十八、补充:bzero 函数)
[使用 bzero](#使用 bzero)
[使用 memset(推荐)](#使用 memset(推荐))
一、客户端套接字创建与封装
我们将UDP客户端封装为一个类,在创建客户端对象时需要进行初始化,主要工作是创建套接字。客户端后续的数据发送和接收操作都将基于这个套接字进行。
1、协议选择与套接字创建
客户端使用与服务器相同的协议族和服务类型:
-
协议族:
AF_INET(IPv4) -
服务类型:
SOCK_DGRAM(UDP)
与服务器不同,客户端不需要显式绑定端口,操作系统会自动分配可用端口。
2、简单的客户端类实现
cpp
class UdpClient
{
public:
bool InitClient()
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0){
std::cerr << "socket create error" << std::endl;
return false;
}
return true;
}
~UdpClient()
{
if (_sockfd >= 0){
close(_sockfd);
}
}
private:
int _sockfd; //文件描述符
};
二、客户端绑定问题分析
1、服务端绑定必要性
服务端必须绑定到特定端口,原因如下:
-
服务发现:服务端需要让客户端知道如何找到它。IP地址通常对应域名、端口号需要是众所周知的(如HTTP的80端口)
-
端口独占:一个端口只能被一个进程绑定、绑定后确保该端口不被其他进程占用
-
稳定性要求:服务端口一旦确定不应轻易更改、客户端依赖固定端口连接服务端
2、客户端不绑定的原因
客户端通常不显式绑定端口,原因如下:
-
端口自动分配:调用
sendto等函数时,操作系统自动分配临时端口、端口号在本次会话中唯一即可(意思是下次使用的端口就可能不是现在的这个了) -
资源利用效率:绑定固定端口会占用系统资源、自动分配允许端口复用
-
灵活性:每次启动可以使用不同端口、只要端口未耗尽,客户端总能启动
3、客户端端口分配机制
-
临时端口范围:操作系统维护一个临时端口范围(通常32768-60999)、从该范围中选择可用端口
-
分配策略:优先选择未使用的端口、使用TIME_WAIT状态检查避免冲突
-
生命周期:端口仅在当前套接字生命周期内有效、套接字关闭后端口可被重用
4、显式绑定客户端的缺点
-
端口冲突风险:固定端口可能已被其他程序占用、导致客户端无法启动
-
资源浪费:即使不使用也占用端口资源、限制系统可同时运行的客户端数量
-
灵活性降低:无法在同一台机器上运行多个相同客户端实例、不利于测试和开发
三、客户端使用示例
cpp
int main() {
UdpClient client;
// 初始化客户端
if (!client.InitClient()) {
std::cerr << "Failed to initialize client" << std::endl;
return 1;
}
// 发送数据到服务器
std::string serverIp = "127.0.0.1";
int serverPort = 8888;
std::string message = "Hello, Server!";
if (!client.SendTo(serverIp, serverPort, message)) {
std::cerr << "Failed to send message" << std::endl;
return 1;
}
// 接收服务器响应(可选)
sockaddr_in senderAddr;
socklen_t addrLen;
std::string response;
if (client.RecvFrom(response, senderAddr, addrLen)) {
char senderIp[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &senderAddr.sin_addr, senderIp, INET_ADDRSTRLEN);
std::cout << "Received response from " << senderIp << ":"
<< ntohs(senderAddr.sin_port) << ": " << response << std::endl;
}
return 0;
}
四、最佳实践建议
-
错误处理 :始终检查系统调用返回值、使用
errno和strerror获取详细错误信息 -
资源管理:确保套接字在不再需要时关闭、考虑使用RAII模式管理资源
-
性能优化 :对于高频通信,考虑重用地址选项(
SO_REUSEADDR)、批量发送数据减少系统调用次数 -
可移植性:检查系统调用返回值而非假设成功、处理不同平台可能的行为差异
-
安全性:验证输入IP地址和端口号、限制接收缓冲区大小防止溢出
通过这种设计,UDP客户端既保持了简单性,又提供了必要的灵活性和健壮性,适合大多数网络通信场景。
五、增加服务端IP地址和端口号
正如上面演示的代码一样,客户端需要明确服务端的IP地址和端口号才能建立连接。因此,在客户端类中需存储这些信息,并通过构造函数在初始化时传入相应参数来完成配置。
cpp
class UdpClient
{
public:
UdpClient(std::string server_ip, int server_port)
:_sockfd(-1)
,_server_port(server_port)
,_server_ip(server_ip)
{}
~UdpClient()
{
if (_sockfd >= 0){
close(_sockfd);
}
}
private:
int _sockfd; //文件描述符
int _server_port; //服务端端口号
std::string _server_ip; //服务端IP地址
};
客户端初始化完成后即可运行。客户端与服务器功能互补,服务器负责接收客户端发送的数据,因此客户端需要向服务器传输相应数据。
六、sendto函数
sendto 是 UDP 套接字编程中最重要的函数之一,用于在无连接的网络通信中发送数据。下面我将从多个方面详细讲解这个函数。
1、函数原型
cpp
#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);

2、参数详解

1.sockfd (int)
-
套接字文件描述符
-
由
socket()函数创建 -
对于 UDP 客户端,通常是通过
socket(AF_INET, SOCK_DGRAM, 0)创建的
2.buf (const void *)
-
指向要发送数据的缓冲区
-
可以是任何类型的数据(字符串、二进制数据等)
-
需要确保缓冲区在发送期间保持有效
3.len (size_t)
-
要发送数据的长度(字节数)
-
如果比实际数据长,会发送多余数据(可能导致问题)
-
如果比实际数据短,只会发送部分数据
4.flags (int)
-
控制发送行为的标志位,通常设为 0
-
常用标志:
-
MSG_CONFIRM:通知链路层协议目标在邻近缓存中 -
MSG_DONTROUTE:不查看路由表,直接将数据包发送给本地网络主机 -
MSG_DONTWAIT:非阻塞操作 -
MSG_EOR:结束记录 -
MSG_MORE:还有更多数据要发送
-

5.dest_addr (const struct sockaddr *)
-
指向目标地址结构的指针
-
对于 IPv4,通常是
struct sockaddr_in类型 -
必须正确设置地址族(通常为
AF_INET)
6.addrlen (socklen_t)
-
目标地址结构的长度
-
对于 IPv4,通常是
sizeof(struct sockaddr_in)
3、返回值
成功时:返回实际发送的字节数
-
应该等于
len参数 -
如果小于
len,可能表示发送缓冲区已满
失败时 :返回 -1,并设置 errno 表示错误
-
常见错误:
-
EACCES:无权限(如尝试发送广播但未设置权限) -
EAGAIN或EWOULDBLOCK:非阻塞套接字且发送缓冲区满 -
EBADF:无效的文件描述符 -
ECONNRESET:连接被对方重置(对于面向连接的套接字) -
EDESTADDRREQ:未设置目标地址(对于无连接套接字) -
EFAULT:缓冲区指针无效 -
EINTR:操作被信号中断 -
EINVAL:无效参数 -
EMSGSIZE:消息太大 -
ENOBUFS:系统缓冲区不足 -
ENOTCONN:套接字未连接(对于面向连接的套接字) -
ENOTSOCK:文件描述符不是套接字
-

4、使用示例
cpp
// 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置目标地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 目标端口
if (inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr) <= 0) {
perror("invalid address");
exit(EXIT_FAILURE);
}
// 准备发送的数据
const char *message = "Hello, Server!";
size_t message_len = strlen(message);
// 发送数据
ssize_t bytes_sent = sendto(sockfd, message, message_len, 0,
(const struct sockaddr *)&server_addr,
sizeof(server_addr));
if (bytes_sent < 0) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Sent %zd bytes to server\n", bytes_sent);
补充:
inet_pton 是一个用于将 点分十进制字符串格式的IP地址(如 "192.168.1.1")转换为 网络字节序的二进制格式(存储在 struct in_addr 或 struct in6_addr 中)的函数。它是 IPv4/IPv6 兼容的,比旧的 inet_addr 或 inet_aton 更推荐使用。
5、关键注意事项
1. 无连接特性:
-
UDP 是无连接的,每次调用
sendto都需要指定目标地址 -
与 TCP 的
send()不同,不需要先调用connect()
2. 数据边界:
-
UDP 保留消息边界,每次
sendto对应一次独立的消息 -
接收方会完整接收这条消息(除非被截断)
3. 缓冲区大小:
-
UDP 数据包最大长度为 65535 字节(包括头部)
-
实际使用时建议不超过链路层的 MTU(通常 1472 字节用于 IPv4)
-
过大的数据包会被分片或丢弃
4. 错误处理:
-
必须检查返回值,即使 UDP 是不可靠的
-
某些错误(如
ECONNREFUSED)在 UDP 中也可能出现
5. 地址结构:
-
必须正确初始化目标地址结构
-
使用
htons()和htonl()处理端口和地址的字节序 -
使用
memset清零结构体避免未定义值
6. 性能考虑:
-
频繁调用
sendto可能有性能开销 -
对于大量数据,考虑批量发送
-
非阻塞模式下,可能需要处理部分发送的情况
6、常见问题解决方案
1. 发送失败(返回 -1):
-
检查
errno确定具体原因 -
验证套接字是否有效
-
检查目标地址是否正确
-
确保网络连接正常
2. 发送部分数据(返回值小于 len):
-
在 UDP 中通常不应该发生,因为 UDP 是面向消息的
-
如果发生,可能需要重新发送剩余数据
3. 广播和多播:
-
广播:使用
255.255.255.255或子网广播地址 -
多播:使用多播组地址(224.0.0.0 到 239.255.255.255)
-
需要设置
SO_BROADCAST套接字选项
4. 非阻塞模式:
-
使用
fcntl设置套接字为非阻塞 -
发送时可能返回
EAGAIN或EWOULDBLOCK -
需要有重试机制或使用
select/poll/epoll
7、高级用法
1. 发送到多个目标:
cpp
struct sockaddr_in addr1, addr2;
// 初始化两个不同的地址...
sendto(sockfd, buf, len, 0, (struct sockaddr*)&addr1, sizeof(addr1));
sendto(sockfd, buf, len, 0, (struct sockaddr*)&addr2, sizeof(addr2));
2. 使用辅助数据(ancillary data):
-
通过
sendmsg可以发送控制信息 -
用于设置 IP_TTL、SO_TIMESTAMP 等选项
3. 发送零长度数据包:
-
技术上允许,但通常没有实际意义
-
某些实现可能返回错误
8、与相关函数的比较
sendto vs send:
-
send用于已连接的套接字(TCP 或已调用connect的 UDP) -
sendto总是需要指定目标地址
sendto vs sendmsg:
-
sendmsg更灵活,可以指定多个缓冲区和控制信息 -
sendto更简单,适合大多数 UDP 场景
sendto vs write:write 只能用于已连接的套接字、不能指定目标地址
9、最佳实践
-
总是检查返回值
-
使用
sizeof计算地址结构长度,而不是硬编码 -
在发送前验证目标地址的有效性
-
考虑使用包装函数处理错误和日志记录
-
对于关键应用,实现重试机制(但注意 UDP 不可靠性)
-
合理设置超时(使用
setsockopt设置SO_SNDTIMEO)
10、完整示例代码
cpp
#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>
int main() {
// 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
return EXIT_FAILURE;
}
// 设置目标地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
perror("invalid address");
close(sockfd);
return EXIT_FAILURE;
}
// 要发送的消息
const char *messages[] = {
"First message",
"Second message",
"Third message",
NULL
};
// 发送多条消息
for (int i = 0; messages[i] != NULL; i++) {
const char *msg = messages[i];
size_t msg_len = strlen(msg);
printf("Sending: %s\n", msg);
ssize_t bytes_sent = sendto(sockfd, msg, msg_len, 0,
(const struct sockaddr *)&server_addr,
sizeof(server_addr));
if (bytes_sent < 0) {
perror("sendto failed");
break;
} else if (bytes_sent != msg_len) {
fprintf(stderr, "Partial send: expected %zu, got %zd\n",
msg_len, bytes_sent);
} else {
printf("Successfully sent %zd bytes\n", bytes_sent);
}
sleep(1); // 间隔1秒
}
close(sockfd);
return EXIT_SUCCESS;
}
通过以上详细讲解,我们现在应该对 sendto 函数有了全面的理解,能够正确地在 UDP 客户端程序中使用它来发送数据。
七、启动客户端函数
客户端在向服务端发送数据时,需持续采集用户输入并实时传输。
处理过程中需注意两点:
-
端口号需由主机字节序转换为网络字节序,通过htons函数转换后存入sockaddr_in结构体
-
字符串格式的IP地址需调用inet_addr函数转换为整数形式,再设置到sockaddr_in结构体中
cpp
class UdpClient
{
public:
void Start()
{
std::string msg;
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
for (;;){
std::cout << "Please Enter# ";
getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
}
}
private:
int _sockfd; //文件描述符
int _server_port; //服务端端口号
std::string _server_ip; //服务端IP地址
};
八、引入命令行参数
为便于配置,我们可在客户端启动时通过命令行参数指定服务端地址。运行时只需在命令后添加目标服务端的IP和端口号即可完成连接设置。
cpp
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];
int server_port = atoi(argv[2]);
UdpClient* clt = new UdpClient(server_ip, server_port);
clt->InitClient();
clt->Start();
return 0;
}
需要特别说明的是,由于argv数组中存储的是字符串格式的数据,而端口号应为整数类型,因此必须使用atoi函数进行类型转换。获取IP地址和端口号后,即可完成客户端的创建与初始化。最后,调用Start函数即可启动客户端程序。
九、完整的客户端测试代码
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class UdpClient
{
public:
UdpClient(std::string server_ip, int server_port)
:_sockfd(-1)
,_server_port(server_port)
,_server_ip(server_ip)
{}
~UdpClient()
{
if (_sockfd >= 0){
close(_sockfd);
}
}
bool InitClient()
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0){
std::cerr << "socket create error" << std::endl;
return false;
}
return true;
}
void Start()
{
std::string msg;
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
for (;;){
std::cout << "Please Enter# ";
getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
}
}
private:
int _sockfd; //文件描述符
int _server_port; //服务端端口号
std::string _server_ip; //服务端IP地址
};
十、本地测试
目前已完成服务端和客户端代码的开发。在本地测试阶段,我们暂时不绑定外网地址,而是使用本地环回地址。具体操作步骤如下:首先启动服务端程序,指定监听端口为8080;然后运行客户端程序,将其访问目标设置为127.0.0.1(本地环回地址),端口号同样设为8080。

当客户端启动后,系统会提示用户输入数据。输入完成后,客户端将数据发送至服务端进行处理。服务端接收数据后,会将其打印显示在终端界面上,用户即可在服务端窗口查看到自己输入的内容。

使用netstat命令查看网络状态时,可以看到服务端端口为8080,客户端端口为576421。客户端端口信息能被netstat检测到,表明客户端已完成动态绑定,这标志着网络通信已成功建立。

十一、云服务器网络测试与IP绑定
我们已完成本地测试环节,接下来将进入网络测试阶段。如果直接将服务端绑定到我的公网IP(例如113.45.79.2),是否就能实现外网访问功能?
从技术原理上讲,这个方法是可行的。以我的服务器为例,其公网IP地址113.45.79.2通过ping命令测试能够正常响应,这验证了该IP的有效性。
bash
ping 113.45.79.2

如果我们不能进行ping操作的话,那么可能是云服务商的安全组规则(必须允许ICMP)导致的,我们就必须加入对应的安全组规则:

1、ICMP(简单了解)
ICMP (Internet Control Message Protocol)可以理解为互联网的"诊断和信使协议" 。它就像是网络世界的"对讲机系统"。ICMP主要用来:检测网络连通性 (比如ping命令)、报告网络错误信息、进行网络诊断。
为什么刚刚的场景需要ICMP?在刚才的问题中:
-
执行
ping 113.45.79.2就是发送 ICMP Echo Request -
如果服务器配置了允许ICMP,就会回复 ICMP Echo Reply
-
但如果安全组阻止了ICMP,就像门卫不让传话,你就收不到回复
重要特点
-
不是传输数据的:ICMP不用于传文件、网页等用户数据
-
是管理协议:专门用于网络管理和故障诊断
-
无需端口:不像HTTP(80)、SSH(22)需要端口号
-
操作系统内置:所有网络设备都支持ICMP
简单来说,ICMP就是网络世界的"健康检查员"和"故障报修员"!
2、实践中的绑定失败原因解析
当看到持续的响应包时,这表明该IP确实具备公网可达性。此时若将服务端程序的监听地址从本地环回地址(127.0.0.1)修改为这个公网IP,直觉上似乎就能实现外网访问。然而实际尝试时会遇到绑定失败的问题,这源于云服务器的特殊网络架构:
-
虚拟化网络层 :云服务商通过SDN(软件定义网络)技术实现资源隔离,分配给用户的"公网IP"往往是经过NAT转换的虚拟IP**(直接理解为是虚拟的,不是真正的IP地址)**
-
安全组限制:即使IP显示可ping通,实际端口访问可能受安全组规则约束
-
绑定权限限制:大多数云平台禁止直接绑定分配的公网IP到用户程序,这是出于安全考虑的设计
具体技术表现:
cpp
// 错误示例:尝试直接绑定公网IP
struct sockaddr_in server_addr;
server_addr.sin_addr.s_addr = inet_addr("113.45.79.2"); // 可能导致绑定失败
当执行此类代码时,系统会返回EINVAL(无效参数)或EADDRNOTAVAIL(地址不可用)错误。
将服务端配置的本地环回地址改为公网 IP 后,重新编译运行程序时会出现服务端绑定失败的情况。如下:

3、正确解决方案:INADDR_ANY的应用
1. INADDR_ANY的技术原理
系统提供的INADDR_ANY宏(值为0)是解决这个问题的关键。其工作机制:
-
监听所有网络接口
-
自动适配可用的IP地址
-
由内核处理具体的地址选择逻辑
2. 正确绑定代码实现
cpp
// 正确示例:使用INADDR_ANY绑定
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY; // 关键修改
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
3. 配套配置要点
要确保外网访问成功,还需完成以下配置:
-
安全组规则:
-
在云控制台开放对应端口(如TCP 8080)
-
设置允许的源IP范围(建议先开放0.0.0.0/0测试)
-
-
防火墙设置:
bash# Ubuntu示例 sudo ufw allow 8080/tcp # CentOS示例 sudo firewall-cmd --add-port=8080/tcp --permanent sudo firewall-cmd --reload -
服务监听验证:
bashnetstat -tulnp | grep 8080 # 应显示类似:tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN
4、特殊场景处理
1. 多IP服务器场景
当服务器配置了多个网络接口时,INADDR_ANY会让服务监听所有接口。如需指定特定接口:
cpp
// 获取特定网卡的IP地址
struct ifaddrs *ifaddr, *ifa;
getifaddrs(&ifaddr);
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET)
continue;
// 假设我们要绑定eth0的IP
if (strcmp(ifa->ifa_name, "eth0") == 0) {
server_addr.sin_addr.s_addr =
((struct sockaddr_in*)ifa->ifa_addr)->sin_addr.s_addr;
break;
}
}
freeifaddrs(ifaddr);
2. IPv6环境配置
对于IPv6网络,应使用IN6ADDR_ANY_INIT:
cpp
struct sockaddr_in6 server_addr6;
server_addr6.sin6_family = AF_INET6;
server_addr6.sin6_port = htons(8080);
server_addr6.sin6_addr = IN6ADDR_ANY_INIT; // IPv6的等效设置
5、验证测试方法
完成配置后,可通过以下步骤验证:
-
本地测试:
bashcurl http://localhost:8080 # 或 telnet localhost 8080 -
内网测试:
bashcurl http://<内网IP>:8080 -
外网测试:
bashcurl http://<公网IP>:8080 # 或使用在线工具如 https://www.canyouseeme.org/ 测试端口可达性
6、常见问题排查
若仍无法访问,请检查:
-
服务是否真正运行:
ps aux | grep your_service -
端口是否监听:
ss -tulnp | grep 8080 -
安全组规则是否正确配置
-
云服务商是否有额外限制(如某些厂商要求绑定弹性公网IP)
7、总结
在云服务器环境中实现外网访问的正确方法是使用INADDR_ANY进行绑定,而非直接绑定分配的公网IP。这种设计既保证了安全性,又提供了最大的灵活性。配合适当的安全组规则和防火墙配置,可以构建既安全又易于访问的网络服务。理解这些底层原理,能帮助开发者更好地应对各种网络部署场景。
十二、绑定INADDR_ANY的详细分析与实现
1、绑定INADDR_ANY的优势
在现代服务器架构中,绑定INADDR_ANY(0.0.0.0)是一种被广泛推荐的做法,特别是在高可用性和多网卡环境下。以下是详细分析:
多网卡环境下的优势
1. 负载均衡与性能优化:
-
当服务器配备多张网卡时,每张网卡都能独立接收数据
-
操作系统内核会自动处理数据包的路由,确保最优路径
-
避免了单网卡成为性能瓶颈的问题
2. 简化配置管理:
-
无需为不同网卡配置不同的服务实例
-
单一服务实例可以处理来自所有网络接口的请求
-
特别适用于需要动态添加/移除网卡的场景
3. 高可用性保障:
-
当某块网卡故障时,服务仍可通过其他网卡继续运行
-
配合负载均衡器使用时,可以实现无缝故障转移
简单事例讲解
当服务器带宽充足时,单台机器的数据接收能力往往会成为IO效率的瓶颈。为此,服务器底层通常会配置多张网卡,从而拥有多个IP地址。值得注意的是,虽然服务器有多个IP,但端口号为8080的服务在整个服务器上仅有一个实例。
当数据到达服务器时,所有网卡都会在底层接收到这些数据。如果这些数据都请求访问8080端口服务,此时不同的绑定方式会产生不同效果:
-
若服务端明确绑定到特定IP地址,则只能通过该IP对应的网卡接收数据
-
若服务端绑定为INADDR_ANY,则系统会将所有发往8080端口的数据,无论来自哪个网卡,都会自动传递给该服务端处理

推荐服务端绑定INADDR_ANY作为首选方案,这也是大多数服务器的标准配置方式。若需要同时满足外网访问和指定IP绑定两个需求,云服务器将无法满足要求。此时建议采用虚拟机或自定义安装的Linux系统,这些环境支持绑定特定IP地址。
具体实现示例
以下是修改后的UDP服务器代码,展示如何正确绑定INADDR_ANY:
为了使外网能够访问我们的服务,需要修改服务器代码:移除硬编码的IP地址,在配置sockaddr_in结构体的网络参数时,将IP地址设置为INADDR_ANY。由于INADDR_ANY本质上等于0,不会涉及大小端转换问题,因此设置时无需进行网络字节序转换。

重新编译运行服务器后,绑定失败的问题已解决。使用netstat命令可以看到,服务器本地IP地址显示为0.0.0.0,这表明该UDP服务器能够接收来自本地所有网卡的数据。
2、验证绑定结果
编译并运行上述程序后,可以通过以下命令验证绑定情况:
bash
netstat -tulnp | grep 8080
# 或
ss -tulnp | grep 8080
输出应显示类似以下内容:

其中0.0.0.0表示服务已成功绑定到所有可用网络接口。
总结的来说就是,0.0.0.0这个IP地址就是指代当前主机(这里指的是云服务器)的全部网卡(也就是所有可用的网络接口),但是8080这个端口号只有一个,所以也就有了唯一性,能够成功绑定socket套接字。也就是说我们不使用云服务器上的公网IP,即113.45.79.2,因为它不能直接转化为可使用的网络接口,而我们如果使用0.0.0.0的话就间接地使用了可使用的网络接口!!!!
3、特殊场景处理
云服务器环境注意事项
在云服务器环境中,有时需要限制服务只监听特定IP:
1. 云服务器限制:
-
某些云平台会强制服务绑定到主私有IP
-
直接绑定
INADDR_ANY可能被安全组规则限制
2. 替代方案:
-
使用虚拟机或自定义Linux环境
-
通过云平台API获取允许绑定的IP列表
-
使用网络地址转换(NAT)规则
高级配置选项
对于需要更精细控制的场景,可以考虑:
1. SO_REUSEADDR/SO_REUSEPORT选项:
cpp
int optval = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
2. IP_FREEBIND选项(允许绑定到不存在的IP):
cpp
setsockopt(_sockfd, IPPROTO_IP, IP_FREEBIND, &optval, sizeof(optval));
4、最佳实践总结
-
默认使用INADDR_ANY:除非有特殊安全需求,否则优先绑定到所有接口、简化配置,提高可用性
-
安全考虑:结合防火墙规则限制访问、使用网络层ACL进行更细粒度控制
-
监控与调试:监控各网卡的流量负载、使用tcpdump等工具验证数据包路由
通过这种方式实现的服务器能够充分利用多网卡环境,同时保持配置的简洁性和可维护性。
十三、私有IP绑定测试

在上面提供的代码中,使用私有IP地址(如172.31.9.74)可以运行服务端,这与使用公网IP或INADDR_ANY(0.0.0.0)有重要区别。以下是详细分析:
1、私有IP vs 公网IP vs INADDR_ANY
(1) 私有IP(如 172.31.9.74)
作用范围 :仅在**云服务器对应的本地局域网(LAN)**内有效,路由器不会将其转发到公网。
绑定行为:
-
服务器仅监听该私有IP对应的网卡。
-
只有目标IP是
172.31.9.74的数据包才会被操作系统交给您的服务。
适用场景:
-
服务仅供内网设备访问(如公司内部服务)。
-
测试环境,避免外部干扰。
限制:
-
无法直接从公网访问(除非通过NAT/端口转发)。
-
如果服务器有多块网卡,绑定私有IP会导致服务仅监听特定网卡。
(2) 公网IP
作用范围:全球唯一,可直接从互联网访问。
绑定行为:
-
服务器监听该公网IP对应的网卡。
-
只有目标IP是公网IP的数据包会被处理。
适用场景:需要对外提供服务(如网站、API)。
限制:
-
直接绑定公网IP可能暴露服务,需配合防火墙。
-
云服务器可能限制直接绑定公网IP(需通过安全组/VPC配置)。
(3) INADDR_ANY(0.0.0.0)
作用范围 :监听所有网络接口 (包括所有私有IP、公网IP、甚至回环地址 127.0.0.1)。
绑定行为:
-
操作系统会将发送到服务器任何IP地址(且端口匹配)的数据包交给服务。
-
例如:即使服务器有
172.31.9.74(私有IP)和203.0.113.45(公网IP),绑定INADDR_ANY后,访问任意IP+端口都能到达服务。
适用场景:
-
服务需要同时被内网和公网访问。
-
服务器有多网卡,希望统一监听所有接口。
优势:
-
灵活性最高,无需关心具体IP。
-
适合云服务器或动态IP环境。
2、为什么上面的代码用私有IP可以运行?
我的服务器位于内网:
-
私有IP
172.31.9.74是局域网地址,绑定后服务仅监听该网卡。 -
如果客户端也在同一局域网(如
172.31.x.x),可以直接访问。
未涉及公网通信:
-
如果客户端尝试从公网访问,需在路由器上配置NAT端口转发 (如将公网IP的8080端口映射到
172.31.9.74:8080)。 -
否则,公网数据包无法路由到您的私有IP。
代码逻辑:
-
代码中的
UdpServer类在绑定时指定了私有IP,因此内核只会接收目标IP为172.31.9.74的UDP数据包。 -
如果服务器还有其它IP(如
127.0.0.1),访问这些IP+端口不会被当前服务处理。
3、关键区别总结
| 绑定目标 | 监听范围 | 公网访问 | 多网卡支持 |
|---|---|---|---|
私有IP (172.31.9.74) |
仅该私有IP对应的网卡 | 需NAT转发 | ❌ 仅监听指定网卡 |
公网IP (203.0.113.45) |
仅该公网IP对应的网卡 | 直接访问 | ❌ 仅监听指定网卡 |
INADDR_ANY (0.0.0.0) |
所有网卡(包括私有/公网/回环) | 直接访问(如果IP已配置) | ✅ 自动处理所有接口 |
4、如何选择绑定方式?
(1) 推荐 INADDR_ANY 的情况
-
服务需要被内网和公网同时访问。
-
服务器有多网卡,希望统一监听。
-
云服务器环境(避免绑定特定IP失败)。
(2) 绑定特定IP的情况
-
需要限制服务仅监听某个接口(如安全隔离)。
-
多网卡环境下,区分不同服务(如一个网卡处理内网,另一个处理公网)。
(3) 代码改进建议
如果目标是让服务可被内网和公网访问,应修改为绑定 INADDR_ANY:
cpp
// 在 UdpServer 类的绑定逻辑中:
local.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
并移除 main 函数中硬编码的私有IP:
cpp
// 修改前:
const std::string ip = "172.31.9.74";
// 修改后:
const std::string ip = "0.0.0.0"; // 或直接不指定IP,由InitServer内部处理
5、验证方法
检查监听状态:
bash
netstat -tulnp | grep <端口号>
# 或
ss -tulnp | grep <端口号>
输出应为 0.0.0.0:<端口>(表示 INADDR_ANY)或特定IP。
测试访问:
-
内网客户端:直接访问私有IP。
-
公网客户端:需确保服务器有公网IP且防火墙放行端口。
6、云服务器的特殊情况
在云平台(如AWS、阿里云)上:
-
绑定私有IP:服务仅在云VPC内网可见。
-
绑定公网IP:通常不允许直接绑定,需通过安全组规则控制访问。
-
最佳实践 :始终绑定
INADDR_ANY,并通过安全组限制来源IP。
7、结论
-
当前代码:绑定私有IP仅限内网访问,适合测试或内部服务。
-
生产环境 :建议改用
INADDR_ANY,结合防火墙/安全组控制访问。 -
公网访问:需确保服务器有公网IP,并正确配置NAT或安全组。
十四、回顾UdpServer(服务端)的主要代码
UdpServer.cc
cpp
#include <iostream>
#include <string>
#include <cstdlib> // 用于atoi函数
#include "UdpServer.hpp" // 假设有UdpServer类的实现
int main(int argc, char* argv[]) {
// 参数检查:必须且只能有一个参数(端口号)
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
std::cerr << "Example: " << argv[0] << " 8080" << std::endl;
return 1;
}
//const std::string ip = "127.0.0.1";// 设置默认IP地址为本地环回地址
//const std::string ip = "113.45.79.2";//现在将IP地址改为当前的公网IP地址
const std::string ip = "172.31.9.74";//现在将IP地址改为当前的私有IP地址
// 将命令行参数(字符串)转换为整数端口号
try {
int port = std::stoi(argv[1]); // 使用更安全的stoi替代atoi
if (port <= 0 || port > 65535) {
std::cerr << "Error: Port number must be between 1 and 65535" << std::endl;
return 1;
}
// 创建并初始化UDP服务器
UdpServer* svr = new UdpServer(ip, port);
svr->InitServer();
// 启动服务器
std::cout << "Starting UDP server on " << ip << ":" << port << std::endl;
svr->Start();
// 清理资源(实际应用中应有更完善的资源管理)
delete svr;
} catch (const std::invalid_argument& e) {
std::cerr << "Error: Invalid port number - must be an integer" << std::endl;
return 1;
} catch (const std::out_of_range& e) {
std::cerr << "Error: Port number out of range" << std::endl;
return 1;
}
return 0;
}
UdpServer.hpp
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class UdpServer {
public:
UdpServer(const std::string& ip, int port) : _ip(ip), _port(port) {}
void InitServer() {
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
std::cerr << "socket error" << std::endl;
exit(1);
}
// 绑定地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(2);
}
}
void Start() {
const int SIZE = 128;
char buffer[SIZE];
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收数据
ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&peer, &len);
if (size > 0) {
buffer[size] = '\0'; // 字符串终止符
// 转换端口和IP格式
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
// 输出接收到的信息
std::cout << "[" << ip << ":" << port << "]# " << buffer << std::endl;
} else {
std::cerr << "recvfrom error, but continue..." << std::endl;
continue; // 错误时不退出,继续服务
}
}
}
~UdpServer() {
if (_sockfd >= 0) {
close(_sockfd);
}
}
private:
int _sockfd = -1;
int _port;
std::string _ip;
};
十五、补充**:UDP 的 Socket 是全双工的**
1、什么是全双工?
-
全双工(Full-Duplex) :通信双方可以同时发送和接收数据(比如打电话,双方能随时说话和听)。
-
半双工(Half-Duplex):同一时间只能单向通信(比如对讲机,按下按钮才能说话,松开才能听)。
2、UDP 的 Socket 是全双工的
-
在 UDP 通信中,同一个 Socket 文件描述符(
sockfd) 既可以发送数据 (sendto/write),也可以接收数据 (recvfrom/read),而且没有限制必须交替进行。 -
例如:
-
服务器可以用同一个
sockfd接收客户端的数据(recvfrom),同时直接回复数据(sendto),无需创建新的 Socket。 -
客户端也可以随时用同一个
sockfd发送请求(sendto)并接收响应(recvfrom)。
-
3、为什么 UDP 是全双工的?
-
UDP 是无连接的协议,没有 TCP 的"发送-确认"机制,数据包独立传输。
-
操作系统内核不会限制 UDP Socket 的读写方向,因此它可以自由地双向通信。
4、对比 TCP
-
TCP 虽然也是全双工,但它的 Socket 通常需要先通过
accept()创建一个新的连接 Socket(服务端),客户端和服务端各自维护独立的读写通道。 -
UDP 不需要维护连接,直接用同一个 Socket 读写,更简单。
5、示例代码(UDP 全双工)
cpp
// 服务器和客户端可以用同一个 sockfd 随时收发数据
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建 UDP Socket
// 接收数据(阻塞等待)
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &len);
// 立即回复(用同一个 sockfd)
const char* reply = "Hello Client!";
sendto(sockfd, reply, strlen(reply), 0, (struct sockaddr*)&client_addr, len);
6、总结
-
UDP 的 Socket 是全双工的 :同一个
sockfd可以随时读写,无需额外配置。 -
适用场景:需要双向实时通信的应用(如视频流、DNS 查询、游戏等)。
-
注意:UDP 不保证可靠性,需应用层处理丢包和乱序问题。
十六、客户端访问服务器需要知道什么?
1、客户端必须知道服务器的 2 个关键信息
-
服务器的 IP 地址 (如
192.168.1.100或公网 IP203.0.113.45):相当于"服务器的门牌号",告诉数据包该往哪里发送。 -
服务器的端口号 (如
80、8080、53):相当于"门牌号下的具体房间",告诉操作系统把数据交给哪个服务(如 HTTP 服务通常用80端口)。
类比:你要给朋友寄快递,必须知道:
-
朋友的家庭住址(IP 地址)
-
朋友的收件房间号(端口号,比如"101 室"是 HTTP 服务,"102 室"是数据库服务)
2、如果客户端和服务器是同一家公司写的,怎么获取 IP 和端口?
情况 1:服务器和客户端在同一台机器(本地测试)
-
IP 地址 :直接用
127.0.0.1(回环地址,表示"本机") -
端口号 :在代码中硬编码(如
8080),或通过配置文件读取。 -
示例:
cpp// 客户端代码(假设服务器在本机运行) const char* server_ip = "127.0.0.1"; int server_port = 8080;
情况 2:服务器和客户端在不同机器(公司内网)
-
IP 地址:
-
如果是内网服务器,可以通过公司网络配置获取(如
192.168.x.x或10.x.x.x)。 -
如果是动态 IP(如服务器重启后 IP 可能变化),可以通过以下方式解决:
-
DNS 域名 :给服务器绑定一个域名(如
api.example.com),客户端通过域名解析获取 IP。 -
配置文件 :客户端从配置文件(如
config.json)中读取服务器 IP。 -
服务发现:通过公司内部的服务注册中心(如 Consul、ZooKeeper)动态获取服务器地址。
-
-
-
端口号:
-
通常由服务器代码定义(如
8080),客户端和服务器约定好同一个端口。 -
也可以通过配置文件或环境变量传递。
-
情况 3:服务器在公网(对外提供服务)
-
IP 地址:
-
如果是固定公网 IP,直接硬编码到客户端代码或配置文件中。
-
如果是动态公网 IP(如家庭宽带),需要:
-
DDNS(动态域名解析) :用域名(如
myapp.ddns.net)绑定动态 IP,客户端通过域名访问。 -
云服务器:使用云服务商提供的固定公网 IP(如 AWS EIP、阿里云 EIP)。
-
-
-
端口号 :通常用知名端口(如
80用于 HTTP,443用于 HTTPS),或自定义端口(如8080)。
3、实际开发中的常见做法
(1) 硬编码(适合简单项目)
cpp
// 客户端代码(直接写死 IP 和端口)
const char* server_ip = "192.168.1.100";
int server_port = 8080;
-
优点:简单直接。
-
缺点:IP 或端口变更时需要重新编译代码。
(2) 配置文件(适合生产环境)
bash
// config.json
{
"server_ip": "192.168.1.100",
"server_port": 8080
}
-
客户端从配置文件中读取 IP 和端口。
-
优点:修改配置无需重新编译代码。
(3) 环境变量(适合容器化部署)
bash
# 启动客户端时通过环境变量传递
export SERVER_IP=192.168.1.100
export SERVER_PORT=8080
./client_app
-
客户端代码通过
getenv("SERVER_IP")获取。 -
优点:适合 Kubernetes、Docker 等容器化部署。
(4) 服务发现(适合微服务架构)
-
使用 Consul 、ZooKeeper 或 etcd 动态注册和发现服务器地址。
-
客户端从服务注册中心获取最新的服务器 IP 和端口。
4、总结
| 场景 | 如何获取 IP 和端口 |
|---|---|
| 本地测试 | IP 用 127.0.0.1,端口硬编码或从配置文件读取。 |
| 公司内网 | IP 通过内网 DNS 或配置文件获取,端口由服务器定义。 |
| 公网服务 | IP 用固定公网 IP 或 DDNS 域名,端口用知名端口或自定义端口。 |
| 动态环境(如云服务) | 通过服务发现(Consul)或环境变量动态获取。 |
关键点:
-
客户端和服务器必须提前约定好 IP 和端口(或通过某种机制动态获取)。
-
如果是同一家公司开发,通常会在文档或配置文件中明确说明服务器的访问方式。
十七、本地环回(Loopback)和网络绑定的关键概念
1、本地环回(Loopback)的作用
-
127.0.0.1(IPv4)和::1(IPv6) 是本地环回地址,用于本机内部通信。 -
数据包不会经过网卡,而是直接在操作系统内部转发,效率极高。
-
典型用途:
-
测试网络代码(无需依赖外部网络)。
-
运行客户端和服务端在同一台机器上(如
client和server都在本地)。
-
示例输出(ifconfig lo 或 ip addr show lo):
这两个命令都用于查看 本地环回接口(Loopback Interface,lo) 的信息,但它们来自不同的命令集:
-
ifconfig是传统的网络管理工具(属于net-tools包)。 -
ip是较新的现代工具(属于iproute2包,推荐使用)。
1. ifconfig lo
作用
显示本地环回接口 lo 的配置信息,包括:
-
IP 地址 (通常是
127.0.0.1) -
子网掩码 (
255.0.0.0) -
IPv6 地址 (
::1) -
数据包统计(接收/发送的包数量、错误等)
示例输出

关键信息解析
-
flags=73<UP,LOOPBACK,RUNNING>-
UP:接口已启用。 -
LOOPBACK:这是一个环回接口(数据不会经过网卡)。 -
RUNNING:接口正在工作。
-
-
mtu 65536:最大传输单元(MTU),环回接口的 MTU 非常大(因为数据只在内存中传输)。 -
inet 127.0.0.1:IPv4 地址是127.0.0.1(本地回环地址)。 -
inet6 ::1:IPv6 的本地回环地址是::1。 -
RX/TX packets:接收(RX)和发送(TX)的数据包数量及流量(这里是 8.8 GB)。
2. ip addr show lo
作用
功能与 ifconfig lo 类似,但输出格式更现代,属于 iproute2 工具集。
示例输出

关键信息解析
-
<LOOPBACK,UP,LOWER_UP>-
LOOPBACK:环回接口。 -
UP:接口已启用。 -
LOWER_UP:物理层(对虚拟接口无意义,但标记为可用)。
-
-
mtu 65536:最大传输单元(MTU)。 -
qdisc noqueue:队列策略(环回接口不需要排队)。 -
inet 127.0.0.1/8:IPv4 地址是127.0.0.1,子网掩码/8(即255.0.0.0)。 -
inet6 ::1/128:IPv6 地址是::1,子网掩码/128(仅单个地址)。 -
scope host:作用域是host(仅限本机访问)。
3. 主要区别
| 特性 | ifconfig lo |
ip addr show lo |
|---|---|---|
| 命令来源 | 传统 net-tools(已逐渐淘汰) |
现代 iproute2(推荐使用) |
| 输出格式 | 简洁,适合快速查看 | 更详细,适合脚本处理 |
| MTU、队列等信息 | 显示较少 | 显示更多底层信息(如 qdisc) |
| IPv6 支持 | 显示但格式较简单 | 显示更详细(如 scope host) |
4. 如何选择?
-
推荐使用
ip addr show lo(更现代,功能更强)。 -
如果系统没有
ip命令(如旧版 Linux),可以用ifconfig lo。 -
在脚本中,
ip命令更易解析(如ip -j addr show lo可输出 JSON 格式)。
总结
-
ifconfig lo和ip addr show lo都用于查看本地环回接口信息。 -
lo接口的作用:让本机客户端和服务端通信(不走物理网卡)。 -
推荐使用
ip命令 (ifconfig已逐渐被淘汰)。
如果只是查看环回接口,两个命令都可以;但如果要更详细的信息或脚本处理,ip 命令更强大。
2、网络绑定的关键规则
(1) 绑定 127.0.0.1(仅限本地访问)
server 绑定 127.0.0.1:
cpp
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定到环回地址
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
效果:
-
只有本机客户端 (
client也运行在同一台机器上)能访问。 -
外部机器无法访问 (因为
127.0.0.1是本地回环地址)。
(2) 绑定内网 IP(如 192.168.1.100)
server 绑定内网 IP:
cpp
addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 绑定到内网 IP
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
效果:
-
本机客户端 (
127.0.0.1或192.168.1.100)可以访问。 -
同一局域网的其他机器 (如
192.168.1.101)也可以访问。 -
但不能用
127.0.0.1访问 (因为server没有绑定127.0.0.1)。
(3) 绑定 0.0.0.0(监听所有网卡)
server 绑定 0.0.0.0 :INADDR_ANY 是一个宏,表示 0.0.0.0(IPv4)或 ::(IPv6)。
cpp
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用 IP
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
效果:
-
本机客户端 (
127.0.0.1或本机内网 IP)可以访问。 -
局域网其他机器 (如
192.168.1.x)可以访问。 -
公网 IP(如果有) 也可以访问(如果路由器做了端口转发)。
(4) 绑定公网 IP(通常不行!)
为什么不能直接绑定公网 IP?
-
公网 IP 通常由路由器或云服务商管理,本地机器没有直接绑定公网 IP 的权限。
-
即使绑定了,外部请求也可能被防火墙或 NAT 拦截。
正确做法:
-
在云服务器上,绑定
0.0.0.0,然后由云平台分配公网 IP。 -
在家庭宽带中,使用 DDNS + 端口转发(让路由器把外部请求转发到内网服务器)。
3、常见问题总结
| Server 绑定的 IP | Client 可以用哪些 IP 访问? | 能否被外部访问? |
|---|---|---|
127.0.0.1 |
仅 127.0.0.1 |
❌ 不能 |
192.168.1.100 |
127.0.0.1 或 192.168.1.100 |
✅ 同一局域网可访问 |
0.0.0.0 |
127.0.0.1 或本机内网 IP |
✅ 可(需 NAT/防火墙放行) |
公网 IP(如 8.8.8.8) |
❌ 不能直接绑定 | ❌ 通常不行 |
4、最佳实践
1. 本地测试:
-
server绑定127.0.0.1或0.0.0.0。 -
client用127.0.0.1访问。
2. 内网服务:
-
server绑定内网 IP 或0.0.0.0。 -
client用内网 IP 或127.0.0.1(如果server也绑定了127.0.0.1)。
3. 公网服务:
-
server绑定0.0.0.0。 -
在路由器或云平台配置端口转发/安全组规则。
5、总结
-
127.0.0.1:仅限本机通信,高效安全,适合测试。 -
内网 IP:适合局域网访问,但不能跨网络。
-
0.0.0.0:监听所有接口,适合需要外部访问的场景。 -
公网 IP:不能直接绑定,需通过云平台或路由器间接实现。
关键原则:
-
✅
client必须使用server绑定的 IP 地址访问! -
❌ 避免硬编码特定 IP,推荐用
0.0.0.0+ 配置文件/环境变量。
十八、补充:bzero 函数
bzero 是一个用于 清零内存 的 C 函数,通常用于初始化缓冲区或结构体(将内存块的所有字节设置为 0)。
1、函数原型
cpp
#include <strings.h> // 或 <string.h>(某些系统)
void bzero(void *s, size_t n);

-
s:指向要清零的内存块的指针(通常是数组或结构体)。 -
n:要清零的字节数。
2、作用
将内存块 s 的前 n 个字节全部设置为 0(相当于 memset(s, 0, n))。
示例:
cpp
char buffer[100];
bzero(buffer, sizeof(buffer)); // 将 buffer 的 100 字节全部置 0
3、类似函数
memset(s, 0, n)(更通用,推荐使用):
cpp
#include <string.h>
memset(buffer, 0, sizeof(buffer)); // 和 bzero 效果相同

calloc(分配并清零内存):
cpp
int *arr = (int *)calloc(10, sizeof(int)); // 分配 10 个 int 并初始化为 0

4、注意事项
-
bzero不是标准 C 函数 (属于 POSIX 标准),部分编译器可能不支持(如 Windows)。更推荐使用memset(标准 C 函数)。 -
bzero已逐渐被淘汰 ,现代代码建议用memset替代。 -
不能用于清零非内存块(如文件、寄存器等)。
5、示例对比
使用 bzero
cpp
#include <strings.h>
#include <stdio.h>
int main() {
char str[20] = "Hello, World!";
bzero(str, sizeof(str)); // 清零整个数组
printf("After bzero: '%s'\n", str); // 输出空(全是 '\0')
return 0;
}
使用 memset(推荐)
cpp
#include <string.h>
#include <stdio.h>
int main() {
char str[20] = "Hello, World!";
memset(str, 0, sizeof(str)); // 和 bzero 效果相同
printf("After memset: '%s'\n", str); // 输出空
return 0;
}
6、总结
-
bzero是一个简单的内存清零函数,但 非标准 C ,建议用memset替代。 -
作用 :将内存块的所有字节设为
0,常用于初始化缓冲区或结构体。 -
推荐写法:
cppmemset(&my_struct, 0, sizeof(my_struct)); // 清零结构体
如果遇到 bzero 不可用的情况,直接换 memset 即可!