Socket--UDP 构建简单聊天室

张开发
2026/4/14 23:18:12 15 分钟阅读

分享文章

Socket--UDP 构建简单聊天室
UDP 是无连接、不可靠、基于数据报的传输协议相比 TCP它不需要建立连接、速度更快。一、函数补充sendtosendto是Linux 下 C 语言用于UDP 套接字发送数据的核心系统调用专门用于无连接的 UDP 通信必须指定目标地址。也就是#include sys/socket.h ssize_t sendto( int sockfd, // 套接字文件描述符 const void *buf, // 要发送的数据缓冲区 size_t len, // 数据长度字节 int flags, // 标志位一般填 0 const struct sockaddr *dest_addr, // 目标地址IP端口 socklen_t addrlen // 目标地址结构体长度 );参数参数说明sockfdUDP 创建的 socket 文件描述符buf要发送的数据指针char 数组、字符串等len发送数据的字节数flags控制选项UDP 正常发送直接填 0dest_addr目标地址结构体必须指定对方 IP 端口addrlen地址结构体的大小一般用sizeof()返回值成功返回实际发送的字节数失败返回 -1并设置 errno 错误码注意事项UDP 必须填写目标地址Linux 用sockaddr_in存储 IP 和端口sendto 核心特点无连接每次发送都必须指定目标地址数据报模式一次 sendto 一个独立 UDP 包不保证到达函数返回成功只代表数据进入发送缓冲区不代表对方收到不粘包一次 sendto 对应对方一次recvfrom客户端 / 服务端都能用UDP 没有严格区分recvfromrecvfrom是 Linux 下用于从 UDP 套接字SOCK_DGRAM接收数据报的核心系统调用它不仅能读取数据还能同时获取数据发送方的 IP 地址和端口号这是 UDP 无连接通信的关键特性。#include sys/types.h #include sys/socket.h ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);参数sockfd已创建并绑定bind的UDP 套接字文件描述符。buf指向接收数据缓冲区的指针用于存放接收到的内容。len缓冲区buf的最大容量字节决定单次最多接收的数据大小。flags控制接收行为的标志位常用值0默认阻塞模式无数据时进程会挂起等待。MSG_DONTWAIT非阻塞模式无数据立即返回-1errno设为EAGAIN。MSG_PEEK窥探数据读取后不清除内核缓冲区下次仍可读到。MSG_WAITALL等待直到接收满len字节UDP 很少用。src_addr输出参数指向struct sockaddr结构体指针接收完成后自动填充发送方的 IP 和端口。实际使用时通常传入struct sockaddr_inIPv4或struct sockaddr_in6IPv6再强制类型转换。不需要源地址可设为NULL但老机器上可能会出现bugaddrlen输入输出参数传入src_addr结构体的初始大小。返回内核写入的实际地址长度。必须初始化为有效地址不能为NULL返回值成功返回实际接收到的字节数ssize_t有符号整型。失败返回-1全局变量errno存储错误码。注意UDP 特有UDP 无连接永远不会返回 0返回 0 属异常 / 网络错误。关键注意事项UDP 数据报边界每次recvfrom严格接收一个完整数据报。数据报 len截断多余字节丢弃errno设为MSG_TRUNC。阻塞 vs 非阻塞默认阻塞无数据时进程休眠等待。非阻塞MSG_DONTWAIT无数据立即返回-1/EAGAIN。地址结构体IPv4 用struct sockaddr_inIPv6 用struct sockaddr_in6。必须初始化addr_len否则地址获取失败。关于sockaddr结构体的介绍请见上一篇。二、代码实现在正式敲代码前我们先考虑一下聊天室的大概逻辑首先有多个用户进行聊天那么就需要将所有用户管理起来其次每个用户都会有自己的IP加Port地址故每个用户的地址也需要管理起来。以及等等……地址管理封装InetAddr#pragma once #include iostream #include string #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include strings.h // 网络地址封装 class InetAddr { public: InetAddr(const struct sockaddr_in address) : _address(address), _len(sizeof(address)) { _ip inet_ntoa(address.sin_addr); _port ntohs(address.sin_port); } InetAddr(uint16_t port, const std::string ip 0.0.0.0) : _ip(ip), _port(port) { // 使用sockaddr_in结构体前先清空,别忘了 bzero(_address, sizeof(_address)); _address.sin_family AF_INET; _address.sin_port htons(_port); // 主机向网络的时候记得转化端口号 _address.sin_addr.s_addr inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式 _len sizeof(_address); } InetAddr() {} struct sockaddr_in *GetNetAddress() { return _address; } socklen_t Len() { return _len; } std::string ToString() { return [ _ip : std::to_string(_port) ]; } ~InetAddr() { } bool operator(const InetAddr addr) { return (this-_ip addr._ip) (this-_port addr._port); } private: struct sockaddr_in _address; std::string _ip; uint16_t _port; socklen_t _len; };用户信息管理UserManager#pragma once #include iostream #include string #include vector #include InetAddr.hpp #include Logger.hpp class UserManager { public: UserManager() {} void AddUser(const InetAddr addr) { if (SearchUser(addr)) return; _users.push_back(addr); } void DelUser(const InetAddr addr) { for (auto its _users.begin(); its _users.end(); its) { if (*its addr) { _users.erase(its); break; //迭代器这里记得break } } } bool SearchUser(const InetAddr addr) { // e是容器中的元素本身 for (auto e : _users) { if (e addr) return true; } return false; } void ModUser(const InetAddr addr) { DelUser(addr); AddUser(addr); } std::vectorInetAddr Users() { return _users; } ~UserManager() { } private: std::vectorInetAddr _users; };路由器消息转发Route// 路由器消息转发 #pragma once #include iostream #include memory #include string #include sys/socket.h #include netinet/in.h #include Mutex.hpp #include UserManager.hpp #include InetAddr.hpp class Route { public: Route() : _uma(std::make_uniqueUserManager()) { } void CheckUser(const InetAddr addr) { LockGuard lockguard(_lock); _uma-AddUser(addr); } void OfflineUser(const InetAddr addr) { LockGuard lockguard(_lock); _uma-DelUser(addr); } void Broadcast(int sockfd, std::string message) { LockGuard lockguard(_lock); auto users _uma-Users(); for (auto user : users) { sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)user.GetNetAddress(), user.Len()); } } ~Route() { } private: std::unique_ptrUserManager _uma; Mutex _lock; };服务端UdpServer#include iostream #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include cstdlib #include string #include strings.h #include functional #include InetAddr.hpp #include Logger.hpp using namespace NS_LOG_MODULE; const static int default_fd -1; // 回调函数 using handler_addr_t std::functionvoid(const InetAddr ); // 管理地址 using handler_msg_t std::functionvoid(int sokcfd, std::string msg); // 管理信息 enum { SUCCESS, SOCKET_ERROR, USAGE_ERROR, BIND_ERR }; class UdpServer { public: // UdpServer(std::string ip, uint16_t port) UdpServer(uint16_t port) : _socketfd(default_fd), _port(port) { } ~UdpServer() { close(_socketfd); } void Init() { // socket函数创建网络通信的接口 / 通道返回值是文件描述符 // AF_INET指定使用 IPv4 协议进行网络通信 // SOCK_DGRAM指定为 UDP 数据报套接字无连接、不可靠传输 // 0让系统自动匹配 UDP 协议无需手动指定 _socketfd socket(AF_INET, SOCK_DGRAM, 0); if (_socketfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERROR); } LOG(LogLevel::INFO) socket success, fd: _socketfd; // 第二步填充网络信息 // 注意了最好了解一下三种sockaddr结构体的组成 // struct sockaddr_in local; // // 使用sockaddr_in结构体前先清空 // bzero(local, sizeof(local)); // local.sin_family AF_INET; // local.sin_port htons(_port); // 主机向网络的时候记得转化端口号 // // local.sin_addr.s_addr inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式 // local.sin_addr.s_addr INADDR_ANY; // 绑定该机器上的任意IP地址 //将上面的地址处理交给封装好的对象处理 InetAddr local(_port); // 第三步bin socket信息 int n bind(_socketfd, (struct sockaddr *)local.GetNetAddress(), local.Len()); if (n 0) { perror(bind failed reason: ); LOG(LogLevel::FATAL) bind socket error; exit(BIND_ERR); } LOG(LogLevel::INFO) bind socket success , port: _port; } void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg) { _handler_addr handler_addr; _handler_msg handler_msg; } void Start() { char inbuffer[1024]; while (true) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 1. 用户发来的数据 // 2. 用户的socket信息 ssize_t n recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)peer, len); if (n 0) { // 服务端 inbuffer[n] 0; // 1. 检测新用户 InetAddr clientaddress(peer); std::string tips clientaddress.ToString(); std::string message tips inbuffer; LOG(LogLevel::DEBUG) message; _handler_addr(clientaddress); // 2. 转发消息 _handler_msg(_socketfd, message); } else { LOG(LogLevel::ERROR) recvfrom error; } } } private: int _socketfd; // std::string _ip; // ip有两种形式点分式和四字节ip这里选点分式 uint16_t _port; // uint16_t 无符号 16 位整数专门用来存端口号等网络数据。 handler_addr_t _handler_addr; handler_msg_t _handler_msg; };客户端函数入口ChatClient#include iostream #include string #include cstring #include cstdlib #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include Thread.hpp #include InetAddr.hpp using namespace NS_THREAD_MODULE; int sockfd 0; std::string server_ip; uint16_t server_port 0; std::string nickname; static void Usage(const std::string proc) { std::cout Usage:\n\t; std::cout proc server_ip server_port std::endl; } static void Online(InetAddr serveraddr) { std::cout Please Set Your Nick Name# ; std::getline(std::cin, nickname); std::string online_message nickname online!; ssize_t n sendto(sockfd, online_message.c_str(), online_message.size(), 0, (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len()); (void)n; } void RecvMessage() { while (true) { // recvfrom char inbuffer[1024] {0}; struct sockaddr_in temp; socklen_t len sizeof(temp); ssize_t m recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)temp, len); if (m 0) { inbuffer[m] 0; std::cerr inbuffer std::endl; // 2 } } } void SendMessage() { InetAddr serveraddr(server_port, server_ip); Online(serveraddr); while (true) { std::string message; // 1. 获取用户输入 std::cout Please Enter# ; // 1 std::getline(std::cin, message); message nickname # message; // 2. clinet 发送数据给 server首次发送即自动bind ssize_t n sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len()); (void)n; } } // 我怎么知道server对方的IP和端口啊 类似IPPort 是被内置到client的 // ./client_udp server_ip server_port int main(int argc, char *argv[]) { if (argc ! 3) { Usage(argv[0]); exit(1); } server_ip argv[1]; server_port std::stoi(argv[2]); // 1. 创建socket sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd 0) { std::cerr socket error std::endl; exit(2); } Thread recver(RecvMessage); Thread sender(SendMessage); recver.Start(); sender.Start(); recver.Join(); sender.Join(); return 0; }服务端函数入口ChatServer#include ThreadPool.hpp // 执行者执行处理动作的人 #include Route.hpp // 任务 #include UdpServer.hpp // 获取事件 #include memory static void Usage(const std::string process) { std::cerr Usage:\n\t; std::cerr process local_port std::endl; } using namespace NS_THREAD_POOL_MODULE; using task_t std::functionvoid(); int main(int argc, char *argv[]) { if (argc ! 2) { Usage(argv[0]); exit(USAGE_ERROR); } ENABLE_CONSOLE_LOG_STRATEGY(); uint16_t server_port std::stoi(argv[1]); // 线程池模块 auto thread_pool ThreadPooltask_t::Instance(); // 路由模块 Route r; // 网络模块 UdpServer usvr(server_port); usvr.Init(); usvr.RegisterService( [r](const InetAddr addr) { r.CheckUser(addr); }, [r, thread_pool](int sockfd, std::string msg) { auto t std::bind(Route::Broadcast, r, sockfd, msg); thread_pool-Enqueue([r, sockfd, msg](){ r.Broadcast(sockfd, msg); }); }); usvr.Start(); return 0; }除了上面给出的函数文件外还额外包含了自个创建的线程、线程池、日志、锁的封装文件。这些基本都能直接使用库中的函数日志则可以找其它大佬的开源懒的话只当练习可以直接去掉。结果预览

更多文章