网络通信:udp套接字实现echoserver和翻译功能
目录一、echoserver功能1.1、服务端1.1.1 创建套接字1.1.2网络与主机序列转化函数1.1.3 sendto/recvfrom实现收发功能1.1.4 服务端完整代码1.2、客户端1.3 运行示例二、添加翻译功能2.1 添加回调函数2.2 编写业务层字典类2.2.1 字典的加载2.2.1 提供翻译接口2.2.3 给服务器绑定回调函数2.2.4 业务层完整代码2.2.5 运行示例一、echoserver功能echoserver回显服务器是网络编程中最基础、最经典的服务端示例程序核心功能是接收客户端发送的任意数据不做任何修改直接原样返回给客户端就像对着山谷喊一声听到的 “回声” 一样。它是学习套接字编程的入门案例主要用来理解TCP/UDP 通信的完整流程服务端绑定、监听、接收连接客户端连接、发送 / 接收数据验证网络通信的连通性确认客户端和服务端能正常收发数据掌握基础的网络 I/O 操作1.1、服务端在服务端主要完成的任务是创建udp套接字消息给udp套接字bind绑定ip与端口完成消息的收发1.1.1 创建套接字我们首先创建一个server.hpp文件并使用一个类server来整体封装服务器消息因为服务器是向客户端公开的所以必须显式的绑定ip与端口这样客户端才能访问到服务器。因此类server创建时必须显式指明将要绑定的ip与端口注意这里构造服务器时传入的ip与端口分别是点分十进制与uint16_t都是主机序列。在初始化服务器时就需要完成udp套接字的创建与接口的bind具体代码如下//网络通信的三个必要头文件 #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includelog.hpp//自定义日志模块 class server { public: server(uint16_t port) :_port(port) {} void Init() { //此时创建了一个基于udp的网络套接字 _sockfdsocket(AF_INET,SOCK_DGRAM,0); if(_sockfd0) { LOG(LogLevel::FATAL)套接字创建失败; return; } //给套接字绑定ip地址与端口号 struct sockaddr_in mess; mess.sin_familyAF_INET; mess.sin_porthtons(_port); mess.sin_addr.s_addrINADDR_ANY; socklen_t lensizeof(mess); int retbind(_sockfd,(const struct sockaddr*)mess,len); if(ret0) { LOG(LogLevel::FATAL)套接字绑定失败; return; } } private: bool _isrunningfalse int _sockfd; uint16_t _port; };在整段代码中_sockfdsocket(AF_INET,SOCK_DGRAM,0);则为我们创建并返回了一个套接字消息AF_INET表示ipv4的协议族如果是ipv6则需要使用AF_INET6SOCK_DGRAM则表示我们的套接字类型是UDP类型我们后续需要以此完成UDP网络通信如果后续使用TCP则需要设置为SOCK_STREAM。第三个参数我们默认设置为0此时接口会根据套接字类型自动选择对应协议如 TCP 对应IPPROTO_TCPUDP 对应IPPROTO_UDP。socket接口的返回值成功返回套接字文件描述符非负整数失败返回-1并设置errno。给套接字绑定对应的IP与端口我们使用bind接口它的具体定义如下#include sys/socket.h int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);在这里第一个整形参数就是创建套接字时返回的套接字消息本质是文件描述符第二个参数是一个const struct sockaddr *类型因为我们要使用套接字完成网络通信所以必须使用struct sockaddr_inIPv4/struct sockaddr_in6IPv6来提前确定绑定的ip与端口信息。这里我们来着重理解一下struct sockaddr_in的成员类型struct sockaddr_in6与其相似struct sockaddr_in { sa_family_t sin_family; // 地址族必须为AF_INET标识IPv4 in_port_t sin_port; // 端口号网络字节序需用htons()转换 struct in_addr sin_addr; // IPv4地址网络字节序用in_addr结构体封装 char sin_zero[8]; // 填充字段置0以兼容struct sockaddr的长度 }; // 嵌套的in_addr结构体存储IPv4地址的32位二进制值 struct in_addr { uint32_t s_addr; // IPv4地址网络字节序可用inet_addr()/inet_pton()转换 };当我们对其中的成员进行赋值的时候需要注意的是必须将主机序列的ip与端口转化为网络序列而包含的头文件中arpa/inet则为我们提供了相关的转化函数1.1.2网络与主机序列转化函数htons/ntohs/htonl/ntohlarpa/inet.h中提供了 4 个最常用的转换函数按处理的数据长度分为 16 位短整型和 32 位长整型两类函数名功能描述适用场景htons()主机字节序 → 网络字节序16 位端口号转换如 80、8080ntohs()网络字节序 → 主机字节序16 位接收网络数据后的端口解析htonl()主机字节序 → 网络字节序32 位IP 地址32 位转换ntohl()网络字节序 → 主机字节序32 位接收网络数据后的 IP 解析为了方便记忆我们可以这样记h host主机、n network网络、s short16 位、l long32 位。例如htons host to network short主机→网络16 位。代码示例#include stdio.h #include arpa/inet.h int main() { // 1. 16位数据转换端口号示例 uint16_t host_port 8080; // 主机字节序的端口号 uint16_t net_port htons(host_port); // 转网络字节序 uint16_t back_port ntohs(net_port); // 转回主机字节序 printf(16位端口转换\n); printf(主机字节序端口%u\n, host_port); printf(网络字节序端口%u\n, net_port); printf(转回主机字节序%u\n\n, back_port); // 2. 32位数据转换IP地址示例将手写主机字节序的32位整数转为网络再转主机 uint32_t ip 3232235777; uint32_t net_ip htonl(ip); // 转网络字节序的32位整数 uint32_t host_ip ntohl(net_ip); // 转主机字节序的32位整数 printf(主机字节序IP整数%u\n, host_ip); printf(网络字节序IP整数%u\n, net_ip); return 0; }运行结果函数参数 / 返回值均为无符号整数uint16_t/uint32_t避免符号位干扰。运行代码后你会看到转换前后的数值变化小端主机上会明显不同但转回后数值与原主机字节序一致。inet_pton/inet_ntoparpa/inet.h还提供了更通用的inet_pton()和inet_ntop()函数支持 IPv4 和 IPv6是更推荐的现代用法两个接口可以直接完成点分十进制的ip与其网络字节序之间的转化无需调用其他辅助接口。#include arpa/inet.h int inet_pton(int af, const char *restrict src, void *restrict dst);功能将点分十进制字符串 → 二进制网络字节序af地址族可选值AF_INET表示 IPv4 地址AF_INET6表示 IPv6 地址src指向点分十进制IPv4或 IPv6 字符串的指针如 192.168.1.1 或 2001:db8::1dst指向存储转换后二进制地址的缓冲区指针需提前分配内存当 afAF_INET 时dst 需指向 struct in_addr 结构体当 afAF_INET6 时dst 需指向 struct in6_addr 结构体返回值1转换成功0src不是有效的 IP 地址格式-1出错如af不是支持的地址族同时设置errno#include arpa/inet.h const char *inet_ntop(int af, const void *restrict src, char *restrict dst, socklen_t size);功能二进制网络字节序 → 点分十进制字符串af地址族同 inet_pton()AF_INET/AF_INET6src指向二进制网络字节序地址的指针当 afAF_INET 时src 指向 struct in_addr当 afAF_INET6 时src 指向 struct in6_addrdst指向存储转换后字符串的缓冲区指针需提前分配足够内存sizedst 缓冲区的大小避免溢出可使用系统宏INET_ADDRSTRLENIPv4 地址字符串最大长度16 字节含末尾\0INET6_ADDRSTRLENIPv6 地址字符串最大长度46 字节含末尾\0成功返回指向dst缓冲区的指针 失败 返回NULL同时设置errno具体的使用方法如下#include stdio.h #include arpa/inet.h int main() { // 点分十进制IP转网络字节序通用版 const char* ip_str 192.168.1.1; struct in_addr ip_addr; inet_pton(AF_INET, ip_str, ip_addr); // AF_INET表示IPv4 printf(网络字节序IP整数%u\n, ip_addr.s_addr); // 网络字节序转点分十进制IP char ip_buf[INET_ADDRSTRLEN]; inet_ntop(AF_INET, ip_addr, ip_buf, INET_ADDRSTRLEN); printf(转回点分十进制IP%s\n, ip_buf); return 0; }扩展知识点分十进制IP与网络字节序转化的另一个接口inet_ntoa与inet_atoninet_ntoa与inet_aton是较早期的 IPv4 专用 IP 地址转换函数仅支持 IPv4不支持 IPv6下面是它们的定义#include arpa/inet.h //将点分十进制字符串转化为二进制网络字节序 int inet_aton(const char *cp, struct in_addr *inp); //将二进制网络字节序转化为点分十进制字符串 char *inet_ntoa(struct in_addr in);我们不推荐使用这两个接口的理由是两个接口只支持IPV4地址不支持IPV6inet_ntoa函数不是线程安全的inet_ntoa函数在转化成功后会返回一个静态缓冲区的指针这个缓冲区不是线程安全的后续可能会存在被覆盖导致数据丢失的风险我们可以通过下面的一个例子来理解一下int main() { // 点分十进制IP转网络字节序通用版 const char* ip_str1 192.168.1.1; const char* ip_str2 192.168.145.11; struct in_addr ip_addr1; struct in_addr ip_addr2; inet_aton(ip_str1,ip_addr1); inet_aton(ip_str2,ip_addr2); printf(第一个网络字节序IP整数%u\n, ip_addr1.s_addr); printf(第二个网络字节序IP整数%u\n, ip_addr2.s_addr); // 网络字节序转点分十进制IP char* ret1inet_ntoa(ip_addr1); char* ret2inet_ntoa(ip_addr2); printf(第一个点分十进制IP%s\n, ret1); printf(第二个点分十进制IP%s\n, ret2); return 0; }我们的例子定义两个了两个点分十进制字符串ip_str1与ip_str2分别将其转化为网络节序后再转化为点分十进制字符串并打印。我们可以发现第一个点分十进制ip_str1似乎并没有成功转化为本地序列。这是因为两次从网络字节序转化为本地序列共用的是一个静态缓冲区第一次将ip_str1成功转化成本地序列192.168.1.1后被第二次转化ip_str2覆盖了。导致该缓冲区中只有92.168.145.11此时打印只会打印两次92.168.145.11。我们可以将代码调整一下第一次转化后就打印一下结果第二次转化后在打印一下第二个结果就可以避免数据覆盖结果正常了inet_addr#include arpa/inet.h in_addr_t inet_addr(const char *cp);in_addr_t本质是uint32_t专门用来存放 IPv4网络字节序的 32 位 IP。inet_addr将点分十进制 IPv4 字符串例如192.168.1.1、127.0.0.1转换成网络字节序大端 的 32 位无符号整数。它的功能与上述inet_pton相似返回值规则解析成功返回网络字节序的 32 位 IP 整数。解析失败返回INADDR_NONE值为0xFFFFFFFF对应 IP255.255.255.255。该接口的返回值直接就是网络序可以直接赋值给sockaddr_in.sin_addr.s_addr⚠️ 致命缺陷重点当输入 IP 恰好是广播地址255.255.255.255inet_addr会把这个合法 IP 当成解析失败因为返回值和错误码完全一样无法区分。 这是它最大的设计缺陷POSIX 标准标记该函数为废弃deprecated新项目不推荐使用优先用inet_pton。当我们给创建的套接字绑定ip与端口号时两种接口都可以使用但是更推荐使用inet_pton。struct sockaddr_in addr; addr.sin_familyAF_INET; addr.sin_porthtons(port); //使用inet_pton将点分十进制ip字符串转化为32位的网络字节序整数更推荐 //inet_pton(AF_INET,ip.c_str(),addr.sin_addr.s_addr); //使用inet_addr将点分十进制ip字符串转化为32位的网络字节序整数 addr.sin_addr.s_addrinet_addr(ip.c_str());接1.1掌握了网络序列与本地序列的相关转化接口后初始化struct sockaddr_in的各个字段我们就可以使用以下的代码来实现初始化struct sockaddr_in mess; mess.sin_familyAF_INET; mess.sin_porthtons(_port); mess.sin_addr.s_addrINADDR_ANY; socklen_t lensizeof(mess);这里初始化IP字段时我们使用INADDR_ANY一个特殊的IPV4地址宏来确定表示“绑定到当前主机的所有可用网络接口”意味着该bind的套接字会监听当前主机所有网卡的对应端口相当于绑定了本台机器的所有IP。此时我们就完成了struct sockaddr_in结构的各个字段的声明但是需要注意的是此时套接字还未完成绑定我们只是将将要绑定的端口、IP和协议家族声明在了一个struct sockaddr_in结构中。接着我们需要将该struct sockaddr_in结构与套接字进行绑定我们使用bind接口#include sys/socket.h int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数含义sockfd已创建的套接字文件描述符由socket()函数返回addr指向struct sockaddr结构体的指针实际常用struct sockaddr_in强转存储要绑定的 IP 和端口addrlenaddr结构体的大小通常用sizeof(struct sockaddr_in)返回值成功返回0失败返回-1并设置errno这里需要注意的是bind的第二个参数是const struct sockaddr *类型我们需要将事先声明好的struct sockaddr_in指针强制类型转化为const struct sockaddr *。代码如下struct sockaddr_in mess; mess.sin_familyAF_INET; mess.sin_porthtons(_port); mess.sin_addr.s_addrINADDR_ANY; socklen_t lensizeof(mess); int retbind(_sockfd,(const struct sockaddr*)mess,len); if(ret0) { LOG(LogLevel::FATAL)套接字绑定失败; return; }1.1.3 sendto/recvfrom实现收发功能在我们上面创建的server类中初始化Init()接口为我们创建了套接字并完成了IP与端口还有协议家族的绑定下面我们来完成数据的收发功能在服务端我们想要的效果是服务器启动后每当客户端发送消息服务端收到后需要将该消息直接原样返回给客户端这就涉及到了套接字编程中的核心数据收发接口sendto套接字发送接口recvfrom套接字接受接口。下面我们从函数定义、参数、用法和完整示例理解一下这两个接口sendto和recvfrom最大的特点是自带地址参数无需提前建立连接如 UDP每次收发都要指定 / 获取对方的 IP 和端口#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套接字文件描述符UDP 需创建SOCK_DGRAM类型的套接字buf要发送的数据缓冲区指针len要发送的数据长度字节flags发送标志通常设为0常用可选值MSG_DONTWAIT非阻塞发送dest_addr指向目标地址结构体struct sockaddr_in的指针指定接收方的 IP 端口addrlen目标地址结构体的大小sizeof(struct sockaddr_in)返回值成功返回实际发送的字节数失败返回-1并设置errno如EINVAL参数错误、ENOMEM内存不足。#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常用可选值MSG_PEEK只查看数据不读取src_addr输出参数指向地址结构体的指针用于存储发送方的 IP 端口addrlen输入输出参数传入结构体大小返回实际使用的大小需传指针返回值成功返回实际接收的字节数失败返回-1并设置errno0仅 TCP 有效连接关闭UDP 不会返回 0无连接。在我们的server类中创建一个Start方法利用recvfrom与sendto来完成消息的收发功能void Start() { _isrunningtrue; while(_isrunning) { //peer变量用来存储客户端的相关IP端口信息 sockaddr_in peer; socklen_t lensizeof(peer); char buffer[1024]; int retrecvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)peer,len); if(ret0) { LOG(LogLevel::ERROR)server recvfrom fail!; return; } buffer[ret]0; std::coutclient say :bufferstd::endl; const std::string messbuffer; std::string sendmessserver say:mess; if(!sendto(_sockfd,sendmess.c_str(),sendmess.size(),0,(const struct sockaddr*)peer,len)) { LOG(LogLevel::ERROR)server sendto fail!; return; } } _isrunningfalse; }服务端通过循环持续监听客户端请求先以recvfrom()从 UDP 套接字接收客户端发送的消息为接收到的字符串补充结束符后转换为字符串类型再拼接 “server say:” 前缀生成新消息最后通过sendto()将拼接后的消息回传给对应的客户端若接收或发送数据失败会打印错误日志并退出当前函数1.1.4 服务端完整代码#pragma once #includelog.hpp #includesys/socket.h #includenetinet/in.h #includearpa/inet.h //server.hpp class server { public: server(uint16_t port) :_port(port) ,_sockfd(-1) {} void Init() { //此时创建了一个基于udp的网络套接字 _sockfdsocket(AF_INET,SOCK_DGRAM,0); if(_sockfd0) { LOG(LogLevel::FATAL)套接字创建失败; return; } //给套接字绑定ip地址与端口号 struct sockaddr_in mess; mess.sin_familyAF_INET; mess.sin_porthtons(_port); mess.sin_addr.s_addrINADDR_ANY; socklen_t lensizeof(mess); int retbind(_sockfd,(const struct sockaddr*)mess,len); if(ret0) { LOG(LogLevel::FATAL)套接字绑定失败; return; } } void Start() { _isrunningtrue; while(_isrunning) { //peer变量用来存储客户端的相关IP端口信息 sockaddr_in peer; socklen_t lensizeof(peer); char buffer[1024]; int retrecvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)peer,len); if(ret0) { LOG(LogLevel::ERROR)server recvfrom fail!; return; } buffer[ret]0; std::coutclient say :bufferstd::endl; const std::string messbuffer; std::string sendmessserver say:mess; if(!sendto(_sockfd,sendmess.c_str(),sendmess.size(),0,(const struct sockaddr*)peer,len)) { LOG(LogLevel::ERROR)server sendto fail!; return; } } _isrunningfalse; } private: int _sockfd; uint16_t _port; bool _isrunningfalse; }; //server.cc #includeserver.hpp #includeiostream #includestring #includememory int main(int argc,char* argv[]) { if(argc!2) { std::coutUsage:./server portstd::endl; return 0; } uint16_t portstd::stoi(argv[1]); std::unique_ptrserver Serverstd::make_uniqueserver(port); Server-Init(); Server-Start(); }1.2、客户端#includelog.hpp #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includecstring //./Client server_ip server_port int main(int argc,char* argv[]) { if(argc3) { std::cerrUsage:argv[0]server_ip server_portstd::endl; return 1; } std::string server_ipargv[1]; uint16_t server_portstd::stoi(argv[2]); //创建套接字 int sockfdsocket(AF_INET,SOCK_DGRAM,0); if(sockfd0) { LOG(LogLevel::FATAL)套接字创建失败; return 2; } //由服务器给客户端随机分配端口 //首次发送消息的时候OS会自动给Clientbind //客户端的端口是几不重要唯一确定的就可以 struct sockaddr_in server; server.sin_familyAF_INET; server.sin_porthtons(server_port); server.sin_addr.s_addrinet_addr(server_ip.c_str()); while(1) { std::string input; std::coutPlease Enter:; std::getline(std::cin,input); //向服务器发送数据 ssize_t retsendto(sockfd,input.c_str(),sizeof(input),0,(struct sockaddr*)server,sizeof(server)); (void)ret; //接受服务器发来的数据 char buffer[1024]; struct sockaddr_in peer; socklen_t lensizeof(peer); memset(buffer,0,sizeof(buffer)); ssize_t nrecvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)peer,len); if(n0) { buffer[n]0; std::coutbufferstd::endl; } } return 0; }客户端我们用采用过多的封装而是直接在main函数中编写。在整段代码中我们主要完成了以下内容从命令行参数中获取要连接的服务端的IP与端口本地序列创建UDP套接字实现数据的收发编写客户端时我们需要注意客户端不需要显式地绑定IP与端口而是由操作系统自动分配。这是因为客户端通常不需要固定端口只要能够连接到服务器即可。而服务器需要绑定固定端口以便客户端能够知道连接到哪里。1.3 运行示例二、添加翻译功能上述的代码中当服务器收到一条来自客户端的消息的时候只是进行了简单的字符串拼接后就返回给了客户端这里收到消息并进行字符串拼接可以理解为一种简单的业务。在网络通信中服务器可以对接上层的业务从而对客户端的消息进行业务处理从而完成某种业务。在这部分我们要改进上面写的echoserver的代码并引入业务层。当服务器收到客户端的一条信息的时候我们将该条信息理解为一个中文的字符串或者英文字符串并相互进行简单的翻译功能。代码设计流程如下2.1 添加回调函数首先我们需要在服务器内添加一个回调函数成员这个回调函数会将我们传入的中/英文字符串翻译并返回。当服务器收到客户端的信息的时候我们将该消息理解为一个需要翻译的中文或者英文字符串调用回调函数并将返回的翻译结果发送给客户端。2.2 编写业务层字典类在业务层我们可以设计一个字典类它的作用是加载一个文本文件dictionary.txt字典建立中英文的映射关系并提供翻译接口。2.2.1 字典的加载#pragma once #includeiostream #includeunordered_map #includefstream #includestring #includelog.hpp std::string default_dict_path./dictionary.txt; std::string sep:; class dict { public: dict(const std::string dictdefault_dict_path) :_dict_path(dict) {} //字典的加载 void Loaddict() { //这里需要的是输入流 std::ifstream in(_dict_path); std::string line; while(std::getline(in,line)) { //处理字符串 auto posline.find(sep); if(posstd::string::npos) { LOG(LogLevel::ERROR)字典格式错误; //继续读 continue; } std::string englishline.substr(0,pos); std::string chineseline.substr(possep.size()); if(english.empty()||chinese.empty()) { LOG(LogLevel::ERROR)字典格式错误; continue; } _dict.insert(std::make_pair(english,chinese)); std::cout加载 line 成功std::endl; } in.close(); } ~dict() {} private: std::string _dict_path; std::unordered_mapstd::string,std::string _dict; };Loaddict()是字典翻译业务层的核心加载接口核心目标是从指定的词典文本文件中读取英文 - 中文对照数据解析成程序可快速查询的键值对结构哈希表同时处理文件读取、格式解析中的各种异常情况保证词典加载的健壮性。它的执行步骤如下步骤 1打开词典文件接口首先尝试以只读模式打开构造函数中指定的词典文件默认是./dictionary.txt如果文件不存在、权限不足或无法打开会记录错误日志。步骤 2逐行读取文件内容打开文件后接口会循环读取文件的每一行内容直到文件末尾EOF。步骤 3解析单行文本核心对每一行有效文本执行以下解析逻辑以:为界将一行内容拆分为英文单词键和中文释义值两部分。如果行中没有找到:分隔符记录错误日志并跳过该行如果拆分后英文部分为空或中文部分为空同样记录警告日志跳过该行。如果文本格式合法则将数据存入哈希表若拆分后的键值对都有效将英文单词作为unordered_map的key中文释义 作为value插入到业务层的成员变量中。步骤 4关闭文件并返回结果所有行解析完成后关闭文件句柄释放文件资源。2.2.1 提供翻译接口//提供翻译功能 std::string Translate(std::string english) { auto iter_dict.find(english); if(iter_dict.end()) { LOG(LogLevel::INFO)没有找到; return None; } return iter-second; }2.2.3 给服务器绑定回调函数当我们设计好了业务层代码后我们需要服务器接收到用户发来的信息时调用回调函数并跳转到业务层的Translate接口Translate接口会拿着用户消息我们视为一个英文字符串在哈希表中找到中文释义并返回此时服务器接收到返回的中文释义后会发送给客户端。关键在于我们如何将业务层的接口绑定给服务器的回调函数实现模块间的跳转。C11引入Lambda 表达式可以在需要函数对象的场景中直接定义无需额外声明独立函数或仿函数让代码更简洁、内聚。Lambda 的完整语法结构如下[捕获列表](参数列表) mutable(可选) noexcept(可选) - 返回值类型(可选) { // 函数体 }捕获列表[]最核心的部分定义 Lambda 可以访问的外部变量值捕获 / 引用捕获。参数列表()和普通函数的参数列表一致无参数时可省略。mutable可选允许修改值捕获的变量默认值捕获的变量是const拷贝。noexcept可选声明函数不会抛出异常。返回值类型- type可选编译器通常会自动推导返回值复杂场景需显式指定。在创建服务器的时候我们可以使用Lambda表达式给服务器绑定回调函数代码如下#includeserver.hpp #includedict.hpp #includeiostream #includestring #includememory int main(int argc,char* argv[]) { if(argc!2) { std::coutUsage:./server portstd::endl; return 0; } uint16_t portstd::stoi(argv[1]); //创建业务层 std::unique_ptrdict mydictstd::make_uniquedict(); //加载字典 mydict-Loaddict(); //创建服务器并绑定回调业务 std::unique_ptrserver Serverstd::make_uniqueserver(port, [mydict](std::string english)-std::string{ return mydict-Translate(english); }); Server-Init(); Server-Start(); }需要注意的是业务层被调用的业务接口服务器事先声明的回调函数还有Lambda中被捕获对象的参数返回值必须严格一一对应负责就会导致服务器无法找到对应回调函数出现报错。2.2.4 业务层完整代码dict.hpp#pragma once #includeiostream #includeunordered_map #includefstream #includestring #includelog.hpp std::string default_dict_path./dictionary.txt; std::string sep:; class dict { public: dict(const std::string dictdefault_dict_path) :_dict_path(dict) {} //字典的加载 void Loaddict() { //这里需要的是输入流 std::ifstream in(_dict_path); std::string line; while(std::getline(in,line)) { //处理字符串 auto posline.find(sep); if(posstd::string::npos) { LOG(LogLevel::ERROR)字典格式错误; //继续读 continue; } std::string englishline.substr(0,pos); std::string chineseline.substr(possep.size()); if(english.empty()||chinese.empty()) { LOG(LogLevel::ERROR)字典格式错误; continue; } _dict.insert(std::make_pair(english,chinese)); std::cout加载 line 成功std::endl; } in.close(); } //提供翻译功能 std::string Translate(std::string english) { auto iter_dict.find(english); if(iter_dict.end()) { LOG(LogLevel::INFO)没有找到; return None; } return iter-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.2.5 运行示例

相关新闻

LIMIT

LIMIT

LIMIT 是 SQL 中用于限制查询结果集行数的核心子句,几乎所有数据库(MySQL、PostgreSQL 等)都支持(Oracle 用 ROWNUM/FETCH 替代),常用来实现分页、取 TopN 数据,以下从基础用法、分页场景、性能…

2026/9/24 15:26:41 阅读更多 →
企业对于数据资产场景

企业对于数据资产场景

面对2026年数据要素市场全面加速落地的产业背景,企业对于数据资产入表、数据交易合规等高阶数据人才的需求已从“可选项”转变为“必选项”。然而,市面上数据人才培训产品良莠不齐,缺乏统一的能力评估标准,导致企业在选机构时极易…

2026/9/24 15:26:40 阅读更多 →
pH 响应水相自组装材料|PS-b-PAA 聚苯乙烯‑b‑聚丙烯酸 两亲性 pH 响应 AB 型嵌段高分子

pH 响应水相自组装材料|PS-b-PAA 聚苯乙烯‑b‑聚丙烯酸 两亲性 pH 响应 AB 型嵌段高分子

PS-b-PAA(聚苯乙烯-b-聚丙烯酸)与之前讨论的 PS-b-PMMA 属于同一类“结构材料”,但功能逻辑完全不同:PS-b-PMMA 靠 PS/PMMA 的“不相容”微相分离造纳米结构,而 PS-b-PAA 的核心是 PAA 链段的“pH 响应性”——羧基的质…

2026/9/24 15:26:40 阅读更多 →

最新新闻

Python基础零基础入门:从环境搭建到实战的完整学习路线

Python基础零基础入门:从环境搭建到实战的完整学习路线

如果你现在拿着“Python基础”这四个字在搜索引擎里翻来翻去,大概率已经被“七天速成”“零基础逆袭”这类标题搞得越来越焦虑了。作为一个用Python写了好几年代码、也带过不少新人入门的从业者,我先给你一颗定心丸:Python基础真的不难&#…

2026/9/24 20:27:45 阅读更多 →
Python类机制进阶:属性访问、描述符与元类深入解析

Python类机制进阶:属性访问、描述符与元类深入解析

看到这个标题可能有人会问:面向对象编程写到第四篇,还能讲什么?基础语法、类定义、继承、多态前面都过了一遍,再往下挖,就要碰到 Python 类机制的内裤了。这一篇我打算聊的东西,既基础又经常被忽略——属性…

2026/9/24 20:27:45 阅读更多 →
深入理解Python面向对象编程:从类到魔术方法的实践指南

深入理解Python面向对象编程:从类到魔术方法的实践指南

先说个真实感受:Python我用了好几年,写业务代码、写脚本、做数据清洗都没问题,但真正对面向对象编程产生“原来如此”的顿悟,还是在系统翻完《Python3 面向对象编程(第三版)》之后。网上聊Python OOP的文章…

2026/9/24 20:27:45 阅读更多 →
Vue+Node.js+Element UI实战:水厂多渠道抄表管理系统开发全记录

Vue+Node.js+Element UI实战:水厂多渠道抄表管理系统开发全记录

前阵子帮一家自来水厂做了一套抄表管理系统,技术栈就是标题里写的 Vue Node.js Element UI,开发加调试前后忙了大半年。这套系统的名字听起来像是一个练手项目,但真正把“多渠道抄表”这几个字吃透并落地,过程比预想中复杂不少。…

2026/9/24 20:27:45 阅读更多 →
B站直播开放平台API接入全攻略:HTTP、WebSocket与Webhook链路详解

B站直播开放平台API接入全攻略:HTTP、WebSocket与Webhook链路详解

B站直播开放平台现在能做的远不止“挂个弹幕机器人”。我在做直播间数据中台的时候,把能用到的官方API和接入方式几乎过了一遍,整理出一套从申请权限到跑通功能的最小路径。这篇不是贴文档,是把20多个常用直播功能背后的技术路线拆开讲明白&a…

2026/9/24 20:27:45 阅读更多 →
全自动点焊机如何实现移动电源电芯焊接的高效精准?

全自动点焊机如何实现移动电源电芯焊接的高效精准?

做移动电源的朋友都知道,电芯焊接这道工序是绕不过去的坎。电池 Pack 内部,电芯正负极和保护板之间必须通过镍片连接,而这个连接质量直接决定了整组电池的寿命、内阻和安全性能。早年大多数小作坊都是人工拿手持式点焊机一个一个戳&#xff0…

2026/9/24 20:26:44 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →