文章首發(fā):聊聊第一個開源項目 - CProxy 作者:會玩code
為開平等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務(wù),及開平網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為成都做網(wǎng)站、成都網(wǎng)站建設(shè)、開平網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
最近在學C++,想寫個項目練練手。對網(wǎng)絡(luò)比較感興趣,之前使用過ngrok(GO版本的內(nèi)網(wǎng)穿透項目),看了部分源碼,想把自己的一些優(yōu)化想法用C++實現(xiàn)一下,便有了這個項目。
CProxy是一個反向代理,用戶可在自己內(nèi)網(wǎng)環(huán)境中啟動一個業(yè)務(wù)服務(wù),并在同一網(wǎng)絡(luò)下啟動CProxyClient,用于向CProxyServer注冊服務(wù)。CProxyClient和CProxyServer之間會創(chuàng)建一個隧道,外網(wǎng)可以通過訪問CProxyServer,數(shù)據(jù)轉(zhuǎn)發(fā)到CProxyClient,從而被業(yè)務(wù)服務(wù)接收到。實現(xiàn)內(nèi)網(wǎng)服務(wù)被外網(wǎng)訪問。
https://github.com/lzs123/CProxy.git
bash build.sh
// 啟動服務(wù)端
{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
PublicClient先將請求打到CProxyServer,CProxyServer識別請求是屬于哪個CProxyClient,然后將數(shù)據(jù)轉(zhuǎn)發(fā)到CProxyClient,CProxyClient再識別請求是屬于哪個LocalServer的,將請求再轉(zhuǎn)發(fā)到LocalServer,完成數(shù)據(jù)的轉(zhuǎn)發(fā)。
先介紹CProxyServer端的兩個概念:
在CProxyClient端,也會維護一個TunnelMap,每個Tunnel對應(yīng)一個LocalServer服務(wù),只不過Client端的Tunnel與Server端的Tunnel存儲的內(nèi)容略有差異
為了避免頻繁創(chuàng)建銷毀proxy連接,在完成數(shù)據(jù)轉(zhuǎn)發(fā)后,會將proxyConn放到空閑隊列中,等待下次使用。
proxy_conn有兩種模式 - 數(shù)據(jù)傳輸模式和空閑模式。在數(shù)據(jù)傳輸模式中,proxy_conn不會去讀取解析緩沖區(qū)中的數(shù)據(jù),只會把數(shù)據(jù)通過pipe管道轉(zhuǎn)發(fā)到local_conn; 空閑模式時,會讀取并解析緩沖區(qū)中的數(shù)據(jù),此時的數(shù)據(jù)是一些控制信息,用于調(diào)整proxy_conn本身。
當有新publicClient連接時,會先從空閑列表中獲取可用的proxy_conn,此時proxy_conn處于空閑模式,CProxyServer端會通過proxy_conn向CProxyClient端發(fā)送StartProxyConnReqMsg,
CLient端收到后,會為這個proxy_conn綁定一個local_conn, 并將工作模式置為數(shù)據(jù)傳輸模式。之后數(shù)據(jù)在這對proxy_conn上進行轉(zhuǎn)發(fā)。
close和shutdown的區(qū)別
- close
int close(int sockfd)
在不考慮so_linger的情況下,close會關(guān)閉兩個方向的數(shù)據(jù)流。
- 讀方向上,內(nèi)核會將套接字設(shè)置為不可讀,任何讀操作都會返回異常;
- 輸出方向上,內(nèi)核會嘗試將發(fā)送緩沖區(qū)的數(shù)據(jù)發(fā)送給對端,之后發(fā)送fin包結(jié)束連接,這個過程中,往套接字寫入數(shù)據(jù)都會返回異常。
- 若對端還發(fā)送數(shù)據(jù)過來,會返回一個rst報文。
注意:套接字會維護一個計數(shù),當有一個進程持有,計數(shù)加一,close調(diào)用時會檢查計數(shù),只有當計數(shù)為0時,才會關(guān)閉連接,否則,只是將套接字的計數(shù)減一。
2. shutdownint shutdown(int sockfd, int howto)
shutdown顯得更加優(yōu)雅,能控制只關(guān)閉連接的一個方向
howto = 0
關(guān)閉連接的讀方向,對該套接字進行讀操作直接返回EOF;將接收緩沖區(qū)中的數(shù)據(jù)丟棄,之后再有數(shù)據(jù)到達,會對數(shù)據(jù)進行ACK,然后悄悄丟棄。howto = 1
關(guān)閉連接的寫方向,會將發(fā)送緩沖區(qū)上的數(shù)據(jù)發(fā)送出去,然后發(fā)送fin包;應(yīng)用程序?qū)υ撎捉幼值膶懭氩僮鲿祷禺惓#╯hutdown不會檢查套接字的計數(shù)情況,會直接關(guān)閉連接)howto = 2
0+1各操作一遍,關(guān)閉連接的兩個方向。
項目使用shutdown去處理數(shù)據(jù)連接的斷開,當CProxyServer收到publicClient的fin包(CProxyClient收到LocalServer的fin包)后,通過ctlConn通知對端,
對端收到后,調(diào)用shutdown(local_conn_fd/public_conn_fd, 2)關(guān)閉寫方向。等收到另一個方向的fin包后,將proxyConn置為空閑模式,并放回空閑隊列中。
在處理鏈接斷開和復用代理鏈接這塊遇到的坑比較多
數(shù)據(jù)在Server和Client都需進行轉(zhuǎn)發(fā),將數(shù)據(jù)從一個連接的接收緩沖區(qū)轉(zhuǎn)發(fā)到另一個連接的發(fā)送緩沖區(qū)。如果使用write/read系統(tǒng)調(diào)用,整個流程如下圖
數(shù)據(jù)先從內(nèi)核空間復制到用戶空間,之后再調(diào)用write系統(tǒng)調(diào)用將數(shù)據(jù)復制到內(nèi)核空間。每次系統(tǒng)調(diào)用,都需要切換CPU上下文,而且,兩次拷貝都需要CPU去執(zhí)行(CPU copy),所以,大量的拷貝操作,會成為整個服務(wù)的性能瓶頸。
在CProxy中,使用splice的零拷貝方案,數(shù)據(jù)直接從內(nèi)核空間的Source Socket Buffer轉(zhuǎn)移到Dest Socket Buffer,不需要任何CPU copy。
splice通過pipe管道“傳遞”數(shù)據(jù),基本原理是通過pipe管道修改source socket buffer和dest socket buffer的物理內(nèi)存頁
splice并不涉及數(shù)據(jù)的實際復制,只是修改了socket buffer的物理內(nèi)存頁指針。
CProxyClient和CProxyServer均采用多線程reactor模型,利用線程池提高并發(fā)度。并使用epoll作為IO多路復用的實現(xiàn)方式。每個線程都有一個事件循環(huán)(One loop per thread)。線程分多類,各自處理不同的連接讀寫。
為了避免業(yè)務(wù)連接處理影響到Client和Server之間控制信息的傳遞。我們將業(yè)務(wù)數(shù)據(jù)處理與控制數(shù)據(jù)處理解耦。在Server端中設(shè)置了三種線程:
client端比較簡單,只有兩種線程:
在使用ab壓測時,在完成了幾百個轉(zhuǎn)發(fā)后,就卡住了,通過tcpdump抓包發(fā)現(xiàn)客戶端使用A端口連接,但服務(wù)端accept后打印的客戶端端口是B。
數(shù)據(jù)流在【publicClient->CProxyServer->CProxyClient->LocalServer】是正常的;
但回包方向【LocalServer->CProxyClient->CProxyServer-?->publicClient】,目前還沒有找到分析方向。。。
喜歡本文的朋友,歡迎關(guān)注公眾號「會玩code」,專注大白話分享實用技術(shù)