【计网】从零开始使用UDP进行socket编程 --- 服务端业务实现

news2024/11/15 19:54:50

在这里插入图片描述

在我们每个人都曾经历过“沮丧”时刻里,
如果我们不能对别人说有益的好话,
那我们最好还是什么也别说。
--- 卡耐基 《人性的弱点》---

从零开始使用UDP进行socket编程

  • 1 前情提要
  • 2 单词翻译
    • 2.1 业务需求
    • 2.2 设计字典类
    • 2.3 服务端与客户端逻辑
    • 2.4 运行效果
  • 3 多人聊天室
    • 3.1 业务需求
    • 3.2 路由转发Route类
    • 3.3 客户端的改造
    • 3.4 运行测试
  • 4 总结

1 前情提要

上一篇文章中,我们通过UDP协议实现了客户端和服务端的通信:客户端与服务端通信实现

  1. 通过socket接口创建socket文件,注意服务端可以主动绑定端口,客户端只可以进行被动绑定!!!
  2. 通过sendto接口根据目标IP地址以及端口号进行发送数据,发送的数据会讲发送者的IP地址和端口一并发送!
  3. 通过recvfrom接口从socket文件中进行获取信息,并得到发送者信息!

通过这三个接口我们实现了服务端和客户端之间的通信过程,接下来我们就来添加一些业务逻辑,让我们的客户端与服务端的通信更加实用!!!

下面我们将进行两个小项目:

  • 模拟实现单词翻译交互
  • 模拟实现多人聊天室

2 单词翻译

2.1 业务需求

我们需要实现的是:

  1. 服务端根据配置文件形成字典数据结构,可以通过单词快速检索汉语翻译
  2. 客户端可以向服务端发送单词,服务端获取到单词后,在字典数据结构中搜索释义,然后处理之后传送给客户端
  3. 客户端获取到单词释义,进行打印操作,将释义展示出来!

这就是一个单词翻译的基本逻辑,接下来我们来实现一下:

2.2 设计字典类

现在我们从零设计字典类:

  1. 字典内部需要一个数据结构来储存单词与翻译的映射关系,可以使用哈希表来进行!
  2. 字典内部还需要配置文件的路径,方便创建时主动传入配置文件路径
  3. 构造时,根据配置文件中的内容快速建立映射关系
  4. 使用一个核心翻译接口,通过单词寻找到汉语释义

这里的配置文件可以是各式各样的,我这里使用的是如下格式的.txt文件:

hello: 你好,用作见面时的礼貌问候语
goodbye: 再见,分别时说的告别语 
summer: 夏天,一年四季中的第二个季节,通常气候炎热 
winter:冬天,一年四季中的最后一个季节,通常气候寒冷
...

代码实现中会使用到文件流操作,这里使用的是C++风格的流操作,按行读取配置文件中的数据!
翻译接口使用的是简单的哈希表查询,不再赘述!

#include<unordered_map>
#include<string>
#include<fstream>

#include"Log.hpp"

using namespace log_ns;

//默认配置文件路径
const std::string gpath = "./dict.txt";
//文件间隔符
const std::string sep = ": ";

class Dict
{
private:
    void LoadDict()
    {
    	//建立文件流对象
        std::fstream in(_path , std::ios_base::in);
        if(!in.is_open())
        {
            LOG(FATAL , "The configuration file failed ! \n");
            exit(0);
        }
        //进行读取
        std::string line;
        while(std::getline(in , line))
        {
           if(line.empty()) continue;
           auto pos = line.find(sep);
           if (pos == std::string::npos) continue;

           std::string key = line.substr(0 , pos); 
           if(key.empty()) continue;
            
           std::string value = line.substr(pos + sep.size());
           if(value.empty()) continue;

           _dict[key] = value;

            LOG(DEBUG , "%s : %s load success\n", key.c_str() , value.c_str());
        }   
    }
public:
    Dict(const std::string& path = gpath) :_path(path)
    {
        LOG(DEBUG , "Dictionaries are being created! \n");
        LoadDict();
    }
    std::string Translate(std::string str)
    {
        auto ret = _dict.find(str);
        if(ret != _dict.end())
        {
            return ret->second;
        }
        else
        {
            return "我不会 , 你换个词问吧!";
        }
    }
    ~Dict()
    {
    }
private:
    std::unordered_map<std::string , std::string> _dict;
    std::string _path;
};

2.3 服务端与客户端逻辑

首先,为了服务端可以实现核心函数的运行,需要在服务器类中加入回调函数,这里我们使用function包装器来进行优化:

// 数据处理的核心 --- 回调函数
using func_t = std::function<std::string(std::string)>;

之后我们就加入一个回调函数成员变量,并在构造函数中进行初始化!

之后就要考虑如何将字典类中的Translate函数传给服务器类中了,首先类函数默认都有一个参数this,这里使用bind包装器进行绑定:

#include "UdpServer.hpp"

int main(int argc , char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip " << std::endl;
        exit(0);
    }
    EnableScreen();
    uint16_t port = std::stoi(argv[1]);
    //创建字典
    Dict d;
    func_t func = std::bind( &Dict::Translate , &d , std::placeholders::_1);
    std::unique_ptr<UdpServer> ptr = std::make_unique<UdpServer>(func , port);
    
    ptr->InitServer();
    ptr->Start();
    return 0;
}

这样服务器端的运行逻辑就写好了!接下来看客户端,客户端其实并不需要进行改变,因为客户端只是进行一个数据的发送操作和数据的获取操作,客户端要做的就是将用户输入的单词传给服务器端,剩下的就不需要进行额外操作了!

2.4 运行效果

现在一切都已经写好,我们来看看我们的单词翻译软件可不可以进行单词翻译的工作:

服务器加载配置文件成功:
在这里插入图片描述

启动客户端程序,进行单词查询,效果良好!!!

这样单词翻译的程序就写好了!!!接下来我们来实现更加有意思的多人聊天室!!!

3 多人聊天室

3.1 业务需求

多人聊天室的需求是比较直观的,就是通过创建一个类似微信群聊的聊天室。只有两个基础需求

  1. 用户可以接受群中其他人的消息,并且可以知道发送者的信息!
  2. 用户可以发送消息,发送的消息经过服务器转发给其他用户!

只要实现这俩个功能,聊天室的基础需求就已经完成了!!!为了实现这个功能我们需要:

  1. 在线用户列表:可以知道有哪些用户在线
  2. 路由转发函数:可以根据在线用户列表发送消息

我们可以直接设计一个路由转发类进行这样的功能!

3.2 路由转发Route类

我们来使用一个路由转发类:

  1. 使用vetcor容器来管理用户信息InetAddr,只要知道了用户的IP地址和端口就可发送回去消息
  2. 设计检查是否在线函数,在线就直接进行转发,不在线就进行插入。
  3. 用户可以输入指定的内容退出聊天,这里设计一个删除函数
  4. 我们可以加入线程池并发执行转发任务!这样可以快速实现多个用户的转发工作,效率就提升上来了!

线程池参考自之前的文章:【Linux】线程池项目详解

#include <vector>

#include "UdpServer.hpp"
#include "ThreadPool.hpp"

using namespace ThreadMouble;

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

class Route
{
private:
    void CheckOnlineUser(InetAddr &who)
    {
        LockGuard lock(&_mtx);
        auto it = _online_user.begin();
        for (; it < _online_user.end(); it++)
        {
            if (who == *it)
                return;
        }
        // 没有就进行插入
        _online_user.push_back(who);
    }
    void Remove(InetAddr &who)
    {
        auto it = _online_user.begin();
        for (; it < _online_user.end(); it++)
        {
            if (who == *it)
            {
                _online_user.erase(it);
                break;
            }
        }
    }
    //                 发送
    void ForwardHelper(int sockfd, const std::string &message)
    {
        LockGuard lock(&_mtx);
        // 遍历一遍在线用户列表进行发送消息
        for (auto &user : _online_user)
        {
            struct sockaddr_in peer = user.Addr();
            // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
            ::sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
        }
    }
public:
    Route()
    {
    }
    void Foward(int sockfd, const std::string &message, InetAddr &who)
    {
        
        // 1.先对用户进行检查
        CheckOnlineUser(who); // 现在一定是在用户列表中
        // 2.检查信息内容是否是退出信息
        if (message == "QUIT" || message == "Q")
        {
            // 从用户列表中移除
            Remove(who);
        }
        // 3.进行群发消息 ForwardHelper(sockfd , message );
        // 4.使用线程池进行并发操作
        task_t t = std::bind( &Route::ForwardHelper, this , sockfd, message);
        ThreadPool<task_t>::GetInstance()->Equeue(t);
    }
    ~Route()
    {
    }

private:
    std::vector<InetAddr> _online_user; // 在线用户列表
    pthread_mutex_t _mtx;
};

这样Route类就完成了,在对Route进行使用时进bind绑定,以匹配服务器中回调函数的类型。不得不说bind包装器和function包装器真的太好用了!!!简直是天才的设计!!!


int main(int argc , char *argv[])
{
	//...
    //转发功能
    Route temp;
    //进行绑定     void Foward(int sockfd, const std::string &message, InetAddr &who)
    service_t func = std::bind(&Route::Foward , &temp , std::placeholders::_1 , std::placeholders::_2 , std::placeholders::_3);
    std::unique_ptr<UdpServer> ptr = std::make_unique<UdpServer>(func , port);
    ptr->InitServer();
    ptr->Start();
    
    return 0;
}

这样在服务器端就可以使用多线程并发进行消息的路由转发任务!!!

3.3 客户端的改造

客户端需要为用户提供一个输入栏,允许用户可以输入信息!并且客户端需要实时接收其他用户发送的消息,并及时的打印出来。
如果按照单词翻译的代码逻辑来进行,会出现问题。单词翻译中的接收与发送是一对一进行的,只有发送了消息才会收到一个信息。但是聊天室的不管发没发消息都应该收到其他人发送的消息!所以需要对接收和发送进行解耦,让两个任务通过两个不同的线程进行运行,达到并发执行的效果!

下面是改造后的代码:

代码中创建了两个单独的线程来执行发送和接收任务!
但是接收和发送函数与线程内部的回调函数类型不匹配!怎么办?直接进行一手bind绑定!!!bind绑定简直是神!!!
这样就实现了发送和接收的解耦,互不影响,完全做到同时并发进行!!!
这样的解耦操作实在是太优雅了!!!

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

#include "Log.hpp"
#include "Thread.hpp"

enum
{
    SOCKER_FD = 1,
    SOCKET_BIND
};

using namespace log_ns;
using namespace ThreadMouble;

int InitClient()
{
    // 建立套接字socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(FATAL, "socket failed!\n");
        exit(SOCKER_FD);
    }
    LOG(DEBUG, "Client create socket success , _sockfd:%d \n", sockfd);
    return sockfd;
}

void SenderMessage(int sockfd, std::string ip, int port, std::string &name)
{
    // //设置服务器结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 数据归零
    server.sin_family = AF_INET;
    server.sin_port = htons(port);                  // 端口号
    server.sin_addr.s_addr = inet_addr(ip.c_str()); // ip地址
    while (1)
    {
        // 发送数据
        std::string line;
        std::cout << "Please Enter: ";
        std::getline(std::cin, line);

        // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if(n <= 0) break;
    }
}

void RecverMessage(int sockfd, std::string &name)
{
    while (true)
    {
        // 进行获取数据
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[512];
        // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cerr << buffer << std::endl;
        }
        else
        {
            std::cerr << "m < 0 程序退出 !" << std::endl;
            break;
        }
    }
}

int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    // 根据传入的参数获取服务端的IP和端口号
    std::string ip = argv[1];
    int port = std::stoi(argv[2]);
    int sockfd = InitClient();

    // 客户端使用两个线程分别执行发送和接收
    func_t sendfunc = std::bind(SenderMessage, sockfd, ip, port, std::placeholders::_1);
    func_t recvfunc = std::bind(RecverMessage, sockfd, std::placeholders::_1);
    Thread Sender("Sender-Thread", sendfunc);
    Thread Recver("Sender-Thread", recvfunc);

    Recver.Start();
    Sender.Start();

    Sender.Join();
    Recver.Join();

    ::close(sockfd);

    return 0;
}

客户端的代码逻辑就实现了,接下来就可以进行运行测试了,让我们看看多人聊天室是否可以运行起来

3.4 运行测试

接下来我们创建两个终端来测试是否可以做到多人聊天!
首先我们先解决一个问题:我们现在的输入和输出是在一个终端下,这样会显的比较混乱。

所以这里通过管道文件来进行解决,我们即将客户端接收的信息写入到管道中,这样就可以将输入栏和对话框分离,观感更好!!!
来看效果:
在这里插入图片描述
多人聊天系统这样就完成了!!!
欧耶欧耶欧耶~~~

4 总结

通过两篇文章我们熟悉了UDP协议下的的通信过程,认识了主机信息结构体,使用这个结构体可以通过sendto和recvfrom进行不同主机的通信!!!

实现了基础的通信之后,我们加入了业务逻辑。毕竟通信的根本目的是进行数据的处理。服务器将数据处理完再传回对应的数据,这样完整的通信过程就完成了!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2138706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQLite安装(含安装包)

安装包&#xff1a; 通过百度网盘分享的文件&#xff1a;sqlite-dll-win-x64-3460100.zip 链接&#xff1a;https://pan.baidu.com/s/1852coiq51QcNkeaHdu1Oyg 提取码&#xff1a;v2y6 解压 设置环境变量 验证安装成功 SQLite设置完成

ros学习笔记.4 Path Planning Part 2 (避障)

避障是如何工作的什么是局部规划器&#xff1f;什么是局部成本图&#xff1f;路径规划回顾如何使用动态重新配置和其他 Rviz 工具 局部规划器 一旦全局规划器计算出要遵循的路径&#xff0c;该路径就会发送给局部规划器。然后&#xff0c;局部规划器将执行全局规划的每个部分&…

唯徳知识产权管理系统 DownloadFileWordTemplate 文件读取漏洞复现

0x01 产品简介 唯徳知识产权管理系统,由深圳市唯德科创信息有限公司精心打造,旨在为企业及代理机构提供全方位、高效、安全的知识产权管理解决方案。该系统集成了专利、商标、版权等知识产权的全面管理功能,并通过云平台实现远程在线办公,提升工作效率。是一款集知识产权申…

【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C 目录 前言 一、取地址运算符重载 1. const修饰成员函数 2. 取地址运算符重载 二、深究构造函数 三、类型转换 四、static修饰成员 1. static修饰成员变…

监控系列之-prometheus部署说明

一、Prometheus介绍 Prometheus是一款开源的监控系统&#xff0c;主要用于收集、存储和查询时间序列数据&#xff0c;以便于对系统进行监控和分析Prometheus的架构由四个主要组件组成&#xff1a; 1、Prometheus Server &#xff1a;Prometheus Server是Prometheus的核心组件&a…

带你0到1之QT编程:十二、视图宝典,点通views的任督二脉

此为QT编程的第十二谈&#xff01;关注我&#xff0c;带你快速学习QT编程的学习路线&#xff01; 每一篇的技术点都是很很重要&#xff01;很重要&#xff01;很重要&#xff01;但不冗余&#xff01; 我们通常采取总-分-总和生活化的讲解方式来阐述一个知识点&#xff01; …

text2sql(NL2Sql)综述《The Dawn of Natural Language to SQL: Are We Fully Ready?》

《The Dawn of Natural Language to SQL: Are We Fully Ready?》(github)出自2024年6月的NL2SQL(Natural language to SQL )综述论文。这篇论文尝试回答如下三个问题&#xff1a; 问题1:NL2SQL的现状是什么&#xff1f;(Q1:Where Are we Now?) 论文图1总结了近20年NL2SQL方法…

【移动端】菜单的自动展开与收回

前言 为了满足手机上菜单栏随用户移动&#xff0c;菜单的自动展示与隐藏&#xff0c;特此记录 基本原理 实现逻辑 window.addEventListener(‘scroll’, debouncedScrollHandler) – 监听文档视图滚动事件 document.querySelector(‘.header’) – 选择器匹配元素 创建show和h…

论文速递!Auto-CNN-LSTM!新的锂离子电池(LIB)剩余寿命预测方法

论文标题&#xff1a;A Data-Driven Auto-CNN-LSTM Prediction Model for Lithium-Ion Battery Remaining Useful Life 期刊信息&#xff1a;IEEE TII (中科院1区, JCR Q1, IF11.7) 引用&#xff1a;Ren L, Dong J, Wang X, et al. A data-driven auto-CNN-LSTM prediction m…

JavaScript web API part3

web API DOM 日期对象 > 得到当前系统的时间 new这个操作就是实例化 语法 const date new Date() or const date new Date(2004-11-3 08:00:00) 可以指定时间 > 可应用于通过系统时间和指定时间实现倒计时的操作 //得到当前时间const date new Date()console.lo…

多维时序 | Matlab基于BO-LSSVM贝叶斯优化最小二乘支持向量机数据多变量时间序列预测

多维时序 | Matlab基于BO-LSSVM贝叶斯优化最小二乘支持向量机数据多变量时间序列预测 目录 多维时序 | Matlab基于BO-LSSVM贝叶斯优化最小二乘支持向量机数据多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab基于BO-LSSVM贝叶斯优化最小二乘支…

Vue介绍、窗体内操作、窗体间操作学习

系列文章目录 第一章 基础知识、数据类型学习 第二章 万年历项目 第三章 代码逻辑训练习题 第四章 方法、数组学习 第五章 图书管理系统项目 第六章 面向对象编程&#xff1a;封装、继承、多态学习 第七章 封装继承多态习题 第八章 常用类、包装类、异常处理机制学习 第九章 集…

树莓派5上手

1 安装系统 Raspberry Pi OS 是基于 Debian 的免费操作系统&#xff0c;针对 Raspberry Pi 硬件进行了优化。Raspberry Pi OS 支持超过 35,000 个 Debian 软件包。树莓派 5 可以安装各种系统&#xff0c;但是如果对于系统没有特殊的要求&#xff0c;还是安装 Raspberry Pi OS …

【MySQL】MySQL索引与事务的透析——(超详解)

前言 &#x1f31f;&#x1f31f;本期讲解关于MySQL索引事务&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【MySQL】MySQL表的增删改查&#xff08;进阶篇&#xff09;——之查询操作&#xff08;超级详解&#xff09;-CSDN博客 &#x1f308;感…

CSP-CCF★★★201903-2二十四点★★★

目录 一、问题描述 二、解答 方法一&#xff1a;穷举法&#xff08;只列举了一部分&#xff09; 方法二&#xff1a;中缀表达式直接求值&#xff0c;两个栈&#xff0c;一个存放数值&#xff0c;一个存放符号 方法三&#xff1a;将中缀表达式转换为后缀来计算注意&#xff…

台风,也称为热带气旋,是一种在热带海洋上形成的强烈风暴系统。台风的形成需要满足以下几个条件:

台风&#xff0c;也称为热带气旋&#xff0c;是一种在热带海洋上形成的强烈风暴系统。台风的形成需要满足以下几个条件&#xff1a; 1. **温暖的海水**&#xff1a;台风通常在海面温度至少达到26.5C&#xff08;79.7F&#xff09;的海域形成&#xff0c;因为温暖的海水能够提供…

八股(8)——Spring,SpringBoot

八股&#xff08;8&#xff09;——Spring&#xff0c;SpringBoot 基础1.Spring 是什么&#xff1f;特性&#xff1f;有哪些模块&#xff1f;Spring 有哪些特性呢&#xff1f; 2.Spring 有哪些模块呢&#xff1f;3.Spring 有哪些常用注解呢&#xff1f;Web 开发方面有哪些注解呢…

利用模糊综合评价法进行数值评分计算——算法过程

1、‌模糊综合评价法概述 ‌模糊综合评价法是一种基于模糊数学的综合评价方法&#xff0c;它通过模糊数学的隶属度理论将定性评价转化为定量评价&#xff0c;适用于解决复杂、难以量化的问题。该方法具有结果清晰、系统性强的特点&#xff0c;能够处理多种因素制约下的综合评价…

热门数据恢复软件大盘点

现在大家的数据都喜欢存放在一些电子设备里保存吧。这样既方便存放&#xff0c;也方便我们查找。但是这些设备可能因为病毒、误删除等原因造成数据的丢失。这篇文章我将介绍几款类似易我数据恢复软件的数据恢复工具&#xff0c;减少为数据丢失给我们造成损失。 1.FOXIT数据恢复…

vue国际化

前言 现在的大公司都走国际化路线&#xff0c;我们应用程序也不例外。今天就在 Vue3 项目中整一个比较简单的国际化 背景 之前搞国际化的时候&#xff0c;也搜索了很多帖子&#xff0c;但是没有一个可以完整的实现。今天有空搞了一版&#xff0c;大家有什么问题欢迎留言探讨…