跳到主要内容Linux 网络编程:UDP Socket 群聊模型实现与细节分析 | 极客日志C++
Linux 网络编程:UDP Socket 群聊模型实现与细节分析
Linux 网络编程中 UDP Socket 群聊模型通过服务端接收消息并转发给在线用户,客户端发送消息并接收广播实现。服务端需绑定端口维护用户表,利用 recvfrom 获取客户端地址;客户端无需绑定端口由系统分配。代码采用 C++ 类封装服务端逻辑,多线程处理收发。关键技术点包括 SOCK_DGRAM 选择、inet_addr 监听所有网卡、端口号避免冲突及无连接特性理解。该模型适用于实时性高允许丢包的场景如聊天室或游戏同步。
黑客帝国2 浏览 Linux 网络编程:UDP Socket 群聊模型实现与细节分析
本文基于 Linux 网络编程,使用 C/C++ 的 socket API,实现一个简单的 UDP 群聊程序,以加深对 UDP 通信模型的理解。
整体设计思路
Server 负责收消息、记住客户端并转发消息;Client 负责发消息和收广播。
Server 的核心职责
socket(AF_INET, SOCK_DGRAM):创建 UDP 套接字
bind:占住一个众所周知的端口,等客户端来找
recvfrom:接收数据,顺带感知客户端的 IP 和端口
- 用
ip + port 作为 key,维护一个在线用户表
- 每收到一条消息,就
sendto 给所有用户 → 群聊效果
Client 的核心职责
- 不
bind,交给 OS 随机分配端口
- 一个线程:从标准输入读数据,
sendto 发给 server
- 一个线程:
recvfrom 等 server 的广播消息
UDP 服务端
class Server {
public:
Server(std::string server_ip, uint16_t server_port)
: server_ip_(server_ip), server_port_(server_port) {}
void init() {
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0) {
printf("socket fail!\n");
exit(1);
}
printf("socket successful! socket:%d\n", sockfd_);
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(server_port_);
local.sin_addr.s_addr = inet_addr(server_ip_.c_str());
socklen_t address_len = sizeof(local);
if (bind(sockfd_, (struct sockaddr *)&local, address_len) < 0) {
printf("bind fail!!!\n");
exit(2);
}
printf("bind successful!!!\n");
}
void broadcast(std::string info, std::string client_ip, uint16_t client_port) {
std::string message = "[ ";
message += client_ip;
message += " : ";
message += std::to_string(client_port);
message += " ] ";
message += info;
for (auto user : users) {
sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&user.second, sizeof(user.second));
}
}
void run() {
while (1) {
char buffer[1024];
char key[256];
memset(buffer, 0, sizeof(buffer));
memset(key, 0, sizeof(key));
struct sockaddr_in client;
bzero(&client, sizeof client);
socklen_t client_len = sizeof(client);
int n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &client_len);
std::string client_ip = inet_ntoa(client.sin_addr);
uint16_t client_port = ntohs(client.sin_port);
snprintf(key, sizeof key, "%s-%d", client_ip.c_str(), client_port);
auto is_exist = users.find(key);
if (is_exist == users.end()) {
users.insert(std::make_pair(key, client));
printf("new user add\n");
}
if (n > 0) {
buffer[n] = 0;
std::string info = buffer;
broadcast(info, client_ip, client_port);
}
}
}
~Server() {
if (sockfd_ > 0) {
close(sockfd_);
}
}
private:
std::string server_ip_;
uint16_t server_port_;
int sockfd_;
std::unordered_map<std::string, struct sockaddr_in> users;
};
int main() {
Server* scr = new Server("0.0.0.0", 8080);
scr->init();
scr->run();
}
UDP 客户端
struct ThreadData {
int sockfd;
struct sockaddr_in server;
};
void *send_msg(void *arg) {
ThreadData *td = (ThreadData *)arg;
std::string message;
while (1) {
std::getline(std::cin, message);
sendto(td->sockfd, message.c_str(), message.size(), 0, (sockaddr *)&(td->server), sizeof(td->server));
}
return nullptr;
}
void *recv_msg(void *arg) {
ThreadData *td = (ThreadData *)arg;
char buffer[1024];
while (1) {
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (s > 0) {
buffer[s] = 0;
std::cerr << buffer << std::endl;
}
}
return nullptr;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("\n\t usage : %s server_ip server_port\n", argv[0]);
exit(1);
}
ThreadData td;
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
td.server = server;
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0) {
printf("socket fail!!!\n");
exit(1);
}
printf("socket successful, sockfd : %d\n", td.sockfd);
pthread_t send, recv;
pthread_create(&send, nullptr, send_msg, &td);
pthread_create(&recv, nullptr, recv_msg, &td);
pthread_join(send, nullptr);
pthread_join(recv, nullptr);
close(td.sockfd);
return 0;
}

为什么调用 socket 系统调用时使用的是 SOCK_DGRAM,而不是 SOCK_STREAM?
- TCP:面向连接、可靠传输、面向字节流
- UDP:无连接、不可靠传输、面向报文
这是因为 UDP 是面向数据报的传输层协议,所以选用 SOCK_DGRAM。当我们需要进行 TCP 网络编程时,就需要使用 SOCK_STREAM。
为什么 server 要 bind 具体的端口号,而 client 却不用?
首先客户端是需要 bind 端口号的,只不过不需要我们显式地进行 bind,可以让操作系统随机选择即可。并且一个端口号只能被一个进程 bind,不能被多个进程同时 bind,这是因为端口号具有唯一标识主机进程的作用。如果一个端口被多个进程 bind,对端发过来的消息,我们就不知道给哪一个进程发送了。
现在假如我们将客户端也 bind 一个具体的端口号的话,那么就会很容易造成冲突。比如我们现在正在悠闲的躺在床上刷着抖音,而我们使用的抖音客户端假如 bind 的是 1234 这个端口号,现在到中午了,我们要开始觅食了,这个时候我们要打开美团进行点外卖了,但是美团的客户端也 bind 了 1234 这个端口号,这个时候一旦你要打开美团这个软件,不好意思,这个时候你已经打不开了,因为你正在刷抖音的客户端已经占用了 1234 这个端口号,所以美团想要打开是不可以的。所以对于客户端直接 bind 一个具体的端口号是不现实的,我们手机上面这么多软件,总不能让所有的这些软件的公司都协商一下,一人用一个吧,所以客户端不能直接 bind 一个具体的端口号,只能交给我们的操作系统,让我们操作系统随机为我们分配一个端口号就可以了,只要这个端口号唯一,我们就可以与远程的服务器进行通信,从而让我们再快乐刷抖音的同时,可以打开美团点外卖了。
这就是为什么 server 要 bind 具体的端口号,而 client 却不需要。
inet_addr("0.0.0.0") 是什么意思?
这个其实就是表示监听我这个主机中所有的网卡,只要是发给我这个主机的,统统接收。因为我们的主机不止一个网卡,会有许多的网卡,其它的不用多说,起码有线网卡和无线网卡就有两个,现在我们大多使用的都是无线网卡,还有像虚拟机中的虚拟网卡等等都会进行监听,所有的数据只要发给我这台主机,统统照单全收。还可以使用以下的方式:
local.sin_addr.s_addr = htonl(INADDR_ANY);
0.0.0.0 = INADDR_ANY,都是表示监听所有网卡,异曲同工之妙。
recvfrom 为什么要传 sockaddr_in*?
这个就很简单了,sockaddr_in 保存了发给我们数据的对端的 IP 和端口号,这样我们就知道这个数据是谁给我们发过来的,这样我们在处理完数据之后就可以通过这个 IP 和端口号再将我们想要回复的消息通过 sendto 系统调用接口给对方返回回去。
UDP 为什么不需要 connect?
这是因为 UDP 是无连接,不可靠的传输层协议,是不存在连接状态的维护,并且 sendto / recvfrom 每次都携带目标地址,同时 UDP 也不需要 listen 和 accept,这些都是 TCP 专用接口,我们再接下来的 TCP 网络编程中会见到这些接口的详细使用的,现在不需要着急。
server 绑定的端口号不要选择 [0,1024],尽量选择 1024 以上
这是因为在操作系统中,0~1023 这区间内的端口号通常已经被系统服务或标准网络协议占用。
| 端口号 | 服务 |
|---|
| 22 | SSH |
| 21 | FTP |
| 23 | Telnet |
| 25 | SMTP |
| 53 | DNS |
| 80 | HTTP |
| 443 | HTTPS |
所以为了避免端口冲突,选择 1024 以上的端口,可以尽可能的减少冲突。
UDP 网络编程的核心思想并不在于'接口有多复杂',而在于对无连接模型的正确理解:
- 没有连接状态
- 每个数据包自带源地址
- 通信关系由数据'自然形成'
正是这种特性,使得 UDP 非常适合实时性要求高、允许少量丢包的场景,例如聊天室、直播、游戏同步等。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online