linux下使用hiredis非同步API實現sub/pub訊息訂閱和釋出的功能
redis_publisher.cpp/************************************************************************* > File Name: redis_publisher.h > Author: chenzengba > Mail: [email protected] > Created Time: Sat 23 Apr 2016 10:15:09 PM CST > Description: 封裝hiredis,實現訊息釋出給redis功能 ************************************************************************/ #ifndef REDIS_PUBLISHER_H #define REDIS_PUBLISHER_H #include <stdlib.h> #include <hiredis/async.h> #include <hiredis/adapters/libevent.h> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #include <boost/tr1/functional.hpp> class CRedisPublisher { public: CRedisPublisher(); ~CRedisPublisher(); bool init(); bool uninit(); bool connect(); bool disconnect(); bool publish(const std::string &channel_name, const std::string &message); private: // 下面三個回撥函式供redis服務呼叫 // 連接回調 static void connect_callback(const redisAsyncContext *redis_context, int status); // 斷開連線的回撥 static void disconnect_callback(const redisAsyncContext *redis_context, int status); // 執行命令回撥 static void command_callback(redisAsyncContext *redis_context, void *reply, void *privdata); // 事件分發執行緒函式 static void *event_thread(void *data); void *event_proc(); private: // libevent事件物件 event_base *_event_base; // 事件執行緒ID pthread_t _event_thread; // 事件執行緒的訊號量 sem_t _event_sem; // hiredis非同步物件 redisAsyncContext *_redis_context; }; #endif
redis_subscriber.h/************************************************************************* > File Name: redis_publisher.cpp > Author: chenzengba > Mail: [email protected] > Created Time: Sat 23 Apr 2016 10:15:09 PM CST > Description: ************************************************************************/ #include <stddef.h> #include <assert.h> #include <string.h> #include "redis_publisher.h" CRedisPublisher::CRedisPublisher():_event_base(0), _event_thread(0), _redis_context(0) { } CRedisPublisher::~CRedisPublisher() { } bool CRedisPublisher::init() { // initialize the event _event_base = event_base_new(); // 建立libevent物件 if (NULL == _event_base) { printf(": Create redis event failed.\n"); return false; } memset(&_event_sem, 0, sizeof(_event_sem)); int ret = sem_init(&_event_sem, 0, 0); if (ret != 0) { printf(": Init sem failed.\n"); return false; } return true; } bool CRedisPublisher::uninit() { _event_base = NULL; sem_destroy(&_event_sem); return true; } bool CRedisPublisher::connect() { // connect redis _redis_context = redisAsyncConnect("127.0.0.1", 6379); // 非同步連線到redis伺服器上,使用預設埠 if (NULL == _redis_context) { printf(": Connect redis failed.\n"); return false; } if (_redis_context->err) { printf(": Connect redis error: %d, %s\n", _redis_context->err, _redis_context->errstr); // 輸出錯誤資訊 return false; } // attach the event redisLibeventAttach(_redis_context, _event_base); // 將事件繫結到redis context上,使設定給redis的回撥跟事件關聯 // 建立事件處理執行緒 int ret = pthread_create(&_event_thread, 0, &CRedisPublisher::event_thread, this); if (ret != 0) { printf(": create event thread failed.\n"); disconnect(); return false; } // 設定連接回調,當非同步呼叫連線後,伺服器處理連線請求結束後呼叫,通知呼叫者連線的狀態 redisAsyncSetConnectCallback(_redis_context, &CRedisPublisher::connect_callback); // 設定斷開連接回調,當伺服器斷開連線後,通知呼叫者連線斷開,呼叫者可以利用這個函式實現重連 redisAsyncSetDisconnectCallback(_redis_context, &CRedisPublisher::disconnect_callback); // 啟動事件執行緒 sem_post(&_event_sem); return true; } bool CRedisPublisher::disconnect() { if (_redis_context) { redisAsyncDisconnect(_redis_context); redisAsyncFree(_redis_context); _redis_context = NULL; } return true; } bool CRedisPublisher::publish(const std::string &channel_name, const std::string &message) { int ret = redisAsyncCommand(_redis_context, &CRedisPublisher::command_callback, this, "PUBLISH %s %s", channel_name.c_str(), message.c_str()); if (REDIS_ERR == ret) { printf("Publish command failed: %d\n", ret); return false; } return true; } void CRedisPublisher::connect_callback(const redisAsyncContext *redis_context, int status) { if (status != REDIS_OK) { printf(": Error: %s\n", redis_context->errstr); } else { printf(": Redis connected!\n"); } } void CRedisPublisher::disconnect_callback( const redisAsyncContext *redis_context, int status) { if (status != REDIS_OK) { // 這裡異常退出,可以嘗試重連 printf(": Error: %s\n", redis_context->errstr); } } // 訊息接收回調函式 void CRedisPublisher::command_callback(redisAsyncContext *redis_context, void *reply, void *privdata) { printf("command callback.\n"); // 這裡不執行任何操作 } void *CRedisPublisher::event_thread(void *data) { if (NULL == data) { printf(": Error!\n"); assert(false); return NULL; } CRedisPublisher *self_this = reinterpret_cast<CRedisPublisher *>(data); return self_this->event_proc(); } void *CRedisPublisher::event_proc() { sem_wait(&_event_sem); // 開啟事件分發,event_base_dispatch會阻塞 event_base_dispatch(_event_base); return NULL; }
redis_subscriber.cpp:/************************************************************************* > File Name: redis_subscriber.h > Author: chenzengba > Mail: [email protected] > Created Time: Sat 23 Apr 2016 10:15:09 PM CST > Description: 封裝hiredis,實現訊息訂閱redis功能 ************************************************************************/ #ifndef REDIS_SUBSCRIBER_H #define REDIS_SUBSCRIBER_H #include <stdlib.h> #include <hiredis/async.h> #include <hiredis/adapters/libevent.h> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #include <boost/tr1/functional.hpp> class CRedisSubscriber { public: typedef std::tr1::function<void (const char *, const char *, int)> NotifyMessageFn; // 回撥函式物件型別,當接收到訊息後呼叫回撥把訊息傳送出去 CRedisSubscriber(); ~CRedisSubscriber(); bool init(const NotifyMessageFn &fn); // 傳入回撥物件 bool uninit(); bool connect(); bool disconnect(); // 可以多次呼叫,訂閱多個頻道 bool subscribe(const std::string &channel_name); private: // 下面三個回撥函式供redis服務呼叫 // 連接回調 static void connect_callback(const redisAsyncContext *redis_context, int status); // 斷開連線的回撥 static void disconnect_callback(const redisAsyncContext *redis_context, int status); // 執行命令回撥 static void command_callback(redisAsyncContext *redis_context, void *reply, void *privdata); // 事件分發執行緒函式 static void *event_thread(void *data); void *event_proc(); private: // libevent事件物件 event_base *_event_base; // 事件執行緒ID pthread_t _event_thread; // 事件執行緒的訊號量 sem_t _event_sem; // hiredis非同步物件 redisAsyncContext *_redis_context; // 通知外層的回撥函式物件 NotifyMessageFn _notify_message_fn; }; #endif
/*************************************************************************
> File Name: redis_subscriber.cpp
> Author: chenzengba
> Mail: [email protected]
> Created Time: Sat 23 Apr 2016 10:15:09 PM CST
> Description:
************************************************************************/
#include <stddef.h>
#include <assert.h>
#include <string.h>
#include "redis_subscriber.h"
CRedisSubscriber::CRedisSubscriber():_event_base(0), _event_thread(0),
_redis_context(0)
{
}
CRedisSubscriber::~CRedisSubscriber()
{
}
bool CRedisSubscriber::init(const NotifyMessageFn &fn)
{
// initialize the event
_notify_message_fn = fn;
_event_base = event_base_new(); // 建立libevent物件
if (NULL == _event_base)
{
printf(": Create redis event failed.\n");
return false;
}
memset(&_event_sem, 0, sizeof(_event_sem));
int ret = sem_init(&_event_sem, 0, 0);
if (ret != 0)
{
printf(": Init sem failed.\n");
return false;
}
return true;
}
bool CRedisSubscriber::uninit()
{
_event_base = NULL;
sem_destroy(&_event_sem);
return true;
}
bool CRedisSubscriber::connect()
{
// connect redis
_redis_context = redisAsyncConnect("127.0.0.1", 6379); // 非同步連線到redis伺服器上,使用預設埠
if (NULL == _redis_context)
{
printf(": Connect redis failed.\n");
return false;
}
if (_redis_context->err)
{
printf(": Connect redis error: %d, %s\n",
_redis_context->err, _redis_context->errstr); // 輸出錯誤資訊
return false;
}
// attach the event
redisLibeventAttach(_redis_context, _event_base); // 將事件繫結到redis context上,使設定給redis的回撥跟事件關聯
// 建立事件處理執行緒
int ret = pthread_create(&_event_thread, 0, &CRedisSubscriber::event_thread, this);
if (ret != 0)
{
printf(": create event thread failed.\n");
disconnect();
return false;
}
// 設定連接回調,當非同步呼叫連線後,伺服器處理連線請求結束後呼叫,通知呼叫者連線的狀態
redisAsyncSetConnectCallback(_redis_context,
&CRedisSubscriber::connect_callback);
// 設定斷開連接回調,當伺服器斷開連線後,通知呼叫者連線斷開,呼叫者可以利用這個函式實現重連
redisAsyncSetDisconnectCallback(_redis_context,
&CRedisSubscriber::disconnect_callback);
// 啟動事件執行緒
sem_post(&_event_sem);
return true;
}
bool CRedisSubscriber::disconnect()
{
if (_redis_context)
{
redisAsyncDisconnect(_redis_context);
redisAsyncFree(_redis_context);
_redis_context = NULL;
}
return true;
}
bool CRedisSubscriber::subscribe(const std::string &channel_name)
{
int ret = redisAsyncCommand(_redis_context,
&CRedisSubscriber::command_callback, this, "SUBSCRIBE %s",
channel_name.c_str());
if (REDIS_ERR == ret)
{
printf("Subscribe command failed: %d\n", ret);
return false;
}
printf(": Subscribe success: %s\n", channel_name.c_str());
return true;
}
void CRedisSubscriber::connect_callback(const redisAsyncContext *redis_context,
int status)
{
if (status != REDIS_OK)
{
printf(": Error: %s\n", redis_context->errstr);
}
else
{
printf(": Redis connected!");
}
}
void CRedisSubscriber::disconnect_callback(
const redisAsyncContext *redis_context, int status)
{
if (status != REDIS_OK)
{
// 這裡異常退出,可以嘗試重連
printf(": Error: %s\n", redis_context->errstr);
}
}
// 訊息接收回調函式
void CRedisSubscriber::command_callback(redisAsyncContext *redis_context,
void *reply, void *privdata)
{
if (NULL == reply || NULL == privdata) {
return ;
}
// 靜態函式中,要使用類的成員變數,把當前的this指標傳進來,用this指標間接訪問
CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(privdata);
redisReply *redis_reply = reinterpret_cast<redisReply *>(reply);
// 訂閱接收到的訊息是一個帶三元素的陣列
if (redis_reply->type == REDIS_REPLY_ARRAY &&
redis_reply->elements == 3)
{
printf(": Recieve message:%s:%d:%s:%d:%s:%d\n",
redis_reply->element[0]->str, redis_reply->element[0]->len,
redis_reply->element[1]->str, redis_reply->element[1]->len,
redis_reply->element[2]->str, redis_reply->element[2]->len);
// 呼叫函式物件把訊息通知給外層
self_this->_notify_message_fn(redis_reply->element[1]->str,
redis_reply->element[2]->str, redis_reply->element[2]->len);
}
}
void *CRedisSubscriber::event_thread(void *data)
{
if (NULL == data)
{
printf(": Error!\n");
assert(false);
return NULL;
}
CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(data);
return self_this->event_proc();
}
void *CRedisSubscriber::event_proc()
{
sem_wait(&_event_sem);
// 開啟事件分發,event_base_dispatch會阻塞
event_base_dispatch(_event_base);
return NULL;
}
問題1:hiredis官網沒有非同步介面的實現例子。
hiredis提供了幾個非同步通訊的API,一開始根據API名字的理解,我們實現了跟redis伺服器建立連線、訂閱和釋出的功能,可在實際使用的時候,程式並沒有像我們預想的那樣,除了能夠建立連線外,任何事情都沒發生。 網上查了很多資料,原來hiredis的非同步實現是通過事件來分發redis傳送過來的訊息的,hiredis可以使用libae、libev、libuv和libevent中的任何一個實現事件的分發,網上的資料提示使用libae、libev和libuv可能發生其他問題,這裡為了方便就選用libevent。hireds官網並沒有對libevent做任何介紹,也沒用說明使用非同步機制需要引入事件的介面,所以一開始走了很多彎路。 關於libevent的使用這裡就不再贅述,詳情可以見libevent官網。 libevent官網:http://libevent.org/ libevent api文件:https://www.monkey.org/~provos/libevent/doxygen-2.0.1/include_2event2_2event_8h.html#6e9827de8c3014417b11b48f2fe688ae CRedisPublisher和CRedisSubscriber的初始化過程: 初始化事件處理,並獲得事件處理的例項:_event_base = event_base_new();
在獲得redisAsyncContext *之後,呼叫
redisLibeventAttach(_redis_context, _event_base);
這樣就將事件處理和redis關聯起來,最後在另一個執行緒呼叫
event_base_dispatch(_event_base);
啟動事件的分發,這是一個阻塞函式,因此,建立了一個新的執行緒處理事件分發,值得注意的是,這裡用訊號燈_event_sem控制執行緒的啟動,意在程式呼叫
redisAsyncSetConnectCallback(_redis_context,
&CRedisSubscriber::connect_callback);
redisAsyncSetDisconnectCallback(_redis_context,
&CRedisSubscriber::disconnect_callback);
之後,能夠完全捕捉到這兩個回撥。
問題2 奇特的‘ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context’錯誤
有些人會覺得這兩個類設計有點冗餘,我們發現CRedisPublisher和CRedisSubscriber很多邏輯是一樣的,為什麼不把他們整合到一起成一個類,既能夠釋出訊息也能夠訂閱訊息。其實一開始我就是這麼幹的,在使用的時候發現,用同個redisAsynContex *物件進行訊息訂閱和釋出,與redis服務連線會自動斷開,disconnect_callback回撥會被呼叫,並且返回奇怪的錯誤:ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context,因此,不能使用同個redisAsyncContext *物件實現釋出和訂閱。這裡為了減少設計的複雜性,就將兩個類的邏輯分開了。 當然,你也可以將相同的邏輯抽象到一個基類裡,並實現publish和subscribe介面。問題3 相關依賴的庫
編譯之前,需要安裝hiredis、libevent和boost庫,我是用的是Ubuntu x64系統。 hiredis官網:https://github.com/redis/hiredis 下載原始碼解壓,進入解壓目錄,執行make && make install命令。 libevent官網:http://libevent.org/下載最新的穩定版 解壓後進入解壓目錄,執行命令 ./configure -prefix=/usr sudo make && make install boost庫:直接執行安裝:sudo apt-get install libboost-dev 如果你不是用std::tr1::function的函式物件來給外層通知訊息,就不需要boost庫。你可以用介面的形式實現回撥,把介面傳給CRedisSubscribe類,讓它在接收到訊息後呼叫介面回撥,通知外層。問題4 如何使用
最後貼出例子程式碼。 publisher.cpp,實現釋出訊息:/*************************************************************************
> File Name: publisher.cpp
> Author: chenzengba
> Mail: [email protected]
> Created Time: Sat 23 Apr 2016 12:13:24 PM CST
************************************************************************/
#include "redis_publisher.h"
int main(int argc, char *argv[])
{
CRedisPublisher publisher;
bool ret = publisher.init();
if (!ret)
{
printf("Init failed.\n");
return 0;
}
ret = publisher.connect();
if (!ret)
{
printf("connect failed.");
return 0;
}
while (true)
{
publisher.publish("test-channel", "Test message");
sleep(1);
}
publisher.disconnect();
publisher.uninit();
return 0;
}
subscriber.cpp實現訂閱訊息:
/*************************************************************************
> File Name: subscriber.cpp
> Author: chenzengba
> Mail: [email protected]
> Created Time: Sat 23 Apr 2016 12:26:42 PM CST
************************************************************************/
#include "redis_subscriber.h"
void recieve_message(const char *channel_name,
const char *message, int len)
{
printf("Recieve message:\n channel name: %s\n message: %s\n",
channel_name, message);
}
int main(int argc, char *argv[])
{
CRedisSubscriber subscriber;
CRedisSubscriber::NotifyMessageFn fn =
bind(recieve_message, std::tr1::placeholders::_1,
std::tr1::placeholders::_2, std::tr1::placeholders::_3);
bool ret = subscriber.init(fn);
if (!ret)
{
printf("Init failed.\n");
return 0;
}
ret = subscriber.connect();
if (!ret)
{
printf("Connect failed.\n");
return 0;
}
subscriber.subscribe("test-channel");
while (true)
{
sleep(1);
}
subscriber.disconnect();
subscriber.uninit();
return 0;
}
關於編譯的問題:在g++中編譯,注意要加上-lhiredis -levent引數,下面是一個簡單的Makefile:
EXE=server_main client_main
CC=g++
FLAG=-lhiredis -levent
OBJ=redis_publisher.o publisher.o redis_subscriber.o subscriber.o
all:$(EXE)
$(EXE):$(OBJ)
$(CC) -o publisher redis_publisher.o publisher.o $(FLAG)
$(CC) -o subscriber redis_subscriber.o subscriber.o $(FLAG)
redis_publisher.o:redis_publisher.h
redis_subscriber.o:redis_subscriber.h
publisher.o:publisher.cpp
$(CC) -c publisher.cpp
subscriber.o:subscriber.cpp
$(CC) -c subscriber.cpp
clean:
rm publisher subscriber *.o
致謝: redis非同步API使用libevent:http://www.tuicool.com/articles/N73uuu
相關推薦
linux下使用hiredis非同步API實現sub/pub訊息訂閱和釋出的功能
本文轉載自連結: 最近使用redis的c介面——hiredis,使客戶端與redis伺服器通訊,實現訊息訂閱和釋出(PUB/SUB)的功能,我把遇到的一些問題和解決方法列出來供大家學習。 廢話不多說,先貼程式碼。 redis_publisher.
Linux下mysql基於MyCat實現主從復制和讀寫分離
mycat1.1 MyCat介紹及應用場景MyCat介紹MyCat是一個開源的分布式數據庫系統,是一個實現了MySQL協議的服務器,前端用戶可以把它看作是一個數據庫代理,用MySQL客戶端工具和命令行訪問,而其後端可以用MySQL原生協議與多個MySQL服務器通信,也可以用JDBC協議與大多數主流數據庫服務器
Linux下Wi-Fi的實現:wireless_tools和wpa_supplicant
erl 密碼 fig 而是 tar.gz 方式 控制 nec dbm 轉載於:https://www.cnblogs.com/lidabo/p/6069455.html 平臺為hi35XX,在Liunx下借助wireless_tools和wpa_supplicant(因為現
Linux下的簡易shell實現
Linux系統的shell作為作業系統的外殼,為使用者提供使用作業系統的介面。 它是命令語言、命令解釋程式及程式設計語言的統稱。 相當於bash的一個子程序,父程序等待,子程序進行程式替換。 shell充當一個橋樑:將使用者的命令翻譯給核心(kernel)處理;同時,將核心的
Linux下網路socket程式設計——實現伺服器(select)與多個客戶端通訊
Linux下網路socket程式設計——實現伺服器(select)與多個客戶端通訊 置頂 2017年06月23日 14:44:37 閱讀數:3225 標籤: socket程式設計伺服器與多個客戶端通epoll多路複用C語言網路程式設計 更多
linux下操作svn,實現根據時間段檢視某個指定使用者提交的記錄
svn log -v -r {2018-9-10}:{2018-9-25} | sed -n '/danni/,/--$/ p' 其中: -r {2018-9-10}:{2018-9-25}:通過-r來指定時間段 -v:表示顯示詳細資訊 sed -n '/danni/,/--$/
Linux下進度條的實現(加彩色版本)
一:進度條 關於進度條不用我多說,當我們在安裝程式或者軟體時通常會看到軟體安裝進度,而這個給我以視覺感受的進度表示,通常被叫做進度條。 二:進度條需要的知識儲備 緩衝區: 對於緩衝區這個概念我們其實並不
Linux下用c語言實現發送http請求 方式可以Get或者Post例程參考
sockaddr select sleep online 創建 線程終止 index -s lse [1].[代碼] Linux下用c語言實現發送http請求 方式可以Get或者Post 跳至 [1] ? 1 2 3 4 5 6 7 8 9 10 11 12 1
Linux下用c語言實現傳送http請求 方式可以Get或者Post例程參考
[1].[程式碼] Linux下用c語言實現傳送http請求 方式可以Get或者Post 跳至 [1] ? 1 2
Linux下併發伺服器的實現
實現併發伺服器的方式有多種,下面說一下我瞭解到的幾種解決方案。 方案一:多程序併發伺服器 主程序監聽、accept()連線,子程序負責處理業務邏輯和流的讀取。 缺點:程序需
Linux下安裝Redis並實現遠端連線,Redis Desktop Manager視覺化連線
1.下載redis redis不是安裝包,例如tomcat,mysql等都是安裝包直接解壓就可以使用,redis是原始檔,需要用編譯後才可以使用。 2.使用xftp把壓縮包拖入到root/redis/資料夾下,並解壓 tar -zxvf redis-5.0.0.
Linux下用c語言實現傳送http請求
前言 在linux下,使用socket進行程式設計,需要到伺服器上進行獲取資料,伺服器使用的php程式設計,需要使用http的方式進行獲取資料。 程式碼 #include <stdio.h> #include <string.h&
linux下使用crontab如何實現mysql資料庫每天自動備份定時備份
直接上教程,基本上出現的問題都在下面了; 首先檢查你的伺服器是否安裝了crontab,命令如下: 命令: crontab 如果提示-bash: crontab:commandnot fo
live555在Linux下最簡單地實現實時流媒體點播
通過Live555交叉編譯後執行發現,上面實現的流媒體實時通過檔案伺服器的檔案點播,沒有相關的流媒體實現方式, 但在Linux下,可以通過某些技巧實現Live555伺服器實時流媒體伺服器,並且是傻瓜式的,簡易程度不需要修改Live555下面一行程式碼。 首先,需要
Linux下如何用C實現MD5加密
md5典型應用是對一段資訊(Message)產生資訊摘要(Message-Digest),以防止被篡改。比如,在UNIX下有很多軟體在下載的時候都有一個檔名相同,副檔名為.md5的檔案,在這個檔案中通常只有一行文字,大致結構如:MD5 (tanajiya.tar.gz)
linux下利用openssl來實現證書的頒發(詳細步驟)
1、首先需要安裝openssl,一個開源的實現加解密和證書的專業系統。在centos下可以利用yum安裝。 2、openssl的配置檔案是openssl.cnf,我們一般就是用預設配置就可以。如果證書有特殊要求的話,可以修改配置適應需求。這樣必須把相關的檔案放到配置檔
Linux下通用掃描器API——SANE( Scanner Access Now Easy)
一、SANE簡介 SANE( Scanner Access Now Easy),是一個應用程式程式設計介面(API),它提供給任何光柵影象掃描器硬體標準化的訪問(平板掃描器,手持式掃描器,視訊和靜止相機,影象採集卡等。 )。該API是公共領域,它的討論和發展,是對所
Linux下簡易web伺服器實現
今天突然對http的web伺服器感興趣了,就研究了一下,發現linux下的web伺服器就是一個socket程式設計的伺服器端,而我們用的ie,chrome等瀏覽器就是客戶端,只不過傳送和接收資料按照http網頁格式,就相當於對資料進行了封裝,相當於加上了檔案頭和檔案
linux下socket程式設計 select實現非阻塞模式多臺客戶端與伺服器通訊
select函式原型如下: int select (int maxfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select系統呼叫是用來讓我們的程式
Linux下使用libevent庫實現伺服器端程式設計
一、背景 TCP伺服器端一般用到非阻塞Socket、IO複用,Linux可以支援epoll、select,而Windows支援select、IOCP,考慮平臺適用性,需要對IO事件進行封裝相容; 二、相關知識 2.1 事件驅動(I/O複用) 服務端常用到的 select