当前位置: 首页 > news >正文

UDP网络编程:从客户端封装到服务端绑定的深度实践

目录

一、客户端套接字创建与封装

1、协议选择与套接字创建

2、简单的客户端类实现

二、客户端绑定问题分析

1、服务端绑定必要性

2、客户端不绑定的原因

3、客户端端口分配机制

4、显式绑定客户端的缺点

三、客户端使用示例

四、最佳实践建议

五、增加服务端IP地址和端口号

六、sendto函数

1、函数原型

2、参数详解

1.sockfd (int)

2.buf (const void *)

3.len (size_t)

4.flags (int)

5.dest_addr (const struct sockaddr *)

6.addrlen (socklen_t)

3、返回值

4、使用示例

5、关键注意事项

6、常见问题解决方案

7、高级用法

8、与相关函数的比较

9、最佳实践

10、完整示例代码

七、启动客户端函数

八、引入命令行参数

九、完整的客户端测试代码

十、本地测试

十一、云服务器网络测试与IP绑定

1、ICMP(简单了解)

2、实践中的绑定失败原因解析

3、正确解决方案:INADDR_ANY的应用

1. INADDR_ANY的技术原理

2. 正确绑定代码实现

3. 配套配置要点

4、特殊场景处理

1. 多IP服务器场景

2. IPv6环境配置

5、验证测试方法

6、常见问题排查

7、总结

十二、绑定INADDR_ANY的详细分析与实现

1、绑定INADDR_ANY的优势

多网卡环境下的优势

简单事例讲解

具体实现示例

2、验证绑定结果

3、特殊场景处理

云服务器环境注意事项

高级配置选项

4、最佳实践总结

十三、私有IP绑定测试

1、私有IP vs 公网IP vs INADDR_ANY

(1) 私有IP(如 172.31.9.74)

(2) 公网IP

(3) INADDR_ANY(0.0.0.0)

2、为什么上面的代码用私有IP可以运行?

3、关键区别总结

4、如何选择绑定方式?

(1) 推荐 INADDR_ANY 的情况

(2) 绑定特定IP的情况

(3) 代码改进建议

5、验证方法

6、云服务器的特殊情况

7、结论

十四、回顾UdpServer(服务端)的主要代码

UdpServer.cc

UdpServer.hpp

十五、补充:UDP 的 Socket 是全双工的

1、什么是全双工?

2、UDP 的 Socket 是全双工的

3、为什么 UDP 是全双工的?

4、对比 TCP

5、示例代码(UDP 全双工)

6、总结

十六、客户端访问服务器需要知道什么?

1、客户端必须知道服务器的 2 个关键信息

2、如果客户端和服务器是同一家公司写的,怎么获取 IP 和端口?

情况 1:服务器和客户端在同一台机器(本地测试)

情况 2:服务器和客户端在不同机器(公司内网)

情况 3:服务器在公网(对外提供服务)

3、实际开发中的常见做法

(1) 硬编码(适合简单项目)

(2) 配置文件(适合生产环境)

(3) 环境变量(适合容器化部署)

(4) 服务发现(适合微服务架构)

4、总结

十七、本地环回(Loopback)和网络绑定的关键概念

1、本地环回(Loopback)的作用

1. ifconfig lo

作用

示例输出

关键信息解析

2. ip addr show lo

作用

示例输出

3. 主要区别

4. 如何选择?

总结

2、网络绑定的关键规则

(1) 绑定 127.0.0.1(仅限本地访问)

(2) 绑定内网 IP(如 192.168.1.100)

(3) 绑定 0.0.0.0(监听所有网卡)

(4) 绑定公网 IP(通常不行!)

3、常见问题总结

4、最佳实践

5、总结

十八、补充:bzero 函数

1、函数原型

2、作用

3、类似函数

4、注意事项

5、示例对比

使用 bzero

使用 memset(推荐)

6、总结


一、客户端套接字创建与封装

        我们将UDP客户端封装为一个类,在创建客户端对象时需要进行初始化,主要工作是创建套接字。客户端后续的数据发送和接收操作都将基于这个套接字进行。

1、协议选择与套接字创建

客户端使用与服务器相同的协议族和服务类型:

  • 协议族:AF_INET (IPv4)

  • 服务类型:SOCK_DGRAM (UDP)

与服务器不同,客户端不需要显式绑定端口,操作系统会自动分配可用端口。

2、简单的客户端类实现

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、显式绑定客户端的缺点

  • 端口冲突风险:固定端口可能已被其他程序占用、导致客户端无法启动

  • 资源浪费:即使不使用也占用端口资源、限制系统可同时运行的客户端数量

  • 灵活性降低:无法在同一台机器上运行多个相同客户端实例、不利于测试和开发


三、客户端使用示例

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;
}

四、最佳实践建议

  • 错误处理:始终检查系统调用返回值、使用errnostrerror获取详细错误信息

  • 资源管理:确保套接字在不再需要时关闭、考虑使用RAII模式管理资源

  • 性能优化:对于高频通信,考虑重用地址选项(SO_REUSEADDR)、批量发送数据减少系统调用次数

  • 可移植性:检查系统调用返回值而非假设成功、处理不同平台可能的行为差异

  • 安全性:验证输入IP地址和端口号、限制接收缓冲区大小防止溢出

通过这种设计,UDP客户端既保持了简单性,又提供了必要的灵活性和健壮性,适合大多数网络通信场景。


五、增加服务端IP地址和端口号

        正如上面演示的代码一样,客户端需要明确服务端的IP地址和端口号才能建立连接。因此,在客户端类中需存储这些信息,并通过构造函数在初始化时传入相应参数来完成配置。

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、函数原型

#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、使用示例

// 创建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. 发送到多个目标

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 writewrite 只能用于已连接的套接字、不能指定目标地址

9、最佳实践

  • 总是检查返回值

  • 使用 sizeof 计算地址结构长度,而不是硬编码

  • 在发送前验证目标地址的有效性

  • 考虑使用包装函数处理错误和日志记录

  • 对于关键应用,实现重试机制(但注意 UDP 不可靠性)

  • 合理设置超时(使用 setsockopt 设置 SO_SNDTIMEO

10、完整示例代码

#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 客户端程序中使用它来发送数据。


七、启动客户端函数

客户端在向服务端发送数据时,需持续采集用户输入并实时传输。

处理过程中需注意两点:

  1. 端口号需由主机字节序转换为网络字节序,通过htons函数转换后存入sockaddr_in结构体

  2. 字符串格式的IP地址需调用inet_addr函数转换为整数形式,再设置到sockaddr_in结构体中

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和端口号即可完成连接设置。

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函数即可启动客户端程序。


九、完整的客户端测试代码

#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的有效性。

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到用户程序,这是出于安全考虑的设计

具体技术表现:

// 错误示例:尝试直接绑定公网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. 正确绑定代码实现

// 正确示例:使用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. 配套配置要点

要确保外网访问成功,还需完成以下配置:

  1. 安全组规则

    • 在云控制台开放对应端口(如TCP 8080)

    • 设置允许的源IP范围(建议先开放0.0.0.0/0测试)

  2. 防火墙设置

    # Ubuntu示例
    sudo ufw allow 8080/tcp# CentOS示例
    sudo firewall-cmd --add-port=8080/tcp --permanent
    sudo firewall-cmd --reload
  3. 服务监听验证

    netstat -tulnp | grep 8080
    # 应显示类似:tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN

4、特殊场景处理

1. 多IP服务器场景

当服务器配置了多个网络接口时,INADDR_ANY会让服务监听所有接口。如需指定特定接口:

// 获取特定网卡的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的IPif (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

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、验证测试方法

完成配置后,可通过以下步骤验证:

  1. 本地测试

    curl http://localhost:8080
    # 或
    telnet localhost 8080
  2. 内网测试

    curl http://<内网IP>:8080
  3. 外网测试

    curl http://<公网IP>:8080
    # 或使用在线工具如 https://www.canyouseeme.org/ 测试端口可达性

6、常见问题排查

若仍无法访问,请检查:

  1. 服务是否真正运行:ps aux | grep your_service

  2. 端口是否监听:ss -tulnp | grep 8080

  3. 安全组规则是否正确配置

  4. 云服务商是否有额外限制(如某些厂商要求绑定弹性公网IP)

7、总结

        在云服务器环境中实现外网访问的正确方法是使用INADDR_ANY进行绑定,而非直接绑定分配的公网IP。这种设计既保证了安全性,又提供了最大的灵活性。配合适当的安全组规则和防火墙配置,可以构建既安全又易于访问的网络服务。理解这些底层原理,能帮助开发者更好地应对各种网络部署场景。


十二、绑定INADDR_ANY的详细分析与实现

1、绑定INADDR_ANY的优势

在现代服务器架构中,绑定INADDR_ANY(0.0.0.0)是一种被广泛推荐的做法,特别是在高可用性和多网卡环境下。以下是详细分析:

多网卡环境下的优势

1. 负载均衡与性能优化

  • 当服务器配备多张网卡时,每张网卡都能独立接收数据

  • 操作系统内核会自动处理数据包的路由,确保最优路径

  • 避免了单网卡成为性能瓶颈的问题

2. 简化配置管理

  • 无需为不同网卡配置不同的服务实例

  • 单一服务实例可以处理来自所有网络接口的请求

  • 特别适用于需要动态添加/移除网卡的场景

3. 高可用性保障

  • 当某块网卡故障时,服务仍可通过其他网卡继续运行

  • 配合负载均衡器使用时,可以实现无缝故障转移

简单事例讲解

        当服务器带宽充足时,单台机器的数据接收能力往往会成为IO效率的瓶颈。为此,服务器底层通常会配置多张网卡,从而拥有多个IP地址。值得注意的是,虽然服务器有多个IP,但端口号为8080的服务在整个服务器上仅有一个实例。

        当数据到达服务器时,所有网卡都会在底层接收到这些数据。如果这些数据都请求访问8080端口服务,此时不同的绑定方式会产生不同效果:

  1. 若服务端明确绑定到特定IP地址,则只能通过该IP对应的网卡接收数据

  2. 若服务端绑定为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、验证绑定结果

编译并运行上述程序后,可以通过以下命令验证绑定情况:

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选项

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):

setsockopt(_sockfd, IPPROTO_IP, IP_FREEBIND, &optval, sizeof(optval));

4、最佳实践总结

  • 默认使用INADDR_ANY:除非有特殊安全需求,否则优先绑定到所有接口、简化配置,提高可用性

  • 安全考虑:结合防火墙规则限制访问、使用网络层ACL进行更细粒度控制

  • 监控与调试:监控各网卡的流量负载、使用tcpdump等工具验证数据包路由

通过这种方式实现的服务器能够充分利用多网卡环境,同时保持配置的简洁性和可维护性。


十三、私有IP绑定测试

        在上面提供的代码中,使用私有IP地址(如172.31.9.74)可以运行服务端,这与使用公网IP或INADDR_ANY0.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

// 在 UdpServer 类的绑定逻辑中:
local.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口

并移除 main 函数中硬编码的私有IP:

// 修改前:
const std::string ip = "172.31.9.74";// 修改后:
const std::string ip = "0.0.0.0"; // 或直接不指定IP,由InitServer内部处理

5、验证方法

检查监听状态

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

#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替代atoiif (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

#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 全双工)

// 服务器和客户端可以用同一个 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 或公网 IP 203.0.113.45):相当于“服务器的门牌号”,告诉数据包该往哪里发送。

  • 服务器的端口号(如 80808053):相当于“门牌号下的具体房间”,告诉操作系统把数据交给哪个服务(如 HTTP 服务通常用 80 端口)。

类比:你要给朋友寄快递,必须知道:

  • 朋友的家庭住址(IP 地址)

  • 朋友的收件房间号(端口号,比如“101 室”是 HTTP 服务,“102 室”是数据库服务)

2、如果客户端和服务器是同一家公司写的,怎么获取 IP 和端口?

情况 1:服务器和客户端在同一台机器(本地测试)

  • IP 地址:直接用 127.0.0.1(回环地址,表示“本机”)

  • 端口号:在代码中硬编码(如 8080),或通过配置文件读取。

  • 示例

    // 客户端代码(假设服务器在本机运行)
    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) 硬编码(适合简单项目)

// 客户端代码(直接写死 IP 和端口)
const char* server_ip = "192.168.1.100";
int server_port = 8080;
  • 优点:简单直接。

  • 缺点:IP 或端口变更时需要重新编译代码。

(2) 配置文件(适合生产环境)

// config.json
{"server_ip": "192.168.1.100","server_port": 8080
}
  • 客户端从配置文件中读取 IP 和端口。

  • 优点:修改配置无需重新编译代码。

(3) 环境变量(适合容器化部署)

# 启动客户端时通过环境变量传递
export SERVER_IP=192.168.1.100
export SERVER_PORT=8080
./client_app
  • 客户端代码通过 getenv("SERVER_IP") 获取。

  • 优点:适合 Kubernetes、Docker 等容器化部署。

(4) 服务发现(适合微服务架构)

  • 使用 ConsulZooKeeper 或 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 loip 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

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

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.0INADDR_ANY 是一个宏,表示 0.0.0.0(IPv4)或 ::(IPv6)。

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 绑定的 IPClient 可以用哪些 IP 访问?能否被外部访问?
127.0.0.1仅 127.0.0.1❌ 不能
192.168.1.100127.0.0.1 或 192.168.1.100✅ 同一局域网可访问
0.0.0.0127.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、函数原型

#include <strings.h>  // 或 <string.h>(某些系统)void bzero(void *s, size_t n);

  • s:指向要清零的内存块的指针(通常是数组或结构体)。

  • n:要清零的字节数。

2、作用

将内存块 s 的前 n 个字节全部设置为 0(相当于 memset(s, 0, n))。

示例

char buffer[100];
bzero(buffer, sizeof(buffer));  // 将 buffer 的 100 字节全部置 0

3、类似函数

memset(s, 0, n)(更通用,推荐使用):

#include <string.h>
memset(buffer, 0, sizeof(buffer));  // 和 bzero 效果相同

calloc(分配并清零内存):

int *arr = (int *)calloc(10, sizeof(int));  // 分配 10 个 int 并初始化为 0

4、注意事项

  • bzero 不是标准 C 函数(属于 POSIX 标准),部分编译器可能不支持(如 Windows)。更推荐使用 memset(标准 C 函数)。

  • bzero 已逐渐被淘汰,现代代码建议用 memset 替代。

  • 不能用于清零非内存块(如文件、寄存器等)。

5、示例对比

使用 bzero

#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(推荐)

#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,常用于初始化缓冲区或结构体。

  • 推荐写法

    memset(&my_struct, 0, sizeof(my_struct));  // 清零结构体

如果遇到 bzero 不可用的情况,直接换 memset 即可!

http://www.dtcms.com/a/605373.html

相关文章:

  • Arbess从初级到进阶(4) - 使用Arbess+GitLab实现React.js 项目自动化部署
  • 内网穿透技术
  • asp.net做织梦网站长沙商城网站开发
  • [免费]基于Python的深度学习豆瓣电影数据可视化+情感分析推荐系统(Flask+Vue+LSTM+scrapy)【论文+源码+SQL脚本】
  • SQL 分类
  • 微信小程序项目上传到git仓库(完整操作)
  • Vue 3响应式系统的底层机制:Proxy如何实现依赖追踪与自动更新?
  • 【MySQL】MySQL库的操作
  • 研发管理知识库(10)AWS云的核心DevOps工具介绍
  • PostgreSQL 备份导致的 Cache Pollution(缓存污染)
  • 拒绝繁杂,一款轻量,极致简洁的开源DevOps平台 - TikLab
  • 深入解析Flink会话窗口机制
  • 南京建设网站企业wordpress的伪静态
  • redis的下载和安装详解
  • 搜索智能体
  • 第27集科立分板机:东莞科立自动化流水线带领生产新变革
  • 物流网站开发实训离型剂技术支持东莞网站建设
  • Ubuntu 24.04 一站式 Flask 生产部署:pyenv + PyCharm + Gunicorn + Nginx + systemd
  • 青海省公路建设服务网站模块化网站开发
  • 开源CICD工具-Drone
  • 给予虚拟成像台尝鲜版十之二,完善支持 HTML 原型模式
  • 原生表格文本过长展示问题,参考layui长文本,点击出现文本域
  • 桂林网站建设培训asp.net网站建设
  • Ubuntu 24.04 MariaDB 完整安装与配置文档
  • [特殊字符] 在 Linux 上设置 SQLite
  • Arbess从初级到进阶(2) - 使用Arbess+GitLab实现Vue.js项目自动化部署
  • 网站开发外文参考文献邯郸小学网站建设
  • C语言编译器最新版 | 提升开发效率,优化性能
  • 手游网站怎么做企业型网站
  • 用Rust实现二进制文件差异工具