【计网】从零开始使用TCP进行socket编程 ---服务端业务模拟Xshell

embedded/2024/9/22 12:48:07/

在这里插入图片描述

最糟糕的情况,
不是你出了错,
而是你没有面对出错的勇气。

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

  • 1 通信过程的多版本实现
    • 1.1 多进程版本
    • 1.2 多线程版本
  • 2 服务端业务模拟Xshell
    • 2.1 整体框架设计
    • 2.2 Command类设计

1 通信过程的多版本实现

在前一篇的文章中,实现了基于TCP协议的服务端与客户端的通信过程!当时我们是使用“不靠谱版本”,直接通过service函数执行代码,这样导致服务端只能为一个客户端进行服务,另一个客户端进入时就阻塞住了,只有上一个客户端连接退出,才会再次接入新的连接,这样可不行,服务器需要能够同时接入多个客户端!
那么帮助服务端实现同时接入多个客户端的做法有以下两种:

  1. 多进程版本:接收到连接后,创建子进程去执行任务。
  2. 多线程版本:接收到连接后,创建新线程去执行任务。

1.1 多进程版本

我们来实现多进程版本,多进程之前详细讲过:进程控制
创建的子进程会对父进程的数据进行写时拷贝,父子进程分别拥有独立的地址空间,但是需要注意的是:子进程的数据是根据父进程数据写时拷贝获取的,那么文件描述符也会一同拷贝,但是文件只打开了一份!所以为了避免不必要的问题要及时关闭文件描述符!!!

void Loop(){_isrunning = true;while (_isrunning){// accept接收sockfdstruct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");sleep(1);continue;}InetAddr addr(client);// 读取数据LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);//version 2 --- 多进程版本int n = fork();//signal(SIGCHLD , SIG_IGN);//忽略子进程退出的信息!if(n == 0){//child::close(_listensockfd);//关闭listen文件 子进程不需要if(fork() > 0) exit(0);//孙子进程!!!//数据会进行写时拷贝 子进程中直接执行任务就可以!Service(sockfd, addr);exit(0);}//parent::close(sockfd); //父进程不需要管连接文件!!!}_isrunning = false;}

来看效果:
在这里插入图片描述
现在就可以适配多个客户端的情况了,但是我们知道切换进程时,CPU会切换上下文和热点数据。在并发场景下多进程的不断切换会消耗大量的性能!

而作为轻量级进程的线程就可以避免这样的问题!

1.2 多线程版本

现在我们来实现多线程的版本,我们先使用原生线程:

//...
// version 3 --- 多线程版本
pthread_t tid;
ThreadData td(sockfd , addr , this);
pthread_create(&tid, nullptr, Execute, &td);
pthread_detach(tid) ;//线程分离!!!
//...

这里需要为线程提供一个void*(void*)类型的函数,新线程就去执行这个任务。这个函数中为了可以执行Service任务,我们就需要传入对应的TcpServer类对象的指针、sockfd文件描述符以及InetAddr addr发送者的信息。

那么我们就设计一个结构体,里面储存着这些数据,一起通过void*传入!

class ThreadData{public:int _sockfd;InetAddr _addr;TcpServer *_this;public:ThreadData(int sockfd,  InetAddr addr ,TcpServer *p) : _sockfd(sockfd),_this(p),_addr(addr){}};

这样在Execute函数中就可以执行任务了

	// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!static void *Execute(void *args){//执行Service函数TcpServer::ThreadData* td = static_cast<TcpServer::ThreadData*>(args);td->_this->Service(td->_sockfd , td->_addr);delete td;return nullptr;}

来看效果:
在这里插入图片描述
效果非常的好!!!

说到多线程了,那为什么不来使用线程池来实现呢???
线程池实际上并不适合当前场景,TCP通信是长服务,那么这个线程就会长时间运行,不能做到高效率的高并发
也就是说线程池在长服务场景不会提高效率!

但是我们也来实现一下线程池版本,帮助我们巩固知识!

  1. 首先我们设置一个task_t类型,这是线程池中需要执行的任务!
  2. 通过bind包装器将Service函数包装为task_t类型!
  3. 之后就等线程池分配线程执行任务即可!
using task_t = std::function<void()>;
//...
void Loop(){_isrunning = true;while (_isrunning){// accept接收sockfdstruct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");sleep(1);continue;}InetAddr addr(client);// 读取数据LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);// version 4 --- 线程池版本task_t t = std::bind(&TcpServer::Service , this , sockfd , addr);ThreadPool<task_t>::GetInstance()->Equeue(t);}_isrunning = false;}

来看效果:
在这里插入图片描述

2 服务端业务模拟Xshell

我们实现服务端与客户端的通信逻辑,接下来就来加入业务逻辑!

这次选择的业务逻辑是模拟实现Xshell远程控制主机,之前我们实现过一个本地操作的shell程序在这里我们就实现过识别字符串指令然后进行进程替换执行任务!今天我们不再需要自己编写,我们直接使用popen接口:

NAMEpopen, pclose - pipe stream to or from a processSYNOPSIS#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

popen函数中会自动帮我们识别字符串指令,并创建进程去执行,然后将结果通过文件返回!
我们来逐步实现一下!

2.1 整体框架设计

首先我们要做到工作是将各个模块进行解耦:

  1. TcpServer类只负责获取客户端与服务端的连接。进行accept接收客户端连接,然后去执行回调函数任务,再将结果返回给客户端。
  2. Command类负责对字符串指令进行执行,并将结果返回!

为了做到这样的效果,TcpServer类中需要加入回调函数,在构造时就确定好回调函数,然后通过新线程去执行回调函数!回调函数的类型和Service一致:

using command_service_t = std::function<void(int sockfd, InetAddr addr)>;

2.2 Command类设计

Command类首先需要一个对外的HandlerHelper接口,这个接口是作为TcpServer类对象构造时的回调函数。函数中执行的任务就去从连接流中获取客户端传入的数据,通过Execute函数去执行指令任务,并返回对应的结果!

HandlerHelper执行的逻辑其实和原本的Service是一致的:

  1. sockfd文件中获取客户端传入的数据!
  2. 然后传给核心函数去执行任务!
  3. 最后将结果发送回去!

需要注意的是:不是所有这里都可以让客户端执行,如果客户端可以执行rm -rf这样的指令,那么破坏性是很强的,这里可以采用白名单(或黑名单)的方法去规避一下!如果要做到无敌防御就要麻烦的多,这里只是简单模拟一下!

#include <set>
#include <iostream>
#include <string>
#include <cstring>
#include <stdio.h>#include "InetAddr.hpp"
#include "Log.hpp"using namespace log_ns;class Command
{
private://指令白名单 保证安全!void InitCommand(){_command.insert("ls");_command.insert("pwd");_command.insert("mkdir");_command.insert("sleep");_command.insert("clear");_command.insert("touch");}bool CheckCommand(std::string &command){for (auto &e : _command){// LOG( DEBUG , "%s : %s", command.c_str(), e.c_str() );if (strncmp(command.c_str(), e.c_str(), e.size()) == 0){return true;}}return false;}public:Command(){InitCommand();}std::string Execute(std::string command){// 先进行安全检查if (!CheckCommand(command)){return "Unsafe command!!!";}// 开始执行指令FILE *fp = popen(command.c_str(), "r"); // 以读方式进行// 读取结果std::string result;char line[1024];if (fp){while (fgets(line, sizeof(line), fp)){result += line;}pclose(fp);return result.empty() ? "success" : result;}return "execute error";}void HandlerHelper(int sockfd, InetAddr addr){LOG(INFO, "service start!!!\n");while (true){char buffer[1024];ssize_t n = ::recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;LOG(INFO, "sockfd read success!!! buffer: %s\n", buffer);std::string str = Execute(buffer);send(sockfd, str.c_str(), str.size(), 0);}else if (n == 0){LOG(INFO, "client %s quit!\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}}::close(sockfd);}~Command(){}private:std::set<std::string> _command;
};

来看效果:
在这里插入图片描述
非常好!这样我们就完成了Xshell的模拟项目!!!

后续我们来学习序列化与反序列化!!!


http://www.ppmy.cn/embedded/115033.html

相关文章

SSL/TSL 总结

参考&#xff1a;https://blog.csdn.net/qq153471503/article/details/109524764 &#xff08;一&#xff09;生成CA证书 1、创建CA证书私钥 openssl genrsa -aes256 -out ca.key 2048 &#xff08;密码&#xff1a;ca1234567890&#xff09; 2、请求证书 openssl req -new -s…

linux cat命令的实现

cat 是 Linux 和其他 Unix-like 系统中的一个常用命令&#xff0c;它的名称来源于 "concatenate"&#xff08;连接&#xff09;的缩写。cat 命令主要用于查看、创建和拼接文件。它读取一个或多个文件的内容&#xff0c;并将它们显示在标准输出&#xff08;通常是终端…

centos7 添加中文字体

一、打开C:\Windows\Fonts 复制 复制出来再拷贝到linux服务器目录&#xff1a;/usr/share/fonts/jtwin #执行 #mkdir /usr/share/fonts/jtwin chmod -R 755 /usr/share/fonts/jtwin yum -y install ttmkfdir ttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dir 编辑&…

【Godot4.x】Mesh相关知识总结

概述 很早之前发布过一篇关于几何体程序生成的文章&#xff0c;当时对于三角面和网格的构造其实还没有特别深入的认识&#xff0c;直到自己脑海里想到用二维数组和点更新的方式构造2D类型的多边形Mesh结构&#xff0c;也意识到在Godot中其实Mesh不仅是3D网格&#xff0c;也可以…

WPS生成目录

导航窗格&#xff1a;视图->导航窗格 可修改标题的样式&#xff0c;之后的标题直接套用即可 修改其他标题样式也是这样 添加编号&#xff1a;可以选上面的模版 也可自定义编号 生成目录&#xff1a;引用->目录->选用一个 但是我想把目录插到另一页 当我添加几个标题…

状态模式:将对象行为与状态解耦

状态模式&#xff08;State Pattern&#xff09;是一种行为设计模式&#xff0c;它允许对象在其内部状态改变时改变其行为&#xff0c;使对象看起来好像修改了其类。 状态模式的核心思想是将对象的行为封装在不同的状态对象中&#xff0c;每个状态对象都代表了对象在某一特定状…

linux部署Java项目时,阿里云OSS报错“超出了最大允许的时间偏差范围“

项目场景&#xff1a; linux部署Java项目时&#xff0c;阿里云OSS报错—RequestTimeTooSkewed 问题描述 linux部署Java项目时&#xff0c;阿里云OSS报错—RequestTimeTooSkewed 详细错误信息如下 [ErrorCode]: RequestTimeTooSkewed [RequestId]: 66ED6295352E0D3332BE4CC7 …

如何进行IP清洗

在数据抓取、网络爬虫或网络营销等活动中&#xff0c;IP地址的清洗是一个至关重要的环节。IP清洗旨在移除无效、受污染或可能引发问题的IP地址&#xff0c;从而提高网络活动的效率和安全性。本文将详细介绍如何进行IP清洗&#xff0c;包括识别问题IP、选择清洗工具、执行清洗过…