天天看點

Redis筆記(2)單機資料庫實作

1.前言

  上節總結了一下redis的資料結構和對象構成,本章介紹redis資料庫一個基本面貌,是如何設計的。

2.資料庫

  伺服器結構redisServer:

    redisDb *db: 一個數組,儲存伺服器中所有的資料庫

    dbnum:  伺服器的資料庫數量,預設16個

    saveparam *saveparams: 設定的儲存選項

    long dirty: 修改計數器

    time_t lastsave: 上一次執行儲存的時間

    sds aof_buf: AOF緩沖區

    list *clients: 連結清單,儲存了所有用戶端狀态

  預設情況下連接配接的資料庫是0,可以通過select指令切換到目标資料庫。

  用戶端結構redisClient:

    redisDb *db: 記錄用戶端目前正在使用的資料庫

    sds querybuf: 輸入緩沖區

    robj **argv: 指令解析的内容

    int argc: argv數組的長度

    struct redisCommand *cmd: 指令指向的資料結構

  資料庫結構redisDb:

    dict *dict 資料庫鍵空間,儲存着資料庫所有鍵值對

    dict *expires: 儲存所有鍵的過期時間

  指令的基本操作都是查找鍵、再取值。

  讀寫鍵空間的維護操作:

    讀取一個鍵後,伺服器會根據鍵是否存在來更新伺服器的鍵命中次數或不命中次數,可以通過INFO stats指令檢視keyspace_hits屬性和keyspace_misses屬性中檢視。

    讀取一個鍵後,會更新該鍵的lru時間。

    讀取一個鍵時,發現該鍵已過期,會先删除再進行後續操作。

    WATCH一個鍵後,如果修改了,會被标記為髒

    每修改一次鍵,都會對髒計數+1,會觸發持久化操作和複制操作。

    如果開啟了通知功能,修改後會發送對應資料庫通知。

  設定鍵過期:

    EXPIRE 秒級過期時間

    PEXPIRE 毫秒過期時間

    SETEX指令隻針對字元串鍵,可以在設定時同時設定過期時間。

    EXPIREAT和PEXPIREAT設定具體的過期時間,是unix時間戳。

  四個指令最終EXPIRE、PEXPIRE、EXPIREAT都是通過PEXPIREAT實作的。

  儲存過期時間:

    設定了過期時間的鍵會放入db的過期時間字典表中。

  移除過期時間:

    PERSIST

  剩餘生存時間:

    TTL

  過期鍵的判定:

    1.是否存在于過期鍵的表中

    2.是否已經過期

  過期鍵的删除政策:

    1.定時删除,為過期鍵建立定時器,到時間就立刻删除。

      優點:快速釋放資源

      缺點:消耗CPU,此外定時器使用到時間事件,實作方式是無序連結清單,O(N)複雜度。該方法不現實。

    2.惰性删除:取鍵的時候檢測過期時間,過期了就删除。

      優先:不消耗CPU

      缺點:消耗記憶體

    3.定期删除:每一段時間執行一次删除操作,可以限制頻率和時長,減少對CPU的影響。折中手段。

    Redis采取了惰性删除和定期删除的政策。

    定期删除過程:1.從一定數量的資料庫中(小于16個用全部,大于等于取16個)中取出一定數量的随機鍵(預設20),删除其中的過期鍵

           2.current_db記錄目前檢查的進度,下次檢查時,接着處理。比如這次處理到10,下次就處理11号資料庫。

           3.檢查了所有的資料庫,重置current_db為0。

  AOF、RDB和複制功能對過期鍵的處理:

    RDB:生成RDB時,過期鍵不會被儲存。主伺服器啟動時,未過期被載入,過期忽略。從伺服器啟動所有鍵都被載入,資料同步時從伺服器會被清空,是以一般不會影響。

    AOF:鍵過期了,但沒有被檢測删除,無影響,檢測删除後會追加一條DEL指令。AOF重寫時會忽略過期鍵。

    複制:主從複制模式中,隻有主伺服器會删除過期鍵,再發送一個消息給從伺服器删除,在删除之前,從伺服器會将過期鍵傳回用戶端。

  資料庫通知:

    redis 2.8之後用戶端可以訂閱給定的頻道或者模式,獲知資料庫中鍵的變化,以及資料庫中指令的執行情況。

    通知有兩種類型:鍵空間通知——某個鍵執行了什麼指令,關注的是具體鍵

            鍵事件通知——某個指令被什麼鍵執行了,關注的是具體指令

    這個可以設定notify-keyspace-events來決定發送的通知類型:

      二者都要,設定 AKE

      所有類型的鍵空間,設定AK

      所有類型的鍵事件,設定AE

      隻關注字元串鍵有關的鍵空間和事件,設定K$

      隻關注清單鍵有關的鍵事件通知,設定El

    其餘的具體見官方文檔。

3.RDB持久化

  RDB檔案的建立與載入:

    手動方式有兩個指令:SAVE、BGSAVE。

    SAVE會阻塞伺服器,BGSAVE會fork一個子程序,由其處理。

    載入是在伺服器啟動時自動執行的,是以沒有專門的載入指令。

    注意:AOF檔案更新頻率比RDB檔案要高,是以如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案來還原資料庫。隻有AOF關閉的情況下才會使用RDB檔案。

    BGSAVE執行期間:SAVE指令會被拒絕,BGSAVE指令也會被拒絕,BGREWRITEAOF不能同時執行:BGSAVE執行期間,BGREWRITEAOF會等BGSAVE執行完畢再執行,如果是後者執行期間,BGSAVE會被拒絕。

  自動間隔性儲存:

    可以配置save選項,讓伺服器隔一段時間自動執行BGSAVE指令。

    save 900 1      900秒内修改了一次

    save 300 10      300秒内修改了10次

    save 60 10000     60秒内修改了10000次

  檢查儲存條件:

    serverCron函數預設每隔100毫秒執行一次,檢查儲存條件是否滿足,滿足就會執行BGSAVE,重置lastsave和dirty計數。

  RDB檔案結構:

    REDIS: 5個位元組,儲存REDIS五個字元,說明該檔案是RDB檔案

    db_version: 4個位元組,字元串整數,記錄RDB檔案的版本号。

    database: 包含若幹個資料庫的資料

      SELECTDB:1個位元組,意味着是個資料庫,後面是資料庫号碼

      db_number:資料庫号碼,可以是1位元組、2位元組、5位元組。伺服器會調用SELECT指令,根據号碼切換資料庫。

      key_value_pairs:儲存了所有鍵值對資料,如果帶有過期時間,會和鍵值對儲存在一起。長度不定。

        EXPIRETIME_MS:标志帶有過期時間

        ms: 8位元組帶符号整數,記錄毫秒為機關的unix時間戳。

        TYPE:記錄了value的類型,長度1位元組,可以是以下類型:

            string、list、set、zset、hash、list_ziplist、set_inset、zset_ziplist、hash_ziplist

        key:一個字元串對象,與string類型一緻

        value:

            string類型結構:  encoding: int(8,16,32)   raw.

                    小于等于20位元組原樣,大于20位元組壓縮(開啟了壓縮,否則原樣)

                   壓縮結構:LZF标志LZF算法  compressed_len壓縮長度,origin_len原長度,compressed_string 壓縮後字元串

            list結構:list_length:元素個數 item長度 item值

            hash結構:hash_size: key1 value1,都是長度和值

            有序集合對象:sorted_set_size member1 socre1 也是長度和值 2 "pi" 4  "3.14"

            intset集合:将整型轉成字元串對象,再儲存。

            ziplist: 轉換成一個字元串對象,儲存。讀取的時候判斷是ziplist,轉換成原來的對象。

    EOF:1位元組,标志檔案内容結束。

    check_sum: 8位元組長的無符号整數,儲存着一個校驗和,對前面四部分進行計算得到的值,用于檢測檔案是否損壞。

4.AOF持久化

  AOF持久化:

    該持久化通過儲存寫指令來記錄資料庫的狀态。分為3個步驟:指令追加,檔案寫入,檔案同步三個步驟。

    AOF開啟時,有寫指令,會按協定格式寫入伺服器的aof_buf中。

    redis伺服器是個事件循環,循環接收用戶端請求,向用戶端發送指令,每次結束一個循環後,會決定是否将aof_buf的内容寫入AOF檔案中。

    可以配置appendfsync決定持久化行為:

      always: 将所有緩沖資料寫入AOF檔案,并同步

      everysec: 将aof_buf所有内容寫入AOF檔案,如果上次同步時間距離現在超過1s,再次同步,同步操作由一個線程專門負責。預設是這個選項。

      no: 将緩沖區寫入AOF檔案,但不立刻同步,由作業系統決定。

    檔案的寫入和同步的含義在于:現代作業系統為了提高檔案寫的效率,會将資料暫存到記憶體緩存區,等緩沖區填滿或者超過了指定的時限,才寫入磁盤。雖然提升了效率,但也帶來了風險。計算機如果停機,那麼儲存的資料也會丢失。是以系統提供了fsync和fdatasync兩個同步函數,可以将緩存區中的資料立刻寫入磁盤,保證資料安全性。

  AOF檔案的加載和資料還原:

    1.建立一個僞用戶端,因為redis指令隻在用戶端上下文中執行。

    2.從AOF檔案中分析讀取一條寫指令

    3.使用僞用戶端執行被讀出的寫指令

    4.重複2,3步驟,直到全部處理完畢。

  這樣資料庫就被還原成原本的狀态了。

  AOF重寫:

  因為AOF是通過儲存寫指令來記錄狀态的,意味着每次修改都會産生記錄,檔案内容會越來越多,遠超實際資料量,是以需要對AOF檔案進行優化。redis提供了AOF檔案重寫功能來解決這一問題。

  雖然稱為重寫,但不是讀取現有的AOF來完成的,而是直接根據目前資料庫狀态實作的。比如目前資料有個資料number 值為3,就添加一條寫記錄SET number 3。而且對于list, set類型可以将多個值合并成一個指令,比如RPUSH list a b c d e。整個過程是:周遊資料庫,周遊讀取未過期的鍵,根據鍵類型進行重寫,如果有過期時間,過期時間也要重寫。新的AOF檔案是目前資料庫的所有狀态,是以不會浪費空間。

  注意,由于list,set等會将多個值合并到一個指令中,就可能産生一個指令過長,導緻緩沖區溢出。是以在重寫list,hash,set,zset時會檢查元素數量,如果超過redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,就會使用多個指令分批處理。目前是64。

  AOF背景重寫:

  同樣的原因,如果資料量很大,很容易阻塞服務的線程,是以redis采取子程序的方法執行。之是以是子程序不是子線程在于子程序帶有伺服器程序的資料副本,可以在避免使用鎖的情況下,保證資料安全。

  這樣會産生一個問題,在子程序重寫過程中,處理新的寫入操作,AOF會丢失這部分資料。為了解決該問題,redis服務設定了一個AOF重寫緩沖區,在建立子程序之後開始使用。redis執行完一個寫指令後,會将這個寫指令同時發送給aof緩存區和aof重寫緩沖區。這樣可以保證在重寫過程中,原AOF檔案按照原步驟一樣繼續寫入,重寫的AOF檔案也可以在重寫緩沖區中讀取到這段時間新增加的資料。寫完資料庫狀态後,子程序會發一個信号給父程序,父程序開始将AOF重寫緩沖區的資料寫入新的AOF檔案,再替換掉舊的檔案。這個過程由父程序完成,就避免了在這段時間内有新的請求發送過來,或者是指令隻寫了aof緩存區還沒來得及寫入aof重寫緩沖區造成丢失資料。全部完成之後,就可以像往常一樣處理資料了。

5.事件

  redis伺服器是一個事件驅動程式,主要處理兩類事件:檔案事件和時間事件。

5.1 檔案事件

  redis基于reactor模式開發了自己的網絡事件處理器,檔案事件處理器。使用IO多路複用程式來監聽多個套接字,為套接字目前執行的任務來為套接字關聯不同的事件處理器。應答,讀取,寫入,關閉等操作都會關聯相關的事件處理器來處理這些事件。

  檔案事件處理器的構成有四個部分:套接字、IO多路複用程式、檔案事件分派器、事件處理器。盡管多個檔案事件可以并發出現(套接字),但是通過多路IO複用程式就會成有序,同步每次一個套接字的方式向檔案事件分派器傳遞套接字,上一個處理完了,才會輪到下一個。

  實作都是使用常見的select、epoll、evport、kqueue這些庫來實作的,會根據作業系統自動選擇底層實作。

  檔案事件的處理器:

    連接配接應答處理器:有套接字連接配接的時候服務端就會産生AE_READABLE事件,引發連接配接應答處理器執行。

    指令請求處理器:讀取用戶端發送的指令請求内容。用戶端産生AE_READABLE事件,執行指令請求處理器。

    指令回複處理器:用戶端産生AE_WRITEABLE事件,執行指令回複處理器。

5.2 時間事件

  redis事件分為2類:

    定時事件:讓一段程式在指定時間之後執行一次。比如說:讓程式X在目前時間的30毫秒之後執行一次。

    周期性事件:讓一段程式每隔指定時間執行一次。比如說:讓程式Y每隔30毫秒執行一次。

  一個時間事件包含3部分:id 全局唯一  when 事件執行時間  timeProc時間事件處理器。

  如果事件是定時事件,執行完後就會移除。周期事件就會更新when的值。所有的事件都放入了一個無序連結清單中,每次執行時間事件會周遊整個連結清單,查詢可執行的時間事件進行執行。無序指的是事件不按when排列,是以需要周遊整個連結清單。

  時間事件的應用就是serverCron函數,其主要為了維護redis的資源,保證redis長期穩定運作。主要工作如下:

    更新伺服器的各類統計資訊,比如時間、記憶體占用、資料庫占用情況

    清理資料庫中過期的鍵值對

    關閉和清理連接配接失效的用戶端

    嘗試進行AOF或RDB持久化操作

    如果伺服器是主伺服器,那麼對從伺服器進行定期同步

    如果處于叢集模式,對叢集進行定期同步和連接配接測試

  serverCron在redis2.6中每秒運作10次。

6.用戶端

6.1用戶端屬性

  屬性分為2類:通用屬性,特定功能相關的屬性,比如操作資料庫時需要使用的db和dictid屬性,執行事務時需要用到的mstate屬性,以及watch指令時需要的watched_keys屬性。

  int fd 套接字描述符,屬性值為-1 僞用戶端  大于-1的整數普通用戶端。CLIENT list顯示所有用戶端

  robj *name 用戶端名稱,可以通過CLIENT setname設定,更清晰是哪個用戶端

  int flags 用戶端的标志屬性,記錄用戶端的角色和目前所處狀态。

    REDIS_MASTER 主伺服器

    REDIS_SLAVE 從伺服器

    REDIS_LUA_CLIENT 處理lua腳本裡面包含的redis指令的僞用戶端

    REDIS_MONITOR 用戶端在執行monitor指令

    REDIS_UNIX_SOCKET 伺服器使用unix套接字來連接配接用戶端

    REDIS_BLOCKED 用戶端被brpop blpop等指令阻塞

    REDIS_UNBLOCKED 标志表示用戶端從BLOCKED狀态脫離

    REDIS_MULTI标志用戶端正在執行事務。

    REDIS_DIRTY_CAS 表明事務使用watch指令已經被修改

    REDIS_DIRTY_EXEC 事務在指令入隊時出現了錯誤

    REDIS_CLOSE_ASAP 用戶端緩沖區大小超過伺服器限制

    REDIS_CLOSE_AFTER_REPLY 用戶端發起了Client kill指令或者用戶端發送了錯誤的協定内容

    REDIS_ASKING 标志表示用戶端向叢集節點發送了ASKING指令

    REDIS_FORCE_AOF 強制伺服器将目前執行的指令寫入AOF檔案裡面

    REDIS_FORCE_REPL 将主伺服器的指令複制給所有從伺服器。

6.2 輸入緩沖區

  用戶端發送指令請求,會寫入到redisClient的querybuf中,最大不能超過1G,否則伺服器将關閉這個用戶端。伺服器會對指令進行解析,存在用戶端的argv和argc屬性中,前者是一個資料,放置解析後的指令,後者是一個計數,記錄argv的長度。之後伺服器根據argv[0]的指令将cmd指向具體的資料結構。然後指向指令。

6.3 輸出緩沖區

  用戶端有兩個輸出緩沖區,一個固定大小,用于輸出長度比較小的回複,比如ok等。另一個是可變大小的緩沖區,用于儲存長度較大的回複。

  char buf[REDIS_REPLY_CHUNK_BYTES]  預設大小是16*1024 16kb, bufpos用于記錄使用的位置。如果該空間用完了,或者無法放入,就會使用可變大小的緩沖區,list *reply。

6.4 身份驗證

  int authenticated,該值如果為0,表示用戶端未通過身份驗證,如果為1則通過。未通過的時候隻接受AUTH指令。

5.5 普通用戶端關閉的原因

  1.用戶端退出或者被殺死,網絡連接配接将被關閉,進而用戶端被關閉

  2.用戶端發送了帶有不符合協定格式的指令請求,這個用戶端也會被伺服器關閉

  3.用戶端成了CLIENT KILL的目标,那麼也會被關閉

  4.使用者在伺服器上設定了timeout配置項,用戶端空轉時間超過這個值會被關閉。例外情況:用戶端是主伺服器,被BLPOP等指令阻塞,或者在指向SUBSCRIBE、PSUBSCRIBE等訂閱指令不會因為空轉被關閉。

  5.輸入緩沖區超過1G,被關閉

  6.輸出緩沖區超過限制,會執行相應的操作:一種是硬性限制,超過就會被關閉,一種是軟性限制,超過會被監控,如果指定時間内一直超過會被關閉,恢複正常範圍則不會。

    client-output-buffer-limit名額   normal 0 0 0

                  slave 256mb 64mb 60

                  pubsub 32mb 8mb 60

7 伺服器

7.1 指令執行過程

  1.用戶端發送指令,轉成協定格式,最後通過套接字傳遞給伺服器。

  2.伺服器監聽到可讀事件,讀取指令請求,放入用戶端的輸入緩沖區。對輸入緩沖區進行解析,提取指令和參數儲存在argv和argc屬性中。調用指令執行器,執行用戶端指定的指令。

    執行過程:

      1)根據argv[0]參數在指令表中查找到指定的指令,儲存在cmd屬性中。

      2)預處理:

          檢查cmd是否為null

          參數個數是否正确,

          是否通過身份認證,

          如果打開了maxmemory功能,檢查記憶體情況,決定是否需要回收記憶體。

          檢查上一次BGSAVE指令是否出錯,并且如果打開了stop-writes-on-bgsave-error功能,拒絕寫指令。

          用戶端是否在用SUBSCRIBE指令訂閱頻道,隻會執行SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE指令,其它的會被拒絕。

          如果伺服器在載入資料,用戶端發送的指令必須帶有1辨別(INFO、SHUTDOWN、PUBLISH)才會執行,其它拒絕。

          如果服務端執行Lua腳本逾時阻塞,隻會執行SHUTDOWN nosave指令和SCRIPT KILL 指令,其它拒絕。

          如果用戶端執行事務,隻會執行EXEC、DISCARD、MULTI、WATCH指令,其它會被放入事務隊列。

          如果伺服器打開了螢幕功能,伺服器會将要執行的指令和參數等資訊發送給螢幕。

      3)調用指令的實作函數,傳入參數,傳回結果會被儲存在用戶端狀态的輸出緩沖區。

      4)執行後的後續工作:

          如果服務端開始了慢查詢日志功能,那麼慢查詢日志子產品會檢查是否需要為剛剛執行的指令添加一條新的慢查詢日志

          根據剛剛執行指令所耗費的時長,更新被執行指令的redisCommand結構的milliseconds,将calls計數器的值增加1.

          如果開啟了AOF持久化,會将其寫入AOF緩沖區。

          如果有從伺服器正在複制這個伺服器,将指令廣播給所有從伺服器。

  3.指令回複會儲存在用戶端的輸出緩沖區,變成可寫狀态,伺服器會執行指令回複處理器,将回複内容發送給用戶端。

7.2 serverCron函數

  該函數每100毫秒執行一次,負責管理伺服器資源。

  1.redis中很多地方使用到了時間,為了減少系統調用,使用了緩存。redisServer中的time_t unixtime 儲存了秒級精度的系統目前unix時間戳,long mstime儲存了毫秒級的。每100毫秒會更新這兩個屬性,這兩個精度并不高,redis隻會列印日志、更新LRU時鐘、決定是否執行持久化任務,計算伺服器上線時間這類精度不高要求的功能上使用這些屬性。對于鍵的過期時間,慢查詢日志這種精度高的還是會執行系統調用擷取目前時間。

  2.更新LRU時鐘。redisServer中有個lruclock屬性,預設每10秒更新一次,用于計算鍵的空轉時長,redisObject中有一個lru屬性,儲存了對象最後一次被指令通路的時間,鍵的空轉時間就用lruclock - lru的時間。

  3.更新伺服器每秒執行指令次數,trackOperationsPerSecond函數會以100毫秒一次的頻率執行,抽樣計算的方式,估計記錄伺服器在最近1秒處理的指令數量,可以通過INFO status的instantaneous_ops_per_sec域檢視。是一個估計值

  4.更新伺服器記憶體峰值記錄,記錄目前伺服器使用記憶體數量,比較stat_peak_memory,如果大于就替換。INFO memory指令的used_memory_peak和used_memory_peak_human用兩種格式記錄了記憶體峰值。

  5.處理sigterm信号,啟動伺服器時,redis會為伺服器程序的sigterm信号關聯處理器sigtermHandler,在伺服器接受到sigterm信号時,打開伺服器狀态的shutdown_asap辨別,serverCron會檢查這個屬性,決定是否關閉伺服器。

  6.管理用戶端資源,對一定數量的用戶端進行2個檢查:如果連接配接逾時,釋放用戶端。如果用戶端在上一次執行指令請求後,輸入緩沖區的大小超過了一定長度,那麼程式會釋放用戶端目前的輸入緩沖區,并建立一個預設大小的輸入緩沖區,防止用戶端的輸入緩沖區耗費過多記憶體。

  7.管理資料庫資源,删除過期鍵

  8.執行被延遲的BGREWRITEOF指令,aof_rewrite_scheduled辨別記錄了伺服器是否延遲了BGREWRITEAOF指令。

  9.檢查持久化操作的運作狀态,通過rdb_child_pid和aof_child_pid來判斷BGSAVE或者BGREWRITEAOF指令是否正在執行。如果有一個值不為-1,程式就會執行一次wait3函數,檢查子程序是否有信号發來伺服器程序。如果有意味着備份完成,則進行後續操作,替換新的AOF檔案之類的,沒有意味着未完成,不處理。如果沒有執行備份操作,檢查BGREWRITEAOF是否被延遲了,檢查自動儲存條件是否被滿足了,檢查AOF重寫條件是否滿足了。

  10.将AOF緩沖區中的内容寫入AOF檔案。

  11.關閉異步用戶端。

  12.增加cronloops的值,記錄serverCron執行次數。

7.3 初始化伺服器

  1.初始化伺服器狀态結構:redis.c/initServerConfig函數完成。主要工作是:

    設定伺服器運作ID

    設定伺服器的預設運作頻率

    設定伺服器的預設配置檔案路徑

    設定伺服器的運作架構

    設定伺服器的預設端口号

    設定伺服器的預設RDB持久化條件和AOF持久化條件

    初始化伺服器的LRU時鐘

    建立指令表

  2.載入配置選項

    可以通過指令的方式  redis-server --port xxx 或者配置檔案的方式 redis-server redis.conf

    如果沒有設定,就使用代碼裡預設寫的配置。

  3.初始化伺服器資料結構

    initServerConfig函數初始化隻建立了指令表一個資料結構,還有其它的沒完成,比如:

      server.clients連結清單,記錄了所有與伺服器項相連的用戶端的狀态結構

      server.db 伺服器的所有資料庫

      server.pubsub_channels字典,server.pubsub_patterns連結清單,釋出訂閱使用

      lua環境server.lua

      用于儲存慢查詢日志的server.slowlog屬性

    這個時候才做的原因在于必須先載入配置選項才能正确初始化資料結構。

    這裡還做了其它操作:

      為伺服器設定程序信号處理器

      建立共享對象,比如OK  整數字元串等等

      打開伺服器的監聽端口,并為監聽套接字關聯連接配接應答事件處理器

      為serverCron建立時間事件

      如果AOF持久化打開,那麼打開現有的AOF檔案,如果不存在建立一個,為寫入準備

      初始化伺服器的背景IO子產品。

  4.還原資料庫狀态

    如果設定了AOF使用AOF還原,否則使用RDB還原。

  5.執行事件循環,初始化完成,等待用戶端的連接配接請求。

轉載于:https://www.cnblogs.com/lighten/p/9242384.html