首发地址
day04 高性能服务设计思路
我的项目仓库地址
https://github.com/lzs123/CProxy,欢送 fork and star!
往期教程
day01- 从一个根底的 socket 服务说起
day02 真正的高并发还得看 IO 多路复用
day03 C++ 我的项目开发配置最佳实际(vscode 近程开发配置、格式化、代码查看、cmake 治理配置)
通过后面 3 节,咱们曾经具备了开发一个高性能服务的基础知识,并且还能搭建一个比拟好用的 C ++ 开发环境。
从这一节开始,将正式开始 CProxy 的开发。
需要明确
首先,先明确下咱们的我的项目到底是能干嘛?所谓内网穿透,简略讲就是在通过内网穿透工具 (CProxy), 能够让局域网的服务(LocalServer) 被公网 (PublicClient) 拜访到。
原理说起来也简略,CProxy 自身有一个公网 ip,LocalServer 注册到 CProxy 上,PublicClient 拜访 CProxy 的公网 ip 和端口,之后 CProxy 再将数据转发到 LocalServer,下图是整体拜访的数据流
CProxy 具体分为 CProxyClient 和 CProxyServer,CProxyClient 部署在与 LocalServer 同一个局域网内,CProxyServer 部署到公网服务器上;
CProxyClient 启动时,将须要进行数据转发的 LocalServer 注册到 CProxyServer 上,每注册一个 LocalServer,CProxyServer 就会多监听一个公网 ip:port,这样,公网的 PublicClient 就能通过拜访 CProxyServer,最初将数据转发到内网的 LocalServer 上。
我的项目标准
在扣具体实现细节前,咱们先讲讲我的项目开发时的一些标准
我的项目根本目录构造
根本的目录构造其实在第三节就曾经给进去了
├── client
│ ├── xxx.cpp
│ ├── ...
├── lib
│ ├── xxx.cpp
│ ├── ...
├── server
│ ├── xxx.cpp
│ ├── ...
├── include
│ ├── ...
server 目录是 CProxy 服务端目录,client 目录是 CProxy 客户端目录,server 和 client 别离能构建出可执行的程序;lib 目录则寄存一些被 server 和 client 调用的库函数;include 目录则是寄存一些第三方库。
引入第三方库 – spdlog 日志库
spdlog 是我的项目引入的一个日志库,也是惟一一个第三方库,次要是我的项目波及到多线程,间接用 print 打日志调试切实是不不便;spdlog 提供了比拟丰盛的日志格局,能够把日志工夫戳、所在线程 id、代码地位这些信息都打印进去。
引入步骤
-
我的项目 spdlog 代码仓库
git clone https://github.com/gabime/spdlog.git
- 将 spdlog/include/spdlog 目录间接拷贝到 CProxy 我的项目的 include 目录下
-
代码应用
// 因为在 CMakeLists 中曾经 `include_directories(${PROJECT_SOURCE_DIR}/include)`, // 示意在索引头文件时会查找到根目录下的 include,// 所以上面的写法最终会找到 ${PROJECT_SOURCE_DIR}/include/spdlog/spdlog.h #include "spdlog/spdlog.h" // 初始化日志格局 // format docs: https://github.com/gabime/spdlog/wiki/3.-Custom-formatting spdlog::set_pattern("[%@ %H:%M:%S:%e %z] [%^%L%$] [thread %t] %v"); // 打印日志 SPDLOG_INFO("cproxy server listen at: {}:{}", ip, port);
引入 ccache 减速编译
引入 spdlog 之后,能够发现每次编译都须要一首歌的工夫,这开发调试时频繁编译,净听歌了。为了减速编译速度,我的项目引入了 ccache,编译速度快过 5G。ccache 的原理和装置应用在《day03 C++ 我的项目开发配置最佳实际》中有具体介绍,这里就不过多废话了。
命名规定
原谅我这该死的代码洁癖,我的项目会规定一些命名规定,让代码读起来更优雅。。。,命名规定并没有什么规范,只有一个团队或一个我的项目内对立就行。
我的项目的命名规定大体是参考 google 的 C ++ 我的项目格调:https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/
- 文件名:全副小写,单词之间通过下划线连贯,C++ 文件用.cpp 结尾,头文件用.h 结尾
- 类型 / 构造体:每个单词首字母均大写,如:MyExcitingClass
- 变量名:全副小写, 单词之间用下划线连贯;类的公有成员变量以下划线结尾
-
函数名:惯例函数名中每个单词首字母均大写,如:AddTableEntry;对于类的公有办法,首字母小写。
次要设计思路
业务概念设计
CProxyServer
- Control:在 CProxyServer 中会保护一个 ControlMap,一个 Control 对应一个 CProxyClient,存储 CProxyClient 的一些元信息和管制信息。
-
Tunnel:每个 Control 中会保护一个 TunnelMap,一个 Tunnel 对应一个 LocalServer 服务。
CProxyClient
-
Tunnel:在 CProxyClient 端,也会保护一个 TunnelMap,每个 Tunnel 对应一个 LocalServer 服务
CProxyServer 端的 Tunnel 与 CProxyClient 端的 Tunnel 存储的内容不一样,设计为两个不同的类。
连贯设计
整个内网穿透须要的连贯可分为两类:CProxyClient 和 CProxyServer 之间传输元信息的管制连贯和用于传输数据的数据连贯
- 管制连贯(ctl_conn)
用于 CProxyClient 和 CProxyServer 之间各种事件的告诉,比方 CProxyClient 向 CProxyServer 申请注册 LocalServer、CProxyServer 告诉 CProxyClient 有新申请拜访等
- 数据连贯(tran_conn)
用于承载转发理论业务数据;业务数据传输会在 LocalServer<->CProxyClient、CProxyClient<->CProxyServer 和 CProxyServer<->PublicClient 三处中央进行。所以数据连贯又细分为 local_conn、proxy_conn 和 public_conn,不便进行不同的解决逻辑。
线程模型设计
咱们次要采纳多 Reactor 多线程模型配合 IO 多路复用实现高性能解决。
对 Reactor 线程模型和 IO 多路复用不熟的同学能够先回顾下第二节《day02 真正的高并发还得看 IO 多路复用》,再持续往下学习。
- thread_pool
thread_pool 保护了一个工作线程列表,当 reactor 线程监听到有新连贯建设时,能够从 thread_pool 中获取一个可用的工作线程,并由该工作线程解决新连贯的读写事件,将每个连贯的 IO 操作与 reactor 线程拆散。
- event_loop_thread
event_loop_thread 是 reactor 的线程实现,每个线程都有一个事件循环(One loop per thread),事件的监听及事件产生时的回调解决都是在这个线程中实现的。
thread_pool 中的每个工作线程都是一个 event_loop_thread, 次要负责连贯套接字的 read/write 事件处理。
反应堆模式设计
这一部分讲的是 event_loop_thread 实现事件散发和事件回调的设计思路。
- event_loop
event_loop 就是下面提到的 event_loop_thread 中的事件循环。简略来说,一个 event_loop_thread 被选中用来解决连贯套接字 fd 时,fd 会注册相干读写事件到该线程的 event_loop 上;当 event_loop 上注册的套接字都没有事件触发时,event_loop 会阻塞线程,期待 I / O 事件产生。
- event_dispatcher
咱们设计一个基类 event_dispatcher,每个 event_loop 对象会绑定一个 event_dispatcher 对象,具体的事件散发逻辑都是由 event_dispatcher 提供,event_loop 并不关怀。
这是对事件散发的一种形象。咱们能够实现一个基于 poll 的 poll_dispatcher,也能够是一个基于 epoll 的 epoll_dispatcher。切换时 event_loop 并不需要做批改。
- channel
对各种注册到 event_loop 上的套接字 fd 对象,咱们都封装成 channel 来示意。比方用于监听新连贯的 acceptor 自身就是一个 channel,用于替换元信息的管制连贯 ctl_conn 和用于转发数据的 tran_conn 都有绑定一个 channel 用于寄存套接字相干信息。
数据读写
- buffer
试想一下,有 2kB 的数据须要发送,但套接字发送缓冲区只有 1kB。有两种做法:
循环调用 write 写入
在一个循环中一直进行 write 调用,等到零碎将发送缓冲区的数据发送到对端后,缓冲区的空间就又能从新写入,这个时候能够把残余的 1kB 数据写到发送缓冲区中。
这种做法的毛病很显著,咱们并不知道零碎什么时候会把发送缓冲区的数据发送到对端,这与过后的网络环境有关系。在循环过程中,线程无奈解决其余套接字。
基于事件回调
在写入 1kB 之后,write 返回,将残余 1kB 数据寄存到一个 buffer 对象中,并且监听套接字 fd 的可写事件(比方 epoll 的 EPOLLOUT)。而后线程就能够去解决其余套接字了。等到 fd 的可写事件触发(代表以后 fd 的发送缓冲区有闲暇空间),再调用 write 将 buffer 中的 1kB 数据写入缓冲区。这样能够明显提高线程的并发解决效率。
buffer 屏蔽了套接字读写的细节。将数据写入 buffer 后,只有在适合的机会(可写事件触发时),通知 buffer 往套接字写入数据即可,咱们并不需要关怀每次写了多少,还剩多少没写。
buffer 设计时次要思考尽量减少读写老本、防止频繁的内存扩缩容以及尽量减少扩缩容时的老本耗费。
总结
咱们先是明确了我的项目的具体性能需要,而后提了开发过程中的一些标准,以便放弃我的项目代码整洁。最初再是带着大家讲了下 CProxy 整个我的项目的设计思路。
从下一节开始,就开始配合代码深刻理解这些设计的具体实现。有能力的读者也能够先间接去看我的项目,读完这节后再看整个我的项目应该会清晰很多。
github 地址:https://github.com/lzs123/CProxy,欢送 fork and star!
如果本文对你有用,点个赞再走吧!或者关注我,我会带来更多优质的内容。