当前位置: 首页 > news >正文

Linux实现翻译以及群通信功能

1.翻译功能实现

UdpServer.hpp文件

构造函数

接收一个端口号和一个回调函数,回调函数是传入一个执行方法,比如翻译方法。

 UdpServer(uint16_t port,func_t func)
        :_sockfd(defaultfd),
        _port(port),
        _isrunning(false),
        _func(func)
        {}

Init函数

首先创建了一个端口号,然后使用setsockopt函数对端口进行控制,在定义了一个sockaddr_in结构体来作为参数,为发送做准备,需要把sockaddr_in进行初始化变成有效的参数,sin_family是协议,sin_port是端口,sin_addr.s_addr是地址,INADDR_ANY为地址来接收所有发送的信息,bind函数是这个在栈区上的端口变得有效,需要到内核态处理。

 void Init()
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket error";
            exit(1);
        }
        int opt = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        LOG(LogLevel::INFO)<<"socket success, sockfd:"<<_sockfd;

        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;

        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"bind error";
            exit(2);
        }
        LOG(LogLevel::INFO)<<"bind success,sockfd:"<<_sockfd;
    }

 start函数

设置running状态为运行状态,创建一个buffer来接收消息,设置一个sockaddr_in结构体作为输出型参数,因为发送消息,是需要包含发送方的属性信息,所以要把结构体进行初始化为0,还需要传入这个结构体长度,就可以发送消息出去了,s是recvfrom函数的返回值,表示实际发送的字节个数,接收成功s一定会大于0,进入if里面执行,里面定义了一个client对象,类型是InetAddr类型的,用来处理端口号以及地址的序列形式,然后调用回调函数,传入接收消息的buffer和client对象,回调函数是在UdpServer.cc文件里传入的,回调函数是一个lambda形式,会调用Translate这个函数进行翻译工作。

 void Start()
    {
        _isrunning=true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            memset(&peer,0,len);
               std::cout<<"recvfrom"<<std::endl; 
            
            ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
               std::cout<<"recvfrom     ok:" << s <<std::endl; 
                std::cout<<s<<std::endl;
            if(s>0)
            {
               InetAddr client(peer);
               buffer[s]=0;
               std::string result=_func(buffer,client);
               std::cout<<"send success"<<std::endl; 
               std::cout<<"结果:"<<result<<std::endl;
               sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);

            }

        }

    }

 std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port, [&dict](const std::string& word,InetAddr& cli)->std::string {
        return dict.Translate(word,cli);

UdpServer.cc文件

通过命令行参数来获取端口号,设置日志模式输出到控制台,创建Dict类型对象,并执行这个对象的方法,然后是智能指针管理一个UdpServer类型的对象,传入参数端口号和回调函数的实现方法,启动Init函数和Start函数。

#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include <functional> 
#include "Dict.hpp"
std::string defaulthandler(const std::string& message)
{
    std::cout<<"打印"<<std::endl;
    std::string hello="hello, ";
    hello+=message;
    return hello;
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cerr<<"Usage:"<<argv[0]<<" port"<<std::endl;
        return 1;
    }

    uint16_t port=std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    Dict dict;
    dict.LoadDict();

    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port, [&dict](const std::string& word,InetAddr& cli)->std::string {
        return dict.Translate(word,cli);

    });
    usvr->Init();
    usvr->Start();
    return 0;
}

Dict.hpp文件

构造函数

把给定的文件路径赋值给成员变量,path就是一个完整的路径和文件名字,sep是分隔符,分开中英文。

#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict="./dictionary.txt";
const std::string sep=": ";



  Dict(const std::string& path=defaultdict):_dict_path(path)
    {

    }

private:
    std::string _dict_path;//路径+文件名
    std::unordered_map<std::string,std::string> _dict;

LoadDict函数

创建ifstream类型的对象in就会自动尝试打开所提供路径的文件,这个是通过ifstream的构造函数实现的,接着创建一个string类型的line,调用getline函数,参数是in和line,就会把输入流in中读取内容到string类型的line中存储,然后遇到换行符(\n)就会停下。读取成功进入循环体函数,pos迭代器指向sep符号,如果没有找到这个符号就打印解析失败,创建两个string类型的对象,调用string的substr函数进行部分拷贝,0到pos就是英文单词部分,二pos+sep的大小就是中文位置,其中之一读取为空就说明缺失内容,然后把读取的英文和中文放到哈希表里,就可以通过英文找到中文意思,最后关闭打开的输入流。

 }
const std::string defaultdict="./dictionary.txt";
const std::string sep=": ";


    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if(!in.is_open())
        {
            LOG(LogLevel::DEBUG)<<"打开字典"<<_dict_path<<"错误";
            return false;
        }

        std::string line;
        while(std::getline(in,line))
        {
            auto pos=line.find(sep);
            if(pos==std::string::npos)
            {
                LOG(LogLevel::WARNING)<<"解析"<<line<<"失败";
                continue;
            }
            std::string english=line.substr(0,pos);
            std::string chinese=line.substr(pos+sep.size());
            if(english.empty()||chinese.empty())
            {
                LOG(LogLevel::WARNING)<<"没有有效内容:"<<line;
                continue;
            }

            _dict.insert(std::make_pair(english,chinese));
            LOG(LogLevel::DEBUG)<<"加载"<<line;
        }

        in.close();
        return true;
    }

Translate函数

参数给了一个英文单词,调用find函数就找到这个单词的位置,不等于end的位置就说明找到了,用传入的client对象可以获取端口号和地址进行日志输出,返回iter的second,就会pair的第二个对象的值,也就是中文。

std::string Translate(const std::string& word,InetAddr& client)
    {
        auto iter=_dict.find(word);
        if(iter==_dict.end())
        {
            LOG(LogLevel::DEBUG)<<"进入到翻译模块,["<<client.Ip()<<" : "<<client.Port()<<"]#"<<word<<"->Mone";
            return "None";
        }

        LOG(LogLevel::DEBUG)<<"进入到翻译模块,["<<client.Ip()<<" : "<<client.Port()<<"]#"<<word<<"->"<<iter->second;
        return iter->second;

    }

UdpClient.cc文件

命令行参数读取端口号和地址,创建端口并设置为udp模式,创建sockaddr_in结构体,并初始化创建的结构体,然后不用绑定端口号,避免端口冲突,会默认随机绑定一个空的端口号,使用getline函数从cin输入流中读取内容并存储到input中,使用sendto函数发送消息,除了套接字,发送内容以及大小,发送行为的设置,还要传入一个指针,指定数据的接收地方的地址,目标地址的长度。

可以从命令行参数得到接收方的地址在那里,在创建一个buffer来接收消息,创建一个sockaddr_in类型的结构体,初始化结构体,指向函数recvfrom函数进行接收,接收成功就打印buffer内容。

昨天代码的客户端没有使用bind函数,是为了避免端口冲突,服务端就像110,客户端就像人,只要电话号码是唯一就行,是什么没有关系,110是不能随便改的。

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

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)
    {
        std::cerr<<"socket error"<<std::endl;
        return 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 input;
        std::cout<<"Please Enter#";
        std::getline(std::cin,input);

        std::cout << "客户端发送数据: " << input.c_str() << std::endl;
        ssize_t n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));
        (void)n;
            std::cout<<"n:" << n<<std::endl;
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        ssize_t m=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(m>0)
        {
            buffer[m]=0;
            std::cout<<"进入判断"<<std::endl;
             std::cout<<"buffer:"<<buffer<<std::endl;
            std::cout<<buffer<<std::endl;
        }
    }

    return 0;
}

InnetAddr.hpp文件

把处理端口号和地址序列转换进行封装。

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

//用于将网络地址和主机地址之间进行类型转换

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr):_addr(addr)
    {
        _port=ntohs(_addr.sin_port);
        _ip=inet_ntoa(_addr.sin_addr);
    }
     uint16_t Port() {return _port;}
    std::string Ip() {return _ip;}

    ~InetAddr()
    {}
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

字典文件内容

 
 

apple: 苹果

banana: 香蕉

cat: 猫

dog: 狗

book: 书

pen: 笔

happy: 快乐的

sad: 悲伤的

hello:

: 你好

run: 跑

jump: 跳

teacher: 老师

student: 学生

car: 汽车

bus: 公交车

love: 爱

hate: 恨

hello: 你好

goodbye: 再见

summer: 夏天

winter: 冬天

2.多人通信实现

代码存在缺陷,下一篇文章改正,仅提供思路

全部代码文件链接:http://lesson39/3.ChatServer

 UdpServer.hpp文件

Start函数

这里则不进行执行任务了,而是执行回调函数来实现任务,把套接字和信息以及接收方端口信息作为参数传过去

void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                InetAddr client(peer);
                buffer[s] = 0;
                // TODO
                _func(_sockfd, buffer, client);


                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??

                // 2. 发消息
                // std::string echo_string = "server echo@ ";
                // echo_string += buffer;
                // sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

UdpServer.cc文件

全局定义了一个task_t类型的包装器,主函数就正常接收命令行参数,创建Route类型对象,调用单例的线程池,用tp指针指向这个线程池,然后使用智能指针指向make_unique创建的一个UdpServer对象,接收的端口号以及自定义的lambda作为参数,lambda的主体是用task_t类型对象与Route的成员函数及其参数进行绑定,也就是说bind创建了一个function<void()>类型的对象t,绑定了成员函数Route::MessageRoute和其参数,调用t()时,会自动调用MessageRoute函数,传递绑定的参数,就像星期四绑定了吃汉堡,只要是星期四,就会执行吃汉堡,不用想吃什么(传递什么参数),已经是确定好的。把这个绑定的对象作为参数放到线程池的任务队列里,等待被线程处理,执行Init函数和Start函数。

#include <iostream>
#include <memory>
#include "Route.hpp"
#include "UdpServer.hpp" // 网络通信的功能
#include "ThreadPool.hpp"

using namespace ThreadPoolModule;

// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

// 需求
// 1. 翻译系统,字符串当成英文单词,把英文单词翻译成为汉语
// 2. 基于文件来做

using task_t = std::function<void()>;

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    // 1. 路由服务
    Route r;

    // 2. 线程池
    auto tp = ThreadPool<task_t>::GetInstance();

    // 3. 网络服务器对象,提供通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string &message, InetAddr&peer){
        task_t t = std::bind(Route::MessageRoute, &r, sockfd, message, peer);
        tp->Enqueue(t);
    });


    // std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r](int sockfd, const std::string &message, InetAddr&peer){
    //     r.MessageRoute(sockfd, message, peer);
    // });
    usvr->Init();
    usvr->Start();
    return 0;
}

Route.hpp文件

这里实现的是发送消息,以及管理在线用户

实现判断在线用户和加入用户方法

Isexist函数参数是一个InetAddr类型的,这个类管理的是客服端的端口号以及地址相关信息,端口号+地址就是一个确定的进程,也就是一个用户,把形参接收的实参与用户队列的所有用户进行比较,判断标志是端口号和地址相等就说明存在,否则就是不存在,InetAddr类重载了==,可以进行比较相等。加新用户进来,就会打印新用户的信息,然后尾插在vector中管理。删除用户通过遍历vector找到与删除用户信息一样的对象,使用erase函数进行删除。

bool IsExist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (user == peer)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(InetAddr &peer)
    {
        LOG(LogLevel::INFO) << "新增一个在线用户: " << peer.StringAddr();
        _online_user.push_back(peer);
    }

    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";
                _online_user.erase(iter);
                break;
            }
        }
    }


// 首次给我发消息,等同于登录
    std::vector<InetAddr> _online_user; // 在线用户

消息发送实现

消息发送的方法需要套接字,消息内容和发送方的信息,要发送信息前提是用户,先判断是否是用户,不是就加入陌生用户到vector中管理,创建message把发送方的地址端口号和要发生的具体信息进行结合,表示谁发了什么内容出来,使用范围for把这个发给所有用户(包括发送方),有了进入也要有退出,如果发送了QUIT信息,表示退出,就会日志显示删除用户及用户信息,调用删除用户函数。

 void MessageRoute(int sockfd, const std::string &message, InetAddr &peer)
    {
        if (!IsExist(peer))
        {
            AddUser(peer);
        }

        std::string send_message = peer.StringAddr() + "# " + message; // 127.0.0.1:8080# 你好

        // TODO
        for (auto &user : _online_user)
        {
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));
        }
        
        // 这个用户一定已经在线了
        if (message == "QUIT")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();
            DeleteUser(peer);
        }
    }

InetAddr.hpp文件

对客服端的端口号和地址的各种处理进行封装

构造函数接收了一个类型为sockaddr_in的对象,使用 . 获取对象的成员变量进行初始化,得到了端口号和地址信息,接着是Port函数直接返回端口信息,Ip函数返回地址,NetAddr函数返回接收到的sockaddr_in结构体,实现了重载函数==,StringAddr函数返回ip和端口号结合的string类型的变量。

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);           // 从网络中拿到的!网络序列
        _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
    }
    uint16_t Port() {return _port;}
    std::string Ip() {return _ip;}
    const struct sockaddr_in &NetAddr() { return _addr; }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

UdpClient.cc文件

这里要创建两个线程,一个执行发送任务,一个执行接收任务,目的是为了保证信息不会因为c输入流没有执行而阻塞住,导致消息的滞后性,需要输入消息才能看到阻塞住的消息,所以通过两个线程一直执行输入和输出,就不会因为没有输入而看不到其它人输出的消息了,

include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;

using namespace ThreadModlue;

void Recv()
{
    while (true)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 2
        }
    }
}
void Send()
{
    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());

    const std::string online = "inline";
    sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));

    while (true)
    {
        std::string input;
        std::cout << "Please Enter# "; // 1
        std::getline(std::cin, input); // 0

        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;
        if (input == "QUIT")
        {
            pthread_cancel(id);
            break;
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 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;
        return 2;
    }

    // 2. 创建线程
    Thread recver(Recv);
    Thread sender(Send);

    recver.Start();
    sender.Start();

    id = recver.Id();

    recver.Join();
    sender.Join();

    // 2. 本地的ip和端口是什么?要不要和上面的“文件”关联呢?
    // 问题:client要不要bind?需要bind.
    //       client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
    //   为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
    //   client端的端口号是几,不重要,只要是唯一的就行!
    // 填写服务器信息

    return 0;
}

总的流程

在server.cc文件里确定了线程池模板参数的类型是task_t,然后回调函数确定是lambda,执行start函数后会在server.hpp文件中执行回调函数_func函数,绑定t对象,入队列,enqueue函数会把这个对象放到任务队列中,然后唤醒执行wakeone函数,会执行cond的signal函数唤醒线程,需要注意的是tp是指向一个创建好的线程库,而getinstace函数内部又会指向new ThreadPool<T>(),就会调用线程池的构造函数,把Handlertask函数放到vector<Thread>中,handlertask函数会把任务队列的队头对象取出来给t,t是T t创建的,T就是task_t类型,然后t()就会执行绑定函数也就是MessageRoute函数,会发送信息出去,而队列要有任务的前提是在server.hpp文件中的recvfrom的返回值大于0,这样才能进入判断,走到回调函数,就会执行tp->Enqueue函数,所以有客服端发送消息就会入一个任务,然后线程池唤醒线程去处理这个任务,也就是把任务打出来。

相关文章:

  • 深度学习与力学建模融合的骨力学性能研究
  • 二叉树-算法小结
  • MATLAB双目标定
  • 零基础HTML·笔记(持续更新…)
  • 生成式AI与RAG架构:如何选择合适的向量数据库?
  • 山东大学软件学院创新项目实训(11)之springboot+vue项目接入deepseekAPI
  • c++STL——string学习的模拟实现
  • opencv 识别运动物体
  • springboot解析
  • Ubuntu 下通过 Docker 部署 WordPress 服务器
  • SpringBoot3-web开发笔记(下)
  • Rockchip 显示架构
  • python基础:数据类型转换、运算符(算术运算符、比较运算符、逻辑运算符、三元运算符、位运算符)
  • 【力扣hot100题】(084)零钱兑换
  • Ubuntu24.04装机安装指南
  • Elasticsearch生态
  • C++ 编程指南34 - C++ 中 ABI 不兼容的典型情形
  • cursor+高德MCP:制作一份旅游攻略
  • NModbus 库在 C# 中的使用
  • 深入理解linux操作系统---第4讲 用户、组和密码管理
  • 习近平同俄罗斯总统普京会谈
  • 安顺市原副市长、市公安局原局长顾长华任贵州省民委副主任
  • 辽宁召开假期安全生产工作调度会:绝不允许层层失守,绝不允许“带病运行”
  • 一金两银一铜!中国田径从柯桥望向世界大赛
  • 研究完蚂蚁搬家,我好像明白了为什么我们总是堵车
  • 党政机关停车场免费、食堂开放,多地“五一”游客服务暖心周到