首发地址day04高性能服务设计思想项目仓库地址https://github.com/lzs123/CProxy,欢迎fork和star!上一篇教程day01-从一个基础的socket服务开始day02真正的高并发依赖于IO多路复用day03C++项目开发配置最佳实践(vscode远程开发配置、格式化、代码检查、cmake管理配置)经过前面3节,我们已经有了开发高性能服务的基础知识,也可以搭建一个相对简单易用的C++开发环境。从这一节开始,CProxy的开发将正式开始。需求明确首先我们明确一下我们的项目能做什么?所谓内网穿透,简单来说就是利用内网穿透工具(CProxy),让局域网(LocalServer)的服务可以被公网(PublicClient)访问。原理说起来简单,CProxy本身有一个公网ip,LocalServer注册到CProxy,PublicClient访问CProxy的公网ip和端口,然后CProxy将数据转发给LocalServer,下图是整体访问数据的具体分解flowCProxy对于CProxyClient和CProxyServer,CProxyClient和LocalServer部署在同一局域网,CProxyServer部署在公网服务器上;CProxyClient启动时,将需要转发数据的LocalServer注册到CProxyServer,每注册一个LocalServer,CProxyServer就会多监听一个公网ip:port,让公网的PublicClient访问CProxyServer,最后转发数据到内网的LocalServer。在推导项目规范的具体实现细节之前,先说说项目开发过程中的一些标准项目基本目录结构。基本的目录结构在第三节已经给出。├──客户端│├──xxx.cpp│├──...├──lib│├──xxx.cpp│├──...├──服务器│├──xxx.cpp├──...├──包括│├──。..server目录是CProxy服务器目录,client目录是CProxy客户端目录。服务器端和客户端可以分别构建可执行程序;lib目录存放一些服务端和客户端调用的库函数;include目录存放了一些第三方库。引入第三方库——spdlog日志库spdlog是项目引入的日志库,也是唯一的第三方库。主要原因是项目涉及到多线程,直接使用print进行日志调试不方便;spdlog提供了比较丰富的日志格式,可以打印出日志时间戳、线程id、代码位置等信息。导入步骤项目spdlog代码仓库gitclonehttps://github.com/gabime/spdlog.git将spdlog/include/spdlog目录直接复制到CProxy项目的include目录下。代码中使用//因为`include_directories(${PROJECT_SOURCE_DIR}/include)`,//表示索引头文件时会找到根目录下的include,//所以下面的写法最终会找到${PROJECT_SOURCE_DIR}/include/spdlog/spdlog.h#include"spdlog/spdlog.h"//初始化日志格式//格式文档:https://github.com/gabime/spdlog/wiki/3.-Custom-formattingspdlog::set_pattern("[%@%H:%M:%S:%e%z][%^%L%$][线程%t]%v");//打印日志SPDLOG_INFO("cproxy服务器侦听于:{}:{}",ip,端口);import将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;对于类的私有方法,首字母小写。主要设计思路业务概念设计CProxyServerControl:CProxyServer中维护了一个ControlMap,一个Control对应一个CProxyClient,其中存放了CProxyClient的一些元信息和控制信息。Tunnel:每个Control中维护一个TunnelMap,一个Tunnel对应一个LocalServer服务。CProxyClientTunnel:在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,方便不同的处理逻辑。线程模型设计我们主要采用IO多路复用的multi-reactor多线程模型来实现高性能处理。对Reactor线程模型和IO多路复用不熟悉的同学可以在继续学习前复习第二节《day02 真正的高并发还得看IO多路复用》。thread_poolthread_pool维护工作线程列表。当reactor线程检测到有新连接建立时,可以从thread_pool中获取一个可用的worker线程,worker线程会处理新连接的读写事件,每个连接的IO操作与reactor线程分离.event_loop_threadevent_loop_thread是reactor的线程实现。每个线程都有一个事件循环(每个线程一个循环)。事件发生时的事件监听和回调处理都在这个线程中完成。thread_pool中的每一个工作线程都是一个event_loop_thread,主要负责connectionsocket的读/写事件处理。这部分reactor模式设计是关于event_loop_thread实现事件分发和事件回调的设计思路。event_loopevent_loop就是上面提到的event_loop_thread中的事件循环。Simplyput,whenanevent_loop_threadisselectedtohandletheconnectionsocketfd,fdwillregisterrelatedreadandwriteeventstotheevent_loopofthethread;当在event_loop上注册的套接字上没有事件触发时,event_loop将阻塞线程,等待I/O事件发生。event_dispatcher我们设计了一个基类event_dispatcher,每个event_loop对象都会绑定一个event_dispatcher对象,具体的事件分发逻辑由event_dispatcher提供,event_loop不关心。这是对事件分发的抽象。我们可以实现基于轮询的poll_dispatcher,或者基于epoll的epoll_dispatcher。切换时不需要修改event_loop。Channel注册在event_loop上的各种socketfd对象,我们把它们封装成channels来表示。例如,用于监听新连接的acceptor本身就是一个channel,用于交换元信息的控制连接ctl_conn和用于转发数据的tran_conn都绑定到一个channel上,用于存储socket相关信息。数据读写缓冲区想象一下有2kB的数据要发送,但是socket发送缓冲区只有1kB。有两种方法:循环调用writewrite和循环调用writecontinuously。系统将发送缓冲区中的数据发送给对端后,可以再次写入缓冲区中的空间。此时,剩余的1kB数据被写入发送缓冲区。这种方法的缺点是显而易见的。我们不知道系统什么时候会将sendbuffer中的数据发送给对端,这与当时的网络环境有一定关系。在循环期间,线程无法处理其他套接字。根据事件回调写入1kB后,write返回,将剩余的1kB数据存放在一个buffer对象中,监听socketfd的可写事件(如epoll的EPOLLOUT)。然后该线程可以自由处理其他套接字。等到fd的writable事件被触发(意味着fd当前的sendbuffer有空闲空间),然后调用write将buffer中的1kB数据写入buffer中。这样可以显着提高线程的并发处理效率。Buffer屏蔽了socket读写的细节。将数据写入缓冲区后,我们只需要告诉缓冲区在合适的时间(触发可写事件时)将数据写入套接字即可。我们不需要关心每次写了多少,还有多少没写。缓冲区设计主要考虑最小化读写开销,避免内存频繁扩缩容,最小化扩缩容时的成本消耗。总结我们首先明确了项目的具体功能需求,然后在开发过程中提出了一些规范,以保持项目代码的整洁。最后带大家说说整个CProxy项目的设计思路。从下一节开始,我们将开始结合代码深入了解这些设计的具体实现。有能力的读者也可以先直接上项目。看完这一节,再看整个项目应该就清楚多了。github地址:https://github.com/lzs123/CProxy,欢迎fork和star!如果本文对您有用,请点个赞,走起!或者关注我,我会带来更多优质内容。
