文章首發:聊聊第一個開源專案 - 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時,才會關閉連接,否則,只是將套接字的計數減一,
2. shutdownint shutdown(int sockfd, int howto)shutdown顯得更加優雅,能控制只關閉連接的一個方向
howto = 0關閉連接的讀方向,對該套接字進行讀操作直接回傳EOF;將接識訓沖區中的資料丟棄,之后再有資料到達,會對資料進行ACK,然后悄悄丟棄,howto = 1關閉連接的寫方向,會將發送緩沖區上的資料發送出去,然后發送fin包;應用程式對該套接字的寫入操作會回傳例外(shutdown不會檢查套接字的計數情況,會直接關閉連接)howto = 20+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」,專注大白話分享實用技術

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/438015.html
標籤:其他
上一篇:Python實作直播彈幕自動發送
