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

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

在这里插入图片描述

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

从零开始使用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()
{// 建立套接字socketint 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进行不同主机的通信!!!

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 相亲交友中的用户画像构建方法探讨
  • cfs三层靶机——内网渗透
  • centos中yum方式部署Jenkins
  • git github仓库管理
  • idea激活页面怎么打开
  • 搜索二叉树BSTree的原理及实现
  • 监控系列之-prometheus部署说明
  • 服务器搭建FTP服务
  • SurfaceTexture OnFrameAvailableListener 调用流程分析
  • C++11的部分新特性
  • 《微信小程序实战(1)· 开篇示例 》
  • 工作流activiti笔记(四)审批人设置
  • Python | Leetcode Python题解之第403题青蛙过河
  • 如何使用 Vue 3 的 Composition API
  • ICPC网络赛 以及ACM训练总结
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • extjs4学习之配置
  • iOS 颜色设置看我就够了
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • linux学习笔记
  • nodejs调试方法
  • puppeteer stop redirect 的正确姿势及 net::ERR_FAILED 的解决
  • React 快速上手 - 06 容器组件、展示组件、操作组件
  • Redis的resp协议
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 回顾2016
  • 基于Android乐音识别(2)
  • 力扣(LeetCode)965
  • 嵌入式文件系统
  • 如何设计一个微型分布式架构?
  • 深入浅出webpack学习(1)--核心概念
  • 使用API自动生成工具优化前端工作流
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 用 vue 组件自定义 v-model, 实现一个 Tab 组件。
  • 在Unity中实现一个简单的消息管理器
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • ​补​充​经​纬​恒​润​一​面​
  • ​决定德拉瓦州地区版图的关键历史事件
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • !!java web学习笔记(一到五)
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • #每日一题合集#牛客JZ23-JZ33
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (HAL库版)freeRTOS移植STMF103
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (六)c52学习之旅-独立按键
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (十一)c52学习之旅-动态数码管
  • (四)Linux Shell编程——输入输出重定向
  • .NET Core 发展历程和版本迭代
  • .NET Core中Emit的使用