网络通信:udp套接字实现echoserver和翻译功能

目录

一、echoserver功能

1.1、服务端

[1.1.1 创建套接字](#1.1.1 创建套接字)

1.1.2网络与主机序列转化函数

[1.1.3 sendto/recvfrom实现收发功能](#1.1.3 sendto/recvfrom实现收发功能)

[1.1.4 服务端完整代码](#1.1.4 服务端完整代码)

1.2、客户端

[1.3 运行示例](#1.3 运行示例)

二、添加翻译功能

[2.1 添加回调函数](#2.1 添加回调函数)

[2.2 编写业务层(字典类)](#2.2 编写业务层(字典类))

[2.2.1 字典的加载](#2.2.1 字典的加载)

[2.2.1 提供翻译接口](#2.2.1 提供翻译接口)

[2.2.3 给服务器绑定回调函数](#2.2.3 给服务器绑定回调函数)

[2.2.4 业务层完整代码](#2.2.4 业务层完整代码)

[2.2.5 运行示例](#2.2.5 运行示例)

一、echoserver功能

echoserver(回显服务器)是网络编程中最基础、最经典的服务端示例程序,核心功能是接收客户端发送的任意数据,不做任何修改,直接原样返回给客户端,就像对着山谷喊一声,听到的 "回声" 一样。它是学习套接字编程的入门案例,主要用来:

  1. 理解TCP/UDP 通信的完整流程(服务端绑定、监听、接收连接;客户端连接、发送 / 接收数据);
  2. 验证网络通信的连通性(确认客户端和服务端能正常收发数据);
  3. 掌握基础的网络 I/O 操作

1.1、服务端

在服务端主要完成的任务是:

  • 创建udp套接字消息
  • 给udp套接字bind绑定ip与端口
  • 完成消息的收发

1.1.1 创建套接字

我们首先创建一个server.hpp文件并使用一个类server来整体封装服务器消息,因为服务器是向客户端公开的所以必须显式的绑定ip与端口,这样客户端才能访问到服务器。因此类server创建时必须显式指明将要绑定的ip与端口(注意这里构造服务器时传入的ip与端口分别是点分十进制与uint16_t都是主机序列)。

在初始化服务器时就需要完成udp套接字的创建与接口的bind,具体代码如下:

cpp 复制代码
//网络通信的三个必要头文件
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#include"log.hpp"//自定义日志模块
class server
{
    public:
    server(uint16_t port)
    :_port(port)
    {}
    void Init()
    {
        //此时创建了一个基于udp的网络套接字
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"套接字创建失败!";
            return;
        }
        //给套接字绑定ip地址与端口号
        struct sockaddr_in mess;
        mess.sin_family=AF_INET;
        mess.sin_port=htons(_port);
        mess.sin_addr.s_addr=INADDR_ANY;
        socklen_t len=sizeof(mess);
        int ret=bind(_sockfd,(const struct sockaddr*)&mess,len);
        if(ret<0)
        {
            LOG(LogLevel::FATAL)<<"套接字绑定失败!";
            return;
        }
    }
    private:
    bool _isrunning=false
    int _sockfd;
    uint16_t _port;

};

在整段代码中

cpp 复制代码
_sockfd=socket(AF_INET,SOCK_DGRAM,0);

则为我们创建并返回了一个套接字消息,AF_INET表示ipv4的协议族如果是ipv6则需要使用AF_INET6,SOCK_DGRAM则表示我们的套接字类型是UDP类型,我们后续需要以此完成UDP网络通信,如果后续使用TCP则需要设置为SOCK_STREAM。第三个参数我们默认设置为0,此时接口会根据套接字类型自动选择对应协议,如 TCP 对应IPPROTO_TCP,UDP 对应IPPROTO_UDP。

socket接口的返回值成功返回套接字文件描述符(非负整数),失败返回-1并设置errno。

给套接字绑定对应的IP与端口我们使用bind接口,它的具体定义如下:

cpp 复制代码
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在这里第一个整形参数就是创建套接字时返回的套接字消息(本质是文件描述符),第二个参数是一个const struct sockaddr *类型,因为我们要使用套接字完成网络通信所以必须使用struct sockaddr_in(IPv4)/struct sockaddr_in6(IPv6)来提前确定绑定的ip与端口信息。这里我们来着重理解一下struct sockaddr_in的成员类型(struct sockaddr_in6与其相似):

cpp 复制代码
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网络与主机序列转化函数

arpa/inet.h中提供了 4 个最常用的转换函数,按处理的数据长度分为 16 位(短整型)和 32 位(长整型)两类:

函数名 功能描述 适用场景
htons() 主机字节序 → 网络字节序(16 位) 端口号转换(如 80、8080)
ntohs() 网络字节序 → 主机字节序(16 位) 接收网络数据后的端口解析
htonl() 主机字节序 → 网络字节序(32 位) IP 地址(32 位)转换
ntohl() 网络字节序 → 主机字节序(32 位) 接收网络数据后的 IP 解析

为了方便记忆我们可以这样记:

  • h = host(主机)、n = network(网络)、s = short(16 位)、l = long(32 位)。
  • 例如 htons = host to network short(主机→网络,16 位)。

代码示例:

cpp 复制代码
#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位整数)
    const char* ip_str = "192.168.1.1";
    uint32_t host_ip = inet_addr(ip_str);  // 先转主机字节序的32位整数
    uint32_t net_ip = htonl(host_ip);      // 转网络字节序
    uint32_t back_ip = ntohl(net_ip);      // 转回主机字节序

    printf("32位IP转换:\n");
    printf("主机字节序IP(整数):%u\n", host_ip);
    printf("网络字节序IP(整数):%u\n", net_ip);
    printf("转回主机字节序IP:%u\n", back_ip);

    return 0;
}

运行结果:

  1. inet_addr() 是辅助函数,用于将点分十进制的 IP 字符串(如 "192.168.1.1")转换为 32 位整数形式的主机字节序数据。
  2. 函数参数 / 返回值均为无符号整数(uint16_t/uint32_t),避免符号位干扰。
  3. 运行代码后,你会看到转换前后的数值变化(小端主机上会明显不同),但转回后数值与原主机字节序一致。

<arpa/inet.h> 还提供了更通用的 inet_pton()inet_ntop() 函数,支持 IPv4 和 IPv6,是更推荐的现代用法,两个接口可以直接完成点分十进制的ip与其网络字节序之间的转化,无需调用其他辅助接口。

cpp 复制代码
#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::1")

dst:指向存储转换后二进制地址的缓冲区指针(需提前分配内存):

  • 当 af=AF_INET 时,dst 需指向 struct in_addr 结构体
  • 当 af=AF_INET6 时,dst 需指向 struct in6_addr 结构体

返回值:

  • 1:转换成功
  • 0src 不是有效的 IP 地址格式
  • -1:出错(如 af 不是支持的地址族),同时设置 errno
cpp 复制代码
#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_INET6)

src:指向二进制网络字节序地址的指针:

  • 当 af=AF_INET 时,src 指向 struct in_addr
  • 当 af=AF_INET6 时,src 指向 struct in6_addr

dst:指向存储转换后字符串的缓冲区指针(需提前分配足够内存)

size:dst 缓冲区的大小(避免溢出),可使用系统宏:

  • INET_ADDRSTRLEN:IPv4 地址字符串最大长度(16 字节,含末尾\0)
  • INET6_ADDRSTRLEN:IPv6 地址字符串最大长度(46 字节,含末尾\0)

成功:返回指向 dst 缓冲区的指针 失败: 返回 NULL,同时设置 errno

具体的使用方法如下:

cpp 复制代码
#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_aton

inet_ntoa与inet_aton是较早期的 IPv4 专用 IP 地址转换函数(仅支持 IPv4,不支持 IPv6),下面是它们的定义:

cpp 复制代码
#include <arpa/inet.h>
//将点分十进制字符串转化为二进制网络字节序
int inet_aton(const char *cp, struct in_addr *inp);
//将二进制网络字节序转化为点分十进制字符串
char *inet_ntoa(struct in_addr in);

我们不推荐使用这两个接口的理由是:

  1. 两个接口只支持IPV4地址,不支持IPV6
  2. inet_ntoa函数不是线程安全的

inet_ntoa函数在转化成功后会返回一个静态缓冲区的指针,这个缓冲区不是线程安全的后续可能会存在被覆盖导致数据丢失的风险,我们可以通过下面的一个例子来理解一下:

cpp 复制代码
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* ret1=inet_ntoa(ip_addr1);
    char* ret2=inet_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。

我们可以将代码调整一下,第一次转化后就打印一下结果,第二次转化后在打印一下第二个结果就可以避免数据覆盖,结果正常了:


接1.1

掌握了网络序列与本地序列的相关转化接口后初始化struct sockaddr_in的各个字段我们就可以使用以下的代码来实现初始化:

cpp 复制代码
struct sockaddr_in mess;
mess.sin_family=AF_INET;
mess.sin_port=htons(_port);
mess.sin_addr.s_addr=INADDR_ANY;
socklen_t len=sizeof(mess);

这里,初始化IP字段时我们使用INADDR_ANY(一个特殊的IPV4地址宏)来确定,表示 "绑定到当前主机的所有可用网络接口"意味着该bind的套接字会监听当前主机所有网卡的对应端口,相当于绑定了本台机器的所有IP。

此时我们就完成了struct sockaddr_in结构的各个字段的声明,但是需要注意的是,此时套接字还未完成绑定,我们只是将将要绑定的端口、IP和协议家族声明在了一个struct sockaddr_in结构中。接着我们需要将该struct sockaddr_in结构与套接字进行绑定,我们使用bind接口:

cpp 复制代码
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 含义
sockfd 已创建的套接字文件描述符(由 socket() 函数返回)
addr 指向 struct sockaddr 结构体的指针(实际常用 struct sockaddr_in 强转),存储要绑定的 IP 和端口
addrlen addr 结构体的大小(通常用 sizeof(struct sockaddr_in)

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

这里需要注意的是,bind的第二个参数是const struct sockaddr *类型我们需要将事先声明好的struct sockaddr_in指针强制类型转化为const struct sockaddr *。代码如下:

cpp 复制代码
struct sockaddr_in mess;
mess.sin_family=AF_INET;
mess.sin_port=htons(_port);
mess.sin_addr.s_addr=INADDR_ANY;
socklen_t len=sizeof(mess);
int ret=bind(_sockfd,(const struct sockaddr*)&mess,len);
if(ret<0)
{
     LOG(LogLevel::FATAL)<<"套接字绑定失败!";
     return;
}

1.1.3 sendto/recvfrom实现收发功能

在我们上面创建的server类中初始化Init()接口为我们创建了套接字并完成了IP与端口还有协议家族的绑定,下面我们来完成数据的收发功能:

在服务端我们想要的效果是,服务器启动后每当客户端发送消息服务端收到后需要将该消息直接原样返回给客户端,这就涉及到了套接字编程中的核心数据收发接口sendto(套接字发送接口)recvfrom(套接字接受接口)。下面我们从函数定义、参数、用法和完整示例理解一下这两个接口:

sendto和recvfrom最大的特点是自带地址参数,无需提前建立连接(如 UDP),每次收发都要指定 / 获取对方的 IP 和端口;

cpp 复制代码
#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 内存不足)。
cpp 复制代码
#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,并设置 errno
  • 0:仅 TCP 有效(连接关闭),UDP 不会返回 0(无连接)。

在我们的server类中创建一个Start方法,利用recvfrom与sendto来完成消息的收发功能:

cpp 复制代码
void Start()
{
   _isrunning=true;
   while(_isrunning)
   {
        //peer变量用来存储客户端的相关IP端口信息
        sockaddr_in peer;
        socklen_t len=sizeof(peer);
        char buffer[1024];
        int ret=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(ret<0)
        {
            LOG(LogLevel::ERROR)<<"server recvfrom fail!";
            return;
        }
        buffer[ret]=0;
        std::cout<<"client say :"<<buffer<<std::endl;
        const std::string mess=buffer;
        std::string sendmess="server say:"+mess;
        if(!sendto(_sockfd,sendmess.c_str(),sendmess.size(),0,(const struct sockaddr*)&peer,len))
        {
            LOG(LogLevel::ERROR)<<"server sendto fail!";
            return;
        }
   }
    _isrunning=false;
}

服务端通过循环持续监听客户端请求,先以recvfrom()从 UDP 套接字接收客户端发送的消息,为接收到的字符串补充结束符后转换为字符串类型,再拼接 "server say:" 前缀生成新消息,最后通过sendto()将拼接后的消息回传给对应的客户端;若接收或发送数据失败,会打印错误日志并退出当前函数

1.1.4 服务端完整代码

cpp 复制代码
#pragma once
#include"log.hpp"
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//server.hpp
class server
{
    public:
    server(uint16_t port)
    :_port(port)
    ,_sockfd(-1)
    {}
    void Init()
    {
        //此时创建了一个基于udp的网络套接字
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"套接字创建失败!";
            return;
        }
        //给套接字绑定ip地址与端口号
        struct sockaddr_in mess;
        mess.sin_family=AF_INET;
        mess.sin_port=htons(_port);
        mess.sin_addr.s_addr=INADDR_ANY;
        socklen_t len=sizeof(mess);
        int ret=bind(_sockfd,(const struct sockaddr*)&mess,len);
        if(ret<0)
        {
            LOG(LogLevel::FATAL)<<"套接字绑定失败!";
            return;
        }
    }
    void Start()
    {
       _isrunning=true;
       while(_isrunning)
       {
            //peer变量用来存储客户端的相关IP端口信息
            sockaddr_in peer;
            socklen_t len=sizeof(peer);
            char buffer[1024];
            int ret=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(ret<0)
            {
                LOG(LogLevel::ERROR)<<"server recvfrom fail!";
                return;
            }
            buffer[ret]=0;
            std::cout<<"client say :"<<buffer<<std::endl;
            const std::string mess=buffer;
            std::string sendmess="server say:"+mess;
            if(!sendto(_sockfd,sendmess.c_str(),sendmess.size(),0,(const struct sockaddr*)&peer,len))
            {
                LOG(LogLevel::ERROR)<<"server sendto fail!";
                return;
            }
       }
       _isrunning=false;
    }
    private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning=false;
};

//server.cc
#include"server.hpp"
#include<iostream>
#include<string>
#include<memory>
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cout<<"Usage:./server port"<<std::endl;
        return 0;
    }
    uint16_t port=std::stoi(argv[1]);
    std::unique_ptr<server> Server=std::make_unique<server>(port);
    Server->Init();
    Server->Start();
}

1.2、客户端

cpp 复制代码
#include"log.hpp"
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
//./Client server_ip server_port
int main(int argc,char* argv[])
{
    if(argc<3)
    {
        std::cerr<<"Usage:"<<argv[0]<<"server_ip server_port"<<std::endl;
        return 1;
    }
    std::string server_ip=argv[1];
    uint16_t server_port=std::stoi(argv[2]);

    //创建套接字:
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        LOG(LogLevel::FATAL)<<"套接字创建失败!";
        return 2;
    }
    //由服务器给客户端随机分配端口
    //首次发送消息的时候,OS会自动给Clientbind
    //客户端的端口是几不重要,唯一确定的就可以
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(server_port);
    server.sin_addr.s_addr=inet_addr(server_ip.c_str());
    while(1)
    {
        std::string input;
        std::cout<<"Please Enter:";
        std::getline(std::cin,input);
        //向服务器发送数据
        ssize_t ret=sendto(sockfd,input.c_str(),sizeof(input),0,(struct sockaddr*)&server,sizeof(server));
        (void)ret;

        //接受服务器发来的数据
       char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        memset(&buffer,0,sizeof(buffer));
        ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n>0)
        {
            buffer[n]=0;
            std::cout<<buffer<<std::endl;
        }
    }
    return 0;
}

客户端我们用采用过多的封装而是直接在main函数中编写。在整段代码中我们主要完成了以下内容:

  1. 从命令行参数中获取要连接的服务端的IP与端口(本地序列)
  2. 创建UDP套接字
  3. 实现数据的收发

编写客户端时我们需要注意,客户端不需要显式地绑定IP与端口而是由操作系统自动分配。这是因为客户端通常不需要固定端口,只要能够连接到服务器即可。而服务器需要绑定固定端口,以便客户端能够知道连接到哪里。

1.3 运行示例

二、添加翻译功能

上述的代码中,当服务器收到一条来自客户端的消息的时候只是进行了简单的字符串拼接后就返回给了客户端,这里收到消息并进行字符串拼接可以理解为一种简单的业务。

在网络通信中,服务器可以对接上层的业务从而对客户端的消息进行业务处理从而完成某种业务。

在这部分,我们要改进上面写的echoserver的代码并引入业务层。当服务器收到客户端的一条信息的时候,我们将该条信息理解为一个中文的字符串或者英文字符串并相互进行简单的翻译功能。代码设计流程如下:

2.1 添加回调函数

首先,我们需要在服务器内添加一个回调函数成员,这个回调函数会将我们传入的中/英文字符串翻译并返回。

当服务器收到客户端的信息的时候(我们将该消息理解为一个需要翻译的中文或者英文字符串)调用回调函数并将返回的翻译结果发送给客户端。

2.2 编写业务层(字典类)

在业务层我们可以设计一个字典类,它的作用是加载一个文本文件dictionary.txt(字典)建立中英文的映射关系并提供翻译接口。

2.2.1 字典的加载

cpp 复制代码
#pragma once
#include<iostream>
#include<unordered_map>
#include<fstream>
#include<string>
#include"log.hpp"


std::string default_dict_path="./dictionary.txt";
std::string sep=":";
class dict
{
    public:
    dict(const std::string dict=default_dict_path)
    :_dict_path(dict)
    {}
    //字典的加载
    void Loaddict()
    {
        //这里需要的是输入流
        std::ifstream in(_dict_path);
        std::string line;
        while(std::getline(in,line))
        {
            //处理字符串
            auto pos=line.find(sep);
            if(pos==std::string::npos)
            {
                LOG(LogLevel::ERROR)<<"字典格式错误!";
                //继续读
                continue;
            }
            std::string english=line.substr(0,pos);
            std::string chinese=line.substr(pos+sep.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_map<std::string,std::string> _dict;
};

Loaddict()是字典翻译业务层的核心加载接口,核心目标是从指定的词典文本文件中读取英文 - 中文对照数据,解析成程序可快速查询的键值对结构(哈希表),同时处理文件读取、格式解析中的各种异常情况,保证词典加载的健壮性。

它的执行步骤如下:

步骤 1:打开词典文件

接口首先尝试以只读模式打开构造函数中指定的词典文件(默认是 ./dictionary.txt)如果文件不存在、权限不足或无法打开,会记录错误日志。

步骤 2:逐行读取文件内容

打开文件后,接口会循环读取文件的每一行内容,直到文件末尾(EOF)。

步骤 3:解析单行文本(核心)

对每一行有效文本,执行以下解析逻辑:

: 为界,将一行内容拆分为英文单词(键)中文释义(值) 两部分。如果行中没有找到 : 分隔符,记录错误日志并跳过该行;如果拆分后英文部分为空或中文部分为空,同样记录警告日志,跳过该行。

如果文本格式合法则将数据存入哈希表:若拆分后的键值对都有效,将英文单词作为 unordered_mapkey,中文释义 作为 value,插入到业务层的成员变量中。

步骤 4:关闭文件并返回结果

所有行解析完成后,关闭文件句柄,释放文件资源。

2.2.1 提供翻译接口

cpp 复制代码
//提供翻译功能
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接口会拿着用户消息(我们视为一个英文字符串)在哈希表中找到中文释义并返回,此时服务器接收到返回的中文释义后会发送给客户端。关键在于,我们如何将业务层的接口绑定给服务器的回调函数,实现模块间的跳转。

C++11引入Lambda 表达式,可以在需要函数对象的场景中直接定义,无需额外声明独立函数或仿函数,让代码更简洁、内聚。

Lambda 的完整语法结构如下:

cpp 复制代码
[捕获列表](参数列表) mutable(可选) noexcept(可选) -> 返回值类型(可选) {
    // 函数体
}
  • 捕获列表 []:最核心的部分,定义 Lambda 可以访问的外部变量(值捕获 / 引用捕获)。
  • 参数列表 ():和普通函数的参数列表一致,无参数时可省略。
  • mutable:可选,允许修改值捕获的变量(默认值捕获的变量是 const 拷贝)。
  • noexcept:可选,声明函数不会抛出异常。
  • 返回值类型 -> type:可选,编译器通常会自动推导返回值,复杂场景需显式指定。

在创建服务器的时候我们可以使用Lambda表达式给服务器绑定回调函数,代码如下:

cpp 复制代码
#include"server.hpp"
#include"dict.hpp"
#include<iostream>
#include<string>
#include<memory>
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cout<<"Usage:./server port"<<std::endl;
        return 0;
    }
    uint16_t port=std::stoi(argv[1]);
    //创建业务层
    std::unique_ptr<dict> mydict=std::make_unique<dict>();
    //加载字典
    mydict->Loaddict();
    //创建服务器并绑定回调业务
    std::unique_ptr<server> Server=std::make_unique<server>(port,
        [&mydict](std::string& english)->std::string{
            return mydict->Translate(english);
        });
    Server->Init();
    Server->Start();
}

需要注意的是业务层被调用的业务接口,服务器事先声明的回调函数还有Lambda中被捕获对象的参数返回值必须严格一一对应,负责就会导致服务器无法找到对应回调函数出现报错。

2.2.4 业务层完整代码

dict.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<unordered_map>
#include<fstream>
#include<string>
#include"log.hpp"


std::string default_dict_path="./dictionary.txt";
std::string sep=":";
class dict
{
    public:
    dict(const std::string dict=default_dict_path)
    :_dict_path(dict)
    {}
    //字典的加载
    void Loaddict()
    {
        //这里需要的是输入流
        std::ifstream in(_dict_path);
        std::string line;
        while(std::getline(in,line))
        {
            //处理字符串
            auto pos=line.find(sep);
            if(pos==std::string::npos)
            {
                LOG(LogLevel::ERROR)<<"字典格式错误!";
                //继续读
                continue;
            }
            std::string english=line.substr(0,pos);
            std::string chinese=line.substr(pos+sep.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_map<std::string,std::string> _dict;
};

dictionary.txt

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

2.2.5 运行示例

相关推荐
努力的小帅2 小时前
Linux_网络基础(1)
linux·网络·网络协议
EverydayJoy^v^2 小时前
RH134学习进程——九.访问网络附加存储
linux·网络·学习
卓应米老师2 小时前
【eNSP实验配置】点到点链路上的OSPF
网络·智能路由器·华为认证·hcia认证实验指南
数通工程师2 小时前
华为ME60设备单用户带宽限速配置全流程
网络·网络协议·tcp/ip·华为
liulilittle2 小时前
ISIF-COP香港服务器,启用OPENPPP2 VMUX全双工
运维·服务器·网络·信息与通信·通信
啊豪的思想2 小时前
算力为擎,算法为枢,数据为薪:人工智能三大核心要素的协同演进逻辑
网络·人工智能
工程师华哥2 小时前
2026年新版华为HCIA Datacom题库
网络·华为·hcie·hcia·hcip·华为数通·华为题库工具
头发还没掉光光2 小时前
IPV4地址不足,私有IP无法访问,使用NAT技术、内网穿透与打洞逐个解决
linux·网络·网络协议·tcp/ip
FreeBuf_3 小时前
朝鲜黑客武器化VS Code,借微软合法设施渗透韩国政企网络
网络·microsoft