共计 4377 个字符,预计需要花费 11 分钟才能阅读完成。
文章首发:聊聊第一个开源我的项目 – CProxy 作者:会玩 code
初衷
最近在学 C ++,想写个我的项目练练手。对网络比拟感兴趣,之前应用过 ngrok(GO 版本的内网穿透我的项目),看了局部源码,想把本人的一些优化想法用 C ++ 实现一下,便有了这个我的项目。
我的项目介绍
CProxy 是一个反向代理,用户可在本人内网环境中启动一个业务服务,并在同一网络下启动 CProxyClient,用于向 CProxyServer 注册服务。CProxyClient 和 CProxyServer 之间会创立一个隧道,外网能够通过拜访 CProxyServer,数据转发到 CProxyClient,从而被业务服务接管到。实现内网服务被外网拜访。
我的项目地址
https://github.com/lzs123/CProxy.git
应用办法
bash build.sh
// 启动服务端
{ProjectDir}/build/server/Server --proxy_port=8090 --work_thread_nums=4
(另一个终端)
// 启动客户端
{ProjectDir}/build/client/Client --local_server=127.0.0.1:7777 --cproxy_server=127.0.0.1:8080
我的项目亮点
- 应用 epoll 作为 IO 多路复用的实现
- 数据转发时,应用 splice 零拷贝,缩小 IO 性能瓶颈
- 数据连贯和管制连贯接耦,防止相互影响
- 采纳 Reactor 多线程模型,充分利用多核 CPU 性能
流程架构
角色
- LocalServer: 内网业务服务
- CProxyClient: CProxy 客户端,个别与 LocalServer 部署在一起,对接 CProxyServer 和 InnerServer
- CProxyServer: CProxy 服务端
- PublicClient: 业务客户端
数据流
PublicClient 先将申请打到 CProxyServer,CProxyServer 辨认申请是属于哪个 CProxyClient,而后将数据转发到 CProxyClient,CProxyClient 再辨认申请是属于哪个 LocalServer 的,将申请再转发到 LocalServer,实现数据的转发。
工作流程
先介绍 CProxyServer 端的两个概念:
- Control:在 CProxyServer 中会保护一个 ControlMap,一个 Control 对应一个 CProxyClient,存储 CProxyClient 的一些元信息和管制信息
- Tunnel:每个 Control 中会保护一个 TunnelMap,一个 Tunnel 对应一个 LocalServer 服务
在 CProxyClient 端,也会保护一个 TunnelMap,每个 Tunnel 对应一个 LocalServer 服务,只不过 Client 端的 Tunnel 与 Server 端的 Tunnel 存储的内容略有差别
启动流程
CProxyServer
- 实现几种工作线程的初始化。
- 监听一个 CtlPort,期待 CProxyClient 连贯。
CProxyClient
- 实现对应线程的初始化。
- 而后连贯 Server 的 CtlPort,此连贯称为 ctl_conn, 用于 client 和 server 之前管制信息的传递。
- 申请注册 Control,获取 ctl_id。
- 最初再依据 Tunnel 配置文件实现多个 Tunnel 的注册。须要留神的是,每注册一个 Tunnel,Server 端就会多监听一个 PublicPort,作为内部拜访 LocalServer 的入口。
数据转发流程
- Web 上的 PublicClient 申请 CProxyServer 上的 PublicPort 建设连贯;CProxyServer 接管连贯申请,将 public_accept_fd 封装成 PublicConn。
- CProxyServer 通过 ctl_conn 向 client 发送 NotifyClientNeedProxyMsg 告诉 Client 须要创立一个 proxy。
- Client 收到后,会别离连贯 LocalServer 和 CProxyServer:
3.1. 连贯 LocalServer,将 local_conn_fd 封装成 LocalConn。
3.2. 连贯 ProxyServer 的 ProxyPort,将 proxy_conn_fd 封装成 ProxyConn,并将 LocalConn 和 ProxyConn 绑定。 - CProxyServer 的 ProxyPort 收到申请后,将 proxy_accept_fd 封装成 ProxyConn,将 ProxyConn 与 PublicConn 绑定。
- 尔后的数据在 PublicConn、ProxyConn 和 LocalConn 上实现转发传输。
连贯治理
复用 proxy 连贯
为了防止频繁创立销毁 proxy 连贯,在实现数据转发后,会将 proxyConn 放到闲暇队列中,期待下次应用。
proxy_conn 有两种模式 – 数据传输模式和闲暇模式。在数据传输模式中,proxy_conn 不会去读取解析缓冲区中的数据,只会把数据通过 pipe 管道转发到 local_conn; 闲暇模式时,会读取并解析缓冲区中的数据,此时的数据是一些管制信息,用于调整 proxy_conn 自身。
当有新 publicClient 连贯时,会先从闲暇列表中获取可用的 proxy_conn,此时 proxy_conn 处于闲暇模式,CProxyServer 端会通过 proxy_conn 向 CProxyClient 端发送 StartProxyConnReqMsg,
CLient 端收到后,会为这个 proxy_conn 绑定一个 local_conn, 并将工作模式置为数据传输模式。之后数据在这对 proxy_conn 上进行转发。
数据连贯断开解决
close 和 shutdown 的区别
close
int close(int sockfd)
在不思考 so_linger 的状况下,close 会敞开两个方向的数据流。
- 读方向上,内核会将套接字设置为不可读,任何读操作都会返回异样;
- 输入方向上,内核会尝试将发送缓冲区的数据发送给对端,之后发送 fin 包完结连贯,这个过程中,往套接字写入数据都会返回异样。
- 若对端还发送数据过去,会返回一个 rst 报文。
留神:套接字会保护一个计数,当有一个过程持有,计数加一,close 调用时会查看计数,只有当计数为 0 时,才会敞开连贯,否则,只是将套接字的计数减一。
shutdown
int shutdown(int sockfd, int howto)
shutdown 显得更加优雅,能管制只敞开连贯的一个方向
howto = 0
敞开连贯的读方向,对该套接字进行读操作间接返回 EOF;将接收缓冲区中的数据抛弃,之后再有数据达到,会对数据进行 ACK,而后轻轻抛弃。howto = 1
敞开连贯的写方向,会将发送缓冲区上的数据发送进来,而后发送 fin 包;应用程序对该套接字的写入操作会返回异样(shutdown 不会查看套接字的计数状况,会间接敞开连贯)howto = 2
0+ 1 各操作一遍,敞开连贯的两个方向。
我的项目应用 shutdown 去解决数据连贯的断开,当 CProxyServer 收到 publicClient 的 fin 包 (CProxyClient 收到 LocalServer 的 fin 包) 后,通过 ctlConn 告诉对端,
对端收到后,调用 shutdown(local_conn_fd/public_conn_fd, 2)敞开写方向。等收到另一个方向的 fin 包后,将 proxyConn 置为闲暇模式,并放回闲暇队列中。
在解决链接断开和复用代理链接这块遇到的坑比拟多
- 管制对端去 shutdown 连贯是通过 ctl_conn 去告诉的,可能这一方向上对端的数据还没有全副转发实现就收到断开告诉了,须要确保数据全副转发完能力调用 shutdown 去敞开连贯。
- 从闲暇列表中拿到一个 proxy_conn 后,须要发送 StartProxyConnReq,告知对端开始工作,如果此时对端的这一 proxy_conn 还处于数据传输模式,就会报错了。
数据传输
数据在 Server 和 Client 都需进行转发,将数据从一个连贯的接收缓冲区转发到另一个连贯的发送缓冲区。如果应用 write/read 零碎调用,整个流程如下图
数据先从内核空间复制到用户空间,之后再调用 write 零碎调用将数据复制到内核空间。每次零碎调用,都须要切换 CPU 上下文,而且,两次拷贝都须要 CPU 去执行(CPU copy),所以,大量的拷贝操作,会成为整个服务的性能瓶颈。
在 CProxy 中,应用 splice 的零拷贝计划,数据间接从内核空间的 Source Socket Buffer 转移到 Dest Socket Buffer,不须要任何 CPU copy。
splice 通过 pipe 管道“传递”数据,基本原理是通过 pipe 管道批改 source socket buffer 和 dest socket buffer 的物理内存页
splice 并不波及数据的理论复制,只是批改了 socket buffer 的物理内存页指针。
并发模型
CProxyClient 和 CProxyServer 均采纳多线程 reactor 模型,利用线程池进步并发度。并应用 epoll 作为 IO 多路复用的实现形式。每个线程都有一个事件循环(One loop per thread)。线程分多类,各自解决不同的连贯读写。
CProxyServer 端
为了防止业务连贯解决影响到 Client 和 Server 之间管制信息的传递。咱们将业务数据处理与管制数据处理解耦。在 Server 端中设置了三种线程:
- mainThread: 用于监听 ctl_conn 和 proxy_conn 的连贯申请以及 ctl_conn 上的相干读写
- publicListenThread: 监听并接管外来连贯
- eventLoopThreadPool: 线程池,用于解决 public_conn 和 proxy_conn 之间的数据交换。
CProxyClient 端
client 端比较简单,只有两种线程:
- mainThread: 用于解决 ctl_conn 的读写
- eventLoopThreadPool: 线程池,用于解决 proxy_conn 和 local_conn 之间的数据交换
遗留问题(未完待续。。。)
在应用 ab 压测时,在实现了几百个转发后,就卡住了,通过 tcpdump 抓包发现客户端应用 A 端口连贯,但服务端 accept 后打印的客户端端口是 B。
数据流在【publicClient->CProxyServer->CProxyClient->LocalServer】是失常的;
但回包方向【LocalServer->CProxyClient->CProxyServer-❌->publicClient】,目前还没有找到剖析方向。。。
写在最初
喜爱本文的敌人,欢送关注公众号「会玩 code」,专一大白话分享实用技术