跳到主要内容Linux Socket UDP 编程实战:通信、翻译与聊天室 | 极客日志C++
Linux Socket UDP 编程实战:通信、翻译与聊天室
Linux Socket UDP 编程实战,涵盖基础接口、简单通信、字典翻译及多客户端聊天室实现。通过 C++ 封装 UdpServer 类,结合回调函数解耦业务逻辑,演示了 bind 绑定、sendto/recvfrom 使用及 INADDR_ANY 配置。聊天室部分引入线程池与路由表管理在线用户,解决消息混杂问题,并支持跨平台(Linux/Windows)通信。
Linux Socket UDP 编程实战
在网络基础学习中,我们已掌握网络体系结构与传输流程。本篇聚焦 Socket 编程中的 UDP 套接字,通过具体实例演示客户端与服务器的通信机制。我们将实现字典翻译和简单聊天室两个基于 UDP 的具体应用。
1. Socket 通信接口
在使用 UDP 进行通信前,先了解核心接口。
创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain: 协议族,如 AF_INET (IPv4)。
type: 套接字类型,UDP 对应 SOCK_DGRAM。
protocol: 通常填 0,系统自动选择。
成功返回文件描述符,失败返回 -1。
绑定端口
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 套接字描述符。
addr: 存储 IP 和端口的结构体指针。
addrlen: 结构体大小。
绑定成功后返回 0,否则 -1。
对于 UDP 网络通信,需传入 sockaddr_in 结构体并强制转换为 sockaddr。
数据收发
ssize_t recvfrom(int sockfd, void *buf, len, flags,
sockaddr *src_addr, *addrlen);
;
size_t
int
struct
socklen_t
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)
buf: 缓冲区。
len: 缓冲区大小。
flags: IO 标志位。
src_addr/dest_addr: 对方地址信息。
返回值大于等于 0 表示成功字节数,-1 表示失败。
2. 基于 UDP 套接字实现简单通信
接下来使用上述接口实现一个简单的回显服务:客户端发送数据,服务器处理后发回。
Server.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include "log.hpp"
const int defaultfd = -1;
class UdpServer {
public:
UdpServer(u_int16_t port, std::string &ip) : _sockfd(defaultfd), _port(port), _isrunning(false), _ip(ip) {}
void Init();
void Start();
private:
int _sockfd;
uint16_t _port;
std::string _ip;
bool _isrunning;
};
初始化与绑定
void UdpServer::Init() {
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd:" << _sockfd;
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0) {
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
}
注意主机序到网络序的转换(htons, inet_addr)。
消息收发
void UdpServer::Start() {
_isrunning = true;
while (_isrunning) {
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0) {
buffer[s] = 0;
sendto(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, len);
}
}
}
Server.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " ip port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, ip);
usvr->Init();
usvr->Start();
return 0;
}
Client.cc
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error" << std::endl;
return 2;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (1) {
std::string input;
std::cout << "Please input:";
std::getline(std::cin, input);
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr *)&server, sizeof(server));
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (m > 0) {
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
注意:客户端通常不需要手动 bind,操作系统会自动分配本地 IP 和临时端口,避免冲突。
测试与优化
测试时若发现无法连接公网 IP,通常是因为云服务器防火墙未开放端口。此外,服务器绑定特定 IP 可能导致多网卡环境下无法监听所有接口。建议使用 INADDR_ANY 代替具体 IP,允许绑定任意可用地址。
local.sin_addr.s_addr = INADDR_ANY;
using func_t = std::function<std::string(const std::string &)>;
class UdpServer {
func_t _func;
void Start() {
if (s > 0) {
buffer[s] = 0;
std::string result = _func(buffer);
sendto(..., result.c_str(), ...);
}
}
};
3. 基于 UDP 套接字实现字典翻译功能
在简单通信基础上,扩展为翻译服务。服务器读取词典文件,查询后返回中文。
InetAddr 封装
简化地址转换操作,避免重复调用 inet_ntoa 等函数。
#pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
class InetAddr {
public:
InetAddr(struct sockaddr_in &addr) : _addr(addr) {
_port = ntohs(_addr.sin_port);
char buffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, buffer, sizeof(buffer));
_ip = buffer;
}
uint16_t Port() { return _port; }
std::string IP() { return _ip; }
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
说明:推荐使用 inet_ntop 替代 inet_ntoa,后者使用静态缓冲区,多线程下易覆盖;前者使用用户提供的缓冲区,更安全。
Dict 类实现
bool LoadDict() {
std::ifstream in(_dict_path);
if (!in.is_open()) return false;
std::string line;
while (std::getline(in, line)) {
auto pos = line.find(":");
if (pos == std::string::npos) continue;
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + 1);
if (!english.empty() && !chinese.empty()) {
_dict.insert({english, chinese});
}
}
return true;
}
集成运行
主程序中实例化 Dict 并通过 Lambda 传递回调:
Dict dict;
dict.LoadDict();
auto handler = [&dict](const std::string& msg, InetAddr peer) -> std::string {
return dict.Translate(msg, peer);
};
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, handler);
4. 基于 UDP 套接字实现简单聊天室
Route 模块
class Route {
std::vector<InetAddr> _online_user;
Mutex _mutex;
public:
void MessageRoute(int sockfd, const std::string &Message, InetAddr &peer) {
LockGuard lock(_mutex);
if (!_IsExit(peer)) AddUser(peer);
std::string send_msg = peer.StringAddr() + "#" + Message;
for (auto &x : _online_user) {
sendto(sockfd, send_msg.c_str(), send_msg.size(), 0,
(const struct sockaddr *)&x.GetAddr(), sizeof(x.GetAddr()));
}
if (Message == "QUIT") EraseUser(peer);
}
};
线程池与解耦
ThreadPool<func_t2>* tp = ThreadPool<func_t2>::GetInstance();
auto task = std::bind(&Route::MessageRoute, &r, sockfd, message, peer);
tp->Enqueue(task);
客户端优化
为避免输入输出混杂,建议将接收消息输出到独立终端窗口(stderr),输入保留在标准输入。
void Receive() {
while (1) {
char buffer[1024];
if (m > 0) {
buffer[m] = 0;
std::cerr << buffer << std::endl;
}
}
}
跨平台通信
Windows 下可使用 WinSock2 库实现类似功能,需注意 WSAStartup 初始化和 closesocket 清理。
至此,我们完成了从基础通信到复杂业务逻辑的 UDP 开发实践。通过模块化设计与回调机制,代码更易于维护和扩展。
相关免费在线工具
- 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