网络编程:socket编程与两个简单的UdpServer练习

目录

  • [1. UdpServer:英汉字典](#1. UdpServer:英汉字典)
    • [1.1 服务器功能与实现逻辑](#1.1 服务器功能与实现逻辑)
    • [1.2 代码实现](#1.2 代码实现)
      • [1.2.1 class Dict:导入字典信息与提供翻译功能](#1.2.1 class Dict:导入字典信息与提供翻译功能)
      • [1.2.2 网络模块与功能模块](#1.2.2 网络模块与功能模块)
  • [2. UdpServer:简单的聊天室](#2. UdpServer:简单的聊天室)
    • [2.1 服务器功能与实现逻辑](#2.1 服务器功能与实现逻辑)
    • [2.2 代码实现](#2.2 代码实现)
      • [2.2.1 class MessageRoute:将收到的信息对聊天室内的成员进行广播](#2.2.1 class MessageRoute:将收到的信息对聊天室内的成员进行广播)
      • [2.2.2 服务器端的调整与实现](#2.2.2 服务器端的调整与实现)
    • [2.2.3 客户端代码的调整与实现](#2.2.3 客户端代码的调整与实现)

1. UdpServer:英汉字典

1.1 服务器功能与实现逻辑

  • 服务器功能: 向服务器发送字典中存在的英文单词,服务器可以返回对应的中文翻译

  • 实现逻辑:
      通过文件将英汉翻译在服务器本地持久化存储,定义一个字典类,创建对象时,会将英汉翻译导入类中的unordered_map类型的成员变量中。提供成员函数可以通过传入的英文单词查询到对应的中文翻译。

1.2 代码实现

1.2.1 class Dict:导入字典信息与提供翻译功能

成员变量 作用
unordered_map<string, string> _dict 存储导入的翻译信息
string _dict_conf_filepath 翻译文件持久化存储的本地路径
成员函数 作用
Load 打开配置文件,导入翻译
构造 初始化成员变量,调用Load
Traslate 查找_dict返回中文翻译

Load函数实现逻辑:

    1. 使用getline函数按行读取文件中的数据
    1. 通过单词翻译之间的分隔符分别读取英文部分与翻译部分
    1. 读取时,检测读取到的数据是否合法
    1. 将读取的字典信息插入到_dict中

代码实现:

cpp 复制代码
//翻译格式:
//happy: 开心的

const std::string path = "./Dict.txt";
const std::string sep = ":";//分隔符

namespace dict_space
{
    class Dict
    {
        void Load()
        {
            std::ifstream in(_dict_conf_file.c_str());
            
            std::string buf;
            while(std::getline(in, buf))
            {
                //检测不为空
                if(buf.empty()) continue;

                //获取英文部分
                std::string word;
                int pos = buf.find(sep);
                word = buf.substr(0, pos);
                if(word.empty()) continue;

                //获取翻译部分
                std::string translation;
                translation = buf.substr(pos + 2, buf.size());
                if(translation.empty()) continue;

                LOG(INFO, "insert %s: %s", word.c_str(), translation.c_str());

                //插入字典中
                _dict.insert(std::make_pair(word, translation));
            }

            LOG(INFO, "insert success");

            in.close();
        }

    public:
        Dict()
            :_dict_conf_file(path)//创建对象初始化时,此全局变量在对象之前
        {
            Load();
        }
        
        const std::string Translate(const std::string word, bool& ok)
        {
            ok = true;
            if(_dict.find(word) == _dict.end())
            {
                ok = false;
                return "未找到";
            }

            return _dict[word];
        }

        ~Dict()
        {}

    private:
        unordered_map<std::string, std::string> _dict;
        std::string _dict_conf_file;
    };
}

1.2.2 网络模块与功能模块

在前一章的学习中,我们已经搭建起了基础的Udp网络通信。所有服务器本质上都是输入是输出的问题,所以,我们无需重新编写网络模块,将翻译的功能部分添加进去。而是,可以通过函数对象与回调函数的方式,不让网络通信模块与业务模块强耦合,采用模块与模块调用嵌合的方式,提高代码的健壮性。


class UdpServer调整部分:

cpp 复制代码
//以包装器加回调函数的方式,调用翻译模块
using func_t = std::function<std::string(std::string, bool&)>;

class UdpServer
{
public:
    void Start()
    {
        //先收后发
        _isrunning = true;

        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            char request[1024];

            ssize_t n = recvfrom(_sockfd, request, sizeof(request) - 1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                request[n] = 0;
                InetAddr addr(peer);
                LOG(INFO, "get message from [%s:%d]: %s", addr.Ip().c_str(), addr.Port(), request);

                bool ok;
                std::string response = _func(request, ok);
                sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, len);
            }
        }

        _isrunning = false;
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

UdpServer启动与调用方式:

cpp 复制代码
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    EnableScreen();
    Dict dict;


    std::unique_ptr<UdpServer> svr = make_unique<UdpServer>(std::stoi(argv[1]), \
        std::bind(&Dict::Translate, &dict, placeholders::_1, placeholders::_2));
    svr->InitServer();
    svr->Start();

    return 0;
}

2. UdpServer:简单的聊天室

2.1 服务器功能与实现逻辑

  • 服务器功能: 服务器会维护一个聊天室,每当有主机向服务器发送信息,服务器就会将信息发送给所有处在聊天室内的成员

  • 实现逻辑:
      实现一个专用的模块类,用户维护处于聊天室内的成员主机信息,并且提供向所有成员广播信息的接口。此类不直接进行信息的方法,而是将任务发布给线程池,线程池将任务分配给线程池中的线程来完成。

2.2 代码实现

2.2.1 class MessageRoute:将收到的信息对聊天室内的成员进行广播

成员变量 作用
vector _online_user 用于存储维护聊天室内的成员信息
pthread_mutex_t _mutex 用于保护代码中的临界区
成员函数 作用
IsExits 用于检测发送通信申请的成员是否存在聊天室内
构造、析构 维护互斥锁
AddUser 向聊天室内添加新成员
DelUser 删除聊天室内的指定成员
RouteHelper 向聊天室内的所有成员广播信息
Route 将广播信息的任务分配给线程池中的线程

代码实现:

cpp 复制代码
using func_t = std::function<void(void)>;

class MessageRoute
{
private:
    bool IsExits(InetAddr addr)
    {
        //一台主机只有一个进程进行通信
        for(auto& e : _online_user)
        {
            if(e.Ip() == addr.Ip())
            {
                return true;
            }
        }

        return false;
    }

public:
    MessageRoute()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }

    void AddUser(InetAddr user)
    {
        LockGuard lockguard(&_mutex);

        if(IsExits(user))
            return;
        
        _online_user.push_back(user);
    }

    void DelUser(InetAddr user)
    {
        LockGuard lockguard(&_mutex);

        if(!IsExits(user))
            return;

        for(auto it = _online_user.begin(); it != _online_user.end(); it++)
        {
            if(it->Ip() == user.Ip())
            {
                _online_user.erase(it);
                break;//迭代器失效
            }
        }
    }

    //将消息转发给所有成员
    void RouteHelper(int sockfd, std::string message, InetAddr who)
    {
        LockGuard lockguard(&_mutex);

        //IP无法显示?
        std::string sendmessage = "\n[" + who.Ip() + " : " + std::to_string(who.Port()) + "] " + message + "\n";
        for(auto e : _online_user)
        {
            sendto(sockfd, sendmessage.c_str(), sendmessage.size(), 0, (struct sockaddr*)e.Addr(), sizeof(struct sockaddr_in));
        }
    }

    //将发送工作分配给线程池中的线程
    void Route(int sockfd, std::string message, InetAddr who)
    {
        if(!IsExits(who))
            AddUser(who);

        func_t task = std::bind(&MessageRoute::RouteHelper, this, sockfd, message, who);

        ThreadPool<func_t>* threads = ThreadPool<func_t>::GetInstance();
        threads->Enqueue(task);

        if(message == "Quit" || message == "Q")
        {
            DelUser(who);
        }
    }

    ~MessageRoute()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    std::vector<InetAddr> _online_user;
    pthread_mutex_t _mutex;
};

2.2.2 服务器端的调整与实现


class UdpServer的调整:

cpp 复制代码
//定义与调用新的功能模块
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR
};

static int defualt_fd = -1;

using handler_message_t = std::function<void(int, std::string, InetAddr)>;

class UdpServer
{
public:
    UdpServer(uint16_t port, handler_message_t handler_message)
        :_sockfd(defualt_fd), _port(port), _handler_message(handler_message)
    {}

    void InitServer()
    {
        //1. 创建socket fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(FATAL, "socket error: %s, %d", strerror(errno), errno);
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success");

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

        //3. 绑定
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(FATAL, "bind error: %s %d", strerror(errno), errno);
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success");
    }

    void Start()
    {
        //先收后发
        _isrunning = true;

        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            char sendmessage[1024];

            ssize_t n = recvfrom(_sockfd, sendmessage, sizeof(sendmessage) - 1, 0, (struct sockaddr*)&peer, &len);

            if(n > 0)
            {
                sendmessage[n] = 0;
                InetAddr addr(peer);
                LOG(INFO, "get message from [%s:%d]: %s", addr.Ip().c_str(), addr.Port(), sendmessage);

                _handler_message(_sockfd, sendmessage, addr);
            }
        }

        _isrunning = false;
    }

    ~UdpServer()
    {}

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    handler_message_t _handler_message;
};

UdpServer的启动与调用方式:

cpp 复制代码
void Usage(std::string proc)
{
    std::cout << "Usage\n\t" << proc << " local_port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    EnableScreen();

    MessageRoute route;

    std::unique_ptr<UdpServer> svr = make_unique<UdpServer>(std::stoi(argv[1]), \
        std::bind(&MessageRoute::Route, &route, placeholders::_1, placeholders::_2, placeholders::_3));
    svr->InitServer();
    svr->Start();

    return 0;
}

2.2.3 客户端代码的调整与实现

客户端中,我们这里将消息的接收与发送分别使用两个bash命令行窗口,便于可以更清晰的观察收到的信息。并且将消息的接收与发送操作分离,使得客户端即使不进行消息发送也可以接收其他主机发送的消息。

运行客户端时,使用指令:./UdpClient serverip serverport 2> /dev/pts/...,将标准错误重定向到指定的bash命令行文件,从而达成收到信息的分离显示。


UdpClient的代码实现:

cpp 复制代码
void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " server_ip server_port\n" << std::endl;
}

int InitClient(std::string& serverip, uint16_t serverport, struct sockaddr_in* serveraddr)
{
    //1. 创建socket fd
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        LOG(FATAL, "socket create error %s %d", strerror(errno), errno);
    }
    LOG(INFO, "socket create success");
	
	memset(serveraddr, 0, sizeof(sockaddr_in));
	serveraddr->sin_port = htons(serverport);
	serveraddr->sin_family = AF_INET;
	serveraddr->sin_addr.s_addr = serverip;

    return sockfd;
}

void sendmessage(int sockfd, struct sockaddr_in server, std::string name)
{
    while(true)
    {
        std::string message;
        std::cout << "send message# ";
        getline(std::cin, message);
        fflush(stdout);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    }
}

void recvmessage(int sockfd, string name)
{
    while(true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buf[1024] = { 0 };
        recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
        InetAddr addr(peer);

        std::cerr << name << "| " << buf << endl;
    }
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    struct sockaddr_in server;
    
    uint16_t port = std::stoi(argv[2]);
    std::string serverip = inet_addr(argv[1]);

    int sockfd = InitClient(serverip, port, &server);

    function<void(std::string)> recv = std::bind(recvmessage, sockfd, placeholders::_1);
    function<void(std::string)> send = std::bind(sendmessage, sockfd, server, placeholders::_1);

    Thread r(recv, "recver");
    Thread s(send, "sender");

    r.start();
    s.start();

    r.join();
    s.join();

    return 0;
}
相关推荐
The_cute_cat1 小时前
Ubuntu指令的初步学习
linux·运维·ubuntu
python百炼成钢1 小时前
40.linux自带LED驱动
linux·运维·服务器
hhwyqwqhhwy1 小时前
linux 设备树内容和plateform_device
java·linux·数据库
乌萨奇也要立志学C++1 小时前
【Linux】线程概念 线程与进程深度剖析:虚实内存转换、实现机制与优缺点详解
linux·c++
福尔摩斯张1 小时前
使用Linux命名管道实现无血缘关系进程间通信
linux·服务器·网络
会飞的土拨鼠呀1 小时前
linux 重新运行NetworkManager
linux·运维·服务器
Ghost Face...1 小时前
高速图像采集系统架构与实现
网络
shawnyz1 小时前
RHCSE--SHELL02--变量
linux·运维·服务器
z_mazin1 小时前
逆向Sora 的 Web 接口包装成了标准的 OpenAI API 格式-系统架构
linux·运维·前端·爬虫·系统架构