天天看點

RPC架構的連接配接模型

一、RPC的用戶端連接配接模型

1、連接配接成本: 使用TCP協定時,會在用戶端和伺服器之間建立一條虛拟的信道,這條虛拟信道就是指連接配接,而建議這條連接配接需要3次握手,拆毀這條連接配接需要4次揮手,可見,我們建立這條連接配接是有成本的,這個成本就是效率成本,簡單點 說就是時間成本,你要想發送一段資料,必須先3次握手(來往3個包),然後才能發送資料,發送完了,你需要4次揮手(來往4個包)來斷開這個連接配接。 其二,CPU資源成本,三次握手和4次揮手和發送資料都是從網卡裡發送出去和接收的,還有其餘的裝置,比如防火牆,路由器等等,站在作業系統核心的角度來講,如果我們是一個高并發系統的話,如果大量的資料包都經 曆過這麼一個過程,那是很耗CPU的。 其三,每個socket是需要耗費系統緩存的,比如系統提供了一些接口設定socket緩存。

1、短連接配接: client與server通過三次握手建立連接配接,client發送請求消息,server傳回響應,一次連接配接就完成了。這時候雙方任意都可以發起close操作,不過一般都是client先發起close操作。上述可知,短連接配接一般隻會在 client/server間傳遞一次請求操作。短連接配接的優缺點:管理起來比較簡單,存在的連接配接都是有用的連接配接,不需要額外的控制手段。是以對于并發量大,請求頻率低的,建議使用短連接配接。

2、長連接配接:

  • TCP連接配接一旦建立後,是不是這個連接配接可以一直保持? 答案是否定的,作業系統在實作TCP協定的時候都做了一個限制,這個限制可 以參考配置:

    /proc/sys/net/ipv4/tcp_keepalive_time /proc/sys/net/ipv4/tcp_keepalive_intvl /proc/sys/net/ipv4/tcp_keepalive_probes

    .。我們看到預設這個tcp_keepalive_time的值為7200s,也就是2個小時,這個值代表如果TCP連接配接發送完最後一個ACK包後,如果 超過2個小時,沒有資料往來,那麼這個連接配接會斷掉。那麼我們如何才能保持住這個連接配接呢?實際上,這就是TCP的keepalive機 制,哦,說法不嚴謹,TCP協定并沒有規定如此,但是很多的作業系統核心實作TCP協定時,都加上了這個keepalive機制,那麼 這個功能預設是關閉的,那這個keepalive機制到底是如何的呢?也就是,如果TCP之間沒有任何資料來往了在 tcp_keepalive_time(7200s,2h)後,伺服器給用戶端發送一個探測包,如果對方有回應,說明這個連接配接還存活,否則繼續每 隔tcp_keepalive_intvl(預設為75s)給對方發送探測包,如果連續tcp_keepalive_probes(預設為9)次後,依然沒有收到對端 的回複,那麼則認為這個連接配接已經關閉。
  • 長連接配接适用于要進行大量資料傳輸的情況,如:資料庫,redis,memcached等要求快速,資料量大的情況下。 長連接配接通過心跳機制(通信資料很少)來進行連接配接狀态的監測,斷後重新進行連接配接。 資料庫的連接配接就是采用TCP長連接配接。RPC遠端服務調用,在伺服器,一個服務程序頻繁調用另一個服務程序,可使用長連接配接,減少連接配接花費的時間。

3、單連接配接(單長連接配接): 同步方式下用戶端所有請求共用同一連接配接,在獲得連接配接後要對連接配接加鎖在讀寫結束後才解鎖釋放連接配接,性能低下,基本很少采用,唯一優點是實作極其簡單。異步方式下所有請求都帶有消息ID,是以可以批量發送請求(發送到阻塞等待隊列中),異步接收回複,所有請求和回複的消息都共享同一連接配接,信道得到最大化利用,是以吞吐量最大。這個時候接收端的處理能力也要求比較高,一般都是獨立的一個(或者多個)收包線程(或者程序)防止核心緩沖區被填滿影響網絡吞吐量。缺點是實作複雜,需要異步狀态機,需要增加負載均衡和連接配接健康度檢測機制,等等。

4、連接配接池(多長連接配接):

  • 連接配接池,連接配接池是将已經建立好的連接配接儲存在池中,當有請求來時,直接使用已經建立好的連接配接對資料庫進行通路。這樣省略了建立連接配接和銷毀連接配接的過程。這樣性能上得到了提高。維護着一定數量Socket長連接配接的集合。它能自動檢測Socket長連接配接的有效性,剔除無效的連接配接,補充連接配接池的長連接配接的數量。socket連接配接池 發出連接配接的client-port 一般是随機的,可以在用戶端建立多個對目的服務端的連接配接池。每個請求單獨占用一個連接配接,使用完以後把連接配接放回池中,給下一個請求使用。
  • 連接配接池的優點:

    ①資源重用:由于資料庫連接配接得到重用,避免了頻繁建立、釋放連接配接引起的大量性能開銷。在減少系統消耗的基礎上,增進了系統環境的平穩性(減少記憶體碎片以級資料庫臨時程序、線程的數量)。

    ②更快的系統響應速度:資料庫連接配接池在初始化過程中,往往已經建立了若幹資料庫連接配接置于池内備用。此時連接配接池的初始化操作均已完成。對于業務請求處理而言,直接利用現有可用連接配接,避免了資料庫連接配接初始化和釋放過程的時間開銷,進而縮減了系統整體響應時間。

    ③新的資源配置設定手段:對于多應用共享同一資料庫的系統而言,可在應用層通過資料庫連接配接的配置,實作資料庫連接配接技術。

    ④統一的連接配接管理,避免資料庫連接配接洩露: 在較為完備的資料庫連接配接池實作中,可根據預先的連接配接占用逾時設定,強制收回被占用的連接配接,進而避免了正常資料庫連接配接操作中可能出現的資源洩露

  • 連接配接池的缺點:缺點還是網絡使用率不高,因為在等待對端回複的時候,連接配接是空閑的。

5、連接配接模型比對: 小包複用連結才能提高吞吐和降低延時,簡單說減少系統調用,純服務,系統調用是最大的瓶頸。即使是複用的模式下,也是建立多個連結的。一般tars針對目标ip節點的socket是4個。并不都是一個,隻是說這個配置在大多數的情況下夠用,不是固定是1,特别是在發包是10mb以上的時候,還是需要增加connection來提升部分qps。這一部分結合網絡基礎的情況,一個Ip可發起的端口,最大隻有65545,當調用後端有1萬個服務的時候,這個socket是肯定不夠用的。這一部分可以很大程度解決這一個帶來的問題。第二個是大規模的叢集環境下,流量特别大,大量的socket會給路由器帶來負載。是以,隻要包不要太大,其實一個連接配接也可以,關注socket饑餓效應就可以了。這一部分需要大量的理論知識,這個也是tars設計的基石,肯定會有各方面充足的理論基礎給各個設計上面有支撐。

6、最大連接配接數量問題

單個用戶端發出連接配接數量:在連結發起端,受端口号的限制理論上最多可以建立65535左右連結。端口号的理論值範圍是從0到65535,系統端口的是0-1023 ,注冊端口是1024-65535為使用者端口。

/proc/sys/net/ipv4/ip_local_port_range

查詢可用端口區間, 預設是:32768~61000,可用于定義網絡連接配接可用作其源(本地)端口的最小和最大端口的限制,同時适用于TCP和UDP連接配接。說明這台機器本地能向外連接配接61000-32768=28232個連接配接,注意是本地向外連接配接。

單個服務端接收連接配接數量:TCP連接配接中Server IP + Server Port + Client IP + Client Port這個組合辨別一個連接配接。服務端通常固定在某個本地端口上監聽,等待用戶端的連接配接請求。如果不考慮位址重用(SO_REUSEADDR選項)的情況下,即使服務端端有多個網卡ip,本地監聽端口也是獨占的,是以服務端TCP連接配接四元組中隻有Client IP + Client Port是可變的,是以服務端TCP最大連接配接為Client IP 數×Client Port數,對IPV4,不考慮ip位址分類等因素,最大TCP連接配接數約為2的32次方(ip數)×2的16次方(port數),也就是服務端端單機理論最大TCP連接配接數約為2的48次方。但是如上所述最高的并發數量都要受到系統對使用者單一程序同時可打開檔案數量和記憶體占用的限制。這是因為系統為每個TCP連接配接被accept後都要建立一個連接配接套接字socket句柄,每個socket句柄同時也是一個檔案句柄。

  • ulimit -n 輸出的結果,說明對于一個程序而言最多能打開多少個檔案,是以你要采用此預設配置最多也就可以并發上千個TCP連接配接。臨時修改:ulimit -n 1000000,但是這種臨時修改隻對目前登入使用者目前的使用環境有效,系統重新開機或使用者退出後就會失效。永久修改:編輯/etc/rc.local,在其後添加如下内容:ulimit -SHn 1000000
  • 實際情況下,每建立一個連結需要消耗一定的記憶體,大概是4-10kb,是以連結數也受限于機器的總記憶體。

參數調優: 伺服器為支援更大的連接配接數量,大記憶體、檔案描述符限制足夠大可以滿足一定數量的連接配接。參數調優可以增加連結數量。一個Socket連接配接預設是有記憶體消耗的,可以按需調整tcp socket的參數如tcp發送\接收緩沖區的大小。

7、連接配接數過多的問題:

“Cannot assign requested address.”

:是由于Linux配置設定的用戶端連接配接端口用盡或本地可配置設定端口數比較少,無法建立socket連接配接所緻。

cat /proc/sys/net/ipv4/ip_local_port_range

查詢可用端口區間, 預設是:32768 61000,說明這台機器本地能向外連接配接61000-32768=28232個連接配接,注意是本地向外連接配接,不是這台機器的所有連接配接,不會影響這台機器的 80端口的對外連接配接數。參考連結 解決辦法:

vi  /etc/sysctl.conf
net.ipv4.ip_local_port_range = 1024 65535
sysctl -p
           

netstat參數說明:

netstat -[atunlp]
-a :all,表示列出所有的連接配接,服務監聽,Socket資料
-t :tcp,列出tcp協定的服務
-u :udp,列出udp協定的服務
-n :port number, 用端口号來顯示
-l :listening,列出目前監聽服務(顯示作為服務端的連接配接,不加顯示的是作為用戶端的連接配接)
-p :program,列出服務程式的PID
           

netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}'

檢視tcp的連接配接狀态和數目。

RPC架構的連接配接模型

可臨時重新開機(systemctl restart serviceName)相應的服務,使得連接配接數下降。0.0.0.0是一個特殊的IP位址,指的是本機的全部IP位址。如果一個應用綁定了0.0.0.0上的某個端口,意味着隻要是發往這個端口的請求,不管是來自哪個IP位址,都會由這個應用處理。一般伺服器都是多網卡的。

8、一次長連接配接的時長:

在Linux中/etc/sysctl.conf下有長連接配接的全局配置:

  • net.ipv4.tcp_keepalive_time=7200
  • net.ipv4.tcp_keepalive_intvl=75
  • net.ipv4.tcp_keepalive_probes=9

系統預設這個tcp_keepalive_time的值為7200s,也就是2個小時,這個值代表如果TCP連接配接發送完最後一個ACK包後,如果超過2個小時,沒有資料往來,那麼這個連接配接會斷掉。

實際上TCP的keepalive機制會維持長連接配接狀态,但是很多的作業系統核心實作TCP協定時,都加上了這個keepalive機制,那麼這個功能預設是關閉的,如果TCP之間沒有任何資料來往了在tcp_keepalive_time(7200s,2h)後,伺服器給用戶端發送一個探測包,如果對方有回應,說明這個連接配接還存活,否則繼續每隔tcp_keepalive_intvl(預設為75s)給對方發送探測包,如果連續tcp_keepalive_probes(預設為9)次後,依然沒有收到對端的回複,那麼則認為這個連接配接已經關閉。如果收到了對端的回複,TCP為接下來的兩小時複位存活定時器,如果在這兩個小時到期之前,連接配接上發生應用程式的通信,則定時器重新為往下的兩小時複位,并且接着交換資料。tcp keepalive預設不是開啟的,如果想使用KeepAlive,需要在你的應用中設定SO_KEEPALIVE才可以生效。

當用戶端端等待超過一定時間後自動給服務端發送一個空的封包,如果對方回複了這個封包證明連接配接還存活着,如果對方沒有封包傳回且進行了多次嘗試都是一樣,那麼就認為連接配接已經丢失,用戶端就沒必要繼續保持連接配接了。如果對端被關閉然後重新開機的情況,當系統被操作員關閉時,所有的應用程式程序(也就是用戶端程序)都将被終止,用戶端TCP會在連接配接上發送一個FIN。收到這個FIN後,伺服器TCP向伺服器程序報告一個檔案結束,以允許伺服器檢測這種狀态。

二、RPC中的長連接配接(tars舉例)

Tars 用戶端通路服務節點時預設采用長連接配接的形式,這樣在并發請求大的情況下,單個連接配接處理不過就會導緻逾時了。主流的RPC架構都會追求性能選擇使用長連接配接,是以如何保活連接配接就是一個重要的話題。保活一般采用TCP機制keep_alive和應用層心跳檢測。

應用層心跳檢測: 長連接配接實作,一般在應用層也有心跳檢測機制,因為同一條連接配接上關聯的請求太多,為了減小傷害面積,能夠盡早發現異常連接配接狀态越好,那麼其實tcp的keepalive就應該夠了,那麼為啥要實作應用層的心跳呢?這裡其實還有另外的考慮,tcp的保活定時器是系統層面上的東西,對于自己的代碼其實并不能感覺到,就算是對面的程序因為永久性的阻塞而無法服務了,其實保活定時器是無法處理這種情況的。簡單應用層心跳檢測的實作為設定一個定時器Timer, 每隔一段時間發送一個心跳檢測包。比如微服務沒隔一段時間向注冊中心上報自己的心跳狀态。

連接配接逾時機制: 用戶端rpc的逾時一定要有,另外服務端,如果是并發的,不管是線程還是協程,都一定要監控運作狀态,防止永久性的阻塞造成記憶體洩露,設定一些門檻值,在需要的時候直接終止正在執行的rpc請求。

1、tars用戶端是用epoll組織的,tars用戶端預設socket為長連接配接,啟動TCP程式設計裡的keepAlive機制。每個長連接配接有逾時機制。

int NetworkUtil::createSocket(bool udp, bool isLocal/* = false*/, bool isIpv6/* = false*/)
{
    int domain = isLocal ? PF_LOCAL : (isIpv6 ? PF_INET6 : PF_INET);
    int type = udp ? SOCK_DGRAM : SOCK_STREAM;
    int protocol = udp ? IPPROTO_UDP : IPPROTO_TCP;
    int fd = socket(domain, type, protocol);
	...
    if(!udp)
    {
        setTcpNoDelay(fd);

        setKeepAlive(fd);
    }
    return fd;
}
           

2、連續兩次tars調用可能不會複用tcp長連接配接,有兩次負載均衡可能導緻配置設定到的socket連接配接不同。但也有可能配置設定到同一個objectProxy的AdapterProxy對應的長連接配接中。 CommunicatorEpoll()會把該套接字放入epoll中進行監聽資料的收發。

(1)第一層負載均衡:輪詢選擇ObjectProxy(CommunicatorEpoll)和與之相對應的ReqInfoQueue

ServantProxy::selectNetThreadInfo();

(2)第二層負載均衡: 通過EndpointManager選擇AdapterProxy,負載均衡算法(Hash、權重、輪詢)

配置檔案netthread屬性決定了用戶端communicatorEpoll的數量,若配置檔案沒配置則預設為1.

void Communicator::initialize()
{
    TC_LockT<TC_ThreadRecMutex> lock(*this);
	...
    //用戶端網絡線程
    _clientThreadNum = TC_Common::strto<size_t>(getProperty("netthread","1"));

    if(0 == _clientThreadNum)
    {
        _clientThreadNum = 1;
    }
    else if(MAX_CLIENT_THREAD_NUM < _clientThreadNum)
    {
        _clientThreadNum = MAX_CLIENT_THREAD_NUM;
    }
	...
    for(size_t i = 0; i < _clientThreadNum; ++i)
    {
        _communicatorEpoll[i] = new CommunicatorEpoll(this, i);
        _communicatorEpoll[i]->start();
    }
    ...
}
           

3、tcp長連接配接也有逾時,若逾時超過一定比例或連接配接異常,會屏蔽節點關閉連接配接。若沒有逾時或網絡異常,會維持一個長連接配接。

ServantProxy::ServantProxy(Communicator * pCommunicator, ObjectProxy ** ppObjectProxy, size_t iClientThreadNum)
: _communicator(pCommunicator)
, _objectProxy(ppObjectProxy)
, _objectProxyNum(iClientThreadNum)
, _syncTimeout(DEFAULT_SYNCTIMEOUT)
, _asyncTimeout(DEFAULT_ASYNCTIMEOUT)
, _id(0)
, _masterFlag(false)
, _queueSize(1000)
, _minTimeout(100)
    ...
}

/**
     * 預設的同步調用逾時時間
     * 逾時後不保證消息不會被服務端處理
*/
enum { DEFAULT_SYNCTIMEOUT = 3000, DEFAULT_ASYNCTIMEOUT=5000};
msg->request.iTimeout     = (ReqMessage::SYNC_CALL == msg->eType)?_syncTimeout:_asyncTimeout;


//屏蔽結點
void AdapterProxy::setInactive()
{
    _activeStatus  = false;

    _nextRetryTime = TNOW + _objectProxy->checkTimeoutInfo().tryTimeInterval;

    _trans->close();

    TLOGINFO("[TARS][AdapterProxy::setInactive objname:" << _objectProxy->name() << ",desc:" << _endpoint.desc() << ",inactive" << endl);
}
           

4、在用戶端第二層負載均衡:通過EndpointManager選擇AdapterProxy,負載均衡算法(Hash、權重、輪詢),會調用AdapterProxy::checkActive(bool bForceConnect)建立和目标server的tcp/Udp 連接配接。若後續用戶端請求負載均衡到該objectProxy的AdapterProxy中,該連接配接可被向目标server發起請求的用戶端複用。若重新向目标server發起連接配接socket 套接字會更新。

bool AdapterProxy::checkActive(bool bForceConnect)
{
   ...
    //連接配接沒有建立或者連接配接無效, 重建立立連接配接
    if(!_trans->isValid())
    {
        try
        {
            _trans->reconnect();
        }
        catch(exception &ex)
        {
            _activeStatus = false;

            _trans->close();

            TLOGERROR("[TARS][AdapterProxy::checkActive connect ex:" << ex.what() << endl);
        }
    }
...
    return (_trans->hasConnected() || _trans->isConnecting());
}

void Transceiver::connect()
{
...
        fd = NetworkUtil::createSocket(false, false, _ep.isIPv6());
        NetworkUtil::setBlock(fd, false);

        socklen_t len = _ep.isIPv6() ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);
        bool bConnected = NetworkUtil::doConnect(fd, _ep.addrPtr(), len);
        if(bConnected)
        {
            setConnected();
        }
        else
        {
            _connStatus     = Transceiver::eConnecting;
            _conTimeoutTime = TNOWMS + _adapterProxy->getConTimeout();
        }
    }
	_fd = fd//重建立立套接字連接配接
 ...
}

           

5、若用戶端負載均衡到一個adapterProxy中,會調用該adapterProxy的transcever向服務端發送資料,一個AdapterProxy内維持一個transceiver, 一個transceiver内維持一個EndpointInfo(連接配接的節點資訊)資訊和相應的用戶端套接字socket。

int AdapterProxy::invoke(ReqMessage * msg)
{
  ...
    //交給連接配接發送資料,連接配接連上,buffer不為空,直接發送資料成功
    if(_timeoutQueue->sendListEmpty() && _trans->sendRequest(msg->sReqData.c_str(),msg->sReqData.size()) != Transceiver::eRetError)
    {
        TLOGINFO("[TARS][AdapterProxy::invoke push (send) objname:" << _objectProxy->name() << ",desc:" << _endpoint.desc() << ",id:" << msg->request.iRequestId << endl);

        //請求發送成功了,單向調用直接傳回
        if(msg->eType == ReqMessage::ONE_WAY)
        {
        #ifdef _USE_OPENTRACKING
            finishTrack(msg);
        #endif

            delete msg;
            msg = NULL;

            return 0;
        }

     ...
    return 0;
}
           

6、逾時處理

void CommunicatorEpoll::run()
{
    ServantProxyThreadData * pSptd = ServantProxyThreadData::getData();
    assert(pSptd != NULL);
    pSptd->_netThreadSeq = (int)_netThreadSeq;
 while (!_terminate)
    {
        try
        {
            int iTimeout = ((_waitTimeout < _timeoutCheckInterval) ? _waitTimeout : _timeoutCheckInterval);
           int num = _ep.wait(iTimeout);
            if(_terminate)
            {
                break;
            }
            //先處理epoll的網絡事件
            for (int i = 0; i < num; ++i)
            {
                const epoll_event& ev = _ep.get(i);
                uint64_t data = ev.data.u64;
                if(data == 0)
                {
                    continue; //data非指針, 退出循環
                }
                handle((FDInfo*)data, ev.events);
            }
            //處理逾時請求
            doTimeout();
            //資料上報
            doStat();
        }
      ...
}

void CommunicatorEpoll::doTimeout()
{
    int64_t iNow = TNOWMS;
    if(_nextTime > iNow)
    {
        return;
    }

    //每_timeoutCheckInterval檢查一次
    _nextTime = iNow + _timeoutCheckInterval;

    for(size_t i = 0; i < _objectProxyFactory->getObjNum(); ++i)
    {
        const vector<AdapterProxy*> & vAdapterProxy=_objectProxyFactory->getObjectProxy(i)->getAdapters();
        for(size_t iAdapter=0;iAdapter<vAdapterProxy.size();++iAdapter)
        {
            vAdapterProxy[iAdapter]->doTimeout();
        }
        _objectProxyFactory->getObjectProxy(i)->doTimeout();
    }
}
           

7、服務端關閉連接配接。當網絡逾時或者讀取寫入資料失敗時會在服務端關閉連接配接connection。

void TC_EpollServer::NetThread::processNet(const epoll_event &ev)
{
    Connection *cPtr = getConnectionPtr(uid);
    ...
    if (ev.events & EPOLLERR || ev.events & EPOLLHUP)
    {
        delConnection(cPtr,true,EM_SERVER_CLOSE);

        return;
    }
    if(ev.events & EPOLLIN)               //有資料需要讀取
    {
        recv_queue::queue_type vRecvData;

        int ret = recvBuffer(cPtr, vRecvData);

        if(ret < 0)
        {
            delConnection(cPtr,true,EM_CLIENT_CLOSE);

            return;
        }
        if(!vRecvData.empty())
        {
            cPtr->insertRecvQueue(vRecvData);
        }
    }

    if (ev.events & EPOLLOUT)              //有資料需要發送
    {
        int ret = sendBuffer(cPtr);

        if (ret < 0)
        {
            delConnection(cPtr,true,(ret==-1)?EM_CLIENT_CLOSE:EM_SERVER_CLOSE);

            return;
        }
    }

    _list.refresh(uid, cPtr->getTimeout() + TNOW);
}
           

三、Tars 異步長連接配接的實作:

傳統的 HTTP 服務多是基于同步的線程模型,由于 HTTP 協定本身無狀态,是以在協定層面就不支援異步,是以當在用戶端發起一次 HTTP 調用時主調線程必須挂起等待被調響應請求,這個時候主調線程的資源則被浪費了,因為線程資源是有限的,大量線程被挂起等待白白浪費了主調方的運算資源。相比于使用HTTP協定的正常方案,TARS首先提供的特性就是異步長連接配接的RPC調用方式:

發起一個異步調用之後,目前線程并不會被阻塞而是繼續執行,當收到服務端響應之後在回調線程池中通過回調函數來執行結果的處理。這樣所有的處理線程都一直處于工作的狀态中,而不會挂起導緻線程資源的浪費。整體上提升了服務的處理能力。

TARS的異步能力主要是通過兩個部分的異步來實作的,首先是網絡首發包的異步,TARS的網絡層實作采用了Reactor模型,通過nio提供的事件IO實作基于事件的異步網絡IO。第二是線程模型的異步,我們從線程模型上來看TARS如何是做到異步調用的:

RPC架構的連接配接模型

TARS的主要通過上圖的過程來完成異步調用,首先主調線程發起異步調用,主調線程将請求内容加入網絡線程池的發送隊列中,之後該線程繼續執行。網絡線程池使用Reactor模型實作,通過nio提供的Selecter實作事件IO,是以所有網絡線程均是事件驅動的異步IO,當監聽到對應連接配接的寫事件後将請求發送,等待監聽到讀事件後讀取響應并交給回調線程處理響應。這樣所有的線程都避免了IO阻塞達到了更高的利用效率。

同步異步和長短連接配接:

按照長短連接配接、同異步做笛卡爾積的結果一共四種,但是平時我們隻說其中三種:同步短連接配接、同步長連接配接、異步長連接配接,異步短連接配接一般不會提,是因為異步短連接配接是比同步短連接配接效率還要低,實作卻比同步短連接配接複雜很多。

​ 異步短連接配接:client、server各開一個端口監聽,用于接收資料;client連接配接server端口,發送請求,關閉連接配接;server處理完請求,依據請求來源,建立與用戶端的連接配接,發送應答,關閉連接配接;比較複雜。一般不會采用。

​ 同步短連接配接: server開端口監聽,client連接配接server端口,發送請求,服務端處理請求,傳回應答資料,關閉連接配接;因為實作簡單,而且在不穩定網絡狀況下有一些優勢,經常被采用;如果短連接配接被用在了交易頻繁的系統上,會産生很多待釋放的連接配接,會引發一些問題。

​ 同步長連接配接:server開端口,client連接配接server端口,發送請求,服務端處理請求,傳回應答資料,server繼續接收請求……;實作複雜度排名第三,吞吐效率排名第二,時延名額排名第一;其實和同步短連接配接實作起來差不多,多寫個循環,但是效率能改善很多,複雜或不穩定網絡環境下需要進行很多異常判斷;适合于對時延要求很嚴格,但是吞吐效率其次的場景。

​ 異步長連接配接: client采用異步調用的形式發起長連接配接,發送資料後不會一直等待資料結果,而是自己傳回,當服務端資料傳回時會有epoll事件監聽機制處理。複雜度較高,時延和吞吐較好;适合與對吞吐效率、時延名額要求比較平衡的場景。