# 基于muduo的socks4a代理服务器 **Repository Path**: lin-xi-269/socks4 ## Basic Information - **Project Name**: 基于muduo的socks4a代理服务器 - **Description**: 采用c++14的基于muduo的socks4a代理服务器 - **Primary Language**: Unknown - **License**: LGPL-2.1 - **Default Branch**: master - **Homepage**: https://gitee.com/lin-xi-269/ - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-03-05 - **Last Updated**: 2022-06-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # A C++ Lightweight socks4 proxy 这是一个基于muduo的socks4a的代理服务器。理论上来说他是可以支持socks4和socks4a的。 并发模型选择:多线程 + Reactor + 非阻塞 + 异步DNS解析。 ### 开发部署环境 - 操作系统: Ubuntu 20.04 - 编译器: g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 - 版本控制: git - 自动化构建: Makefile - 编辑器: VSCode - 压测工具:[runtest_curl.sh](https://gitee.com/lin-xi-269/socks4/blob/master/test/runtest_curl.sh) - [chenshuo/muduo: Event-driven network library for multi-threaded Linux server in C++11 (github.com)](https://github.com/chenshuo/muduo) - [Ubuntu 20.04安装muduo的脚本](https://gitee.com/lin-xi-269/socks4/blob/master/install_muduo.sh) | Part | Part Ⅱ | | ------------------------------------------------------------ | ------------------------------------------------------------ | | [整体设计](https://gitee.com/lin-xi-269/socks4/blob/master/整体设计.md) | [性能和正确性测试](https://gitee.com/lin-xi-269/socks4/blob/master/性能和正确性测试.md) | ### Usage ```bash make # 如果没有安装muduo的话,得先安装,在根目录运行install_muduo.sh即可 ``` ### 核心功能及技术 - 采取一致性解析socks4格式,这样可以提高效率并方便同步和异步改写 - 采取异步DNS解析域名,使用回调技术 - 采取多线程异步DNS解析域名,使用runInLoop技术,解决了异步竞争问题 - 采用水位调整思想,解决了发送端和连接端速度不匹配的问题。 - 采用muduo提供的Buffer,解决了发送数据连接不匹配问题 - 构建了一个基于TCPid的hash表用于对应每一个连接,防止串话 - 连接断开的时候,应该立即断开另一方,这时如果有新的client连接尽量,如何防止串话:采用muduo的TCPConnection的name来作为key与client构建对应的hash表,则每一个连接都有自己对应的server不会串话了。 - 设置关联,这样我们就可以通过getContext得到对应的client - 为了实现异步DNS保证连接的进行,可以用或许shared_ptr的weak_ptr 也可以copy shared_ptr,延长生命期,这两种方法都是类似的。测试的速度也差不多。上一个版本用的是weak_ptr,这个版本用的是 copy shared_ptr muduo中的一些技术 - muduo中用RAII封装更多是为了方便正确释放文件描述符,以便让一个文件描述符和一个存在的TCP连接融为一体,共生共死,这样就解决了串话的问题。实现上,是TcpConnection拥有一个std::unique_ptr \即可 - TCP有一个any字段,在每一个any字段中设置server,这样也就能通过TcpConnection找到server 主要包括的模块 1. socks4a:socks4a server用于解析socks4协议,持有tunnels 2. tunnels:真正的proxy,是客户端的server,是backend的client 这里面的HighWaterMarkCallback和WriteCompleteMarkCallback 是tunnels中的一个十分有意思的思想。 3. socks4a_test: 是一个正确的发送socks4字段的客户端,支持命令行通信,用于验证proxy的正确性。 4. notsocks4a_test: 是一个不是socks4字段的错误检测程序。 5. socks4Nosync, 无异步的单线程proxy 6. 异步的单线程proxy ### 收获 高水位回调和低水位回调是我在这个项目中收获很大的两种思想,可以很好地用在客户端和服务端交互上。 学习到了TCP资源管理的手段,并且对服务器开发的多种方式。 因为我写的socks4a_test_more需要发送大量的小数量的TCP包来测试socks4a的稳定性。但由于一开始没有 设置 conn->setTcpNoDelay(true);所以我发现包是一阵一阵发送的,这严重影响到了效率,后来我想起了这个nagle算法,解决了这一点,这也证明这个socks4a是能够支持大量连接的。 顺便记录一些muduo的学习总结 ### TCP网络编程本质论 陈硕认为,TCP网络编程最本质的是处理三个半事件: 1.**连接的建立**,包括服务端接受(accept)新连接和客户端成功发起(connect) 连接。TCP连接一旦建立,客户端和服务端是平等的,可以各自收发数据。 2.**连接的断开**,包括主动断开(close、shutdown)和被动断开(read(2)返回0)。 3.**消息到达,文件描述符可读**。这是最为重要的一个事件,对它的处理方式决定了 网络编程的风格(写入操作系统的缓冲区,将由TCP协议栈负责数据的 发送与重传,不代表对方已经收到数据。 1、 在非阻塞网络编程中,如何设计并使用缓冲区?muduo用readv(2)结合栈上空间巧妙地解决了这个问题 2、如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成 内存暴涨?如何做应用层的流量控制? writerComplate And HighWaterMark 3、如何设计并实现定时器?并使之与网络IO共用一个线程,以避免锁。 使用runInLoop,将定时器的添加删除放到IO Loop中 4、muduo EventLoop采用的是epoll(4) level trigger,而不是edge trigger。 1. 一是为了与传统的poll(2)兼容,因为在文件描述符数目较少,活动文件描述符比例较高时,epoll(4)不 见得比poll(2)更高效 13 ,必要时可以在进程启动时切换Poller。 2. 二是level trigger编程更容易,以往select(2)/poll(2)的经验都可以继续用,不可能发生漏掉事件的bug。 3. 三是读写的时候不必等候出现EAGAIN,可以节省系统调用次数,降低延迟。 ## one loop per thread one loop per thread 是muduo的设计思想。 即每个线程至多有一个EventLoop对象,那么我们让EventLoop的static成员函数getEventLoopOfCurrentThread()返回这个对象。返回值可能为NULL,如果当前线程不是IO线程的话。事件循环必须在IO线程执行 ### Reactor 此项目主要是在学习陈硕先生的muduo库中,对muduo库的一次实践使用。 这次项目让我更加清楚地明白了TCP server Reactor的实现: 参考下图:muduo一共实现了以下4个类就串起了Reator 1. EventLoop:它调用Poller::poll()获得当前活动事件的Channel列表,然后依次调用每个Channel的handleEvent()函数 2. Poller:只负责IO multiplexing,不负责事件分发 3. Channel:负责某一个文件描述符的分发(不持有这个文件描述符) 4. TimerQueue:能快速地根据当前时间找到已经到期的Timer,要能高效地添加和删除Timer,EventLoop把它封装为更好用的runAt()、runAfter()、runEvery()等函数。 ![image-20220305132711621](pirture/image-20220305132711621.png) ### TCP网络库 muduo是基于Reactor的TCP网络库,它的逻辑如下图: 从poll(2)返回到再次调用poll(2)阻塞称为一次事件循环 ![image-20220305133855037](pirture/image-20220305133855037.png) muduo提供了TcpServer和TcpClient。 #### TcpServer muduo为了支持TCP server ,新增加了2个类 Acceptor:用于accept(2)新TCP连接,并通过回调通知使用者 TcpConnection:一次TCP连接 TcpConnection使用Channel来获得socket上的IO事件,它会自己处理writable事件,而把readable事件通过MessageCallback传达给客户。TcpConnection拥有TCP socket,它的析构函数会close(fd) TcpServer class的功能是管理accept(2)获得的TcpConnection,TcpServer是供用户直接使用的,生命期由用户控制。TcpServer的接口如下,用户只需要设置好callback,再调用start()即可 TcpServer内部使用Acceptor来获得新连接的fd。它保存用户提供的ConnectionCallback和MessageCallback,在新建TcpConnection的时候会原样传给TcpConnection TcpServer持有目前存活的TcpConnection的shared_ptr 每个TcpConnection对象有一个名字,这个名字是由其所属的TcpServer在创建TcpConnection对象时生成,名字是ConnectionMap(是保存TcpConnection的一个类型,会构造一个对象:connections)的key。 #### 新建连接 TcpServer新建连接的相关函数调用顺序如下所示: ![image-20220305134012586](pirture/image-20220305134012586.png) 在新连接到达时: 1. Acceptor会回调newConnection(), 后者会创建TcpConnection对象,并将它加入到ConnectionMap,设置好callback。 2. 再调用conn-connectEstablished(),其中会回调用户提供的ConnectionCallback #### TcpConnection断开连接 muduo只有一种关闭连接的方式:被动关闭。即对方先关闭连接,本地read(2)返回0,触发关闭逻辑。将来如果有必要也可以给TcpConnection新增forceClose()成员函数,用于主动关闭连接,实现很简单,调用handleClose()即可。 逻辑如下: ![image-20220305141059904](pirture/image-20220305141059904.png) 在单线程事件驱动的程序中,对象的生命期管理有时也不简单。比方说图7-55展示的例子,对方断开TCP连接,这个IO事件会触发Channel::handleEvent()调用,后者会回调用户提供的CloseCallback,而用户代码在onClose()中有可能析构Channel对象,这就造成了灾难。等于说Channel::handleEvent()执行到一半的时候,其所属的Channel对象本身被销毁了。这时程序立刻core dump就是最好的结果了。 ![image-20220302003122649](https://gitee.com/wzjia/picturetwo/raw/master/image-20220302003122649.png) muduo的解决办法是提供Channel::tie(const boost::shared_ptr&)这个函数,用于延长某些对象 的生命期,使之长过Channel::handleEvent()函数 TcpConnection::handleRead()会检查read(2)的返回值,根据返回值分别调用messageCallback_、handleClose()、handleError() TcpConnection::handleClose() 的主要功能是调用closeCallback_,这个回调绑定到TcpServer::removeConnection() TcpConnection::handleError()并没有进一步的行动,只是在日志中输出错误消息,这不影响连接的正常关闭 TcpConnection::connectDestroyed()是TcpConnection析构前最后调用的一个成员函数,它通知用户连接已断开 ### Buffer Buffer是另一个具有值语义的对象 #### TcpConnection使用Buffer作为输入缓冲 一是使用了scatter/gather IO,并且一部分缓冲区取自 stack,这样输入缓冲区足够大,通常一次readv(2)调用就能取完全部数(这里64KiB缓冲足够容纳千兆网在500μs内全速收到的数据) 二是Buffer::readFd()只调用一次read(2),而没有反复调用read(2)直到其返回EAGAIN。首先,这么做是正确的,因为muduo采用level trigger,这么做不会丢失数据或消息。其次,对追求低延迟的程序来说,这么做是高效的,因为每次读数据只需要一次系统调用。再次,这样做照顾了多个连接的公平性,不会因为某个连接上数据量过大而影响其他连接处理消息 假如muduo采用edge trigger,那么每次handleRead()至少调用两次read(2),平均起来比level trigger多一次系统调用,edge trigger不见得更高效 将来的一个改进措施是:如果n == writable+sizeof extrabuf,就再读一次。 #### TcpConnection发送数据 本节会动用Channel::WriteCallback,由于muduo采用level trigger,因此我们只在需要时才关注writable事件,否则就会造成busy loop ![image-20220302095313779](https://gitee.com/wzjia/picturetwo/raw/master/image-20220302095313779.png) shutdown()是线程安全的,它会把实际工作放到shutdownInLoop()中来做,后者保证在IO线程调用。如果当前没有正在写入,则关闭写入端 send()也是一样的,如果在非IO线程调用,它会把message复制一份,传给IO线程中的sendInLoop()来发送。这么做或许有轻微的效率损失,但是线程安全性很容易验证,我认为还是利大于弊。如果真的在乎这点性能,不如让程序只在IO线程调用send()。另外在C++11中可以使用移动语义,避免内存拷贝的开销。 sendInLoop()会先尝试直接发送数据,如果一次发送完毕就不会启用WriteCallback;如果只发送了部分数据,则把剩余的数据放入outputBuffer,并开始关注writable事件,以后在handlerWrite()中发送剩余的数据。如果当前outputBuffer_已经有待发送的数据,那么就不能先尝试发送了,因为这会造成数据乱序 当socket变得可写时,Channel会调用TcpConnection::handleWrite(),这里我们继 续发送outputBuffer_中的数据。一旦发送完毕,立刻停止观察writable事件(L160), 避免busy loop。另外如果这时连接正在关闭(L161),则调用shutdownInLoop(),继续 执行关闭过程。这里不需要处理错误,因为一旦发生错误,handleRead()会读到0字节,继而关闭连接。 注意sendInLoop()和handleWrite()都只调用了一次write(2)而不会反复调用直至它 返回EAGAIN,原因是如果第一次write(2)没有能够发送完全部数据的话,第二次调用 write(2)几乎肯定会返回EAGAIN。读者可以很容易用下面的Python代码来验证这一点。因此muduo决定节省一次系统调用,这么做不影响程序的正确性,却能降低延迟。 在level trigger模式中,数据的发送比较麻烦,因为不能一直关注writable事件, 不过数据的读取很简单 我认为理想的做法是对readable事件采用level trigger,对 writable事件采用edge trigger,但是目前Linux不支持这种设定 SIGPIPE的默认行为是终止进程,在命令行程序中这是合理的 5 ,但是在网络编程中,这意味着如果对方断开连接而本地继续写入的话,会造成服务进程意外退出 ### TcpClient 只管理一个TcpConnection TcpClient具备TcpConnection断开之后重新连接的功能,加上Connector具备反复尝试连接的功能,因此客户端和服务端的启动顺序无关紧要。可以先启动客户端,一旦服务端启动,半分钟之内即可恢复连接(由Connector:: kMaxRetryDelayMs常数控制);在客户端运行期间服务端可以重启,客户端也会自动重连 连接断开后初次重试的延迟应该有随机性,比方说服务端崩溃,它所有的客户连接同时断开,然后0.5s之后同时再次发起连接,这样既可能造成SYN丢包,也可能给服务端带来短期大负载,影响其服务质量。因此每个TcpClient应该等待一段随机的时间(0.5~2s),再重试,避免拥塞 ### 完善TCP #### SIGPIPE 直接用全局变量,构造函数::signal(SIGPIPE, SIG_IGN); #### TCP No Delay和TCP keepalive 前者的作用是禁用Nagle算法 , 避免连续发包出现延迟,这对编写低延迟网络服务很重要 后者的作用是定期探查TCP连接是否还存在 #### WriteCompleteCallback和HighWaterMarkCallback 一方面是§8.8提到的“什么时 候关注writable事件”的问题,这只带来编码方面的难度;另一方面是如果发送数据的 速度高于对方接收数据的速度,会造成数据在本地内存中堆积,这带来设计及安全性方 面的难度 WriteCompleteCallback很容易理解,如果发送缓冲区被清空,就调用它 另外一个有用的callback是HighWaterMarkCallback,如果输出缓冲的长度超过用户 指定的大小,就会触发回调(只在上升沿触发一次) 只考虑 server发给client的数据流(反过来也是一样),为了防止server发过来的数据撑爆C的 输出缓冲区,一种做法是在C的HighWaterMarkCallback中停止读取S的数据,而在C的 WriteCompleteCallback中恢复读取S的数据。这就跟用粗水管往水桶里灌水,用细水管 从水桶中取水一个道理,上下两个水龙头要轮流开合,类似PWM ### 多线程TcpServer #### EventLoopThreadPooll 用one loop per thread的思想实现多线程TcpServer的关键步骤是在新建TcpConnection时从event loop pool里挑选一个loop给TcpConnection用。也就是说多线程TcpServer自己的EventLoop只用来接受新连接,而新连接会用其他EventLoop来执行 IO。 baseLoop + vector TcpServer每次新建一个TcpConnection就会调用getNextLoop()来取得EventLoop, 如果是单线程服务,每次返回的都是baseLoop_ 多线程TcpServer的改动很简单,新建连接只改了3行代码。原来是把TcpServer自用 的loop_传给TcpConnection,现在是每次从EventLoopThreadPool取得ioLoop 总而言之,TcpServer和TcpConnection的代码都只处理单线程的情况(甚至都没有 mutex成员),而我们借助EventLoop::runInLoop()并引入EventLoopThreadPool让多线 程TcpServer的实现易如反掌。注意ioLoop和loop_间的线程切换都发生在连接建立和断 开的时刻,不影响正常业务的性能 muduo目前的设计是每个TcpServer有自己的EventLoopThreadPool #### Connector 发起连接的基本方式是调用connect(2),当socket变 得可写时表明连接建立完毕 Connector只负责建立socket连接,不负责创建TcpConnection,它的 NewConnectionCallback回调的参数是socket文件描述符 ### Epoll 实现上也就是封装了Linux接口 ![image-20220305090847013](https://gitee.com/wzjia/picturetwo/raw/master/image-20220305090847013.png)