Socket 编程-UDP
文章目录
-
- [Socket 编程-UDP](#Socket 编程-UDP)
-
- [1. Version1-Echo Server](#1. Version1-Echo Server)
-
- [1.1 服务端代码](#1.1 服务端代码)
- [1.2 客户端代码](#1.2 客户端代码)
- [1.3 测试](#1.3 测试)
- [2. Version2-Dictionary Server](#2. Version2-Dictionary Server)
-
- [2.1 词典](#2.1 词典)
- [2.2 词典服务器代码](#2.2 词典服务器代码)
- [2.3 词典客户端实现](#2.3 词典客户端实现)
- [2.4 测试](#2.4 测试)
- [3. Version3-简单聊天室](#3. Version3-简单聊天室)
-
- [3.1 路由模块实现](#3.1 路由模块实现)
- [3.2 聊天室服务器代码](#3.2 聊天室服务器代码)
- [3.3 聊天室客户端代码](#3.3 聊天室客户端代码)
- [3.4 测试](#3.4 测试)
- [4. 总结](#4. 总结)
1. Version1-Echo Server
实现一个简单的回显服务器和客户端程序
1.1 服务端代码
UdpServer.hpp:
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp" // 之前编写的日志模块
using namespace LogModule;
class UdpServer
{
int default_file_descriptor = -1;
using func_t = std::function<std::string(void *)>;
public:
UdpServer(uint16_t port, func_t func)
: _socket_fd(default_file_descriptor),
_port(port),
_is_running(false),
_func(func)
{
}
~UdpServer(){}
// 初始化工作
void Initial()
{
// 1. 创建套接字-得到socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
// 2. 填充server的sockaddr_in信息
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info)); // 清空sockaddr_in中原有的内容
server_info.sin_port = htons(_port); // 填充port
server_info.sin_family = AF_INET; // IPv4的地址族
// server通常会有多张网卡,如果绑定某个指定的IP地址,那么client发送到其它IP地址的数据就无法接收
// ./server 8080 192.168.2.203,那么server就只能接收来自192.168.2.203的消息,甚至连127.0.0.1也不能接收
server_info.sin_addr.s_addr = INADDR_ANY; // 填充IP地址(在sin_addr中的s_addr中)
// 3. 绑定server的IP和port
// server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
int ret = bind(_socket_fd, (struct sockaddr *)&server_info, sizeof(server_info));
if (ret < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(1);
}
LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
}
// 启动UDP服务
void Start()
{
// 修改标志位
_is_running = true;
// 一直运行,直到主动退出-长服务
while (_is_running)
{
// server收到client发送的消息要进行echo-再发一次给client
// 1. 接收从client发来的消息
char buffer[1024];
struct sockaddr_in client_info; // client的信息
socklen_t len = sizeof(client_info);
ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_info, &len);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
else
{
buffer[receive_size] = '\0'; // 保证消息完整性
uint16_t peer_port = static_cast<uint16_t>(htons(client_info.sin_port));
std::string peer_ip = inet_ntoa(client_info.sin_addr);
std::cout << "Received a message with the IP address " << peer_ip << " via port " << peer_port << ":" << std::endl;
// 回调处理 - 将具体的处理逻辑交给外部,内部专注于连接
std::string echo_message = _func(buffer);
// 2. 将消息发回给client
ssize_t send_size = sendto(_socket_fd, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr *)&client_info, len);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
}
}
}
private:
int _socket_fd; // 打开socket的文件描述符
uint16_t _port; // 要绑定的port
bool _is_running; // 运行标志位
func_t _func; // 外部回调函数
};
UdpServer.cpp:
cpp
#include <iostream>
#include <memory>
#include <string>
#include "UdpServer.hpp"
std::string Task(void *args)
{
std::cout << "Start processing" << std::endl;
std::string message("Server-side echo \"");
message += static_cast<char *>(args);
message += "\"";
std::cout << "Processing complete" << std::endl;
return message;
}
// ./udp_server 8080 127.0.0.1
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
return 1;
}
// uint16_t port = *argv[1]; // 字符类型->uint16_t
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
// std::string ip = argv[2];
std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(UdpServer(port, Task));
udp_server->Initial();
udp_server->Start();
return 0;
}
1.2 客户端代码
UdpClient.hpp:
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
class UdpClient
{
int default_file_descriptor = -1;
public:
UdpClient(uint16_t port, std::string ip)
: _socket_fd(default_file_descriptor),
_server_port(port),
_server_ip(ip)
{
}
void Initial()
{
// 1. 获取socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
// 2. 绑定IP和端口
// 不同client程序可以使用相同的源端口连接到不同的服务器
// 因为连接是由(源IP,源端口,目标IP,目标端口)四元组唯一确定的
// 如果手动绑定,就可能会出现端口冲突的问题
// client只需要知道server的IP+port就可以发送数据-不用考虑通过哪个port发送(由操作系统自动分配)-也不用绑定IP(由操作系统选择合适的网卡发送)
}
void Start()
{
// 用于填充server信息
struct sockaddr_in server_info;
memset(&server_info, 0, sizeof(server_info)); // 清空
server_info.sin_family = AF_INET;
server_info.sin_port = htons(_server_port);
server_info.sin_addr.s_addr = inet_addr(_server_ip.c_str());
// client也是一样的,不主动退出就一直运行
while (true)
{
std::string message;
message.clear();
std::cout << "Please enter# ";
std::getline(std::cin, message); // C++版的getline
socklen_t len_send = sizeof(server_info);
// 1. 发送消息给server
ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&server_info, len_send);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
// 2. 接收server发回来的消息
char buffer[1024];
struct sockaddr_in receive_info; // server的信息
socklen_t len_receive = sizeof(receive_info);
ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&receive_info, &len_receive);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
buffer[receive_size] = '\0';
std::cout << buffer << std::endl;
}
}
~UdpClient() {}
private:
int _socket_fd;
uint16_t _server_port;
std::string _server_ip;
};
UdpClient.cpp:
c
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"
// ./udp_client 8080 127.0.0.1
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
return 1;
}
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
std::string ip = argv[2];
std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
udp_client->Initial();
udp_client->Start();
return 0;
}
1.3 测试
启动服务端:
bash
$ ./udp_server 8080
[2025-12-13 19:25:27] [INFO] [UdpServer.hpp] [40] - socket create success, socket file descriptor: 3
[2025-12-13 19:25:27] [INFO] [UdpServer.hpp] [60] - bind success, bind file descriptor: 3
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
^C
130 $
启动客户端:
bash
$ ./udp_client 8080 127.0.0.1
[2025-12-13 19:26:07] [INFO] [UdpClient.hpp] [30] - socket create success, socket file descriptor: 3
Please enter# 你好
Server-side echo "你好"
Please enter# Hello
Server-side echo "Hello"
Please enter# Goodbye
Server-side echo "Goodbye"
Please enter# ^C
130 $
可以看到,客户端发送的消息都被服务端正确接收并处理,随后服务端将处理结果回传给客户端,客户端也正确接收并显示了回显内容。UDP 通信实现成功!
期中,服务器通常不绑定特定 IP 地址,而是绑定到 INADDR_ANY,以便监听所有可用的网络接口。这种方式确保服务器能够接收发送到任何本地 IP 地址的数据包,从而提高了服务的可用性和灵活性。
INADDR_ANY 是一个特殊的 IP 地址,表示"所有可用的接口"。当服务器绑定到INADDR_ANY 时,它实际上是在告诉操作系统监听所有网络接口上的数据包,而不仅仅是某个特定的 IP 地址。这对于服务器来说是非常有用的,因为它允许服务器接收来自任何网络接口的数据,而不需要事先知道客户端将连接到哪个具体的 IP 地址。
其具体定义其实就是 0 转换而来:
c
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
2. Version2-Dictionary Server
实现一个简单的英译汉网络词典
2.1 词典
DictServer.hpp:
cpp
#pragma once
#include <string>
#include <map>
#include <fstream>
#include "Log.hpp"
using namespace LogModule;
#define SEP " "
std::string default_dict_file = "./dictionary.txt";
class Dict
{
public:
Dict(std::string path = default_dict_file)
: _dict_file(path)
{
}
bool Load()
{
std::fstream dict_file;
dict_file.open(_dict_file, std::ios::in);
if (!dict_file.is_open())
{
LOG(LogLevel::ERROR) << "Open " << _dict_file.c_str() << " failed";
return false;
}
std::string line;
ssize_t line_number = 0; // 字典行号计数器
while (std::getline(dict_file, line))
{
++line_number;
// abandon v.抛弃,放弃
std::string english;
std::string info;
auto pos_forward = line.find(SEP);
auto pos_reverse = line.rfind(SEP);
english = line.substr(0, pos_forward);
info = line.substr(pos_reverse + 1, line.size());
// LOG(LogLevel::DEBUG) << "English: " << english << ", explain: " << info;
if (english.empty() || info.empty())
{
LOG(LogLevel::ERROR) << _dict_file << " line " << line_number
<< (english.empty() ? " no english content" : "no explain information")
<< "[RAW-LINE - " << line << "]";
}
_dict.emplace(std::make_pair(english, info));
}
dict_file.close();
LOG(LogLevel::INFO) << "Load complete";
return true;
}
std::string Translate(const std::string &word)
{
LOG(LogLevel::INFO) << "Request for translation services";
return _dict[word];
}
~Dict() {}
private:
std::string _dict_file;
std::map<std::string, std::string> _dict;
};
2.2 词典服务器代码
每次手动写 inet* 地址结构和进行地址信息太麻烦了,封装一个地址结构,转换函数 InetAddr.hpp:
cpp
#pragma once
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
class Addr
{
public:
Addr(struct sockaddr_in addr)
: _raw_addr(addr),
_port(static_cast<uint16_t>(htons(addr.sin_port))),
_ip(inet_ntoa(addr.sin_addr))
{
}
~Addr() {}
uint16_t GetPort() { return _port; }
std::string GetIp() { return _ip; }
struct sockaddr_in GetRawAddr() { return _raw_addr; }
private:
struct sockaddr_in _raw_addr; // 原生地址结构
uint16_t _port; // 端口
std::string _ip; // IP地址信息
};
UdpServer.hpp:
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
class UdpServer
{
int default_file_descriptor = -1;
using func_t = std::function<std::string(std::string)>;
public:
UdpServer(uint16_t port, func_t func)
: _socket_fd(default_file_descriptor),
_port(port),
// _ip(ip),
_is_running(false),
_func(func)
{
}
~UdpServer()
{
}
// 初始化工作
void Initial()
{
// 1. 创建套接字-得到socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
// 填充server的sockaddr_in信息
struct sockaddr_in server_info;
bzero(&server_info, sizeof(server_info)); // 清空sockaddr_in中原有的内容
server_info.sin_port = htons(_port); // 填充port
server_info.sin_family = AF_INET; // IPv4的地址族
// server_info.sin_addr.s_addr = inet_addr(_ip.c_str()); // 填充IP地址(在sin_addr中的s_addr中)
// server通常会有多张网卡,如果绑定某个指定的IP地址,那么client发送到其它IP地址的数据就无法接收
server_info.sin_addr.s_addr = INADDR_ANY; // 填充IP地址(在sin_addr中的s_addr中)
// 2. 绑定server的IP和port
// server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
int ret = bind(_socket_fd, (struct sockaddr *)&server_info, sizeof(server_info));
if (ret < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(1);
}
LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
}
// 启动UDP服务
void Start()
{
// 修改标志位
_is_running = true;
// 一直运行,直到主动退出
while (_is_running)
{
// server收到client发送的消息要进行echo-再发一次给client
// 1. 接收从client发来的消息
char word[1024];
struct sockaddr_in client_info; // client的信息
socklen_t len = sizeof(client_info);
ssize_t receive_size = recvfrom(_socket_fd, word, sizeof(word) - 1, 0, (struct sockaddr *)&client_info, &len);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
else
{
// 每次发来的就是一个单词
word[receive_size] = '\0'; // 保证消息完整性
// 封装成为一个类
Addr info(client_info);
// uint16_t peer_port = static_cast<uint16_t>(htons(client_info.sin_port));
// std::string peer_ip = inet_ntoa(client_info.sin_addr);
LOG(LogLevel::INFO) << "Received a word with the IP address " << info.GetIp() << " via port " << info.GetPort();
// 回调处理 -> 不仅仅是回调,控制传出的参数 -> 外部需要的参数读通过这里传出
std::string result = _func(word);
if (result.empty())
result = "No such word found";
// 将处理逻辑交给外部,内部专注于连接
// std::string echo_message("Server-side echo \"");
// echo_message += buffer;
// echo_message += "\"";
// 2. 将消息发回给client
ssize_t send_size = sendto(_socket_fd, result.c_str(), result.size(), 0, (struct sockaddr *)&client_info, len);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
}
}
}
private:
int _socket_fd; // 打开socket的文件描述符
uint16_t _port; // 要绑定的port
bool _is_running; // 运行标志位
func_t _func; // 外部回调函数
};
UdpServer.cc:
cpp
#include <iostream>
#include <memory>
#include <string>
#include "UdpServer.hpp"
#include "Dictionary.hpp"
// ./udp_server 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
std::unique_ptr<Dict> dict = std::make_unique<Dict>();
dict->Load();
// 模块的解耦合,从网络通信模块跳转到翻译模块
std::unique_ptr<UdpServer> udp_server =
std::make_unique<UdpServer>(UdpServer(port, [&dict](std::string word) -> std::string
{ return dict->Translate(word); }));
udp_server->Initial();
udp_server->Start();
return 0;
}
2.3 词典客户端实现
UdpClient.hpp:
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
class UdpClient
{
int default_file_descriptor = -1;
public:
UdpClient(uint16_t port, std::string ip)
: _socket_fd(default_file_descriptor),
_server_port(port),
_server_ip(ip)
{
}
void Initial()
{
// 1. 获取socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
}
void Start()
{
// 填充server信息
struct sockaddr_in server_info;
memset(&server_info, 0, sizeof(server_info));
server_info.sin_family = AF_INET;
server_info.sin_port = htons(_server_port);
server_info.sin_addr.s_addr = inet_addr(_server_ip.c_str());
while (true)
{
std::string message;
message.clear();
std::cout << "Please enter# ";
std::getline(std::cin, message); // C++版的getline
socklen_t len_send = sizeof(server_info);
// 1. 发送消息给server
ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&server_info, len_send);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
// 2. 接收server发回来的消息
char buffer[1024];
struct sockaddr_in receive_info; // server的信息
socklen_t len_receive = sizeof(receive_info);
ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&receive_info, &len_receive);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
buffer[receive_size] = '\0';
std::cout << buffer << std::endl;
}
}
~UdpClient() {}
private:
int _socket_fd;
uint16_t _server_port;
std::string _server_ip;
};
UdpClient.cpp:
cpp
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"
// ./udp_client 8080 127.0.0.1
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
return 1;
}
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
std::string ip = argv[2];
std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
udp_client->Initial();
udp_client->Start();
return 0;
}
2.4 测试
词典文件 dictionary.txt 内容如下:
txt
abandon v.抛弃,放弃
abandonment n.放弃
abbreviation n.缩写
abeyance n.缓办,中止
abide v.遵守
......
启动词典服务器:
bash
$./udp_server 8080
[2025-12-13 19:59:28] [INFO] [Dictionary.hpp] [58] - Load complete
[2025-12-13 19:59:28] [INFO] [UdpServer.hpp] [41] - socket create success, socket file descriptor: 3
[2025-12-13 19:59:28] [INFO] [UdpServer.hpp] [60] - bind success, bind file descriptor: 3
[2025-12-13 19:59:41] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:41] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 19:59:43] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:43] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 19:59:52] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:52] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 20:00:00] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 20:00:00] [INFO] [Dictionary.hpp] [63] - Request for translation services
^C
130 $
启动词典客户端:
bash
[2025-12-13 19:59:40] [INFO] [UdpClient.hpp] [30] - socket create success, socket file descriptor: 3
Please enter# good
adj.好的,善良的
Please enter# evil
adj.坏的,邪恶的
Please enter# goodbye
No such word found
Please enter# nhtg
No such word found
Please enter# ^C
130 $
可以看到,客户端成功发送单词到服务器,服务器查询词典并返回翻译结果,客户端正确接收并显示了翻译内容。英译汉网络词典实现成功!
3. Version3-简单聊天室
实现一个简单的 UDP 聊天室程序
3.1 路由模块实现
Router.hpp:
cpp
#pragma once
#include <vector>
#include <string>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Lock.hpp" // 之前编写的互斥锁模块
using namespace LogModule;
using namespace MutexModule;
class Route
{
private:
bool IsExit(const Identifier &info)
{
for (auto &user : _online_users)
{
if (user == info)
{
return true;
}
}
return false;
}
void AddUser(const Identifier &info)
{
_online_users.push_back(info);
}
void DeleteUser(const Identifier &info)
{
std::vector<Identifier>::iterator first = _online_users.begin();
while (first != _online_users.end())
{
if (*first == info)
{
LOG(LogLevel::INFO) << info.GetIp() << " has logged out";
first = _online_users.erase(first); // 注意迭代器失效问题
break;
}
else
{
++first;
}
}
}
public:
Route() {}
// 目标socket文件,发送内容,发送对象信息
void ForwardMessage(int sock_fd, const std::string message, const Identifier info)
{
// 1、2、3 在多线程场景下是并发执行的,存在并发问题-一个用户已经退了,但是消息广播到房间的时候没有更新
// 简单处理-TODO -> 采用将消息封装(广播信息、退出信息、更新用户信息)的方式做封装
LockGuard lockguard(_mutex);
// 1. 判断用户是否在房间(无登录策略,用户发消息就代表上线)
if (!IsExit(info))
{
AddUser(info);
// A user from 127.0.0.1[8080] is online
LOG(LogLevel::INFO) << "A user from " << info.GetIp() << "[" << info.GetPort() << "] " << "is online";
}
// 2. 将消息发到房间
for (auto &user : _online_users)
{
socklen_t len = user.Size();
// 对消息再做一次封装
std::string send_message = info.GetIp() + " via ";
send_message += std::to_string(info.GetPort());
send_message += " say>>> ";
send_message += message;
// 发送给所有在线用户
ssize_t send_size = sendto(sock_fd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&user.GetRawAddr(), len);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
}
// 3. 检查用户是否退出
if (message == "QUIT")
{
DeleteUser(info);
}
}
~Route() {}
private:
// STL容器本身就不是线程安全的,所以如果存在多线程同时进行访问_online_users就会出现并发问题
std::vector<Identifier> _online_users; // 在线用户
Mutex _mutex;
};
3.2 聊天室服务器代码
更新转换函数 InetAddr.hpp:
cpp
#pragma once
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
// Identifier -> 内部传递参数(IP+Port)使用
class Identifier
{
public:
Identifier(struct sockaddr_in addr) : _raw_addr(addr)
{
// 网络转主机
_port = ntohs(addr.sin_port);
char ip_buffer[64];
memset(ip_buffer, 0, sizeof(ip_buffer));
inet_ntop(AF_INET, &_raw_addr.sin_addr, ip_buffer, sizeof(ip_buffer)); // 更加安全的做法
_ip = (ip_buffer == nullptr ? 0 : ip_buffer);
if (_ip.empty())
LOG(LogLevel::ERROR) << "inet_ntop failed";
}
Identifier(std::string ip, uint16_t port) : _ip(ip), _port(port)
{
// 主机转网络
memset(&_raw_addr, 0, sizeof(_raw_addr));
_raw_addr.sin_family = AF_INET;
_raw_addr.sin_port = htons(_port);
if (ip == "0")
{
_ip = "0.0.0.0";
_raw_addr.sin_addr.s_addr = INADDR_ANY; // 处理特殊情况 -> 避免报invalid format错误
}
else
{
int ret = inet_pton(AF_INET, _ip.c_str(), &_raw_addr.sin_addr); // 更加安全的做法
if (ret != 1)
LOG(LogLevel::ERROR) << (ret == 0 ? "invalid format" : "inet_pton failed");
}
}
~Identifier() {}
bool operator==(const Identifier info) const
{
// 1. 仅以IP作为唯一性标识=
// 2. IP + Port作为唯一性标识
return (info._ip == _ip) && (info._port == _port);
}
uint16_t GetPort() const { return _port; }
std::string GetIp() const { return _ip; }
struct sockaddr_in &GetRawAddr() { return _raw_addr; }
socklen_t Size() const
{
return sizeof(_raw_addr);
}
private:
struct sockaddr_in _raw_addr; // 原始地址结构
uint16_t _port; // 端口
std::string _ip; // IP地址信息
};
UdpServer.hpp:
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
class UdpServer
{
int default_file_descriptor = -1;
using func_t = std::function<void(int socket_fd, const std::string &str, const Identifier &info)>; // 回调函数
public:
UdpServer(uint16_t port, func_t func)
: _socket_fd(default_file_descriptor),
_port(port),
_is_running(false),
_func(func)
{
}
~UdpServer()
{
}
// 初始化工作
void Initial()
{
// 1. 创建套接字-得到socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
// 2. 填充server的sockaddr_in信息
Identifier server_info("0", _port); // √
// 3. 绑定server的IP和port
// server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
int ret = bind(_socket_fd, (struct sockaddr *)&server_info.GetRawAddr(), sizeof(server_info.GetRawAddr()));
if (ret < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(1);
}
LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
}
// 启动Server服务
void Start()
{
_is_running = true;
// 一直运行,直到主动退出
while (_is_running)
{
char message[1024];
struct sockaddr_in client_info; // 用于保存client的信息
socklen_t len = sizeof(client_info);
// 1. 接收来自client的消息
ssize_t receive_size = recvfrom(_socket_fd, message, sizeof(message) - 1, 0, (struct sockaddr *)&client_info, &len);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
else
{
message[receive_size] = '\0'; // 保证消息完整性
// 将client信息封装成为一个类 - 便于往Route层传递
Identifier info(client_info);
// 回调处理 -> 不仅仅是回调,控制传出的参数 -> 外部需要的参数读通过这里传出
// 2. 将消息+Client信息传递给Route模块
_func(_socket_fd, message, info);
}
}
}
private:
int _socket_fd; // 打开socket的文件描述符
uint16_t _port; // 要绑定的port
bool _is_running; // 运行标志位
func_t _func; // 外部回调函数
};
UdpServer.cpp:
cpp
#include <iostream>
#include <memory>
#include <string>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp" // 之前编写的线程池模块
using namespace ThreadPoolModule;
using task_t = std::function<void(void)>;
// ./udp_server 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
// 1. 路由模块 > 消息转发
Route route;
// 2. 创建线程池
ThreadPool<task_t> *pool = ThreadPool<task_t>::GetSingleton();
pool->Start();
// 模块的解耦合,从网络通信模块跳转到翻译模块
// 3. 网络模块 > 通信服务 > 线程池处理消息转发任务
std::unique_ptr<UdpServer> udp_server =
std::make_unique<UdpServer>(UdpServer(port, [&route, &pool](int socket_fd, const std::string &message, const Identifier &info)
{ auto t = std::bind(&Route::ForwardMessage, &route, socket_fd, message, info);
pool->Enqueue(t); }));
udp_server->Initial();
udp_server->Start();
return 0;
}
3.3 聊天室客户端代码
UdpClient.hpp:
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Thread.hpp" // 之前编写的线程模块
#include "InetAddr.hpp"
using namespace LogModule;
using namespace ThreadModule;
class UdpClient
{
private:
int default_file_descriptor = -1;
// 发送线程
void Send(void *agrs)
{
while (_is_online)
{
// 1. 内部传递的Server信息
// Identifier peer_info(server_info);
Identifier peer_info(_server_ip, _server_port);
socklen_t len_send = peer_info.Size();
std::string message;
std::cout << "Please enter# "; // 标准输出 > 1
std::getline(std::cin, message); // 标准输入 > 0
// 2. 对消息做判断
if (message == "QUIT")
{
_is_online = false;
}
// 3. 发送给Server
ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer_info.GetRawAddr(), len_send);
if (send_size < 0)
{
LOG(LogLevel::ERROR) << "sendto failed";
}
}
}
// 接收线程
void Receive(void *agrs)
{
while (_is_online)
{
char buffer[1024];
// 1. 发送方的信息
struct sockaddr_in peer_info;
socklen_t peer_len = sizeof(peer_info);
// 2. 接收来自Server的信息
ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer_info, &peer_len);
if (receive_size < 0)
{
LOG(LogLevel::ERROR) << "recvfrom failed";
}
else
{
buffer[receive_size] = '\0';
// 不能在这里获取到原始Client的IP和Port,因为Route也是Server的一部分
// 只有Server保留了原始Client的IP和Port,并且只传递给了Route模块
// std::cout << buffer << std::endl; // 标准输出 > 1
std::cerr << buffer << std::endl; // 标准错误 > 2
}
// 3. 对消息做判断
if (buffer == "QUIT")
{
_is_online = false;
}
}
}
public:
UdpClient(uint16_t port, std::string ip)
: _socket_fd(default_file_descriptor),
_server_port(port),
_server_ip(ip)
{
}
// 初始化工作
void Initial()
{
// 1. 获取socket文件描述符
_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket_fd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
// 2. 绑定IP和端口 - client不用绑定
}
// 启动收发服务
void Start()
{
_is_online = true;
// 1. 创建收发线程
// 收发数据的执行本质是交给线程执行!
Thread send_thread([this](void *args)
{ this->Send(args); });
Thread receive_thread([this](void *args)
{ this->Receive(args); });
// 2. 启动收发线程
receive_thread.Start();
send_thread.Start();
// 3. 回收收发线程资源(Send和Receive结束后自动回收)
send_thread.Join();
receive_thread.Join();
}
~UdpClient() {}
private:
int _socket_fd; // 向哪个socket文件中发送
uint16_t _server_port; // 向哪个Port发送
std::string _server_ip; // 向哪个IP发送
bool _is_online; // 是否在线
};
UdpClient.cpp:
cpp
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"
using namespace ThreadModule;
// ./udp_client 8080 172.0.0.1
// ./udp_client 8080 127.0.0.1 2>/dev/pts/2
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
return 1;
}
uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
std::string ip = argv[2];
// 创建client实体
std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
udp_client->Initial();
udp_client->Start();
return 0;
}
注意:
- 在启动客户端的时候,需要将标准错误重定向到一个新的终端设备文件中,这样可以区分标准输入输出和接收的消息输出,避免混淆。
- 在代码中,我们使用了标准错误流
std::cerr来输出接收到的消息,这样即使标准输出被重定向,接收的消息仍然可以正确显示在指定的终端设备上
3.4 测试
启动聊天室服务器:
bash
$ ./udp_server 8080
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [62] - Attempt to obtain the singleton
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [69] - Singleton creation successful
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[1] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[2] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[3] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[4] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[5] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[6] start
[2025-12-13 20:21:21] [INFO] [UdpServer.hpp] [40] - socket create success, socket file descriptor: 3
[2025-12-13 20:21:21] [INFO] [UdpServer.hpp] [65] - bind success, bind file descriptor: 3
[2025-12-13 20:21:41] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:21:41] [DEBUG] [Route.hpp] [61] - A user from 127.0.0.1[48582] is online
[2025-12-13 20:21:47] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:21:47] [DEBUG] [Route.hpp] [61] - A user from 139.9.7.213[57987] is online
[2025-12-13 20:21:57] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:22:02] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:08] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:08] [DEBUG] [Route.hpp] [37] - 139.9.7.213 has logged out
[2025-12-13 20:23:13] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:13] [DEBUG] [Route.hpp] [37] - 127.0.0.1 has logged out
^C
130 $
启动聊天室客户端1:
bash
$ ./udp_client 8080 127.0.0.1 2>/dev/pts/2
[2025-12-13 20:21:25] [INFO] [UdpClient.hpp] [111] - socket create success, socket file descriptor: 3
Please enter# 你好
Please enter# 我看到了
Please enter# QUIT
$
对应的/dev/pts/2输出:
bash
$ 127.0.0.1 via 48582 say>>> 你好
139.9.7.213 via 57987 say>>> Hello
139.9.7.213 via 57987 say>>> 我上线了
127.0.0.1 via 48582 say>>> 我看到了
139.9.7.213 via 57987 say>>> QUIT
127.0.0.1 via 48582 say>>> QUIT
启动聊天室客户端2:
bash
$ ./udp_client 8080 139.9.7.213 2>/dev/pts/4
[2025-12-13 20:21:31] [INFO] [UdpClient.hpp] [111] - socket create success, socket file descriptor: 3
Please enter# Hello
Please enter# 我上线了
Please enter# QUIT
$
对应的/dev/pts/4输出:
bash
$ 139.9.7.213 via 57987 say>>> Hello
139.9.7.213 via 57987 say>>> 我上线了
127.0.0.1 via 48582 say>>> 我看到了
139.9.7.213 via 57987 say>>> QUIT
可以看到,两个客户端成功连接到服务器,并且可以相互发送和接收消息。当客户端发送 "QUIT" 消息时,服务器正确地将其从在线用户列表中移除。简单聊天室实现成功!
4. 总结
通过三个案例可以基本总结出 UDP 编程的核心流程:
- 创建套接字
socket() - 绑定地址
bind() - 接收数据
recvfrom() - 发送数据
sendto() - 关闭套接字
close() - 根据实际需求,添加相应的业务逻辑模块,如词典查询、消息路由等
不过要注意的是:
- 服务端一般绑定的地址是
INADDR_ANY,以便接收发送到任意本机 IP 的数据 - 客户端一般不需要绑定地址,由系统自动分配 IP 和端口
- UDP 是无连接的协议,不需要建立连接,直接发送和接收数据