文章首发:聊聊第一个开源项目 - 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会关闭两个方向的数据流。

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

注意:套接字会维护一个计数,当有一个进程持有,计数加一,close调用时会检查计数,只有当计数为0时,才会关闭连接,否则,只是将套接字的计数减一。

2. shutdown

int shutdown(int sockfd, int howto)

shutdown显得更加优雅,能控制只关闭连接的一个方向

  1. howto = 0 关闭连接的读方向,对该套接字进行读操作直接返回EOF;将接收缓冲区中的数据丢弃,之后再有数据到达,会对数据进行ACK,然后悄悄丢弃。
  2. howto = 1 关闭连接的写方向,会将发送缓冲区上的数据发送出去,然后发送fin包;应用程序对该套接字的写入操作会返回异常(shutdown不会检查套接字的计数情况,会直接关闭连接)
  3. 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」,专注大白话分享实用技术

聊聊第一个开源项目(内网穿透) - CProxy的更多相关文章

  1. 内网穿透+VS2015自带IIS express实现本地调试(微信等需要将开发环境暴漏到外网的情况使用)

    今天一个兼职结束了,又要开始寻找新的兼职公司了 ,为了贴补家用啊,为了给儿子更好的生活加油! 抒情完毕进入正题,本篇文章要解决的问题是其实在开发微信支付,微信公众号等回调地址必须是外网可访问的80端口 ...

  2. 分享一个内网穿透工具frp

    首先简单介绍一下内网穿透: 内网穿透:通过公网,访问局域网里的IP地址与端口,这需要将局域网里的电脑端口映射到公网的端口上:这就需要用到反向代理,即在公网服务器上必须运行一个服务程序,然后在局域网中需 ...

  3. 内网穿透访问Vue项目的时候出现Invalid Host header解决办法

    适用场景: 在本地的Vue-cli3项目, 需要其他人浏览. 如果没有外网的服务器, 可以把自己的电脑当做服务器. 这时候需要外网的人能访问到自己的电脑. Mac内网穿透工具:natapp Inval ...

  4. windows(Linux)创建”内网穿透“工具(通过自定义域名访问部署于内网的 web 服务,可以用于调试微信支付,支付宝支付,微信公众号等开发项目)

    此方法需要自有服务器和域名,如果没有这些的开发者, 可以参考钉钉提供的内网穿透方式:https://www.cnblogs.com/pxblog/p/13862376.html 一.准备工作 1.域名 ...

  5. 如何使用 frp 实现内网穿透

    这有一个专注Gopher技术成长的开源项目「go home」 背景 作为一名程序员,家里多多少少会有一些落了灰的电脑,如果把闲置的电脑变成服务器,不仅有良好的配置,还能用来做各种测试,那就再好不过了. ...

  6. 手写内网穿透服务端客户端(NAT穿透)原理及实现

    Hello,I'm Shendi. 这天心血来潮,决定做一个内网穿透的软件. 用过花生壳等软件的就知道内网穿透是个啥,干嘛用的了. 我们如果有服务器(比如tomcat),实际上我们在电脑上开启了服务器 ...

  7. 基于Yarp的http内网穿透库HttpMouse

    简介 前几天发表了<基于Yarp实现内网http穿透>,当时刚刚从原理图变成了粗糙的代码实现,项目连名字都还没有,也没有开放源代码.在之后几天的时间,我不断地重构,朝着"可集成. ...

  8. FastTunnel-开源内网穿透框架

    FastTunnel - 打造人人都能搭建的内网穿透工具 FastTunnel是用.net core开发的一款跨平台内网穿透工具,它可以实现将内网服务暴露到公网供自己或任何人访问. 与其他穿透工具不同 ...

  9. linux之frp服务部署(内网穿透)

    frp服务部署(内网穿透) 目的 更快的进行内网穿透调试以及云端开发测试 服务器为CentOS 7,客户端为win11 frp介绍 frp 是一个开源项目, 采用 C/S 模式,将服务端部署在具有公网 ...

随机推荐

  1. K8s配置配置存活、就绪和启动探测器

    kubelet 使用存活探测器来知道什么时候要重启容器. 例如,存活探测器可以捕捉到死锁(应用程序在运行,但是无法继续执行后面的步骤). 这样的情况下重启容器有助于让应用程序在有问题的情况下更可用. ...

  2. lambda表达式的学习

    Lambda表达式 为什么使用lambda表达式 Lambda表达式可以简化我们的代码,使我们只需要关注主要的代码就可以. //测试用的实体类 public class Employee { priv ...

  3. 幸运转轮(Cakra)

    题目描述 lxx参加了某卫视举办的一场选秀节目,凭借曼妙的舞姿和动人的歌声,他在众多idol中脱颖而出.现在在他的面前,有四个大转轮,这四个转轮将决定他能否赢得最终大奖--出道,机会只有一次!   每 ...

  4. What Goes Up Must Come Down

    跳转链接 题目描述 给定一个序列, 求出将此序列变换为单调递增.单调递减 或者先增后减 样例1 输入 7 3 1 4 1 5 9 2 输出 3 样例2 输入 9 10 4 6 3 15 9 1 1 1 ...

  5. 别人都在认真听课,我埋头写Python为主播疯狂点点点点点赞!

    最近有次在钉钉看直播,发现这个直播非常之精彩,于是情不自禁地想要为主播大佬连刷一波赞: 但我发现,手动连击点赞速度十分不可观.气人的是,钉钉直播不能长按刷赞!这让我很恼怒.心中满怀的激动和兴奋以及对大 ...

  6. 使用JMX Exporter监控Rainbond上的Java应用

    场景 Prometheus 社区开发了 JMX Exporter 用于导出 JVM 的监控指标,以便使用 Prometheus 来采集监控数据.当您的 Java 应用部署在Rainbond上后 可通过 ...

  7. AT2650 [ARC077C] guruguru

    可以发现,如果我们枚举每个理想亮度 \(X\) 然后再求在这个理想亮度情况下的答案是非常难维护的. 不妨反过来,考虑每个位置 \(i, i + 1\) 之间对每个理想亮度 \(X\) 减少次数的贡献. ...

  8. HOOK API(四) —— 进程防终止

    0x00        前言 这算是一个实战吧,做的一个应用需要实现进程的防终止保护,查了相关资料后决定用HOOK API的方式实现.起初学习HOOK API的起因是因为要实现对剪切板的监控,后来面对 ...

  9. IM开发通信协议基础知识(一)---TCP、UDP、HTTP、SOCKET

    感谢大佬:https://www.cnblogs.com/sixindev/p/4723590.html 下面这些内容不了解也可以进行开发,深入了解一下还是收益良多 区别 TCP.UDP.HTTP.S ...

  10. Redis 学习笔记(五)高可用之主从模式

    上一节提到了 Redis 的持久性,也就是在服务器实例宕机或故障时,拥有再恢复的能力.但是在这个服务器实例宕机恢复期间,是无法接受新的数据请求.对于整体服务而言这是无法容忍的,因此我们可以使用多个服务 ...