【计网】从零开始使用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协议实现了客户端和服务端的通信:客户端与服务端通信实现
- 通过socket接口创建socket文件,注意服务端可以主动绑定端口,客户端只可以进行被动绑定!!!
- 通过sendto接口根据目标IP地址以及端口号进行发送数据,发送的数据会讲发送者的IP地址和端口一并发送!
- 通过recvfrom接口从socket文件中进行获取信息,并得到发送者信息!
通过这三个接口我们实现了服务端和客户端之间的通信过程,接下来我们就来添加一些业务逻辑,让我们的客户端与服务端的通信更加实用!!!
下面我们将进行两个小项目:
- 模拟实现单词翻译交互
- 模拟实现多人聊天室
2 单词翻译
2.1 业务需求
我们需要实现的是:
- 服务端根据配置文件形成字典数据结构,可以通过单词快速检索汉语翻译
- 客户端可以向服务端发送单词,服务端获取到单词后,在字典数据结构中搜索释义,然后处理之后传送给客户端
- 客户端获取到单词释义,进行打印操作,将释义展示出来!
这就是一个单词翻译的基本逻辑,接下来我们来实现一下:
2.2 设计字典类
现在我们从零设计字典类:
- 字典内部需要一个数据结构来储存单词与翻译的映射关系,可以使用哈希表来进行!
- 字典内部还需要配置文件的路径,方便创建时主动传入配置文件路径
- 构造时,根据配置文件中的内容快速建立映射关系
- 使用一个核心翻译接口,通过单词寻找到汉语释义
这里的配置文件可以是各式各样的,我这里使用的是如下格式的.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 业务需求
多人聊天室的需求是比较直观的,就是通过创建一个类似微信群聊的聊天室。只有两个基础需求
- 用户可以接受群中其他人的消息,并且可以知道发送者的信息!
- 用户可以发送消息,发送的消息经过服务器转发给其他用户!
只要实现这俩个功能,聊天室的基础需求就已经完成了!!!为了实现这个功能我们需要:
- 在线用户列表:可以知道有哪些用户在线
- 路由转发函数:可以根据在线用户列表发送消息
我们可以直接设计一个路由转发类进行这样的功能!
3.2 路由转发Route类
我们来使用一个路由转发类:
- 使用vetcor容器来管理用户信息InetAddr,只要知道了用户的IP地址和端口就可发送回去消息
- 设计检查是否在线函数,在线就直接进行转发,不在线就进行插入。
- 用户可以输入指定的内容退出聊天,这里设计一个删除函数
- 我们可以加入线程池并发执行转发任务!这样可以快速实现多个用户的转发工作,效率就提升上来了!
线程池参考自之前的文章:【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进行不同主机的通信!!!
实现了基础的通信之后,我们加入了业务逻辑。毕竟通信的根本目的是进行数据的处理。服务器将数据处理完再传回对应的数据,这样完整的通信过程就完成了!!!
相关文章:

【计网】从零开始使用UDP进行socket编程 --- 服务端业务实现
在我们每个人都曾经历过“沮丧”时刻里, 如果我们不能对别人说有益的好话, 那我们最好还是什么也别说。 --- 卡耐基 《人性的弱点》--- 从零开始使用UDP进行socket编程 1 前情提要2 单词翻译2.1 业务需求2.2 设计字典类2.3 服务端与客户端逻辑2.4 运…...

正式发售!《黑神话:悟空》背后的技术力量——UE5与实时云渲染
千呼万唤始出来,《黑神话:悟空》终于在今年8月发售了,相信大家都已经玩起来了! 作为国产游戏的画质巅峰之作,《黑神话:悟空》凭借其令人叹为观止的画面质量和游戏体验,赢得了广泛的好评。这一切…...

qt-creator-10.0.2之后版本的jom.exe编译速度慢下来了
1、Qt的IDE一直在升级,qt-creator的新版本下载地址 https://download.qt.io/official_releases/qtcreator/ 2、本人一直用的是qt-creator-10.0.2版本,官网历史仓库可以下载安装包qt-creator-opensource-windows-x86_64-10.0.2.exe https://download.qt…...

2024CSP-J初赛全真模拟卷选择题篇(原创,难度偏简单)
注意,本卷由再临TSC原创,禁止转载! 本卷难度偏简单,若想要通过初赛本卷应拿80分左右 查看答案的方法: if(设备"PC") { 把光标移到答案上面,选中答案,就会显示(); } …...

【Android 13源码分析】WindowContainer窗口层级-4-Layer树
在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。 对这一块的概念以及相关源码做了详细分析,整理出以下几篇。 【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树 【Android 13源码分析】WindowCon…...
C# 开发教程-中级教程
1.C# 多线程/异步 C# 异步编程Task整理(一) C# 异步编程Task整理(二)异常捕捉 C# 异步编程Task(三) async、await C#中创建线程,创建带参数的线程 C# 线程同步之排它锁/Monitor监视器类 C# lock关键词/lock语句块…...

【C++】c++的继承
目录 思维导图大纲: 1.基类和派生类 1.1 定义格式 1.2 继承方式 1.3 基类和派生类的转换 2. 继承中的作用域(隐藏关系) 2.1 考察继承作⽤域相关选择题 3. 派生类的默认成员函数 4. 继承类模板 5. 一个不能被继承的类 编辑 6.继承与友元 编辑 7. 继…...
【ShuQiHere】 进制转换的世界:从十进制到二进制、十六进制的转换技巧
【ShuQiHere】 在计算机科学中,进制转换(Radix Conversion) 是一个基础且非常重要的技能。无论是理解计算机的存储、数据表示,还是在编程中处理不同的进制数据,进制转换都是不可或缺的。本文将详细讲解 十进制&#x…...
《化工管理》
《化工管理》征稿简则 《化工管理》杂志是由中国石油和化学工业联合会主管、中国化工企业管理协会主办,1986年创刊,在国内外公开发行,国内统一连续出版物号:CN 11—3991/F,中国标准连续出版物号:ISSN 1008—…...
LeetCode70:爬楼梯
class Solution { public:int climbStairs(int n) {if(n 1) return 1;if(n 2) return 2;vector<int> dp(n 1, 0);dp[1] 1;dp[2] 2;for(int i 3; i < n 1; i){dp[i] dp[i - 1] dp[i - 2];}return dp[n];} }; 这个题目也就是最简单的动态规划,题目…...
[程序员] 前人留下的苦难源,我们是否有勇气改正?
最近遇到一个客户现场发现的,表象是网络有问题,分析一圈下来发现是程序进入了某种死循环状态,耗尽CPU。 产品里的很多线程/进程的优先级设置的很高,甚至高过了内核运行程序的优先级,高过了产品内警告处理程序的运行&a…...

聚类_K均值
import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs1.数据预处理 #创建基于高斯分布的样本点, x是点的坐标,y是所属聚类值 x, y make_blobs(n_samples100, centers6, random_state100, cluster_std0.6) # 设置图形尺寸…...

Mac电脑剪切板在哪里找 苹果电脑剪切板打开教程【详解】
Windows 和 Mac 电脑在使用方式上存在一些差异,许多习惯了 Windows 系统的用户初次接触 Mac 时可能会对某些操作感到困惑。比如,很多人会问:Mac 上的剪贴板在哪里?如果你也有这样的疑问,不妨看看下面这篇关于如何在 Ma…...
Python编程 - 三器一包
目录 前言 一、迭代器 (一)基本概念 (二)迭代器和可迭代对象 (三)创建迭代器 (四)内置迭代器函数 (五)优点和局限性 二、生成器 (一&…...

InternVL 多模态模型部署微调实践
友情链接 该文档参考InternVL垂直领域场景微调实践而写成,感谢社区同学法律人的文档。 写在前面(什么是InternVL) InternVL 是一种用于多模态任务的深度学习模型,旨在处理和理解多种类型的数据输入,如图像和文本。它…...
Ruby Dir 类和方法
Ruby Dir 类和方法 Ruby 中的 Dir 类提供了用于处理目录的各种方法。这些方法允许您列出目录内容、更改当前工作目录、创建和删除目录等。本文将详细介绍 Dir 类的常用方法,并通过示例展示如何使用它们。 目录 Dir 类的简介常用方法 Dir.chdirDir.childrenDir.de…...

C++STL~~deque
文章目录 deque的概念deque的使用deque的练习总结 deque的概念 deque(双端队列):是一种序列容器、是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)ÿ…...
SpringCloud的学习,Consul服务注册与发现、分布式配置,以及 服务调用和负载均衡
介绍 Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。 提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,…...

闯关leetcode——26. Remove Duplicates from Sorted Array
大纲 题目地址内容 解题代码地址 题目 地址 https://leetcode.com/problems/remove-duplicates-from-sorted-array/description/ 内容 Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place such that each unique element appear…...

基于A2C与超启发式的航天器星载自主任务规划算法-笔记
1. Actor-Critic 模块 主要文件:AC.py, PolicyNet.py, ValueNet.py作用:该模块实现了 A2C(Advantage Actor-Critic)强化学习算法。其中,ActorCritic 类是核心,它同时管理策略网络(Actor&#x…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...