1. 程式人生 > >ss-libev 原始碼解析local篇(5):ss-local之remote_send_cb

ss-libev 原始碼解析local篇(5):ss-local之remote_send_cb

remote_send_cb這個回撥函式的工作是將從客戶端收取來的資料轉發給ss-server。在之前閱讀server_recv_cb程式碼時可以看到,在STAGE_STREAM階段有幾種可能都會開啟remote->fd的寫事件的監聽,從而當有寫事件觸發時呼叫remote_send_cb。從程式碼結構看,外層的分支是remote->send_ctx->connected是否為0,內部的分支是是否fast_open或直連。而根據實際程式碼執行流向,可看成是否有fast_open或直接。而最普通的情況就是沒開啟fast_open或者remote為直連目標伺服器的情況,先討論這種情況下的connected分支:
1. remote->send_ctx->connected為0時,即第一次進入STAGE_STREAM,尚未連線remote server時。
在server_recv_cb中直接呼叫非阻塞connect後,開啟事件監聽:

ev_io_start(EV_A_ & remote->send_ctx->io);
ev_timer_start(EV_A_ & remote->send_ctx->watcher);

因為非阻塞fd呼叫connect後,當connect成功後,fd可寫。所以這兒監聽了send_ctx->io即寫事件(回憶一下:```ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE);```)

現在看一下remote_send_cb函式中,當connect成功回撥cb時,由於此時 remote->send_ctx->connected==0,所以會進入:

if (!remote_send_ctx->connected) {
        struct sockaddr_storage addr;
        socklen_t len = sizeof addr;
        int r         = getpeername(remote->fd, (struct sockaddr *)&addr, &len);
        if (r == 0) {
            remote_send_ctx->connected = 1;
            ev_timer_stop(EV_A_ &
remote_send_ctx->watcher); ev_timer_start(EV_A_ & remote->recv_ctx->watcher); ev_io_start(EV_A_ & remote->recv_ctx->io); // no need to send any data if (remote->buf->len == 0) { ev_io_stop(EV_A_ & remote_send_ctx->io); ev_io_start(EV_A_ & server->recv_ctx->io); return; } } else { // not connected ERROR("getpeername"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); return; } }

這兒通過getpeername獲取對端地址,即伺服器地址,如果返回值為0表示獲取成功,因此判斷為連線成功(其實我不明白為什麼要判斷,但是這麼判斷肯定沒錯)。連線成功後設置remote_send_ctx->connected=1,並且stop remote send timer。說明這種情況下先前設定的remote send timer只是用來做connect超時用。然後start remote recv的timer,且start remote fd的讀事件監聽(回憶一下:ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ);),即準備從remote接受返回的資料
這之後判斷remote->buf是否為空,如果為空則停止傳送並開始監聽讀取客戶端的資料。當前這種情況,buf不可能為空,因為至少還有socks5相關的頭,即之前的abuf。如果getpeername返回非0則表示連線失敗,關閉remote和server。
繼續看remote_send_cb的後半段:

if (remote->buf->len == 0) {
        // close and free
        close_and_free_remote(EV_A_ remote);
        close_and_free_server(EV_A_ server);
        return;
    } else {
        // has data to send
        ssize_t s = send(remote->fd, remote->buf->data + remote->buf->idx,
                         remote->buf->len, 0);
        if (s == -1) {
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                ERROR("remote_send_cb_send");
                // close and free
                close_and_free_remote(EV_A_ remote);
                close_and_free_server(EV_A_ server);
            }
            return;
        } else if (s < (ssize_t)(remote->buf->len)) {
            // partly sent, move memory, wait for the next time to send
            remote->buf->len -= s;
            remote->buf->idx += s;
            return;
        } else {
            // all sent out, wait for reading
            remote->buf->len = 0;
            remote->buf->idx = 0;
            ev_io_stop(EV_A_ & remote_send_ctx->io);
            ev_io_start(EV_A_ & server->recv_ctx->io);
        }
    }

如果buf為空則關閉連線,目前應該不會為空,所以走到else裡面。使用send傳送資料,傳送的資料的指標是remote->buf->data+remote->buf->idx,回憶一下server_recv_cb裡面,如果未connected的情況下,這個idx是設定為0的,所以這兒傳送資料就是整個buf的資料。因為remote->fd是非阻塞的,send呼叫後就立即返回了,如果返回值s是-1,要檢查一下errno是否為EAGAIN或EWOULDBLOCK,如果不是他倆就真出錯了,斷開連線;如果是他倆說明要等一下再發送,直接return出去等下次remote_send_cb被回撥。下次再回調進來時,remote_send_ctx->connected已經是1了,所以直接進下半段程式碼繼續send。
如果s小於buf->len,說明發送了部分資料,需要調整idx的位置並從len減去s。等下次寫事件觸發可以繼續傳送時,就從idx的位置繼續傳送。
如果s等於buf->len說明全部發送完畢,len和idx清0,stop remote fd的寫事件監聽並sart server fd的讀事件監聽,即繼續從客戶端讀取資料。
小結一下上面的討論:是不使用fast_open或是直接目標伺服器的情況下,之前沒有connect時進行connet呼叫,connect成功後呼叫了remote_send_cb,隨即傳送remote->buf中的全部資料,可能一次send只發送了一部分,那麼回撥就會多次觸發; 如果傳送完成了則需要再次從客戶端讀取資料,此時客戶端還是處於STAGE_STREAM狀態。好了,我們現在回過頭繼續看server_recv_cb,此時已經是connect過了。
2. remote->send_ctx->connected==1的情況。請進入server_recv_cb繼續分析:
在進入if (!remote->send_ctx->connected)對應的else程式碼塊之前。先看一下此時讀取的資料,if (!remote->direct) 裡面之前是第一次進入,會將abuf插入到前面,此時已經沒有abuf了,所以讀取出來的資料直接加密後傳送。從這兒可以看到ss只是TCP一次連線要傳送的所有資料的開頭加上一個頭資料,之後就都是原始資料了,當然所有這些資料都是加密的。好了,繼續看server_recv_cb裡面的傳送程式碼:

else {
                int s = send(remote->fd, remote->buf->data, remote->buf->len, 0);
                if (s == -1) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // no data, wait for send
                        remote->buf->idx = 0;
                        ev_io_stop(EV_A_ & server_recv_ctx->io);
                        ev_io_start(EV_A_ & remote->send_ctx->io);
                        return;
                    } else {
                        ERROR("server_recv_cb_send");
                        close_and_free_remote(EV_A_ remote);
                        close_and_free_server(EV_A_ server);
                        return;
                    }
                } else if (s < (int)(remote->buf->len)) {
                    remote->buf->len -= s;
                    remote->buf->idx  = s;
                    ev_io_stop(EV_A_ & server_recv_ctx->io);
                    ev_io_start(EV_A_ & remote->send_ctx->io);
                    return;
                } else {
                    remote->buf->idx = 0;
                    remote->buf->len = 0;
                }
            }

此處,即server_recv_cb的STAGE_STREAM階段,已經連線上ss-server後,讀取到新的客戶端資料後隨即傳送到ss-server。即呼叫了send,注意這兒的send是從buf->data的開頭髮送的,結合之前的程式碼有個結論,server_recv_cb讀取到資料後即試圖傳送buf,這些資料是新讀取到的,所以從buf開頭髮送,如果傳送了部分資料,則設定idx,然後在remote_send_cb裡面從idx處繼續傳送,全部發送完畢後再次開啟server_recv_cb對應的監聽,繼續讀取客戶端資料然後轉發到ss-server。
因此這兒send之後的程式碼很好理解,出錯處理就不說了都一樣;部分發送就是設定idx和len並停啟相應事件;全部發送就是請空idx,len就行並不需要再設定開啟send監聽了,因為已經發送完了,就等著從客戶端再次讀取資料過來。在server_recv_cb前面,如果讀取到EOF,即返回值為0,則說明客戶端已經沒有資料要傳送了,且斷開了連線,此次轉發傳送成功,回憶一下之前的程式碼:

if (r == 0) {
            // connection closed
            close_and_free_remote(EV_A_ remote);
            close_and_free_server(EV_A_ server);
            return;
        } 

好了,那麼remote_send_cb其實處理的就是server_recv_cb沒有傳送出去的資料,他要繼續傳送,因為已經connect過,所以直接進入上面貼出的remote_send_cb的後半段程式碼,和上面connect成功進入一樣,呼叫send傳送資料,處理部分發送和全部發送的情況。
至此,不使用fast open或者直連目標伺服器的情況已經分析完了。再總結一下整個流程:
- remote_send_ctx->connected==0時,先在server_recv_cb裡面執行connect,connect成功remote_send_cb被回撥
- remote_send_cb第一次被呼叫是connect成功時,通過getpeername判斷是否成功,如果成功會開啟remote fd的讀事件監聽和timer,即準備從ss-server接受資料。然後就是向ss-server傳送資料了。connect時附帶的資料可能分幾次才傳送完。總之傳送完後就再啟用server_recv_cb讀取資料。
- server_recv_cb再次被呼叫後就繼續讀取客戶端的資料然後傳送,如果是部分發送就再次呼叫remote_send_cb。最終沒有資料可讀取則斷開連線。

以上,是不開啟fast open或直連的情況。下面分析開啟fast open的情況。

相關推薦

ss-libev 原始碼解析udp (2)

UDP relay的程式碼基本都在udprelay.c中,無論ss-local還是ss-server的程式碼都在一起,使用巨集MODULE_LOCAL,MODULE_REMOTE等區分開。程式碼雖然不是很多,但是由於ss-local和ss-server以及ss-

ss-libev 原始碼解析local(5):ss-localremote_send_cb

remote_send_cb這個回撥函式的工作是將從客戶端收取來的資料轉發給ss-server。在之前閱讀server_recv_cb程式碼時可以看到,在STAGE_STREAM階段有幾種可能都會開啟remote->fd的寫事件的監聽,從而當有寫事件觸發時

ss-libev 原始碼解析local(2):ss_local和socks5客戶端握手

上一篇說到ss-libev建立listen_ctx_t物件用於監聽客戶端連線,呼叫accept_cb處理來自客戶端的新連線,建立server_t物件用於處理和客戶端之間的互動。本篇分析來自客戶端的SOCK5連線的建立以及傳輸資料的過程。 首先,回憶一下使用ne

dubbo原始碼解析——概要

這次原始碼解析借鑑《肥朝》前輩的dubbo原始碼解析,進行原始碼學習。總結起來就是先總體,後區域性.也就是先把需要注意的概念先丟擲來,把整體架構圖先畫出來.讓讀者拿著"地圖"跟著我的腳步,並且每一步我都

Multibit原始碼解析學習---傳送比特幣

/**package org.multibit.viewsystem.swing.action; * Complete the transaction to work out the fee) and then show the send bitcoin confirm dialog.

史上最全Universal-Image-Loader原始碼解析————快取

背景 在我寫下這篇部落格的時候,我還是一名二本學校計算機專業大四的應屆畢業生,自學Android開發快兩年了,這兩年的時間裡面,其實真的是感慨萬千,兩年的時間裡面,Android的很多事情也都見識過了,其實Android對於新手入門在就業方面是相當不友好的事情。都說第一個吃螃蟹的人最

ceph原始碼解析--osd

Ceph分散式檔案系統的程式碼分析的文章網上是比較少的,本團隊成員對ceph做過詳細的程式碼閱讀,包括mds、osd、client等模組,但是缺少條理清晰的文件總結。暫且先放上OSD的程式碼分析,等後續整理陸續放上其它模組的。 1 OSD的基本結構 主要的類,涉及的執行

Andfix熱修復框架原理及原始碼解析-上

1.不知道如何使用的同學,建議看看我上一篇寫的介紹熱補丁和Andfix的使用,這樣你才有一個大概的框架。通過使用Andfix,其實我們心中會有一個大概的輪廓,它的工作原理,大概就是,所謂的補丁檔

ElementUI 簡要原始碼解析——Basic

Layout 佈局 row 佈局元件中的父元件,用於控制子元件。很簡單的一個佈局標籤,主要通過 justify 和 align 控制子元素的對齊方式,使用 render 函式通過傳入的 tag 屬性控制生成的標籤。 在這裡推薦學習下 render 函式和 JSX 的寫法,因為之後比較複雜的元件都是通過 ren

myBatis原始碼解析-日誌(1)

上半年在進行知識儲備,下半年爭取寫一點好的部落格來記錄自己原始碼之路。在學習原始碼的路上也掌握了一些設計模式,可所謂一舉兩得。本次打算寫Mybatis的原始碼解讀。 準備工作 1. 下載mybatis原始碼 下載地址:https://github.com/mybatis/mybatis-3  2.

myBatis原始碼解析-快取(2)

上一章分析了mybatis的原始碼的日誌模組,像我們經常說的mybatis一級快取,二級快取,快取究竟在底層是怎樣實現的。此次開始分析快取模組 1. 原始碼位置,mybatis原始碼包位於org.apache.ibatis.cache下,如圖 2. 先從org.apache.ibatis.cache下的cac

myBatis原始碼解析-資料來源(3)

前言:我們使用mybatis時,關於資料來源的配置多使用如c3p0,druid等第三方的資料來源。其實mybatis內建了資料來源的實現,提供了連線資料庫,池的功能。在分析了快取和日誌包的原始碼後,接下來分析mybatis中的資料來源實現。 類圖:mybatis中關於資料來源的原始碼包路徑如下:  

myBatis原始碼解析-反射(4)

前沿 前文分析了mybatis的日誌包,快取包,資料來源包。原始碼實在有點難頂,在分析反射包時,花費了較多時間。廢話不多說,開始原始碼之路。 反射包feflection在mybatis路徑如下:      原始碼解析 1  property包-主要對類的屬性進行操作的工

mybatis原始碼-解析配置檔案(三)配置檔案Configuration解析(超詳細, 值得收藏)

1. 簡介 1.1 系列內容 本系列文章講解的是mybatis解析配置檔案內部的邏輯, 即 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFact

mybatis原始碼-解析配置檔案(二)解析的流程

1. 簡介 在之前的文章《mybatis 初步使用(IDEA的Maven專案, 超詳細)》中, 講解了mybatis的初步使用, 並總結了以下mybatis的執行流程: 通過 Resources 工具類讀取 mybatis-config.xml,

mybatis原始碼-解析配置檔案(四)配置檔案Mapper解析

其中, mappers作為configuration節點的一部分配置, 在本文章中, 我們講解解析mappers節點, 即 xxxMapper.xml 檔案的解析。 1 解析入口 在解析 mybatis-config.xml 時, 會進行解析 xxxMapper.xml 的檔案。 在圖示流程的 XMLCo

Android訊息機制原理,仿寫Handler Looper原始碼解析跨執行緒通訊原理--仿寫模擬Handler(四)

前篇總結:上一篇實現了用Looper管理訊息佇列和訊息迴圈,但是訊息的傳送和訊息處理都是在Looper中進行的。寫一個子執行緒使用這樣的Looper怎麼才能獲取到loop()死迴圈訊息佇列取出的訊息呢?用回撥!callBack! 第四節 仿寫Handler來發送訊息,實現回

Spark2.3.2原始碼解析:9.排程系統 DAGScheduler Stage 劃分原始碼詳解

Stage劃分的時候,大家應該都知道是從最後一個stage向根據寬窄依賴,遞迴進行stage劃分。 但是程式碼裡面涉及的邏輯複雜。畢竟涉及到相互遞迴呼叫。讓人似懂非懂。 反正我是炸毛了 o(╥﹏╥)o 本文專門用一篇文章詳細論述DAGScheduler 的 stage 劃分流程

Hystrix 原始碼解析 —— 請求執行(四)失敗回退邏輯

本文主要基於 Hystrix 1.5.X 版本 1. 概述本文主要分享 Hystrix 命令執行(四)之失敗回退邏輯。 建議 :對 RxJava 已經有一定的瞭解的基礎上閱讀本文。 Hystrix 執行命令整體流程如下圖: 紅圈 :Hy

jdk1.8原始碼解析:HashMap底層資料結構連結串列轉紅黑樹的具體時機

前言   本文從三個部分去探究HashMap的連結串列轉紅黑樹的具體時機:     一、從HashMap中有關“連結串列轉紅黑樹”閾值的宣告;     二、【重點】解析HashMap.put(K key, V value)的原始碼;     三、測試;   一、從