多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

04.【网络】网络通信实战-UDP实现网络英汉字典

04.【网络】网络通信实战-UDP实现网络英汉字典 目录1. 格式转换接口扩展1.1 字符串IP地址与网络字节序相互转换支持IPv4和IPv61. inet_pton字符串格式IP转网络字节序2. inet_ntop网络字节序转字符串格式IP2. 字典实现UDP服务-客户端2.1 网络字节序主机字节序转换的类InetAddr封装2.2 字典实现2.3 服务端实现2.4 客户端实现3. 功能验证4. 字典代码1. 格式转换接口扩展1.1 字符串IP地址与网络字节序相互转换支持IPv4和IPv6上一篇文章的2.5小节提到一对“点分十进制IP与网络字节序相互转换”的接口inet_addr和inet_ntoa但是这对接口只能对IPv4格式的IP地址进行转换。以下介绍的inet_pton和inet_ntop既可以同时支持IPv4和IPv6的IP地址转换。了解inet_pton是为支持 IPv6 而出现的函数它在 IPv61995 年通过 RFC 1883 定义提出之后问世最早见于 1996 年左右的 NetBSD 1.3 系统它属于 POSIX 标准的用户空间 C 标准库如 Linux 下的 glibc1. inet_pton字符串格式IP转网络字节序#includesys/socket.h#includenetinet/in.h#includearpa/inet.h// 第一个函数inet_ptonpresentation to numeric字符串格式 → 二进制数值格式intinet_pton(intaf,constchar*src,void*dst);// 功能: 将人类易读的IP地址字符串IPv4点分十进制/IPv6冒号十六进制转换为计算机/网络协议可识别的二进制数值网络字节序/大端序// 供对应套接字地址结构体sockaddr_in/IPv4、sockaddr_in6/IPv6使用是现代跨协议IPv4/IPv6的安全转换函数。// 参数:// int af指定IP协议族仅支持两个合法常量不可混用// AF_INET表示处理IPv4地址对应后续src为点分十进制字符串如192.168.1.100dst为struct in_addr结构体指针。// AF_INET6表示处理IPv6地址对应后续src为冒号十六进制字符串如fe80::a00:27ff:fe12:3456dst为struct in6_addr结构体指针。// const char *src指向合法IP地址字符串的常量指针要求字符串格式与af指定的协议族匹配否则转换失败。// void *dst指向用于存储转换结果的二进制数值缓冲区的通用指针需提前分配内存对应struct in_addr/struct in6_addr// 转换成功后二进制数值网络字节序会写入该缓冲区可直接赋值给对应套接字地址结构体的地址成员。// 返回值:// 转换成功返回1src字符串格式合法且与af协议族匹配二进制数值已成功写入dst缓冲区。// 无错但不兼容返回0src字符串格式合法但与af指定的协议族不匹配如afAF_INETsrc传入IPv6字符串未写入dst。// 转换失败返回-1同时设置全局错误码errno常见错误码// EINVALaf不是合法协议族非AF_INET/AF_INET6或src字符串格式非法如IPv4传入192.168.1.256。// ENOSPCdst缓冲区大小不足无法存储转换后的二进制数值极少出现正常使用对应结构体可避免。2. inet_ntop网络字节序转字符串格式IP#includesys/socket.h#includenetinet/in.h#includearpa/inet.h// 第二个函数inet_ntopnumeric to presentation二进制数值格式 → 字符串格式constchar*inet_ntop(intaf,constvoid*src,char*dst,socklen_t size);// 功能: 将计算机/网络协议可识别的IP二进制数值网络字节序/大端序来自sockaddr_in/IPv4、sockaddr_in6/IPv6// 转换为人类易读的字符串格式IPv4点分十进制/IPv6冒号十六进制支持IPv4/IPv6双协议使用用户提供的缓冲区存储结果线程安全无数据覆盖风险。// 参数:// int af指定IP协议族仅支持两个合法常量// AF_INET处理IPv4二进制数值对应src为struct in_addr结构体指针转换后为点分十进制字符串。// AF_INET6处理IPv6二进制数值对应src为struct in6_addr结构体指针转换后为冒号十六进制字符串。// const void *src指向IP二进制数值缓冲区的通用常量指针数据格式需与af指定的协议族匹配如AF_INET对应struct in_addr否则转换失败。// char *dst指向用户提前分配的输出字符串缓冲区的指针转换后的IP字符串会写入该缓冲区需保证缓冲区大小足够存储结果。// 推荐使用系统预定义常量指定缓冲区大小避免不足// INET_ADDRSTRLENIPv4字符串缓冲区最小大小16字节足够存储最长IPv4点分十进制字符串。// INET6_ADDRSTRLENIPv6字符串缓冲区最小大小46字节足够存储最长IPv6冒号十六进制字符串。// socklen_t size指定dst输出缓冲区的字节大小需与实际缓冲区大小一致如sizeof(dst)、INET_ADDRSTRLEN// 若大小不足转换失败并设置errno为ENOSPC。// 返回值:// 转换成功返回指向dst输出缓冲区的常量字符指针与dst参数值一致缓冲区中已存储合法的IP字符串格式数据。// 转换失败返回NULL同时设置全局错误码errno常见错误码// EINVALaf不是合法协议族或src二进制数值格式非法无法转换为对应IP字符串。// ENOSPCdst缓冲区大小size不足无法存储转换后的IP字符串需使用INET_ADDRSTRLEN/INET6_ADDRSTRLEN规避。2. 字典实现UDP服务-客户端字典的实现相对于上一节的简单UDP服务-客户端通信来讲增加了几个功能对网络字节序主机字节序之间的转换进行封装解耦对网络中接收到的sockaddr_in直接调用InetAddr类即可得到客户端的ip和port。服务端实例化使用了unique_ptr智能指针unique_ptr是C11引入的独占式智能指针核心优势在于能自动释放所管理的动态资源以避免内存泄漏通过独占所有权杜绝重复释放问题同时支持动态数组管理和自定义删除器以实现通用RAII资源管理还可借助移动语义安全地转移资源所有权且几乎无额外性能开销是替代裸指针进行独占资源管理的首选。2.1 网络字节序主机字节序转换的类InetAddr封装网络转主机:sockaddr_in _addr解析出_ip和_portInetAddr.hpp#pragmaonce#includeiostream#includestring#includesys/socket.h#includesys/types.h#includearpa/inet.h#includenetinet/in.h// 网络转主机 格式转换 的类// 1.网络转主机: sockaddr_in _addr 解析出 _ip和_portclassInetAddr{public:// 1.网络转主机: sockaddr_in _addr 解析出 _ip和_portInetAddr(structsockaddr_inaddr):_addr(addr){_portntohs(_addr.sin_port);//从网络中拿到的网络序列_ipinet_ntoa(_addr.sin_addr);// 4字节网络风格的IP - 点分十进制的字符串风格的IP}uint16_tPort(){return_port;}std::stringIp(){return_ip;}~InetAddr(){}private:structsockaddr_in_addr;std::string _ip;uint16_t_port;};2.2 字典实现加载字典翻译功能实现unordered_mapDict.hpp#pragma#includeiostream#includefstream#includestring#includeunordered_map#includeLog.hpp#includeInetAddr.hppconststd::string defaultdict./dictionary.txt;conststd::string sep: ;usingnamespaceLogModule;// 字典实现// 1.加载字典// 2.翻译功能实现unordered_mapclassDict{public:Dict(conststd::stringpathdefaultdict):_dict_path(path){}// 加载字典boolLoadDict(){std::ifstreamin(_dict_path);if(!in.is_open()){LOG(LogLevel::DEBUG)打开字典: _dict_path 错误;returnfalse;}std::string line;while(std::getline(in,line))// 打开并从文件里面一次读入一行{autoposline.find(sep);if(posstd::string::npos){LOG(LogLevel::WARNING)解析: line 失败;continue;}std::string englishline.substr(0,pos);std::string chineseline.substr(possep.size());// sep : if(english.empty()||chinese.empty()){LOG(LogLevel::WARNING)没有有效内容: line;continue;}_dict.insert(std::make_pair(english,chinese));LOG(LogLevel::DEBUG)加载: line;}in.close();returntrue;}// 翻译功能std::stringTranslate(conststd::stringword,InetAddrclient){autoiter_dict.find(word);if(iter_dict.end()){LOG(LogLevel::DEBUG)进入到了翻译模块[client.Ip() : client.Port()]# word-None;returnNone;}LOG(LogLevel::DEBUG)进入到了翻译模块[client.Ip() : client.Port()]# word-iter-second;returniter-second;}~Dict(){}private:std::string _dict_path;// 路径文件名std::unordered_mapstd::string,std::string_dict;};dictionary.txtapple:苹果 banana:香蕉 cat:猫 dog:狗 book:书 pen:笔 happy:快乐的 sad:悲伤的 hello::你好 run:跑 jump:跳 teacher:老师 student:学生 car:汽车 bus:公交车 love:爱 hate:恨 hello:你好 goodbye:再见 summer:夏天 winter:冬天2.3 服务端实现创建套接字socket创建套接字地址sockaddr_inbind绑定套接字与套接字地址发送/接收消息数据UdpServer.hpp#pragma#includeiostream#includestring#includefunctional#includestring.h#includesys/types.h#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeLog.hpp#includeInetAddr.hppusingnamespaceLogModule;usingfunc_tstd::functionstd::string(conststd::string,InetAddr);constintdefaultfd-1;// 网络通信服务端// 1.创建套接字socket// 2.创建套接字地址sockaddr_in// 3.bind绑定套接字与套接字地址// 4.发送/接收消息数据classUdpServer{public:UdpServer(uint16_tport,func_t func):_sockfd(defaultfd),_port(port),_isrunning(false),_func(func){}voidInit(){// 1.创建套接字_sockfdsocket(AF_INET,SOCK_DGRAM,0);if(_sockfd0){LOG(LogLevel::FATAL)socket error!;exit(1);}LOG(LogLevel::INFO)socket success, sockfd : _sockfd;// 2.创建套接字地址sockaddr_in填充协议sin_famil、ip地址(sin_addr)(网络序列)、sin_port端口号structsockaddr_inlocal;bzero(local,sizeof(local));// 内存清0local.sin_familyAF_INET;// IPv4协议是AF_INETlocal.sin_porthtons(_port);// 端口号 //本地格式 - 网络序列 htons()函数转换local.sin_addr.s_addrINADDR_ANY;// 但是实际使用Server服务端要接收发送到本机所有IP的信息。IP不止1个。所以IP地址要使用INADDR_ANY宏定义来赋值// 3.bind绑定套接字与套接字地址// 那么为什么服务器端要显式的bind呢IP和端口必须2是众所周知不能轻易改变的intnbind(_sockfd,(structsockaddr*)local,sizeof(local));if(n0){LOG(LogLevel::FATAL)bind error;exit(2);}LOG(LogLevel::INFO)bind success, sockfd : _sockfd;}// 4.发送/接收消息数据voidStart(){_isrunningtrue;while(_isrunning){charbuffer[1024];structsockaddr_inpeer;socklen_t lensizeof(peer);// 1. 收消息client为什么要向服务器发送消息目的就是要服务器端处理数据ssize_t srecvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(structsockaddr*)peer,len);if(s0){InetAddrclient(peer);// InetAddr类用于接收目标客户端主机的socket套接字信息并修改peerbuffer[s]0;// 2.收到的内容当作英文单词执行翻译功能std::string result_func(buffer,client);// 3.发消息// 将接收到的数据再返回client客户端peer已经是目标客户端主机的套接字sendto(_sockfd,result.c_str(),result.size(),0,(structsockaddr*)peer,len);}}}~UdpServer(){}private:int_sockfd;// 套接字uint16_t_port;// 端口号bool_isrunning;// 运行状态func_t _func;// 服务器的回调 函数对数据进行处理};UdpServer.cc加载字典使用服务-客户端通信实现网络字典翻译功能#includeiostream#includememory#includeDict.hpp// 翻译功能#includeUdpServer.hpp// 网络通信功能// 仅仅是用来测试的std::stringdefaulthandler(conststd::stringmessage){std::string hellohello, ;hellomessage;returnhello;}// 功能网络字典翻译// 1.加载字典// 2.使用服务-客户端通信实现网络字典翻译功能// ./udpserver portintmain(intargc,char*argv[]){if(argc!2){std::cerrusage: argv[0] portstd::endl;return1;}// std::string ip argv[1];uint16_tportstd::stoi(argv[1]);// 字符串转整型Enable_Console_Log_Strategy();// 开启终端打印日志// 1.字典对象提供翻译功能Dict dict;dict.LoadDict();// 2.网络服务器对象提供通信功能std::unique_ptrUdpServerusvrstd::make_uniqueUdpServer(port,[dict](conststd::stringword,InetAddrcli)-std::string{returndict.Translate(word,cli);});usvr-Init();usvr-Start();return0;}2.4 客户端实现创建套接字socket创建套接字地址sockaddr_in填写目标服务器信息发送/接收信息UdpClient.cc#includeiostream#includestring#includecstring#includenetinet/in.h#includearpa/inet.h#includesys/types.h#includesys/socket.h// ./udpclient server_ip server_port// 1.创建套接字socket// 2.创建套接字地址sockaddr_in填写目标服务器信息// 3.发送/接收信息intmain(intargc,char*argv[]){if(argc!3){std::cerrUsage: argv[0] server_ip server_portstd::endl;return1;}std::string server_ipargv[1];// 第2个参数为目标ip地址uint16_tserver_portstd::stoi(argv[2]);// 第3个参数为目标进程端口号// 1.创建套接字socketintsockfdsocket(AF_INET,SOCK_DGRAM,0);if(sockfd0){std::cerrsocket errorstd::endl;return2;}// 关于bind上一节已经提过只需要将服务端的主机socket套接字进行bind固定绑定// 2.创建套接字地址sockaddr_in填写目标服务器信息structsockaddr_inserver;memset(server,0,sizeof(server));server.sin_familyAF_INET;server.sin_porthtons(server_port);server.sin_addr.s_addrinet_addr(server_ip.c_str());// 3.发送/接收消息while(true){std::string input;std::coutPlease Enter# ;std::getline(std::cin,input);// 3.1 发送消息intnsendto(sockfd,input.c_str(),input.size(),0,(structsockaddr*)server,sizeof(server));(void)n;charbuffer[1024];// 3.2 创建套接字地址用于接收信息来源主机的套接字地址(IP和port端口号)structsockaddr_inpeer;socklen_t lensizeof(peer);// 3.3 接受消息intmrecvfrom(sockfd,buffer,sizeof(buffer)-1,0,(structsockaddr*)peer,len);if(m0){buffer[m]0;std::coutbufferstd::endl;}}return0;}3. 功能验证运行./udpserver 8080之后首先加载字典到内存中字典中有问题的单词翻译会解析失败另开一个终端运行./udpclient 127.0.0.1 8080之后输入单词通过网络发送到udpserver之后网络对单词进行翻译并回传则在udpclient终端会得到单词的翻译结果。4. 字典代码【免费】网络通信-UDP-UDP实现网络英汉字典资源-CSDN下载
返回列表