文章首发:聊聊第一个开源我的项目 - 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性能

流程架构

角色

  1. LocalServer: 内网业务服务
  2. CProxyClient: CProxy客户端,个别与LocalServer部署在一起,对接CProxyServer和InnerServer
  3. CProxyServer: CProxy服务端
  4. 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
  1. 实现几种工作线程的初始化。
  2. 监听一个CtlPort,期待CProxyClient连贯。
CProxyClient
  1. 实现对应线程的初始化。
  2. 而后连贯Server的CtlPort,此连贯称为ctl_conn, 用于client和server之前管制信息的传递。
  3. 申请注册Control,获取ctl_id。
  4. 最初再依据Tunnel配置文件实现多个Tunnel的注册。须要留神的是,每注册一个Tunnel,Server端就会多监听一个PublicPort,作为内部拜访LocalServer的入口。

数据转发流程

  1. Web上的PublicClient申请CProxyServer上的PublicPort建设连贯;CProxyServer接管连贯申请,将public_accept_fd封装成PublicConn。
  2. CProxyServer通过ctl_conn向client发送NotifyClientNeedProxyMsg告诉Client须要创立一个proxy。
  3. Client收到后,会别离连贯LocalServer和CProxyServer:
    3.1. 连贯LocalServer,将local_conn_fd封装成LocalConn。
    3.2. 连贯ProxyServer的ProxyPort,将proxy_conn_fd封装成ProxyConn,并将LocalConn和ProxyConn绑定。
  4. CProxyServer的ProxyPort收到申请后,将proxy_accept_fd封装成ProxyConn,将ProxyConn与PublicConn绑定。
  5. 尔后的数据在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的区别

  1. close

    int close(int sockfd)

    在不思考so_linger的状况下,close会敞开两个方向的数据流。

  2. 读方向上,内核会将套接字设置为不可读,任何读操作都会返回异样;
  3. 输入方向上,内核会尝试将发送缓冲区的数据发送给对端,之后发送fin包完结连贯,这个过程中,往套接字写入数据都会返回异样。
  4. 若对端还发送数据过去,会返回一个rst报文。

留神:套接字会保护一个计数,当有一个过程持有,计数加一,close调用时会查看计数,只有当计数为0时,才会敞开连贯,否则,只是将套接字的计数减一。

  1. shutdown

    int shutdown(int sockfd, int howto)

    shutdown显得更加优雅,能管制只敞开连贯的一个方向

  2. howto = 0 敞开连贯的读方向,对该套接字进行读操作间接返回EOF;将接收缓冲区中的数据抛弃,之后再有数据达到,会对数据进行ACK,而后轻轻抛弃。
  3. howto = 1 敞开连贯的写方向,会将发送缓冲区上的数据发送进来,而后发送fin包;应用程序对该套接字的写入操作会返回异样(shutdown不会查看套接字的计数状况,会间接敞开连贯)
  4. howto = 2 0+1各操作一遍,敞开连贯的两个方向。

我的项目应用shutdown去解决数据连贯的断开,当CProxyServer收到publicClient的fin包(CProxyClient收到LocalServer的fin包)后,通过ctlConn告诉对端,
对端收到后,调用shutdown(local_conn_fd/public_conn_fd, 2)敞开写方向。等收到另一个方向的fin包后,将proxyConn置为闲暇模式,并放回闲暇队列中。

在解决链接断开和复用代理链接这块遇到的坑比拟多

  1. 管制对端去shutdown连贯是通过ctl_conn去告诉的,可能这一方向上对端的数据还没有全副转发实现就收到断开告诉了,须要确保数据全副转发完能力调用shutdown去敞开连贯。
  2. 从闲暇列表中拿到一个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端中设置了三种线程:

  1. mainThread: 用于监听ctl_conn和proxy_conn的连贯申请以及ctl_conn上的相干读写
  2. publicListenThread: 监听并接管外来连贯
  3. eventLoopThreadPool: 线程池,用于解决public_conn和proxy_conn之间的数据交换。

CProxyClient端

client端比较简单,只有两种线程:

  1. mainThread: 用于解决ctl_conn的读写
  2. eventLoopThreadPool: 线程池,用于解决proxy_conn和local_conn之间的数据交换


遗留问题(未完待续。。。)

在应用ab压测时,在实现了几百个转发后,就卡住了,通过tcpdump抓包发现客户端应用A端口连贯,但服务端accept后打印的客户端端口是B。
数据流在【publicClient->CProxyServer->CProxyClient->LocalServer】是失常的;
但回包方向【LocalServer->CProxyClient->CProxyServer-❌->publicClient】,目前还没有找到剖析方向。。。

写在最初

喜爱本文的敌人,欢送关注公众号「会玩code」,专一大白话分享实用技术