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

【计网】自定义序列化反序列化(三) —— 实现网络版计算器【下】

🌎实现网络版计算器【下】

  本次序列化与反序列化所用到的代码,Tcp服务自定义序列化反序列化实现网络版计算器。

文章目录:

实实现网络版计算器【下】

    客户端实现
    基于守护进程的改写


🚀客户端实现

  在这之前,我们已经将服务器端的代码部分做好了准备,现在万事俱备只欠客户端发起连接,而客户端在这里不准备那么多的封装了,与之前写的客户端相同,我们想要客户端以 ./cal_client ip port 的形式来创建客户端:

#include <iostream>
#include <string>
#include <memory>
#include <ctime>#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}using namespace socket_ns;
using namespace protocol_ns;// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];// 服务器端ipuint16_t serverport = std::stoi(argv[2]);// 服务器端portreturn 0;
}

  而Udp客户端构建对象就不需要这么麻烦了,因为我们早在最开始已经将Socket类进行了封装,这样我们就不需要在调用原生接口在客户端裸露式调用:

InetAddr serveraddr(serverip, serverport);// 通过ip和port构建InetAddr对象
std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();// 子类对象构造父类指针方便多态式调用
bool res = cli->BuildClientSocket(serveraddr);// 建立客户端Socket 链接

  此时,客户端的Socket服务就已经构建完毕,网络通信已经做好准备,只要服务器通,客户端随时都可以发起连接。接着就是处理客户端的业务逻辑。

  我们要知道客户端是要给服务器端发送请求并且获取相应的一个过程,获取成功之后将响应进行反序列化拿到最终的结果。为了方便测试,我们这里让客户端采用固定的提问方式不断对客户端发送请求获取响应并且解析,我们将构建请求以及接收响应封装为一个 Factory类。

  其中,客户端请求让 x 为 1-10的随机数,y为 0-4的随机数,让他们进行模运算,并将计算构造为Request类,返回值为Request的指针:

class Factory
{
public:Factory(){srand(time(nullptr) ^ getpid());opers = "+/*/%^&|";}std::shared_ptr<Request> BuildRequest(){int x = rand() % 10 + 1;usleep(x * 10);int y = rand() % 5; // [0,1,2,3,4]usleep(y * x * 5);char oper = opers[rand() % opers.size()];std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);return req;}std::shared_ptr<Response> BuildResponse(){return std::make_shared<Response>();}~Factory(){}private:std::string opers;// 操作数: +/-/*/\/% ...
};

  为了方便测试,我们这里只启动一个客户端,这个客户端不停的给服务器发送数据,所以我们需要将待发送请求以及返回的响应放在while循环内不断发送获取解析。

  我们想要积压一批数据,然后在一次性发送,这样就能测试服务器的功能是否有问题,是否能处理多批数据,所以在这里我们一次性构建五个请求让后在发送给服务器端,同样构建请求时需要对数据进行序列化和添加长度报头:

Factory factory;
std::string inbuffer;while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i)// 一次性构建5个请求{auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 发送请求
}

  我们将求情发送之后,客户端就静静等待服务器端返回的响应,当然,与服务器端接收消息相同,客户端接收的每条应答一定就是完整的应答吗?不一定,所以我们将读取到的数据进行Decode(),这样我们对所有的应答进行解析,如果是一条完整的应答Decode接口就会返回一个response对象,对象里就是解析过后一条完整的应答内容:

while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i){auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 读取应答int n = cli->Recv(&inbuffer);if (n <= 0)break;std::string package = Decode(inbuffer);if (package.empty())continue;
}

  那么此后,我们获取的应答就一定是一条完整的应答,但是这个应答此时还是序列化状态,我们需要将其进行反序列化处理,最后输出响应结果即可:

while (res)
{sleep(1);// 构建请求std::string str;for (int i = 0; i < 5; ++i){auto req = factory.BuildRequest();// 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "Serialize: \n"<< send_str << std::endl;// 添加长度报头send_str = Encode(send_str);std::cout << "Encode: \n"<< send_str << std::endl;str += send_str;}// "len"\r\n"{}"\r\ncli->Send(str);// 读取应答int n = cli->Recv(&inbuffer);if (n <= 0)break;std::string package = Decode(inbuffer);if (package.empty())continue;// 读到的package一定是一个完整的应答auto resp = factory.BuildResponse();// 反序列化resp->Deserialize(package);// 拿到了结构化的应答std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;
}

  为了更好地体现服务器端对报文的处理是否正确,我们在TcpServerMain内的Service服务进行细微调整,前面,我们让客户端不断地对服务器端发出请求,那么服务器端的Service也要不断地去处理请求,并发送到客户端:

void ServiceHelper(socket_sptr sockptr, InetAddr client)
{int sockfd = sockptr->SockFd();LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";std::string inbuffer;while (true){sleep(5);Request req;// 1. 读取数据int n = sockptr->Recv(&inbuffer);if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2. 分析数据std::string package;while (true){sleep(1);std::cout << "inbuffer" << inbuffer << std::endl;package = Decode(inbuffer);if (package.empty())break;std::cout << "-----------------------begin----------------------" << std::endl;std::cout << "resq string:\n"<< package << std::endl;// 3.反序列化req.Deserialize(package);// 4. 业务处理Response resp = _cb(req);// 5. 对应答进行序列化std::string send_str;resp.Serialize(&send_str);std::cout << send_str << std::endl;// 6. 添加长度报头send_str = Encode(send_str);// 7. 发送到对端sockptr->Send(send_str);}}
}

  那么我们所有准备都已经做好了,接下来就是通信时刻:

在这里插入图片描述

  这样网路版本计算器我们就实现完成了。在这个项目当中,我们发现,我们把从Tcp内读取的报文,可能读到半个,可能读到一个半,或者其他特殊不完整报文情况,这种情况我们称为 Tcp粘包问题。而我们使用Encode() 和 Decode() 接口就是为了解决tcp粘包问题的。


🚀基于守护进程的改写

  我们知道,我们在连接远程服务器的时候,实际上就是打开一个终端文件,如果有多个连接就会打开多个终端文件,我们从一台设备向另一台设备进行重定向的时候就是如此:

在这里插入图片描述

  并且我们可以将消息发送到另外一个终端文件当中,使用如下命令进行重定向:

echo "message" >> /dev/pts/n #这里n指的是任何一个存在的终端文件

  这里如果你是使用XShell来测试上面的命令,你很可能不会成功,因为版本升级的原因,但是我们能知道这个现象就行。总而言之,当我们连接Linux服务器的时候,会给我们打开一个终端文件,再启动bash命令行解释器

在这里插入图片描述

  首先是创建终端文件,其次bash被启动,而bash又作为所有进程的父进程,bash则会打开创建的终端文件。而一般终端文件与启动的bash会被打包称为一个 会话(具体在以后守护进程章节中看到),而每个会话都会有自己的 会话id(sid),而一般 会话的id是终端中的第一个进程的pid也就是bash

在这里插入图片描述

  但是,只要在当前终端下启动的任何服务(进程),都属于当前的会话!比如:

在这里插入图片描述
  所以会话就像是bash进程中的管理容器,如果一个会话被销毁了,那么会话里的所有进程也都会终止,

在这里插入图片描述
  但是今天,我想要一种不受会话影响的进程,也就是不受用户登录退出的影响,独立于会话之外的进程,比如我们的网络版计算器服务器端,我们不想让其受用户注册销毁的影响,所以我们可以编写代码将其变为 守护进程

  实际上这么做的意义就是创建一个新的会话,在Linux中给我们提供了 setsid() 接口:

在这里插入图片描述

  setsid()会创建出一个新的会话,不过有一个要求:调用进程不能是进程组的组长。那么什么是进程组呢?很简单,每个进程组都有一个唯一的标识符,通常是进程组的组长(Leader)的进程ID,而组长就是他们之中第一个启动的进程:


  所以,我们在程序中直接创建子进程,并且退出父进程,那么,那么当前进程就可以调用setid()接口了,这个进程也就独立出会话之外,成为一个全新的会话,我们称之为 守护进程(精灵进程)!使用类似一下代码:

if(fork() > 0) exit(0);
setsid();

  当然,如果你嫌麻烦,大可不必写长点的代码,因为Linux早就给我们想好了,给我们提供了一个 Daemon() 接口:

在这里插入图片描述

  • nochdir 参数是否更改当前进程的工作目录。如果更改,守护进程的目录就会切换为根目录,如果不更改,则在启动时的路径下
  • nocliose参数是否需要进行输入输出的处理

  Linux每个终端下都会存在一个null文件:/dev/null如果去读取这个文件,文件内是没有任何内容的,如果对该文件进行写,同样也不会保存任何信息,而是立刻丢弃。我们知道,当我们创建了守护进程,也就意味着脱离了原本的会话,所以也就没有原本的终端文件了,而如果我们要使网络计算器变为守护进程,而网络计算器中存在大量的IO操作,为了避免因为没有对应的终端文件进行IO而出错,我们可以将 0,1,2三个文件描述符全部重定向到 /dev/null 当中。

#include <iostream>
#include <unistd.h>int main()
{std::cout << "Pid is: " << getpid() << std::endl;sleep(1);daemon(0, 0);while(true){std::cout << "hello test" << std::endl;sleep(1);}return 0;
}

  以上是一个简单的测试样例,daemon内部会自动的fork并且退出父进程:

在这里插入图片描述

  经过测试我们可以看到,hello.exe 的TTY,也就是终端文件变成了 “?”, 也就表示已经不属于当前的会话了,而SID同样与当前进程的SID不同,并且SID为守护进程的pid。如果我们查看守护进程的工作目录:

在这里插入图片描述
  可以看到,守护进程当前工作目录实际上就是在根目录,如果我们同时查看该守护进程的文件fd就会发现:

在这里插入图片描述

  由此可见,daemon接口的两个参数实际上是bool值类型的,第一个参数表示是否更改工作目录,第二个参数表示是否更改重定向,如果我们把daemon参数设置为daemon(0, 0):

在这里插入图片描述

在这里插入图片描述

  将daemon参数设置为(1, 1)就会导致我们输出的内容还是在上一个会话下,并且Ctrl C 也无法终止进程(可使用 kill -9 process_pid 杀死进程),当我们查询进程工作目录时,也能发现其在当前的工作目录下,而fd也指向了第一个终端文件。

  所以一般情况下,我们直接调用 daemon(0, 0)即可,但是我们网络版计算器不仅仅有许多的IO,还写了很多很重要的日志信息啊,这么设置守护进程,我们就无法在终端上看到日志信息了,不用担心,因为早在编写日志之初,我们就已经给日志设置为两个选择,1. 将信息打印到显示器上。2. 将日志信息打印到终端文件上。我们可以将其打印到日志文件当中:

在这里插入图片描述

  随后启动服务器,将其变为一个守护进程,然后启动一个客户端连接服务器端:

在这里插入图片描述

在这里插入图片描述

  我们之前定义的文件路径就是在当前目录下,而我们创建了守护进程,并且将工作目录改为了根目录,所以我们的log.txt文件只能出现在根目录了。


  以上就是网络版计算器实现的全过程了,如果这三篇文章对您有所帮助的话,还望点赞支持~~

相关文章:

【计网】自定义序列化反序列化(三) —— 实现网络版计算器【下】

&#x1f30e;实现网络版计算器【下】 本次序列化与反序列化所用到的代码&#xff0c;Tcp服务自定义序列化反序列化实现网络版计算器。 文章目录&#xff1a; 实实现网络版计算器【下】 客户端实现     基于守护进程的改写 &#x1f680;客户端实现 在这之前&#xff0c…...

神经网络中的优化方法(一)

目录 摘要Abstract1. 与纯优化的区别1.1 经验风险最小化1.2 代理损失函数1.3 批量算法和小批量算法 2. 神经网络中优化的挑战2.1 病态2.2 局部极小值2.3 高原、鞍点和其他平坦区域2.4 悬崖和梯度爆炸2.5 长期依赖2.6 非精确梯度2.7 局部和全局结构间的弱对应 3. 基本算法3.1 随…...

Linux 计算机网络基础概念

目录 0.前言 1.计算机网络背景 1.1 独立模式 1.2 网络互联 1.3 局域网&#xff08;Local Area Network&#xff0c;LAN&#xff09; 1.4 广域网&#xff08;Wide Area Network&#xff0c;WAN&#xff09; 2.协议 2.1什么是协议 2.2协议分层和软件分层 2.3 OSI七层网络模型 2.3…...

qt QGraphicsEllipseItem详解

1、概述 QGraphicsEllipseItem是Qt框架中QGraphicsItem的一个子类&#xff0c;它提供了一个可以添加到QGraphicsScene中的椭圆项。QGraphicsEllipseItem表示一个带有填充和轮廓的椭圆&#xff0c;也可以用于表示椭圆段&#xff08;通过startAngle()和spanAngle()方法&#xff…...

Python websocket

router.websocket(/chat/{flow_id}) 接口代码&#xff0c;并了解其工作流程、涉及的组件以及如何基于此实现你的新 WebSocket 接口。以下内容将分为几个部分进行讲解&#xff1a; 接口整体概述代码逐行解析关键组件和依赖关系如何基于此实现新功能示例&#xff1a;创建一个新的…...

【MySQL-5】MySQL的内置函数

目录 1. 整体学习的思维导图 2. 日期函数 ​编辑 2.1 current_date() 2.2 current_time() 2.3 current_timestamp() 2.4 date(datetime) 2.5 now() 2.6 date_add() 2.7 date_sub() 2.8 datediff() 2.9 案例 2.9.1 创建一个出生日期登记簿 2.9.2 创建一个留言版 3…...

深度学习笔记之BERT(三)RoBERTa

深度学习笔记之RoBERTa 引言回顾&#xff1a;BERT的预训练策略RoBERTa训练过程分析静态掩码与动态掩码的比较模型输入模式与下一句预测使用大批量进行训练使用Byte-pair Encoding作为子词词元化算法更大的数据集和更多的训练步骤 RoBERTa配置 引言 本节将介绍一种基于 BERT \t…...

C++知识点总结(59):背包型动态规划

背包型动态规划 一、背包 dp1. 01 背包&#xff08;限量&#xff09;2. 完全背包&#xff08;不限量&#xff09;3. 口诀 二、例题1. 和是质数的子集数2. 黄金的太阳3. 负数子集和4. NASA的⻝物计划 一、背包 dp 1. 01 背包&#xff08;限量&#xff09; 假如有这几个物品&am…...

C++:反向迭代器的实现

反向迭代器的实现与 stack 、queue 相似&#xff0c;是通过适配器模式实现的。通过传入不同类型的迭代器来实现其反向迭代器。 正向迭代器中&#xff0c;begin() 指向第一个位置&#xff0c;end() 指向最后一个位置的下一个位置。 代码实现&#xff1a; template<class I…...

webGL入门教程_04vec3、vec4 和齐次坐标总结

vec3、vec4 和齐次坐标总结 1. vec3 和 vec4 1.1 什么是 vec3 和 vec4&#xff1f; vec3&#xff1a; GLSL 中的三维向量类型&#xff0c;包含 3 个浮点数&#xff1a;(x, y, z)。常用于表示三维坐标、RGB 颜色、法线、方向等。 vec4&#xff1a; GLSL 中的四维向量类型&…...

uniapp中父组件数组更新后与页面渲染数组不一致实战记录

简单描述一下业务场景方便理解: 商品设置功能,支持添加多组商品(点击添加按钮进行增加).可以对任意商品进行删除(点击减少按钮对选中的商品设置进行删除). 问题: 正常添加操作后,对已添加的任意商品删除后,控制台打印数组正常.但是与页面显示不一致.已上图为例,选中尾…...

优化 Conda 下载速度:详细的代理配置和网络管理策略

优化 Conda 下载速度&#xff1a;详细的代理配置和网络管理策略 为了彻底解决使用 Conda 下载 PyTorch 时遇到的速度问题&#xff0c;并确保下载过程稳定可靠&#xff0c;这需要一个详细、综合的技术方案。让我们更深入地分析问题原因&#xff0c;然后详尽地解释采取的解决策略…...

服务器遭受DDoS攻击后如何恢复运行?

当服务器遭受 DDoS&#xff08;分布式拒绝服务&#xff09;攻击 后&#xff0c;恢复运行需要快速采取应急措施来缓解攻击影响&#xff0c;并在恢复后加强防护以减少未来攻击的风险。以下是详细的分步指南&#xff1a; 一、应急处理步骤 1. 确认服务器是否正在遭受 DDoS 攻击 …...

MFC音视频播放器-支持电子放大等功能

前言 本播放器在VS2019下开发&#xff0c;使用ffmpegD3D实现视频播放渲染功能。同时本播放器支持录像功能、截图功能、音视频播放功能、码流信息显示、电子放大功能等。D3D的渲染同时支持surface和texture两种方式&#xff0c;电子放大功能是在D3D Texture方式下进行实现。以下…...

c语言编程1.17蓝桥杯历届试题-回文数字

题目描述 观察数字&#xff1a;12321&#xff0c;123321 都有一个共同的特征&#xff0c;无论从左到右读还是从右向左读&#xff0c;都是相同的。这样的数字叫做&#xff1a;回文数字。 本题要求你找到一些5位或6位的十进制数字。满足如下要求&#xff1a; 该数字的各个数位之…...

el-table 纵向 横向 多级表头

<el-table :data"tableData" class"diaTable":span-method"handleSpanMethod"border:header-cell-style"{background:#292929,color:#fff}"><!-- 纵向表头 --><el-table-column label"纵向表头" width"…...

uniapp开发微信小程序笔记8-uniapp使用vant框架

前言&#xff1a;其实用uni-app开发微信小程序的首选不应该是vant&#xff0c;因为vant没有专门给uni-app设置专栏&#xff0c;可以看到目前Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本&#xff0c;并由社区团队维护 React 版本和支付宝小程序版本。 但是vant的优…...

分布式项目使用Redis实现数据库对象自增主键ID

hello。大家好&#xff0c;我是灰小猿&#xff0c;一个超会写bug的程序猿&#xff01; 在分布式项目中&#xff0c;数据表的主键ID一般可能存在于UUID或自增ID这两种形式&#xff0c;UUID好理解而且实现起来也最容易&#xff0c;但是缺点就是数据表中的主键ID是32位的字符串&a…...

npm-运行项目报错:A complete log of this run can be found .......npm-cache_logs\

1.问题 没有找到对应的某种依赖&#xff0c;node_modules出现问题。 2.解决 (1)查看对应依赖是否引入或者是由于合并分支错误 引入js或依赖不存在。谨慎删除依赖包 (2)查找对应引入依赖进行安装最后解决方法-删除依赖包清除缓存 npm cache clean --force (2)重新向同事引入…...

SolarCube: 高分辨率太阳辐照预测基准数据集

太阳能作为清洁能源在减缓气候变化中的作用日益凸显&#xff0c;其稳定的供应对电网管理至关重要。然而&#xff0c;太阳辐照受云层和天气变化的影响波动较大&#xff0c;给光伏电力的管理带来挑战&#xff0c;尤其是在调度、储能和备用系统管理方面。因此&#xff0c;精确的太…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法

深入浅出&#xff1a;JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中&#xff0c;随机数的生成看似简单&#xff0c;却隐藏着许多玄机。无论是生成密码、加密密钥&#xff0c;还是创建安全令牌&#xff0c;随机数的质量直接关系到系统的安全性。Jav…...

【2025年】解决Burpsuite抓不到https包的问题

环境&#xff1a;windows11 burpsuite:2025.5 在抓取https网站时&#xff0c;burpsuite抓取不到https数据包&#xff0c;只显示&#xff1a; 解决该问题只需如下三个步骤&#xff1a; 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...

Mac软件卸载指南,简单易懂!

刚和Adobe分手&#xff0c;它却总在Library里给你写"回忆录"&#xff1f;卸载的Final Cut Pro像电子幽灵般阴魂不散&#xff1f;总是会有残留文件&#xff0c;别慌&#xff01;这份Mac软件卸载指南&#xff0c;将用最硬核的方式教你"数字分手术"&#xff0…...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

10-Oracle 23 ai Vector Search 概述和参数

一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI&#xff0c;使用客户端或是内部自己搭建集成大模型的终端&#xff0c;加速与大型语言模型&#xff08;LLM&#xff09;的结合&#xff0c;同时使用检索增强生成&#xff08;Retrieval Augmented Generation &#…...

JAVA后端开发——多租户

数据隔离是多租户系统中的核心概念&#xff0c;确保一个租户&#xff08;在这个系统中可能是一个公司或一个独立的客户&#xff09;的数据对其他租户是不可见的。在 RuoYi 框架&#xff08;您当前项目所使用的基础框架&#xff09;中&#xff0c;这通常是通过在数据表中增加一个…...

C++ 设计模式 《小明的奶茶加料风波》

&#x1f468;‍&#x1f393; 模式名称&#xff1a;装饰器模式&#xff08;Decorator Pattern&#xff09; &#x1f466; 小明最近上线了校园奶茶配送功能&#xff0c;业务火爆&#xff0c;大家都在加料&#xff1a; 有的同学要加波霸 &#x1f7e4;&#xff0c;有的要加椰果…...

Python 实现 Web 静态服务器(HTTP 协议)

目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1&#xff09;下载安装包2&#xff09;配置环境变量3&#xff09;安装镜像4&#xff09;node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1&#xff09;使用 http-server2&#xff09;详解 …...