使用C++开发的web框架dlagon

论坛 期权论坛 期权     
脚本之家   2019-6-29 20:19   3683   0
脚本之家

你与百万开发者在一起



本项目仅仅是个人的玩具项目, 其中缺陷很多, 问题也很多. 首先整个网络部分是自己看了两三章UNP自己封出来的. 老版本还有一点点错误处理, 新版本完全忽略了错误处理. 另外就是过度设计, 许多的地方没有必要预留变更空间, 我都预留了.
项目地址:https://github.com/lzxZz/dlagon

http通信
一个web服务器, 其通信是基于请求/响应的. 因此一个最基本的web服务器如下图所示:


当然了,web是基于HTTP协议的, 因此请求和响应的格式都需要符合HTTP协议的格式. 请求的协议格式由浏览器控制, 响应的格式由服务端控制. 因此在开发的时候, 我们需要控制的只有响应的格式. 但是请求的格式也是需要关注的. 对于不同的参数我们需要进行不同的控制.
一个最基本的HTTP响应如下所示:
    1. 200 OK  HTTP/1.1
    复制代码
    1. Content-Type : text/html
    复制代码
    1. [/code]
    2. [*][code]Hello World
    复制代码
麻雀虽小, 五脏俱全, 响应中的三个部分(响应行, 响应头, 响应体)都是有的. 需要注意的是,在响应头和响应体之间有一个空行. (更多的信息请查看http协议的格式, 这里有一点偏离主题了).

初代机版本
一个web框架在通信上也无非是和上面做的事情一样. 框架和应用的区别就在于框架封装了应用的稳定部分流程. 假如从接受数据开始,一直编写到数据发送的web应用. 其中要经过以下步骤:
  • 数据读取
  • 协议解析(长链接还可能需要处理粘包的问题)
  • cookie读取, 判断属于哪一个session
  • 请求转发到具体的处理函数(对于不同的页面有不同的处理, 例如静态页面和动态页面就需要分开处理)
  • 具体处理, 生成响应
  • 将响应转化为字符串序列
  • 数据发送
这七个步骤, 除去4,5 步是根据需求变更的, 其他的5个步骤都是固定的. 那么框架所作的任务就是将固定的5个步骤封装起来. 预留出接口, 等待用户(web开发人员)编写处理函数, 注册路由信息即可. 在一些现代的web框架中, 路由的注册往往是通过文件来实现的. 这样一来, 代码中会变动的只有处理函数了.
因此, dlagon的结构如下图所示:


其中缺少了cookie和session的处理, 不过也无伤大雅, 开发的时候自己加上即可.
其中需要用户编写的部分就是路由和处理函数部分了.路由表我使用了
  1. key-value
复制代码
的形式,
  1. key
复制代码
为请求路径,
  1. value
复制代码
为处理函数指针. 在项目的
  1. server/route.h
复制代码
中可以看到定义.
这个版本在能够完成静态文件(只有html, js, css三种文件格式)服务之后, 渐渐的就停止了开发. 然后就对项目又一次的进行了重构. 参考了
  1. koa
复制代码
的洋葱模型.

目前开发版本
上个版本中有不够灵活的问题, 例如我想添加
  1. cookie
复制代码
  1. session
复制代码
部分, 就需要对整个框架进行修改, 在路由和解析中间强行加入一个组件.违背了设计模式中的对扩展开放, 对修改封闭的原则. 因此新的架构如下:


目前这个版本正处于开发之中, 在分支
  1. new_architecture
复制代码
中.

IServer接口
首先介绍核心的
  1. IServer
复制代码
接口.
    1. //作为接口类,
    复制代码
    1. class IServer{
    复制代码
    1. public:
    复制代码
    1.     IServer(IProtocolObjectFactory *factory,
    复制代码
    1.             INetServerSocketAdapter *server,
    复制代码
    1.             Midware *midware)
    复制代码
    1.       : factory_(factory), s
    复制代码
    1.         erver_socket_(server),
    复制代码
    1.         midware_(midware) {}
    复制代码

    1.     // IServer() = delete; // 保留默认构造函数, 用于工厂构造.
    复制代码

    1.     /**
    复制代码
    1.     * @brief 用户使用的唯一程序, 其作用为绑定本地任意ip的端口,并开始监听,处理
    复制代码
    1.     *
    复制代码
    1.     * @param port
    复制代码
    1.     */
    复制代码
    1.     void Run(int port){
    复制代码
    1.        server_socket_->Bind(port);      
    复制代码
    1.        server_socket_->Listen(1024);
    复制代码
    1.        for (;;){
    复制代码
    1.             INetClientSocketAdapter *client = server_socket_->Accept();
    复制代码
    1.             // TODO 应该使用线程池
    复制代码
    1.             std::thread th(Work, this,  client);
    复制代码
    1.             th.detach();  
    复制代码
    1.        }   
    复制代码
    1.     }
    复制代码
    1.     virtual ~IServer(){}
    复制代码

    1. private:
    复制代码
    1.    // 实际工作流程
    复制代码
    1.     static void Work(IServer *self, INetClientSocketAdapter *client){
    复制代码

    1.         // 获取请求
    复制代码
    1.         std::string str = client->Receviced();
    复制代码
    1.         Request *req =  self->factory_->RequestFromString(str);
    复制代码
    1.         Response *res = self->factory_->GetResponse();
    复制代码


    1.         // 调用中间件, 开始处理请求
    复制代码
    1.         self->midware_->WorkFlow(*req, *res);
    复制代码
    1.         //返回响应
    复制代码
    1.         const std::string result = res->ToString();
    复制代码
    1.         client->Send(result);
    复制代码
    1.         delete client;
    复制代码
    1.         delete req;
    复制代码
    1.         delete res;
    复制代码
    1.    }
    复制代码
    1. protected:
    复制代码
    1.     IProtocolObjectFactory *factory_ ;
    复制代码
    1.     INetServerSocketAdapter *server_socket_ ;
    复制代码
    1.     Midware *midware_;
    复制代码

    1. };
    复制代码
首先介绍三个
  1. protected
复制代码
成员
    1. factory_
    复制代码
    是一个抽象工厂, 用来将读取到的消息转化为对应的消息对象.
    1. server_socket_
    复制代码
    是一个网络套接字, 使用了适配器模式, 方便替换其实现为别的网络库.
    1. midware_
    复制代码
    是中间件部分, 所有的消息都通过中间件进行处理.
然后就是
  1. Work
复制代码
方法.这是本类中最最核心的内容. 因为需要用到多线程, 线程只能绑定静态成员函数. 因此函数的首个参数就是
  1. this
复制代码
指针.
然后调用网络客户端的
  1. Recevied
复制代码
方法, 接收信息.
然后使用协议的构造工厂, 构造出请求对象和响应对象.(由于没有做长链接的处理, 所有的套接字都是用一次, 因此没有粘包问题, 这里也就没有特殊处理. 如果加上了长链接这个是需要特殊处理的.)
之后就是调用中间件处理请求. 同时也将响应对象传递进去.
处理完之后, 调用响应对象的
  1. ToString
复制代码
方法, 然后将信息发送回去.
最后清理一下内存(这里打算使用智能指针, 将内存管理全部交给RAII).
接下来介绍一下中间件

Midware

    1. dlagon/interface/midware.h
    复制代码
    1. /**
    复制代码
    1. * @brief 服务器中间件抽象
    复制代码
    1. *
    复制代码
    1. * 在服务器流程中, 会自动的沿着中间件链一直执行, 通过返回值判断是否继续执行
    复制代码
    1. *
    复制代码
    1. */
    复制代码
    1. class Midware{
    复制代码
    1. public:
    复制代码
    1.     enum class MidwareState{
    复制代码
    1.         kStop,
    复制代码
    1.         kContinue,
    复制代码
    1.     };
    复制代码
    1. public:
    复制代码
    1.     void SetNext(Midware *next){
    复制代码
    1.         next_ = next;
    复制代码
    1.     }
    复制代码
  1.          //中间件执行流程
复制代码
    1.     void WorkFlow(const Request &req, Response &res);
    复制代码
    1. protected:
    复制代码
    1.      // 中间件的操作, 返回值指示是否需要继续执行
    复制代码
    1.     virtual MidwareState Handler(const Request &req, Response &res) = 0;
    复制代码
    1.     Midware *next_ = nullptr;
    复制代码
    1. };
    复制代码
    1.    
    复制代码
这个组件十分简单,
    1. next
    复制代码
    指针, 指向下一个中间件.
  • 纯虚函数
    1. Handler
    复制代码
    , 继承后重载该函数,来进行业务流程.
    1. WorkFlow
    复制代码
    函数, 用于判断是否继续执行.
  1. WrokFlow
复制代码
实现如下:
    1. void Midware::WorkFlow(const Request &req, Response &res){
    复制代码
    1.     MidwareState state =  Handler(req, res);
    复制代码
    1.     // 判断链是否继续调用
    复制代码
    1.     if (state == MidwareState::kContinue){
    复制代码
    1.         if (next_){
    复制代码
    1.             next_->Handler(req, res);
    复制代码
    1.         }  
    复制代码
    1.     }
    复制代码
    1. }
    复制代码
这里在使用职责链的时候,做了一些变化, 并没有使用常见的成员变量表示是否继续的方法, 因为考虑到多线程的问题, 而中间件又是单例的, 因此, 如果是数据成员, 就会存在数据竞争的问题. 因此我使用了函数的返回值作为状态标识.
以上就是整个框架的流程部分了. 后面的就是一些实现的细节内容. 只需要继承
  1. Midware
复制代码
, 然后根据自己的需要编写流程函数.
此外还有关于
  1. HTTP
复制代码
的内容没有介绍, 都是一些脏活, 项目中用到的大致上有下面的内容:
  • HTTP解析(字符串解析, 基本技能)
  • MIME-Type(判断请求uri的后缀)
  • cookie, session(这个在老版本有实现, 新版本暂时没有开始)
这里先说一个踩过的坑吧,
  1. HTTP
复制代码
请求的行分隔符是
  1. [/code], 在使用 [code]getline
复制代码
函数的时候, 会自动的省略掉
  1. [/code], 因此行结尾和空行一定是一个 [code]
复制代码
.
对于模板页, 配置文件等暂时都没有开发计划.
本文作者:lzxZz
声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。
写的不错?赞赏一下


长按扫码赞赏我
-END-




“互联网从此没有 BAT”


脚本之家粉丝福利,请查看!

[/url][url=http://mp.weixin.qq.com/s?__biz=MjM5NTY1MjY0MQ==&mid=2650745925&idx=4&sn=b57f206811a0a5831d7306a0ab8d93b3&chksm=befeb90b8989301d7e1a89bf478c18e48a01d9f87c2f1772ddadff9ed862d93a94de4199c1e6&scene=21#wechat_redirect]6月数据库排行:PostgreSQL和MongoDB分数罕见下降
微软劝你别再使用 IE 浏览器
痛的不只是华为,这家西方公司卡住中国芯片的脖子
一个39岁程序员的应聘被拒
入行AI,程序员为什么要学习NLP?
●  五款主流Linux发行版性能对比,不求最强但求稳

小贴士返回 上一级 搜索“Java 女程序员 大数据 留言送书 运维 算法 Chrome 黑客 Python JavaScript 人工智能 女朋友 MySQL 书籍 等关键词获取相关文章推荐。
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:100
帖子:20
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP