ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Socket编程--UDP

Socket编程--UDP 一、V1版本-Echo Server网络通信中必须包含的三个头文件描述网络套接字的结构体填充字段保证结构体符合指定长度为什么需要16位地址类型AFinet第一次传入AFinet是为了告诉操作系统我要创建网络套接字而第二次传入是为了让未来Socket接口函数能够识别出我是用于网络通信的。afinet宏中的##功能拼接a##1拼接后即为a1当我们在进行UDP编程时在UDP 编程服务器端首先需要调用 socket 创建服务器套接字然后填充本地套接字地址信息最后调用 bind将套接字文件描述符与本机的 IP、端口进行绑定。创建套接字所需要使用到的接口--socket()当在进行绑定操作时需要使用到的接口--bind()EchoServer.hpp#ifndef __ECHOSERVER_HPP #define __ECHOSERVER_HPP #include iostream #include string #include cstring #include cstdint // uint16_t 类型依赖原代码用到uint16_t必须加 #include cstdlib // exit() 函数依赖Init中exit退出程序 #include sys/types.h// socket/recvfrom/sendto 底层类型前置依赖 // Linux网络系统调用头文件 #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h // sockaddr_in、INADDR_ANY 定义核心头文件缺失会报INADDR_ANY未定义 #include Mutex.hpp #include Logger.hpp const static int default_fd -1; const static uint16_t default_port 8080; enum { SUCCESS 0, SOCKET_ERR, BIND_ERR, // 补充bind错误码 USAGE_ERR // 补充用法错误码 }; class UdpServer { public: UdpServer(uint16_t port default_port) : _port(port), _sockfd(default_fd) { } ~UdpServer() { if (_sockfd ! default_fd) close(_sockfd); } void Init() { // 第一步创建socket _sockfd socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::INFO) create socket success, sockfd: _sockfd; // 第二步填充网络信息并bind 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()); //字符串 IP - 4字节 网络序由于服务器开发时不建议显示bind IP所以不使用这种方式来绑定IP而是采用下面的方式 local.sin_addr.s_addr INADDR_ANY;//最佳实践任意IP地址bind int n bind(_sockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind socket error; exit(BIND_ERR); } LOG(LogLevel::INFO) bind socket success, ip: _ip , port: _port; } void Start() { // 传递的是字符串,echo server char inbuffer[1024]; while(true) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 1. 用户发来的数据 // 2. 用户的socket信息 ssize_t n recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr *)peer, len); if(n 0) { // 用户的IP Port信息,是recvfrom从网络中获取到的数据,网络序列(大端) - 转换成为主机序列 // peer.sin_port; // peer client port // peer.sin_addr; // peer client i // 把网络序端口转为主机序得到客户端真实端口号 uint16_t client_port ntohs(peer.sin_port); // 将sockaddr_in里4字节网络序IP数值转换成人类可读的点分十进制IP字符串 std::string client_ip inet_ntoa(peer.sin_addr); // 拼接格式化客户端地址格式为 [ip:端口] std::string client_address [ client_ip : std::to_string(client_port) ]; // 用户发来的数据 inbuffer[n] 0; LOG(LogLevel::DEBUG) cilent say# inbuffer; std::string echo_string server echo# ; echo_string inbuffer; // h to n sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)peer, len); } else { LOG(LogLevel::ERROR) recvfrom error; } } } private: int _sockfd; uint16_t _port; //std::string _ip; //由于使用的是任意IP不需要绑定固定IP可以把std::string _ip去掉了只需要固定端口号即可 }; #endif上面的代码中服务端在进行网络通信时需要先创建套接字填充本机 IP 与端口地址信息再调用 bind 函数完成地址绑定很多人会产生疑问客户端通信时是否也需要专属的套接字地址IP 端口答案是肯定的但是客户端在绑定时不能显示地bind IP和端口号。原因是服务端的端口必须明确且固定这样各个客户端发起通信时才能精准找到服务器上对应的服务进程而客户端端口没有固定要求只要本机内端口唯一不冲突就行。如果我们强行给客户端显式绑定固定端口后续客户端去连接不同服务端的时候很容易出现端口占用冲突问题一旦该端口被其他程序占用客户端就无法正常收发数据了。那么客户端如何设置自己的IP和端口呢客户端默认走随机端口这套逻辑全部交给操作系统分配不用我们手动操作。拿 UDP 客户端来说程序第一次调用sendto发数据的时候系统会自动完成隐式bind随机挑一个本机空闲端口同时绑定本机可用 IP自动生成属于这个客户端的 IP 端口。EchoClient.cc#include iostream #include string #include cstdint #include cstdlib #include cstring #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h // 打印使用提示 static void Usage(const std::string process) { std::cerr Usage:\n\t; std::cerr process server_ip server_port std::endl; } // ./client_udp server_ip server_port int main(int argc, char *argv[]) { // 参数校验必须传入服务端IP、端口两个参数总参数数量为3 if(argc ! 3) { Usage(argv[0]); exit(1); } // 从命令行读取目标服务端IP、端口 std::string server_ip argv[1]; uint16_t server_port std::stoi(argv[2]); // 1. 创建UDP socket int sockfd socket(AF_INET, SOCK_DGRAM, 0); if(sockfd 0) { std::cerr socket error std::endl; exit(2); } // 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(true) { std::string message; // 1. 获取用户输入 std::cout Please Enter# ; std::getline(std::cin, message); // 2. client 发送数据给 server首次发送即自动bind ssize_t n sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)server, sizeof(server)); if(n 0) { // 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::cout inbuffer std::endl; } } } return 0; }EchoServerMain.cc#include EchoServer.hpp #include memory #include iostream // std::cerr、std::endl 依赖 #include string // std::string、std::stoi 依赖 #include cstdlib // exit() 函数依赖 #include cstdint // uint16_t 类型依赖 // 打印程序使用帮助 static void Usage(const std::string process) { std::cerr Usage:\n\t; std::cerr process local_port std::endl; } // 程序运行格式./server_udp 端口号 int main(int argc, char *argv[]) { // 参数校验必须传入1个端口参数总参数数量为2 if(argc ! 2) { Usage(argv[0]); exit(USAGE_ERR); } // 开启控制台日志输出策略自定义日志库宏 ENABLE_CONSOLE_LOG_STRATEGY(); // 把命令行传入的端口字符串转为数字端口 uint16_t server_port std::stoi(argv[1]); // 使用智能指针创建UDP服务端对象传入指定监听端口 std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(server_port); // 初始化socket、bind端口 usvr-Init(); // 启动循环收发echo数据 usvr-Start(); return 0; }如果测试时绑定本地环回 IP 127.0.0.1、端口 8080流量只会在本机内核协议栈内部流转报文不会出网卡、不会发到外部物理网络只能本机内部进程互相访问局域网、公网其他设备都无法连接这个服务。服务端开发则有一套最佳实践云服务器千万不能显式 bind 公网 IP日常写服务代码也不建议手动指定固定IP去 bind。一旦显式绑定指定 IP系统只会接收目标地址和该绑定 IP 完全匹配的报文云厂商公网 IP 经过NAT转发后流量目的地址实际是内网IP外部请求根本进不来。但禁止显式绑定公网 IP 不代表公网 IP 不会参与通信外部用户访问服务走的依旧是公网 IP稳妥的写法直接bind 0.0.0.0(INADDR_ANY)监听本机全部网卡地址就能兼容所有访问场景。所以服务端开发时最佳实践是随机bind任意IP地址而客户端开发最佳实践是随机bind任意端口号。二、V2版本-Dict Server前面的网络通信服务只能完成IO功能也就是只能收到或发送信息下面通过封装一层业务软件层来实现一个简单的英译汉英文字典Dict.hpp#pragma once #include iostream #include string #include fstream #include unordered_map #include Logger.hpp static const std::string default_dict ./dict.txt; static const std::string sep : ; using namespace NS_LOG_MODULE; class Dict { public: Dict(const std::string dict_path default_dict) : _dict_path(dict_path) { LoadDict(); } ~Dict() { } void LoadDict() { std::ifstream in(_dict_path); if (!in.is_open()) { LOG(LogLevel::FATAL) open _dict_path error; exit(1); } std::string line; while (std::getline(in, line)) { LOG(LogLevel::FATAL) load: line success; // apple: 苹果 auto pos line.find(sep); if (pos std::string::npos) { LOG(LogLevel::WARNING) Format: line error; continue; } std::string k line.substr(0, pos); // [) std::string v line.substr(pos sep.size()); _dict.insert(std::make_pair(k, v)); } in.close(); LOG(LogLevel::INFO) load done....; } std::string Translate(std::string word) { auto iter _dict.find(word); if(iter ! _dict.end()) { return iter-second; } else { return None; } } private: std::string _dict_path; std::unordered_mapstd::string, std::string _dict; };dict.txtapple: 苹果 banana: 香蕉 cat: 猫 dog: 狗 book: 书 pen: 笔 happy: 快乐的 sad: 悲伤的 run: 跑 jump: 跳 teacher: 老师 student: 学生 car: 汽车 bus: 公交车 love: 爱 hate: 恨 hello: 你好 goodbye: 再见 summer: 夏天 winter: 冬天三、V3版本-Chat Server在网络通信模块中UDPServer会接收到客户端发送的所有信息也会收到所有的客户端。如果要将这些信息和客户端管理起来就必须先描述在组织用链表vector这类数据结构来对这些收到的信息和客户端进行管理。在上面的V1版本中整个服务器都是单进程的工作方式也是串行进行的。而由于UDP Server会以回调的方式调用上层逻辑所以在V1版本-Echo Server的基础上我们可以设计一种转发逻辑形成一个新的数据转发的任务——route_message()将客户端收到的消息转发给所有的在线用户。为了完成信息转发我们可以将这些转发任务放入线程池中经过线程池处理之后由线程池将数据转发给所有人。虽然还是单进程对收到的数据进行收发处理但是经过任务封装和线程池之后我们就能并发地读和写多线程处理收到的任务。这也就是V3版本聊天服务器Chat Server的核心思想。由于需要使用到线程池同时需要引入线程池条件变量的hpp文件Thread.hpp、Threadpool.hpp、Cond.hpp在前面的博客中有下面是V3版本Chat Server的核心代码ChatClient.cc#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; }ChatServerMain.cc#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(); // ./server_udp port int main(int argc, char *argv[]) { if(argc ! 2) { Usage(argv[0]); exit(USAGE_ERR); } 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(t); // thread_pool-Enqueue([r, sockfd, msg](){ // r.Broadcast(sockfd, msg); // }); } ); usvr.Start(); return 0; }InetAddr.hpp#pragma once #include iostream #include string #include strings.h #include sys/socket.h #include netinet/in.h #include arpa/inet.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) { bzero(_address, sizeof(_address)); _address.sin_family AF_INET; _address.sin_port htons(_port); // h-n _address.sin_addr.s_addr inet_addr(_ip.c_str()); // 1. 字符串ip-4字节IP 2. hton _len sizeof(_address); } bool operator (const InetAddr addr) { return (this-_ip addr._ip) (this-_port addr._port); } std::string ToString() { return [ _ip : std::to_string(_port) ]; } InetAddr() {} struct sockaddr_in *GetNetAddress() { return _address; } socklen_t Len() { return _len; } ~InetAddr() {} private: // net address struct sockaddr_in _address; socklen_t _len; // host address std::string _ip; uint16_t _port; };UpdServer.hpp#ifndef __ECHOSERVER_HPP #define __ECHOSERVER_HPP #include iostream #include string #include cstdlib #include strings.h #include sys/socket.h #include netinet/in.h #include functional #include arpa/inet.h #include InetAddr.hpp #include Logger.hpp using namespace NS_LOG_MODULE; const static int default_fd -1; const static int default_port 8888; using handler_addr_t std::functionvoid (const InetAddr ); using handler_msg_t std::functionvoid (int sokcfd, std::string msg); enum { SUCCESS 0, USAGE_ERR, SOCKET_ERR, BIND_ERR, }; class UdpServer { public: // UdpServer(const std::string ip, uint16_t port default_port) UdpServer(uint16_t port default_port) : _port(port), _sockfd(default_fd) { } ~UdpServer() { close(_sockfd); } void Init() { // 第一步: 创建socket, 本质: 打开网卡 --- 系统特性 _sockfd socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::INFO) create socket success, sockfd: _sockfd; InetAddr local(_port); // 第三步bind socket 信息 int n bind(_sockfd, (struct sockaddr *)(local.GetNetAddress()), local.Len()); if (n 0) { 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() { // 传递的是字符串,echo server char inbuffer[1024]; while (true) { struct sockaddr_in peer; socklen_t len sizeof(peer); // 1. 用户发来的数据 // 2. 用户的socket信息 ssize_t n recvfrom(_sockfd, 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. 转发消息 // nickname# message - [ip:port]-nickname# message _handler_msg(_sockfd, message); } else { LOG(LogLevel::ERROR) recvfrom error; } // sleep(3); } } private: int _sockfd; // std::string _ip; // 192.168.2.2(字符串风格的点分十进制IP地址, 让人看的) 4字节IP ??? uint16_t _port; // 用户设置好的server port必须是固定的 handler_addr_t _handler_addr; handler_msg_t _handler_msg; }; #endifUserManager.hpp#pragma once #include iostream #include string #include vector #include InetAddr.hpp #include Logger.hpp using namespace NS_LOG_MODULE; class UserManager { public: UserManager(){} void AddUser(const InetAddr addr) { if(SearchUser(addr)) return; _users.push_back(addr); } void DelUser(const InetAddr addr) { for(auto iter _users.begin(); iter ! _users.end(); iter) { if(*iter addr) { _users.erase(iter); break; } } } bool SearchUser(const InetAddr addr) { for(auto user : _users) { if(user addr) { return true; } } return false; } bool ModUser(const InetAddr addr) { DelUser(addr); AddUser(addr); return true; } std::vectorInetAddr Users() { return _users; } ~UserManager(){} private: std::vectorInetAddr _users; };Route.hpp#pragma once #include iostream #include memory #include string #include sys/socket.h #include Mutex.hpp #include UserManager.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; };
返回列表