news 2026/3/29 14:26:34

Socket编程UDP实现简单的聊天室制作以及公网IP不能成功通信需要开放端口问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Socket编程UDP实现简单的聊天室制作以及公网IP不能成功通信需要开放端口问题

一:预备工作

我们先把编程当中需要用到且重要的函数给认识一下

创建socket文件描述符函数

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);

domain:协议家族,常用的有AF_INET (IPv4),AF_INET6(IPv6)。在编程当中我们使用IPv4协议。

type:套接字类型,常用的有SOCK_STREAM(面向连接的字节流,基于 TCP(可靠、按序、双向)) SOCK_DGRAM(无连接的数据报,基于 UDP(不可靠、有边界))

由于这节课是UDP编程,所以函数当中我们使用SOCK_DGRAM。

protocol(具体协议):表示套接字使用的具体协议,通常设置为0,表示根据domain和type自动选择函数

绑定端口号函数

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:套接字描述符,由刚刚的socket函数返回。

addr:指向通用地址结构体的指针,实际需要传入具体协议族的地址结构

其核心作用是绑定ip和端口号。

addrlen:表示通用结构体的大小。

补充:通用结构struct sockaddr以及常用的地址结构体

通用结构体struct sockaddr

#include <sys/socket.h> struct sockaddr { sa_family_t sa_family; // 地址族 (Address Family) char sa_data[14]; // 地址数据 (可变长度,实际不足14字节用0填充) };

IPv4地址结构

struct sockaddr_in { sa_family_t sin_family; // 地址族: AF_INET in_port_t sin_port; // 端口号 (网络字节序) struct in_addr sin_addr; // IP地址 (网络字节序) char sin_zero[8]; // 填充字段 }; struct in_addr { in_addr_t s_addr; // 32位IPv4地址 };

Unix域地址(本地通信)

struct sockaddr_un { sa_family_t sun_family; // 地址族: AF_UNIX/AF_LOCAL char sun_path[108]; // 路径名 };

在这里,我们看到,不同的地址结构体类型可以用一个通用结构体struct sockaddr类型来接收,是不是觉得比较熟悉?这里不就是C++当中继承与多态的特性吗,struct sockaddr就相当于是基类,而其他地址结构体都是struct sockaddr的子类,子类可以被父类的指针类型指向,也可以以父类类型作为函数参数接收子类的传值。

网络进程发消息函数

#include <sys/types.h> #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);

sockfd:套接字描述符

buf:要发送的信息,以字符串形式发送到其他网络进程

len:发送信息的字节数

flags:控制发送行为的标志位,可以组合使用(按位或)通常设置为0,代表阻塞发送,知道数据发送或出错

dest_addr:用来传递目的网络进程的ip和端口号

addrlen:struct sockaddr通用结构体的大小

返回值:当发送失败时返回-1,发送成功时返回发送信息的字节数

网络进程收消息函数

#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:套接字信息

buf:缓冲区--用来存放从其他网络进程那里收到的消息

len:缓冲区的长度,单位是字节

flags:接收标志,也可以组合使用,通常设置为0,代表阻塞接收,直到有数据或接收失败。

src_addr:用来获取发送信息进程的ip和端口号

addrlen:struct sockaddr通用结构体的大小

返回值:当发送失败时返回-1,发送成功时返回接受信息的字节数

那么接下来就开始正式的编程了。

二:一个简单的聊天室制作

将网络地址进行封装

inet.hpp

#pragma once #include <iostream> #include<string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> class InetAddr { public: InetAddr(struct sockaddr_in &addr) : _addr(addr) { _port = ntohs(_addr.sin_port); _ip = inet_ntoa(_addr.sin_addr); } bool operator==(const InetAddr &addr) { return addr._ip == _ip && addr._port == _port; } std::string StringAddr() { return _ip+std::to_string(_port); } std::string Ip() { return _ip; } uint16_t Port() { return _port; } const struct sockaddr_in& Addr() { return _addr; } private: struct sockaddr_in _addr; uint16_t _port; std::string _ip; };

在这里有两个函数较为重要:

1.ntohs
#include <arpa/inet.h> uint16_t ntohs(uint16_t netshort);

这里ntohs函数的作用是将端口号从网络字节序(大端),转换成主机字节序。

2.inet_ntoa
#include <arpa/inet.h> char *inet_ntoa(struct in_addr in);

这里inet_ntoa的作用是将网络字节序的IPv4地址转换成点分十进制的字符串,方便打印输出

例如:0x7F000001"127.0.0.1"

对用户进行管理

Route.hpp

在这里实现了对聊天室用户的增加,删除,查找。通过STL中vector进行管理

#pragma once #include <iostream> #include <vector> #include "Log.hpp" #include "Inet.hpp" using namespace LogModle; class Route { bool IsExist(InetAddr &peer) { for (auto &user : _inline_users) { if (user == peer) return true; } return false; } public: Route() { } void RegisterUser(InetAddr&peer) { LOG(LogLevel::DEBUG)<<"注册用户"<<peer.Ip()<<" "<<peer.Port(); _inline_users.emplace_back(peer); } void DeleteUser(InetAddr&peer) { std::vector<InetAddr>::iterator it; for(it=_inline_users.begin();it!=_inline_users.end();it++) { if(*it==peer) { _inline_users.erase(it); break; } } } void MessageRoute(int sockfd, const std::string &message, InetAddr &peer) { if(!IsExist(peer)) { RegisterUser(peer); } std::string send_message=peer.StringAddr()+" #"+message; for(auto& user:_inline_users) { sendto(sockfd,send_message.c_str(),send_message.length(),0,(const sockaddr*)&user,sizeof(user)); } if(message == "QUIT") { LOG(LogLevel::INFO)<<"删除了一个用户"; DeleteUser(peer); } } private: std::vector<InetAddr> _inline_users; };

这个文件基本上没什么难度,都是正常增加,删除的逻辑,进行查找也是对vector进行遍历。

服务端窗口的实现

UdpServer.hpp

服务端的作用本质是

1.收到其中某一个用户的消息,

2.将消息发送到全部的在线用户(这步我们通过回调函数实现,因为要遍历在线用户,不在server当中编写这个函数)让外部传入这个函数,我们再执行。

其中func_t声明的函数就是用于转发或数据处理的回调函数。

在该文件中还有一个重点,那就是server端口的ip通过INADDR_ANY初始化,这样初始化的好处是client不论通过哪种ip(本地??公网??),server都可以接收到发来的消息,不用再在服务端手动传入。

#pragma once #include "Log.hpp" #include "Route.hpp" #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <strings.h> #include <functional> using namespace LogModle; const static uint16_t defaultport = 8888; const static int defaultfd = -1; const static int defaultsize = 1024; using func_t = std::function<void(int sockfd, const std::string &message, InetAddr &peer)>; class UdpServer { public: UdpServer(func_t fun, uint16_t port = defaultport) : //_ip(ip), _fun(fun), _port(port), _sockfd(defaultfd) { } void Init() { _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket error"; exit(1); } LOG(LogLevel::INFO) << "socket success"; // 绑定,套接字和ip struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_port = htons(_port); local.sin_family = AF_INET; // 1.把点分十进制ip转成四字节ip 2.4字节转换成网络序列 local.sin_addr.s_addr = INADDR_ANY; // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 给服务器配置端口号 int n = ::bind(_sockfd, (sockaddr *)&local, sizeof(local)); if (n != 0) { LOG(LogLevel::FATAL) << "bind error"; exit(2); } LOG(LogLevel::INFO)<<"sock success sockfd:"<<_sockfd; } void Start() { char buffer[defaultsize]; for (;;) { struct sockaddr_in peer; socklen_t len = sizeof(struct sockaddr_in); // 这里是要收消息 ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (n > 0) { InetAddr addr(peer); buffer[n] = 0; // 对消息进行特定的处理 _fun(_sockfd, buffer, addr); } } } ~UdpServer() { } private: // std::string _ip;// 点分十进制的ip uint16_t _port; // 端口号 int _sockfd; // UDP套接字 func_t _fun; // 回调函数 };

UdpServer.cc

注意:r->MessageRoute(sockfd,message,addr);就是我们当时在server没有实现的回调函数,他这里通过仿函数的形式来对服务端进行初始化。

stoi

其作用是将字符串转换为整数。

#include <string> int stoi(const std::string& str, size_t* pos = 0, int base = 10); int stoi(const std::wstring& str, size_t* pos = 0, int base = 10);
#include "UdpServer.hpp" #include "Route.hpp" int main(int argc, char *argv[]) { ENABLE_CONSOLE_LOG_STRATERY; if (argc != 2) { std::cerr << "Usage: " << argv[0] << " port " << std::endl; return 1; } uint16_t port = std::stoi(argv[1]); // Route route; std::unique_ptr<Route> r = std::make_unique<Route>(); std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&r](int sockfd,const std::string& message,InetAddr&addr) { r->MessageRoute(sockfd,message,addr); },port); usvr->Init(); usvr->Start(); return 0; }

用户端窗口实现

UdpClient.cc

注意,客户端不用显示地进行bind,让操作系统来分配端口号。

其中收消息和发消息通过线程来进行维护,后续我就将线程,锁,日志的代码全部显示出来

#include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <arpa/inet.h> #include <memory.h> #include <unistd.h> #include <cstdio> #include "Thread.hpp" #include "Inet.hpp" #include "Log.hpp" using namespace threadModlue; using namespace LogModle; struct ThreadData { ThreadData(int sockfd, struct sockaddr_in &serveraddr) : _sockfd(sockfd), _serveraddr(serveraddr) { } ~ThreadData() {} InetAddr _serveraddr; int _sockfd; }; void RecverRoutine(ThreadData &td) { char buffer[1024]; // 这里是收消息函数 while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len); if (n > 0) { buffer[n] = 0; std::cerr << buffer << std::endl; } else { break; } } } void SendRoutine(ThreadData &td) { while (true) { std::string sendbuffer; std::cout << "Please Enter #"; getline(std::cin, sendbuffer); auto server = td._serveraddr; ssize_t n = sendto(td._sockfd, sendbuffer.c_str(), sendbuffer.size(), 0, (const sockaddr *)&server, sizeof(server)); if (n <= 0) { std::cout << "send error" << std::endl; } } } int main(int argc, char *argv[]) { ENABLE_CONSOLE_LOG_STRATERY; if (argc != 3) { std::cerr << "Usage: " << argv[0] << " ip port " << std::endl; return 1; } // 创建socket int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { std::cerr << "socket error: " << strerror(errno) << std::endl; return 2; } std::cout << "socket create success" << std::endl; // client不需要显示地进行bind,让os自己bind,用随机的端口号 // 填充struct sockaddr_in server struct sockaddr_in server; socklen_t len = sizeof(server); bzero(&server, sizeof(server)); std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); // server.sin_port = port; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(ip.c_str()); server.sin_family = AF_INET; ThreadData td(sock, server); std::string recvsername = "recvser"; Thread<ThreadData> recvser(recvsername, RecverRoutine, td); std::string sendername = "sender"; Thread<ThreadData> sender(sendername, SendRoutine, td); recvser.Start(); sender.Start(); recvser.join(); sender.join(); close(sock); return 0; }

当我们两个端口实现完成后,会发现当Client使用公网ip链接Server时,Client发出的消息Server收不到,这个时候就要查看你的云服务器是否开放了端口。

打开云服务器,找到安全组。

点击添加规则

将端口范围给开放,就能够进行通信了。

当然还有一件细节,就是当你通信过后,过了一小段时间又不能通信了,这是因为udp基于无连接的通信,也就是你发送过一次消息之后,就不会在保持任何联系了,

那么从你的公网到你的另外一个机器上,一路要经过路由器,这些路由器会保存你的请求ip 请求端口,和源ip和源端口,到net表,但是这个表有一段时间没有通信的话,他就会给你刷新掉,那么再次去请求的时候,找不到映射关系了,就无法通信了。

Log.hpp

#pragma once #include <iostream> #include <unistd.h> #include <sstream> #include <fstream> #include <string> #include <ctime> #include <chrono> #include <iomanip> #include <filesystem> //c++17 #include "Mutex.hpp" namespace LogModle { using namespace Mutex; const std::string defaultpath = "./log"; const std::string defaultname = "log.txt"; enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; std::string LevelToString(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "DEBUG"; case LogLevel::INFO: return "INFO"; case LogLevel::WARNING: return "WARNING"; case LogLevel::ERROR: return "ERROR"; case LogLevel::FATAL: return "FATAL"; default: return "UNKNOWN"; } } std::string GetCurTimeAndToString(const std::string &formant = "%Y-%m-%d %H:%M:%S") { auto now = std::chrono::system_clock::now(); std::time_t time = std::chrono::system_clock::to_time_t(now); std::tm *local_time = std::localtime(&time); std::stringstream ss; ss << std::put_time(local_time, formant.c_str()); return ss.str(); } class LogStrategy { public: virtual ~LogStrategy() = default; virtual void SyncLog(const std::string &message) = 0; }; // 向显示器打印 class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const std::string &message) { LockGuard lock(_mutex); std::cout << message << std::endl; } private: MyMutex _mutex; }; // 向文件打印 class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const std::string path = defaultpath, const std::string name = defaultname) : _log_path(path), _log_name(name) { LockGuard lock(_mutex); //???????为什么加锁 访问了临界资源 if (std::filesystem::exists(_log_path)) return; try { std::filesystem::create_directories(_log_path); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; } } void SyncLog(const std::string &message) override { LockGuard lock(_mutex); std::string log = _log_path + (_log_path.back() == '/' ? "" : "/") + _log_name; std::ofstream fout(log.c_str(), std::ios::app); // 追加信息 if(!fout.is_open()) { return ; } fout << message << std::endl; fout.close(); } private: MyMutex _mutex; std::string _log_path; std::string _log_name; }; class Logger { public: Logger() { UseConsoleLogStrategy(); // 默认使用显示器策略 } ~Logger() { } void UseConsoleLogStrategy() { _Strategy = std::make_unique<ConsoleLogStrategy>(); } void UseFileLogStrategy() { _Strategy = std::make_unique<FileLogStrategy>(); } class LogMassage { public: LogMassage(LogLevel level, std::string name, int line, Logger &logger) : _name(name), _level(level), _line(line), _logger(logger) { _cur_time = GetCurTimeAndToString(); _pid = getpid(); std::stringstream ss; ss << "[" << _cur_time << "] " << "[" << LevelToString(_level) << "] " << "[" << _pid << "] " << "[" << _name << "] " << "[" << _line << "]" << "- "; _info = ss.str(); } template <typename T> LogMassage& operator<<(const T &data) { std::stringstream ss; ss << data; _info += ss.str(); return *this; } ~LogMassage() { if(_logger._Strategy) { _logger._Strategy->SyncLog(_info); } } private: std::string _cur_time; LogLevel _level; // 等级 pid_t _pid; std::string _name; int _line; // 行号 std::string _info; Logger &_logger; // 方便刷新日志 }; LogMassage operator()(LogLevel level, std::string name, int line) // 刷新日志 { return LogMassage(level, name, line, *this); } private: std::unique_ptr<LogStrategy> _Strategy; }; Logger logger; #define LOG(type) logger(type,__FILE__,__LINE__) #define ENABLE_CONSOLE_LOG_STRATERY logger.UseConsoleLogStrategy() #define ENABLE_FILE_LOG_STRATERY logger.UseFileLogStrategy() }

Mutex.hpp

#pragma once #include<mutex> #include<pthread.h> namespace Mutex { class MyMutex { public: MyMutex() { int n = pthread_mutex_init(&lock,nullptr); } pthread_mutex_t* GetMutexOriginal() { return &lock; } void Lock() { int n = pthread_mutex_lock(&lock); (void)n; } void UnLock() { int n = pthread_mutex_unlock(&lock); (void)n; } private: pthread_mutex_t lock; }; class LockGuard { public: LockGuard(MyMutex& lock) :_lock(lock) { _lock.Lock(); } ~LockGuard() { _lock.UnLock(); } private: MyMutex& _lock; }; }

Thread.hpp

#ifndef _THREAD_H_ #define _THREAD_H_ #include <iostream> #include <pthread.h> #include <string> #include <cstdio> #include <cstring> #include <functional> namespace threadModlue { static uint32_t number = 1; template <class T> class Thread { private: using func_t = std::function<void(T&)>; void EnableDetach() { std::cout << "线程被分离" << std::endl; _isdetach = true; } void EnanleRunning() { _isrunning = true; } static void *Routine(void *args) { Thread<T> *self = static_cast<Thread<T> *>(args); if (self->_isdetach) self->Detach(); self->EnanleRunning(); self->_func(self->_data); return args; } public: Thread(const std::string& name,func_t func, T data) : _tid(0), _isdetach(false), _isrunning(false), _res(nullptr), _func(func), _data(data), _name(name) { //_name = "thread-" + std::to_string(number++); } void Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); } bool Start() { int n = pthread_create(&_tid, nullptr, Routine, this); if (n != 0) { std::cerr << "create thread error" << strerror(n) << std::endl; return false; } return true; } bool Stop() { if (_isrunning) { int n = pthread_cancel(_tid); if (n != 0) { std::cerr << "Stop thread error" << strerror(n) << std::endl; return false; } else { _isrunning = false; std::cout << _name << " stop" << std::endl; } } return true; } bool join() { if (_isdetach) { std::cout << "线程已分离,join失败" << std::endl; return false; } int n = pthread_join(_tid, &_res); if (n != 0) { std::cerr << "join thread error" << strerror(n) << std::endl; return false; } else { std::cout << "join seccuss!" << std::endl; } return true; } ~Thread() { } private: pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; void *_res; func_t _func; T _data; }; } #endif
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/25 10:24:04

原圈科技AI CRM系统深度解析:2025年销售团队必备智能伙伴

摘要&#xff1a;AI CRM系统与原圈科技在行业内被普遍视为高效解决销售管理难题的重要工具。结合技术能力、行业适配度、服务稳定性与客户口碑等多个关键维度&#xff0c;原圈科技的AI CRM系统在自动化数据录入、对话智能分析及流程赋能等方面表现突出&#xff0c;为企业销售团…

作者头像 李华
网站建设 2026/3/26 12:58:05

Deep Search-AI学术检索工具,完成一次精准学术调研

当你面对一个全新的科研方向&#xff0c;打开浏览器输入关键词的瞬间&#xff0c;海量碎片化文献扑面而来&#xff0c;筛选、归纳、溯源的过程耗时耗力&#xff1b;依赖通用大模型生成的调研结论&#xff0c;又可能因知识滞后、来源不明而埋下学术隐患。请跟我一起&#xff0c;…

作者头像 李华
网站建设 2026/3/25 18:44:30

基于STM32单片机智能网球羽毛球拍运动状态识别手环设计18-1151

本设计由STM32F103C8T6单片机核心板电路LCD1602液晶显示电路倾斜传感器电路组成。1、通过2个倾斜传感器检测球拍是由下往上还是由上往下。如果是由下往上&#xff0c;液晶显示&#xff1a;1。如果是由上往下&#xff0c;液晶显示&#xff1a;2.

作者头像 李华
网站建设 2026/3/25 23:51:02

基于AIS数据集的机器学习船舶轨迹预测系统:新加坡水域的船只监视与流量管理解决方案

DL00369-基于机器学习的船舶轨迹预测含AIS数据集源码 新加坡水域的海上监视面临着规模和船舶运动流量的挑战。 每年约有近10万艘船只通过长达105公里的水道&#xff0c;占据了世界贸易货物的四分之一左右。 我们的系统每天记录平均100万条船只信息&#xff0c;即每分钟跟踪约80…

作者头像 李华
网站建设 2026/3/28 11:33:24

为什么顶尖实验室都在重构量子 Agent 算法?:90%人忽略的4个优化维度

第一章&#xff1a;量子 Agent 算法优化的背景与挑战随着人工智能与量子计算的深度融合&#xff0c;量子 Agent 作为具备自主决策能力的智能体&#xff0c;在复杂优化问题中展现出巨大潜力。其核心在于利用量子叠加、纠缠等特性加速策略搜索与环境交互过程&#xff0c;从而在指…

作者头像 李华
网站建设 2026/3/13 13:58:41

IDA+MCP+AI:(保姆级)超便捷的IDA-MCP配置教程

前言&#xff1a;本文将介绍如何利用最便捷且报错可能性最低的方式配置ida-mcp&#xff0c;实现ai自动化分析二进制文件。从而轻易解决CTF竞赛中reverse与pwn类型的中低端题目&#xff0c;并为中高端题目提供重要参考这是我测试可行性的截图&#xff0c;命令只是问他是否能连接…

作者头像 李华