1. 程式人生 > >【網路程式設計】說說Redis的服務端設計

【網路程式設計】說說Redis的服務端設計

引子

感覺這東西看過不記一下總會忘,所以手不能懶,及時總結一下。
本文主要針對Redis的服務端模型進行分析,力爭能有總體的思路和部分細緻的深入。原始碼版本3.2.8.

正文

Redis服務端一個典型的單執行緒reactor模型,使用I/O多路複用來完成對檔案描述符的監聽,然後主執行緒依次處理就緒的事件。

I/O多路複用

思路非常的簡單,首先我們知道I/O多路複用有好幾種方式,而這常常是和平臺相關的,所以為了實現的簡潔,擴充套件性,跨平臺性,Redis在這裡進行了一層封裝,通過一套統一的API完成整個網路通訊部分。
以Linux下的epoll為例,讓我們來看一下。

typedef
struct aeApiState { int epfd; struct epoll_event *events; } aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop);

初始化,主要是分配記憶體和呼叫epoll_create生成epoll監聽fd,eventLoop結構體是服務端事件驅動的結構體。

static int aeApiResize(aeEventLoop *eventLoop, int setsize);
eventLoop記錄了能監控的事件最大數,這裡重新調整大小。

static void aeApiFree(aeEventLoop *eventLoop);


釋放直接分配給eventLoop的記憶體空間

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
向eventLoop裡面新增要監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_ADD)

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
刪除監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_DEL)

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);


I/O多路複用的阻塞呼叫,對於epoll來說就是epoll_wait,第二個引數表示epoll要㩐待的最長時間。

static char *aeApiName(void);
返回封裝的I/O多路複用實現,對於epoll來說就是返回“epoll”

而在選擇I/O多路複用的實現時,是按照效能的從高到低。
在Redis的網路庫ae.c最前面是這樣的

#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c" // Linux
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c" // BSD
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

自然,跨平臺的select作為保留選擇,但是由於其效能原因,放在最後了。

兩種事件

Redis的服務端通過事件驅動,I/O事件和定時事件,I/O事件我們都很熟悉,便是可讀/寫或者accept返回的fd,而定時事件的處理之前則沒怎麼接觸,最近也趁機好好學習了《高效能服務端》裡面的 定時器 一章。所以在這裡順便展開小說一下定時器。

定時器

定時器裡面有兩個最基本的屬性,即超時時間和處理函式(回撥函式),當然可能還有其他的屬性,比如是否需要重啟等等,一個定時器就是一個定時事件,我們可以通過連結串列、時間輪、最小堆來組織定時事件。
而在監控定時事件時可以通過訊號(定時發訊號檢測是否到時),I/O多路複用(超時引數)來實現。

服務端事件驅動流程

在服務端啟動之後,首先會進行各種初始化工作,在初始化結束之後,便進入aeMain,開始執行服務端的事件迴圈,等待客戶端連線。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0; // 終止flag
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

可以看到這裡便是不斷呼叫aeProcessEvents,並且監聽兩種種類事件(AE_ALL_EVENTS)

事件種類

#define AE_FILE_EVENTS 1 // I/O事件
#define AE_TIME_EVENTS 2 // 定時事件
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)

每次aeProcessEvents都是一次I/O多路複用的輪詢,讓我們來看一下這個核心函式的細節
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
eventLoop就是我們的事件驅動主結構體,flag則記錄了需要關心的事件(主迴圈似乎是會關心所有事件,這裡我認為主要體現了Redis網網路庫的拓展性,程式也許只需要關心一種事件)。

if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) 
    return 0;
/*
既然只有兩種事件,那麼這兩種事件都不關心自然是直接結束了(return ASAP--as soon as possible)
*/
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { // 需要關心時間事件,並且不是立即返回
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
            /*在最近記錄的定時事件中,查詢還有最短時間的事件*/
        if (shortest) { //找到一個距離到時最近的定時事件
            long now_sec, now_ms;

            aeGetTime(&now_sec, &now_ms);//獲取現在的時間(秒和毫秒)
            tvp = &tv;

            long long ms =
                (shortest->when_sec - now_sec)*1000 +
                shortest->when_ms - now_ms;
            //計算距離到時還有多少時間
            if (ms > 0) { // 如果定時事件還沒到達時間,將剩餘時間記錄在tvp中
                tvp->tv_sec = ms/1000;
                tvp->tv_usec = (ms % 1000)*1000;
            } else {// 已經有定時事件到時了,那麼就不等待,將tvp設定為0
                tvp->tv_sec = 0; 
                tvp->tv_usec = 0;
            }
        } else { //shortest為NULL,目前沒有等待的定時事件
            if (flags & AE_DONT_WAIT) { /AE_DONT_WAIT被設定則不需要等待
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                //永遠等待直到有事件發生
                tvp = NULL; 
            }
        }

經過這一步,我們已經計算出了多路複用需要的超時時間儲存在tvp裡了。然後就是對監聽的fd進行輪詢了。

numevents = aeApiPoll(eventLoop, tvp);//返回發生的事件
        for (j = 0; j < numevents; j++) {//遍歷,處理事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

            if (fe->mask & mask & AE_READABLE) {// 可讀事件
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);//呼叫之前新增的處理函式
            }
            if (fe->mask & mask & AE_WRITABLE) {// 可寫事件
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);// 同上
            }
            processed++;// 成功處理的事件數加1
        }
    }
    // 可以看到,當一個fd 上既可讀又可寫時是優先處理可讀事件的

因為我們剛才設定的傳給aeApiPoll的時間是定時事件剛好到時的剩餘時間,所以現在定時事件已經到時了,我們還需要再去處理定時事件。
當然,因為我們是處理完所有非定時事件之後,才處理定時事件的,所以實際處理定時事件可能要比預定的時間慢一點。

    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    return processed;

讓我們再來看processTimeEvents的細節。

首先是一個異常處理,如果系統時間發生錯亂被移到未來(moved to the future),這裡我們就不關心細節了。

    int processed = 0;
    aeTimeEvent *te, *prev;
    long long maxId;

    time_t now = time(NULL);   // 獲取現在的時間
    eventLoop->lastTime = now; // 設定時間

    prev = NULL;               // 前驅指標
    te = eventLoop->timeEventHead;// te就是定時事件連結串列頭
    maxId = eventLoop->timeEventNextId-1;//定時事件的最大ID
while(te) {// 主迴圈
        long now_sec, now_ms;
        long long id;

        if (te->id == AE_DELETED_EVENT_ID) { // 這個事件要被刪除
            aeTimeEvent *next = te->next;
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
            if (te->finalizerProc)// 清理資源
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);// 釋放資源
            te = next;
            continue;
        }


        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        aeGetTime(&now_sec, &now_ms);
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms)) //如果定時事件到時了(現在的時間大於設定的時間)
        {
            int retval;

            id = te->id;
            retval = te->timeProc(eventLoop, id, te->clientData); //呼叫處理函式處理
            processed++; //處理數+1
            if (retval != AE_NOMORE) { //如果這個事件之後還要執行,也就是隔一段時間再執行的事件
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); //新增時間
            } else {
                te->id = AE_DELETED_EVENT_ID;//否則設定刪除標誌
            }
        }
        prev = te;
        te = te->next;//處理下一個事件
    }
return processed;// 返回處理數

至此,一次對時間事件處理就結束了,aeProcessEvents也就結束了。
而服務端主要就是一個死迴圈aeProcessEvents來進行事件處理。

參考閱讀

《Redis設計與實現》中的 第12章事件,第14章伺服器。
小夥伴Tanswer之前也分析了ae.c的原始碼,並且還有之前學長利用ae寫的demo,請移步 Redis網路庫原始碼淺解

相關推薦

網路程式設計說說Redis服務設計

引子 感覺這東西看過不記一下總會忘,所以手不能懶,及時總結一下。 本文主要針對Redis的服務端模型進行分析,力爭能有總體的思路和部分細緻的深入。原始碼版本3.2.8. 正文 Redis服務端一個典型的單執行緒reactor模型,使用I/O多路複用來

網路程式設計服務的I/O模型,事件處理模式,併發模式

前言之前的前言 本文作於6月中旬,當時對於很多概念不是很理解,所以寫到一半實在進行不下去,通過最近的學習終於理解了一些,趕緊總結記下。 前言 本篇主要總結伺服器端開發中的一些基本的框架。 如果你在東區二樓點過黃燜雞,相信你一定能更好的理解。

網路程式設計tcp伺服器與客戶

TCP與UDP的區別: TCP傳輸控制協議(穩定)(慢一些) UDP使用者資料包協議(不穩定)(快一些) TCP有三次握手,a給b請求資料,b傳送請求確認併發送一個數據包,a收到資料包再發送確認訊息給b

網路程式設計TCP網路程式設計中connect()、listen()和accept()三者之間的關係

舉個簡單的例子(以下程式碼只是示範性的,用於說明不同套接字的作用,實際的函式會需要更多的引數): /* 建立用於監聽和接受客戶端連線請求的套接字 */ server_sock = socket(); /* 繫結監聽的IP地址和埠 */ bind(server_sock); /* 開始監聽 */ li

網路程式設計網路程式設計 筆記

  https://blog.csdn.net/bandaoyu/article/details/83312754 Windows下C語言的Socket程式設計例子 https://blog.csdn.net/bandaoyu/article/details/83312102

3、網路程式設計Socket程式設計

一、Socket定義     Socket:在TCP/IP協議中,“IP地址+TCP或UDP埠號”唯 一標識網路通訊中的一個程序,所以“IP地址+埠號”就稱為socket。 在TCP協議中,建立連線的兩個程序各自有一個socket來標識,那麼這兩個socket組成的socket pair

2、網路程式設計TCP報文段/網路位元組序/主機位元組序/網-主位元組序轉換函式

一、TCP報文段格式     TCP雖然是面向位元組流的,但TCP傳送的資料單元卻是報文段。一個TCP報文段分為首部和資料兩個部分。TCP報文段首部的前20個位元組是固定的,後面有4n位元組是根據需要增加的選項。TCP首部的最小長度是20位元組,最大長度是60位元組。

1、網路程式設計Socket/TCP/UDP/HTTP/HTTPS/網路分層模型

一、簡介 1、相關概念     TCP:傳送控制協議(Transmission Control Protocol)     UDP:使用者資料報協議 (UDP:User Datagram Protocol)     HTTP:全稱是HyperText Transfer Pro

網路程式設計滑動視窗詳解 (TCP流量控制)

滑動視窗 (TCP流量控制) 介紹UDP時我們描述了這樣的問題:如果傳送端傳送的速度較快,接收端接收到資料後處理的速度較慢,而接收緩衝區的大小是固定的,就會丟失資料。TCP協議通過“滑動視窗(Slid

javaweb:servlet服務下載中文名稱檔案應該注意的問題!!!

請看下面的程式碼,與普通的英文名稱檔案下載方式不同,裡面涉及到編碼和解碼的問題! package com.content; import java.io.FileInputStream; import java.io.IOException; import java.io

shell程式設計 nginx 服務的啟動指令碼

#!/bin/bash # # nginx This shell script takes care of starting and stopping # standalone nginx. # config: /usr/local/ngi

網路程式設計之九、事件選擇WSAEventSelect

WSAEventSelect模型是類似於WSAAsyncSelect模型的另一個有用的非同步I/O模型。它允許應用程式在一個或者多個套接字上接收以事件為基礎的網路事件。 在這裡,最主要的差別是在於網路事件會投遞到一個事件物件控制代碼。並不是投遞到一個視窗。 我們使用事件

網路程式設計libevent 入門

#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <assert.h> #include <event2/event.h> #include <event2/

網路程式設計處理定時事件(二)---利用訊號通知

前言 這篇的誕生也很不容易,感謝Jung Zhang學長和瑞神的橘子。 在上一篇,我們通過Redis對定時事件的處理有了一定的認識,今天我們繼續按照《高效能伺服器程式設計》上邊的思路,用C++來實現一個小demo。 本篇中,我們將利用alarm函式來完成定

許可權維持window服務常見後門技術

0x00 前言   未知攻焉知防,攻擊者在獲取伺服器許可權後,通常會用一些後門技術來維持伺服器許可權,伺服器一旦被植入後門,攻擊者如入無人之境。這裡整理一些window服務端常見的後門技術,瞭解攻擊者的常見後門技術,有助於更好去發現伺服器安全問題。 常見的後門技術列表: 1、隱藏、克隆賬戶 2、shift

網路程式設計socket、埠、程序的關係

1. Socket 的概念 埠是TCP/IP協議中的概念,描述的是TCP協議上層的應用(FTP,HTTP,SMTP…),在作業系統中,可以理解為基於TCP的系統服務或者說系統程序!如下圖,FTP就需要佔用特定的TCP埠。TCP/IP 協議棧的實現在系統層,HT

Java基礎知識網路程式設計(瀏覽器&服務

TCP協議傳輸資料時有客戶端和服務端,客戶端和服務端無非就是基於網路應用的程式而已,生活中,瀏覽器就是一個標準的客戶端。 1演示服務端和瀏覽器: 服務端: 服務端即自己的主機,寫一個服務端的小程式碼: import java.io.*; import j

網路程式設計資料傳輸時的位元組序

前言 可能小組的同學很早就聽說過大小端,但是似乎這個順序並沒有什麼卵用。。(我就是這麼想的)不過在學習網路程式設計中,突然對這個問題有了新的認識,趕緊總結下,不然以後肯定踩坑。。。 本文假定讀者已經明白了大小端的區別,並且對於網路程式設計、TCP/IP有一定

VS開發TCP服務如何判斷客戶斷開連線

23.1介紹 在一個空閒的(idle)TCP連線上,沒有任何的資料流,許多TCP/IP的初學者都對此感到驚奇。也就是說,如果TCP連線兩端沒有任何一個程序在向對方傳送資料,那麼在這兩個TCP模組之間沒有任何的資料交換。你可能在其它的網路協議中發現有輪詢(polling),但在TCP中它不存在。言外之意就

網路程式設計非阻塞connect詳解

一、為什麼使用非阻塞connect     TCP連線的建立涉及一個在三路握手過程,阻塞的connect一直等到客戶收到自己的SYN的ACK才返回,這需要至少一個RTT時間,RTT時間波動很大從幾毫秒到幾秒。而且在沒有響應時,會等待數秒再次傳送,(詳見TCPv2第828頁)