【计网】实现reactor反应堆模型 --- 框架搭建

实现Reactor反应堆模型
- 1 前言
- 2 框架搭建
- 3 准备工作
- 4 Reactor类的设计
- 5 Connection连接接口
- 6 回调方法
1 前言
到目前为止,我学习了计算机网络,了解了网络传输的过程,理解网络协议栈的层与层之间的关系。实践了使用TCP进行的网络编程,也了解了协议的编写,实际了http协议下的通信过程。
最近学习了五种IO模型,可以通过多路转接EPOLL提高读取效率。
那么现在是否可以将多路转接与网络结合,编写一个高效处理网络请求的反应堆模型Reactor。今天我们搭建基础的结构。
2 框架搭建

我们想要搭建的是这样的结构:
- 最底层是Reactor:负责事件派发,管理connection套接字连接。可以添加监听套接字与普通套接字,其中都有对应的回调方法。可以通过套接字类型赋予连接对应的回调方法。通过多路转接IO获取就绪事件,找到对应connection执行事件。
- Connection连接:管理文件描述符的连接对象,内部有这个文件描述符的输入输出缓冲区,回调函数,客户端信息,就绪事件集。等待Reactor调用回调方法。
- Listener监听:这是专门管理监听套接字的对象,里面有对于监听套接字的方法,可以获取新连接。作为监听套接字connection的回调方法
- HandlerConnection普通套接字 :这是针对普通套接字的对象,里面有对于普通套接字事件就绪的处理方法类。
最底层的就是这三层结构。下面我们来实现这三层结构。
3 准备工作
在实现三层结构之前,我们先对多路转接IO进行封装,让代码尽可能解耦:
对于多路转接,我们设计一个基类,作为上层调用的统一接口。然后继承出子类Epoll poll select,在子类中分别实现对应的方法。
这里只提供了Epoll的封装:
- 构造函数:构造时创建EPOLL模型,获得EPOLLfd。
- AddEvent:添加事件,调用epoll_ctl_add方法即可。
- Wait:获取底层就绪事件,直接使用epoll_wait即可
#pragma once
#include <iostream>
#include <stdlib.h>
#include <sys/epoll.h>
#include "Log.hpp"
#include "Comm.hpp"using namespace log_ns;// 多路复用基类
class Mutliplex
{
public:Mutliplex(/* args */){}virtual bool AddEvent(int fd, uint32_t events) = 0;virtual int Wait(struct epoll_event revs[], int num, int timeout) = 0;~Mutliplex(){}
};// epoll poll select基类
class Epoller : public Mutliplex
{
private:static const int size = 128;public:Epoller(){_epollfd = ::epoll_create(size);if (_epollfd < 0){LOG(ERROR, "epoll create failed!\n");exit(EPOLL_CREATE);}}std::string EventToString(uint32_t revents){std::string ret;if (revents & EPOLLIN)ret += "EPOLLIN";if (revents & EPOLLOUT)ret += "| EPOLLOUT";return ret;}bool AddEvent(int fd, uint32_t events){struct epoll_event ev;ev.data.fd = fd;ev.events = events;int n = ::epoll_ctl(_epollfd, EPOLL_CTL_ADD, fd, &ev);if (n < 0){LOG(ERROR, "epoll_ctl add failed , errno:%d", errno);return -1;}LOG(INFO, "epoll_ctl add fd:%d , events:%s\n", fd, EventToString(events).c_str());return true;}int Wait(struct epoll_event revs[], int num, int timeout){return ::epoll_wait(_epollfd, revs, num, timeout);}~Epoller(){}private:int _epollfd;
};
4 Reactor类的设计
之前的TcpServer等服务端都要在内部封装_listensock。如果封装了监听套接字那么代码结构就定型了,就必须要有对监听套接字的处理。而这里我们想将Reactor设计一个管理connection连接的类,不需要针对监听套接字进行特殊处理
成员变量:
- 通过fd映射
Connection*对象的哈希表_conn - 判断是否启动
bool isrunning - 构建一个Multipex对象 , 构造时建立epoll指针,负责处理多路转接IO
- 就绪事件组
struct epoll_event revs[gnum] - 针对监听套接字的方法集,在添加连接时可以将方法设置进入
connection中 - 针对普通套接字的方法集。
回调方法的类型为using handler_t = std::function<void(Connection *conn)>;
#pragma once
#include <string>
#include <iostream>
#include <memory>
#include <unordered_map>#include "Connection.hpp"
#include "Epoller.hpp"using namespace log_ns;class Reactor
{
private:static const int gnum = 128;public:Reactor() : _epoller(std::make_unique<Epoller>()), _isrunning(false){}void SetOnNormal(handler_t OnRecver, handler_t OnSender, handler_t OnExcepeter){_OnRecver = OnRecver;_OnSender = OnSender;_OnExcepeter = OnExcepeter;}void SetOnConnect(handler_t OnConnect){_OnConnect = OnConnect;}// 加入连接void AddConnection(int fd, uint32_t events, const InetAddr &addr, int type){}void Dispatcher(){}~Reactor(){}private:// fd 映射连接表std::unordered_map<int, Connection *> _conn;// 是否启动bool _isrunning;std::unique_ptr<Mutliplex> _epoller;// 事件数组struct epoll_event revs[gnum];//_listen新连接到来handler_t _OnConnect;// 处理普通fd IOhandler_t _OnRecver;handler_t _OnSender;handler_t _OnExcepeter;
};
- Addconnection接口 :首先通过
fd events与客户端信息和连接类型建立connection, 进行设置对应的事件集, 然后根据type判断类型,设置connection的上层处理回调方法。注意这里要对conn与Reactor进行关联 !后续connection的模块进行讲解,设置addr方便打印日志(可以知道是哪一个客户端);然后通过fd与events托管给epoll 进行添加事件 。最后将连接放入哈希表中。 - IsConnExists判断当前连接是否存在
- Dispatch()事件派发接口:进行while循环,获取底层哪些事件就绪 储存在成员变量
struct epoll_event revs[gnum],根据返回值 n 对n个事件进行处理!这里只处理 ERR HUP IN OUT 使用if语句ERR HUP直接设置为IN OUT后续统一处理IN事件就绪 事件派发 通过_conn[fd]找到对应连接 执行对应事件的回调函数(注意保证连接存在 且 回调方法存在)。
完整代码如下:
#pragma once
#include <string>
#include <iostream>
#include <memory>
#include <unordered_map>#include "Connection.hpp"
#include "Epoller.hpp"using namespace log_ns;class Reactor
{
private:static const int gnum = 128;public:Reactor() : _epoller(std::make_unique<Epoller>()), _isrunning(false){}void SetOnNormal(handler_t OnRecver, handler_t OnSender, handler_t OnExcepeter){_OnRecver = OnRecver;_OnSender = OnSender;_OnExcepeter = OnExcepeter;}void SetOnConnect(handler_t OnConnect){_OnConnect = OnConnect;}// 加入连接void AddConnection(int fd, uint32_t events, const InetAddr &addr, int type){// 1. 通过 fd 构建一个 connection指针 set对应的事件集Connection *conn = new Connection(fd);conn->SetReactor(this);conn->SetEvents(events);conn->SetConnectionType(type);conn->SetAddr(addr);// 2. TODO 设置对connection的上层处理 设置回调方法if (conn->Type() == ListenConnection){conn->RegisterHandler(_OnConnect, nullptr, nullptr); // 设置方法}else{conn->RegisterHandler(_OnRecver, _OnSender, _OnExcepeter); // 设置方法}// 3. fd 与 events 托管给epoll 添加事件 出错直接 return;int n = _epoller->AddEvent(fd, events);// 4. 托管给_connection_conn.insert(std::make_pair(fd, conn));// 添加连接成功}// 判断连接是否存在bool IsConnExist(int fd){return _conn.find(fd) != _conn.end();}void LoopOnce(int timeout){// 获取底层事件int n = _epoller->Wait(revs, gnum, -1);for (int i = 0; i < n; i++){// 文件描述符int fd = revs[i].data.fd;// 就绪事件uint32_t revents = revs[i].events;// 处理IN OUT ERR HUPif (revents & EPOLLERR)revents |= (EPOLLIN | EPOLLOUT);if (revents & EPOLLHUP)revents |= (EPOLLIN | EPOLLOUT);if (revents & EPOLLIN){// 调用回调方法if (IsConnExist(fd) && _conn[fd]->_handler_recver)_conn[fd]->_handler_recver(_conn[fd]);}if (revents & EPOLLOUT){// 调用回调方法if (IsConnExist(fd) && _conn[fd]->_handler_sender)_conn[fd]->_handler_sender(_conn[fd]);}}}void Dispatcher(){_isrunning = true;int timeout = -1;while (true){LoopOnce(timeout);PrintDebug();//打印托管的fd列表}_isrunning = false;}void PrintDebug(){std::string s = "已建立的连接:";for (auto &conn : _conn){s += std::to_string(conn.first) + ' ';}LOG(DEBUG, "epoll 管理的fd列表: %s\n", s.c_str());}~Reactor(){}private:// fd 映射连接表std::unordered_map<int, Connection *> _conn;// 是否启动bool _isrunning;std::unique_ptr<Mutliplex> _epoller;// 事件数组struct epoll_event revs[gnum];//_listen新连接到来handler_t _OnConnect;// 处理普通fd IOhandler_t _OnRecver;handler_t _OnSender;handler_t _OnExcepeter;
};
5 Connection连接接口
- 成员变量:
- 文件描述符
fd - 需要关心的事件集
events - 输入缓冲区 输出缓冲区
- 三种事件的回调方法
- 设置一个Reactor* _R
- 文件描述符
- SetEvents接口:通过传入events 初始化 events
- Events接口返回事件集
- Sockfd返回对应fd
- RegisterHandler接口快速设置回调方法
- SetReactor(Reactor* R)接口 connection与Reactor进行绑定,执行自己属于的Reactor
对于这个Reactor* _R 指针,是监听套装字获取到连接时发挥作用。当监听套接字的事件就绪,在回调方法中可以通过参数Connection取出内部的_R指针,找到对应的Reactor,进行AddConnection操作。
#pragma once#include <iostream>
#include <string>
#include <functional>#include "InetAddr.hpp"class Connection;
class Reactor;using handler_t = std::function<void(Connection *conn)>;#define ListenConnection 0
#define NormalConnection 1class Connection
{
public:Connection(int fd) : _sockfd(fd){}void RegisterHandler(handler_t recver, handler_t sender, handler_t excepter){_handler_recver = recver; // 处理读取_handler_sender = sender; // 处理写入_handler_excepter = excepter; // 处理异常}void SetEvents(uint32_t events){_events = events;}void SetAddr(const InetAddr &addr){_addr = addr;}int Sockfd(){return _sockfd;}uint32_t Events(){return _events;}int Type(){return _type;}void SetReactor(Reactor *R){_R = R;}void SetConnectionType(int type){_type = type;}Reactor *GetReactor(){return _R;}InetAddr GerInetAddr(){return _addr;}void AppendInbuffer(const std::string &in){_inbuffer += in;}std::string &Inbuffer(){return _inbuffer;}~Connection(){}private:int _sockfd; // 套接字fduint32_t _events; // 事件集std::string _inbuffer; // 输入缓冲区std::string _outbuffer; // 输出缓冲区Reactor *_R;int _type;InetAddr _addr;public:handler_t _handler_recver; // 处理读取handler_t _handler_sender; // 处理写入handler_t _handler_excepter; // 处理异常
};
6 回调方法
这里需要两种回调方法类,一种针对监听套接字,一种针对普通套接字。
- Listener统一管理Tcp连接模块,管理_listensock
- 成员变量 :
- std::unique_ptr _listensock Tcp套接字对象
- int _port;端口号
- 通过端口号进行构造TcpSocket
ListenSock接口返回_listensock的fd。Accepter(conn* , int* code)方法获取连接并得到文件描述符 (这里采用ET模式)首先将listensockfd读取设置为非阻塞读取,然后进行while(true)进行非阻塞读取 ,根据Accepter返回的错误码通过code返回 通过错误码进行判断,当读取到一个新的fd时,通过conn的Reactor指针调用AddConnection加入新连接!
#pragma once
#include <memory>
#include <iostream>
#include "Socket.hpp"
#include "Connection.hpp"using namespace log_ns;
using namespace socket_ns;// 处理listen套接字的读写
class Listener
{
public:Listener(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>(port)){_listensock->BuildListenSocket(_port);}int ListenSockfd(){return _listensock->GetSockfd();}void Accepter(Connection *conn){LOG(DEBUG, "%d socket ready\n", conn->Sockfd());// 非阻塞式读取while (true){errno = 0;int code = 0;InetAddr addr;int sockfd = _listensock->Accepter(&addr, &code);if (sockfd > 0){LOG(INFO, "成功获取连接, 客户端:%s sockfd:%d\n", addr.AddrStr().c_str(), sockfd);conn->GetReactor()->AddConnection(sockfd, EPOLLIN | EPOLLET, addr, NormalConnection);}else{if (code == EWOULDBLOCK){// 读取完毕LOG(INFO, "底层数据全部读取完毕!\n");break;}// 信号中断else if (code == EINTR){continue;}else{LOG(ERROR, "获取连接失败!\n");break;}}}}~Listener(){}private:uint16_t _port;std::unique_ptr<Socket> _listensock;
};
- 处理普通连接读写问题,这个的设计就比较简单了,注意其只复杂数据的读取,协议解析需要交给上层进行处理!
- HandlerRecver(conn*):我们先实现读取的逻辑!
- HandlerSender(conn*):后续实现
- HandlerExcepter(conn*):后续实现
#include <sys/types.h>
#include <sys/socket.h>
// 不应该让HandlerConnection处理报文
class HandlerConnection
{
private:const static int buffersize = 512;public:HandlerConnection(handler_t process) : _process(process){}void Recver(Connection *conn){// LOG(DEBUG , "client发送信息: %d\n" , conn->Sockfd());// 进行正常读写 --- 非阻塞读取while (true){char buffer[buffersize];int n = ::recv(conn->Sockfd(), buffer, sizeof(buffer) - 1, 0);if (n > 0){// buffer是一个数据块 添加到conn的输入缓冲区中buffer[n] = 0;conn->AppendInbuffer(buffer);// 数据交给上层处理}else if (n == 0){// 连接断开LOG(INFO, "客户端[%s]退出, 服务器准备关闭fd: %d\n", conn->GerInetAddr().AddrStr().c_str(), conn->Sockfd());conn->_handler_excepter(conn); // 统一执行异常处理}else{// 本轮数据读完了if (errno == EWOULDBLOCK){// 这是唯一出口break;}// 信号中断else if (errno == EINTR){continue;}// 出现异常else{conn->_handler_excepter(conn);return;}}}// 读取完毕,我们应该处理数据了!// 加入协议std::cout << "Inbuffer 内容:" << conn->Inbuffer() << std::endl;_process(conn);}void Sender(Connection *conn){}void Excepter(Connection *conn){}~HandlerConnection(){}private:handler_t _process;
};
至此,Reactor反应堆模型的框架已经搭建好了,下一篇文章我们将在这个的基础之上进行协议解析与数据处理!并设计如何将数据发回。这里只是简单的实现读取数据的逻辑!
相关文章:
【计网】实现reactor反应堆模型 --- 框架搭建
没有一颗星, 会因为追求梦想而受伤, 当你真心渴望某样东西时, 整个宇宙都会来帮忙。 --- 保罗・戈埃罗 《牧羊少年奇幻之旅》--- 实现Reactor反应堆模型 1 前言2 框架搭建3 准备工作4 Reactor类的设计5 Connection连接接口6 回调方法 1 …...
力扣中等难度热题——长度为K的子数组的能量值
目录 题目链接:3255. 长度为 K 的子数组的能量值 II - 力扣(LeetCode) 题目描述 示例 提示: 解法一:通过连续上升的长度判断 Java写法: C写法: 相比与Java写法的差别 运行时间 时间复杂…...
JSON格式
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人和机器阅读和解析。它基于JavaScript的对象表示法,但被广泛用于多种编程语言。 JSON中的数据类型 字符串(String):用双引…...
O-RAN前传Spilt Option 7-2x
Spilt Option 7-2x 下行比特处理上行比特处理相关文章: Open Fronthaul wrt ORAN 联盟被称为下层拆分(LLS),其目标是提高电信市场的灵活性和竞争力。下层拆分是指无线电单元(RU) 和分布式单元(DU) 之间的拆分。 O-RAN前传接口可以在 eCPRI 上传输。eCPR…...
【GeoJSON在线编辑平台】(2)吸附+删除+挖孔+扩展
前言 在上一篇的基础上继续开发,补充上吸附功能、删除矢量、挖孔功能。 实现 1. 吸附 参考官方案例:Snap Interaction 2. 删除 通过 removeFeature 直接移除选中的要素。 3. 挖孔 首先是引入 Turf.js ,然后通过 mask 方法来实现挖孔的…...
确定图像的熵和各向异性 Halcon entropy_gray 解析
1、图像的熵 1.1 介绍 图像熵(image entropy)是图像“繁忙”程度的估计值,它表示为图像灰度级集合的比特平均数,单位比特/像素,也描述了图像信源的平均信息量。熵指的是体系的混乱程度,对于图像而言&#…...
大数据-214 数据挖掘 机器学习理论 - KMeans Python 实现 算法验证 sklearn n_clusters labels
点一下关注吧!!!非常感谢!!持续更新!!! 目前已经更新到了: Hadoop(已更完)HDFS(已更完)MapReduce(已更完&am…...
算法通关(3) -- kmp算法
KMP算法的原理 从题目引出 有两个字符串s1和s2,判断s1字符串是否包含s2字符串,如果包含返回s1包含s2的最左开头位置,不包含返回-1,如果是按照暴力的方法去匹配,以s1的每个字符作为开头,用s2的整体去匹配,…...
5G网卡network connection: disconnected
日志 5G流程中没有报任何错误,但是重新拿地址了,感觉像是驱动层连接断开了,dmesg中日志如下: [ 1526.558377] ippassthrough:set [ ip10.108.40.47 mask27 ip_net10.108.40.32 router10.108.40.33 dns221.12.1.227 221.12.33.227] br-lan […...
微积分复习笔记 Calculus Volume 1 - 4.9 Newton’s Method
4.9 Newton’s Method - Calculus Volume 1 | OpenStax...
Flutter自定义矩形进度条实现详解
在Flutter应用开发中,进度条是一个常见的UI组件,用于展示任务的完成进度。本文将详细介绍如何实现一个支持动画效果的自定义矩形进度条。 功能特点 支持圆角矩形外观平滑的动画过渡效果可自定义渐变色可配置边框宽度和颜色支持进度更新动画 实现原理 …...
如何设置 TORCH_CUDA_ARCH_LIST 环境变量以优化 PyTorch 性能
引言 在深度学习领域,PyTorch 是一个广泛使用的框架,它允许开发者高效地构建和训练模型。为了充分利用你的 GPU 硬件,正确设置 TORCH_CUDA_ARCH_LIST 环境变量至关重要。这个变量告诉 PyTorch 在构建过程中应该针对哪些 CUDA 架构版本进行优…...
CSS的三个重点
目录 1.盒模型 (Box Model)2.位置 (position)3.布局 (Layout)4.低代码中的这些概念 在学习CSS时,有三个概念需要重点理解,分别是盒模型、定位、布局 1.盒模型 (Box Model) 定义: CSS 盒模型是指每个 HTML 元素在页面上被视为一个矩形盒子。…...
【笔记】前后端互通中前端登录无响应
后来的前情提要 : 后端的ip地址在本地测试阶段应该设置为localhost 前端中写cors的配置 后端也要写cors的配置 且两者的url都要为localhost 前端写的baseUrl是指定对应的后端的ip地址以及端口号 很重要 在本地时后端的IP的地址也必须为本地的 F12的网页报错是&a…...
AI引领PPT创作:迈向“免费”时代的新篇章?
AI引领PPT创作:迈向“免费”时代的新篇章? 在信息爆炸的时代,演示文稿(PPT)作为传递信息和展示观点的重要工具,其制作效率和质量直接关系到演讲者的信息传递效果。随着人工智能(AI)…...
HTB:Perfection[WriteUP]
目录 连接至HTB服务器并启动靶机 1.What version of OpenSSH is running? 使用nmap对靶机TCP端口进行开放扫描 2.What programming language is the web application written in? 使用浏览器访问靶机80端口页面,并通过Wappalyzer查看页面脚本语言 3.Which e…...
鸿蒙next打包流程
目录 下载团结引擎 添加开源鸿蒙打包支持 打包报错 路径问题 安装DevEcoStudio 可以在DevEcoStudio进行打包hap和app 包结构 没法直接用previewer运行 真机运行和测试需要配置签名,DevEcoStudio可以自动配置, 模拟器安装hap提示报错 安装成功,但无法打开 团结1.3版本新增工具…...
uni-app 实现自定义底部导航
原博:https://juejin.cn/post/7365533404790341651 在开发微信小程序,通常会使用uniapp自带的tabBar实现底部图标和导航,但现实有少量应用使用uniapp自带的tabBar无法满足需求,这时需要自定义底部tabBar功能。 例如下图的需求&am…...
Vue前端开发:animate.css第三方动画库
在实际的项目开发中,如果自定义元素的动画,不仅效率低下,代码量大,而且还存在浏览器的兼容性问题,因此,可以借助一些优秀的第三动画库来协助完成动画的效果,如animate.css和gsap动画库ÿ…...
Java中的I/O模型——BIO、NIO、AIO
1. BIO(Blocking I/O) 1. 1 BIO(Blocking I/O)模型概述 BIO,即“阻塞I/O”(Blocking I/O),是一种同步阻塞的I/O模式。它的主要特点是,当程序发起I/O请求(比如…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
基于stm32F10x 系列微控制器的智能电子琴(附完整项目源码、详细接线及讲解视频)
注:文章末尾网盘链接中自取成品使用演示视频、项目源码、项目文档 所用硬件:STM32F103C8T6、无源蜂鸣器、44矩阵键盘、flash存储模块、OLED显示屏、RGB三色灯、面包板、杜邦线、usb转ttl串口 stm32f103c8t6 面包板 …...
CMS内容管理系统的设计与实现:多站点模式的实现
在一套内容管理系统中,其实有很多站点,比如企业门户网站,产品手册,知识帮助手册等,因此会需要多个站点,甚至PC、mobile、ipad各有一个站点。 每个站点关联的有站点所在目录及所属的域名。 一、站点表设计…...
python打卡day47
昨天代码中注意力热图的部分顺移至今天 知识点回顾: 热力图 作业:对比不同卷积层热图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import D…...
Caliper 配置文件解析:config.yaml 和 fisco-bcos.json 附加在caliper中执行不同的合约方法
Caliper 配置文件解析:config.yaml 和 fisco-bcos.json Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO…...
在 Vue 的template中使用 Pug 的完整教程
在 Vue 的template中使用 Pug 的完整教程 引言 什么是 Pug? Pug(原名 Jade)是一种高效的网页模板引擎,通过缩进式语法和简洁的写法减少 HTML 的冗长代码。Pug 省略了尖括号和闭合标签,使用缩进定义结构,…...
KKCMS部署
目录 账号 网站目录 快看CMS使用手册 http://10.141.19.241/kkcms/install/ 常规思路:页面点点观察url变化,参数 常规思路:点一个功能模块抓包看什么东西,正确是什么样,错误的是什么样,构造参数。 账号…...
