libevent從入門到掌握<四>
數據緩衝Bufferevent
很多時候,除了響應事件之外,應用還希望做一定的數據緩衝。比如說,寫入數據的時候 ,通常的運行模式是:
- 決定要向連接寫入一些數據,把數據放入到緩衝區中
- 等待連接可以寫入
- 寫入儘量多的數據
- 記住寫入了多少數據,如果還有更多數據要寫入,等待連接再次可以寫入
這種緩衝 IO 模式很通用,libevent 爲此提供了一種通用機制,即bufferevent。
bufferevent 由一個底層的傳輸端口(如套接字 ),一個讀取緩衝區和一個寫入緩衝區組成。與通常的事件在底層傳輸端口已經就緒,可以讀取或者寫入的時候執行回調不同的是,bufferevent 在讀取或者寫入了足夠量的數據之後調用用戶提供的回調。
有多種共享公用接口的 bufferevent 類型,編寫本文時已存在以下類型:
-
基於套接字的 bufferevent
:使用 event_*接口作爲後端,通過底層流式套接字發送或者接收數據的 bufferevent -
異步 IO bufferevent
:使用 Windows IOCP 接口,通過底層流式套接字發送或者接收數據的 bufferevent(僅用於 Windows,試驗中) -
過濾型 bufferevent
:將數據傳輸到底層 bufferevent 對象之前,處理輸入或者輸出數據的 bufferevent:比如說,爲了壓縮或者轉換數據。 -
成對的 bufferevent
:相互傳輸數據的兩個 bufferevent。
注意
:截止2.0.2-alpha 版,這裏列出的 bufferevent 接口還沒有完全正交於所有 的 bufferevent 類型。也就是說,下面將要介紹的接口不是都能用於所有bufferevent 類型。libevent 開發 者在未來版本中將修正這個問題。
也請注意
:當前 bufferevent 只能用於像 TCP 這樣的面向流的協議,將來纔可能會支持 像 UDP 這樣的面向數據報的協議。
bufferevent和evbuffer
每個 bufferevent 都有一個輸入緩衝區和一個輸出緩衝區 ,它們的類型都是“struct evbuffer”。 有數據要寫入到 bufferevent 時,添加數據到輸出緩衝區 ;bufferevent 中有數據供讀取的時候,從輸入緩衝區抽取(drain)數據。
回調和水位
每個 bufferevent 有兩個數據相關的回調:一個讀取回調和一個寫入回調。
默認情況下,從底層傳輸端口讀取了任意量的數據之後會調用讀取回調 ;
輸出緩衝區中足夠量的數據被清空到底層傳輸端口後寫入回調會被調用。
通過調整 bufferevent 的讀取和寫入 “水位 (watermarks )”可以覆蓋這些函數的默認行爲。
每個 bufferevent 有四個水位:
-
讀取低水位
:讀取操作使得輸入緩衝區的數據量在此級別或者更高時 ,讀取回調將被調用。默認值爲 0,所以每個讀取操作都會導致讀取回調被調用。 -
讀取高水位
:輸入緩衝區中的數據量達到此級別後, bufferevent 將停止讀取,直到輸入緩衝區中足夠量的數據被抽取 ,使得數據量低於此級別 。默認值是無限 ,所以永遠不會因爲輸入緩衝區的大小而停止讀取。 -
寫入低水位
:寫入操作使得輸出緩衝區的數據量達到或者低於此級別時 ,寫入回調將被調用。默認值是 0,所以只有輸出緩衝區空的時候纔會調用寫入回調。 -
寫入高水位
:bufferevent 沒有直接使用這個水位。它在 bufferevent 用作另外一 個 bufferevent 的底層傳輸端口時有特殊意義。請看後面關於過濾型 bufferevent 的介紹。
bufferevent 接口彙總
1.bufferevent_socket_new()
struct bufferevent *bufferevent_socket_new(
struct event_base *base,
evutil_socket_t fd,
enum bufferevent_options options);
enum bufferevent_options options:
-
BEV_OPT_CLOSE_ON_FREE :釋放 bufferevent 時關閉底層傳輸端口。這將關閉底層套接字,釋放底層 bufferevent 等。
-
BEV_OPT_THREADSAFE :自動爲 bufferevent 分配鎖,這樣就可以安全地在多個線程中使用 bufferevent。
-
BEV_OPT_DEFER_CALLBACKS :設置這個標誌時, bufferevent 延遲所有回調,如上所述。
-
BEV_OPT_UNLOCK_CALLBACKS :默認情況下,如果設置 bufferevent 爲線程安全 的,則 bufferevent 會在調用用戶提供的回調時進行鎖定。設置這個選項會讓 libevent 在執行回調的時候不進行鎖定。
用於創建基於套接字的 bufferevent。
2.bufferevent_socket_connect()
int bufferevent_socket_connect(struct bufferevent *bev,
struct sockaddr *address, int addrlen);
address 和 addrlen 參數跟標準調用 connect()的參數相同。如果還沒有爲 bufferevent 設置套接字,調用函數將爲其分配一個新的流套接字,並且設置爲非阻塞的。
3.bufferevent_free()
void bufferevent_free(struct bufferevent *bev);
這個函數釋放 bufferevent。bufferevent 內部具有引用計數,所以,如果釋放 時還有未決的延遲迴調,則在回調完成之前 bufferevent 不會被刪除。
如果設置了 BEV_OPT_CLOSE_ON_FREE 標誌,並且 bufferevent 有一個套接字或者底層 bufferevent 作爲其傳輸端口,則釋放 bufferevent 將關閉這個傳輸端口。
4.bufferevent_setcb()
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
short events, void *ctx);
void bufferevent_setcb(struct bufferevent *bufev,
bufferevent_data_cb readcb, bufferevent_data_cb writecb,
bufferevent_event_cb eventcb, void *cbarg);
void bufferevent_getcb(struct bufferevent *bufev,
bufferevent_data_cb *readcb_ptr,
bufferevent_data_cb *writecb_ptr,
bufferevent_event_cb *eventcb_ptr,
void **cbarg_ptr);
bufferevent_setcb()函數修改 bufferevent 的一個或者多個回調 。readcb、writecb和eventcb函數將分別在已經讀取足夠的數據 、已經寫入足夠的數據 ,或者發生錯誤時被調用 。
每個回調函數的第一個參數都是發生了事件的bufferevent ,最後一個參數都是調用bufferevent_setcb()時用戶提供的 cbarg 參數:可以通過它向回調傳遞數據。事件回調 的 events 參數是一個表示事件標誌的位掩碼
- bufferevent_enable()
void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);
short bufferevent_get_enabled(struct bufferevent *bufev);
可以啓用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。沒有啓用讀取或者寫入事件時, bufferevent 將不會試圖進行數據讀取或者寫入。
沒有必要在輸出緩衝區空時禁用寫入事件: bufferevent 將自動停止寫入,然後在有數據等 待寫入時重新開始。
類似地,沒有必要在輸入緩衝區高於高水位時禁用讀取事件 :bufferevent 將自動停止讀取, 然後在有空間用於讀取時重新開始讀取。
默認情況下,新創建的 bufferevent 的寫入是啓用的,但是讀取沒有啓用。 可以調用 bufferevent_get_enabled()確定 bufferevent 上當前啓用的事件。
通過bufferevent得到evbuffer
如果只是通過網絡讀取或者寫入數據 ,而不能觀察操作過程,是沒什麼好處的。bufferevent 提供了下列函數用於觀察要寫入或者讀取的數據。
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
這兩個函數提供了非常強大的基礎 :它們分別返回輸入和輸出緩衝區 。關於可以對 evbuffer 類型進行的所有操作的完整信息,請看下一章。
如果寫入操作因爲數據量太少而停止(或者讀取操作因爲太多數據而停止 ),則向輸出緩衝 區添加數據(或者從輸入緩衝區移除數據)將自動重啓操作。
向bufferevent的輸出緩衝區添加數據
int bufferevent_write(struct bufferevent *bufev,
const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
這些函數向 bufferevent 的輸出緩衝區添加數據。 bufferevent_write()將內存中從 data 處開 始的 size 字節數據添加到輸出緩衝區的末尾 。bufferevent_write_buffer()移除 buf 的所有內 容,將其放置到輸出緩衝區的末尾。成功時這些函數都返回 0,發生錯誤時則返回-1。
從bufferevent的輸入緩衝區移除數據
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
這些函數從 bufferevent 的輸入緩衝區移除數據。bufferevent_read()至多從輸入緩衝區移除 size 字節的數據,將其存儲到內存中 data 處。函數返回實際移除的字節數。 bufferevent_read_buffer()函數抽空輸入緩衝區的所有內容,將其放置到 buf 中,成功時返 回0,失敗時返回 -1。
注意,對於 bufferevent_read(),data 處的內存塊必須有足夠的空間容納 size 字節數據。
bufferevent的清空操作
int bufferevent_flush(struct bufferevent *bufev,
short iotype, enum bufferevent_flush_mode state);
清空 bufferevent 要求 bufferevent 強制從底層傳輸端口讀取或者寫入儘可能多的數據 ,而忽略其他可能保持數據不被寫入的限制條件 。函數的細節功能依賴於 bufferevent 的具體類型。
otype 參數應該是 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE,用於指示應該處 理讀取、寫入,還是二者都處理。 state 參數可以是 BEV_NORMAL、BEV_FLUSH 或者 BEV_FINISHED。BEV_FINISHED 指示應該告知另一端,沒有更多數據需要發送了; 而 BEV_NORMAL 和 BEV_FLUSH 的區別依賴於具體的 bufferevent 類型。
失敗時 bufferevent_flush()返回-1,如果沒有數據被清空則返回 0,有數據被清空則返回 1。
一般操作流程:(服務端)
socket->bind->listen->event_base_new()->event_new()->accept->bufferevent_socket_new()->bufferevent_setcb()->bufferevent_enable()->event_add->event_base_dispatch
提供一個網上的案例:
cli.cpp:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<event.h>
#include<event2/bufferevent.h>
#include<event2/buffer.h>
#include<event2/util.h>
int tcp_connect_server(const char* server_ip, int port);
void cmd_msg_cb(int fd, short events, void* arg);
void server_msg_cb(struct bufferevent* bev, void* arg);
void event_cb(struct bufferevent *bev, short event, void *arg);
int main(int argc, char** argv)
{
if( argc < 3 )
{
printf("please input 2 parameter\n");
return -1;
}
//兩個參數依次是服務器端的IP地址、端口號
int sockfd = tcp_connect_server(argv[1], atoi(argv[2]));
if( sockfd == -1)
{
perror("tcp_connect error ");
return -1;
}
printf("connect to server successful\n");
struct event_base* base = event_base_new();
struct bufferevent* bev = bufferevent_socket_new(base, sockfd,
BEV_OPT_CLOSE_ON_FREE);
//監聽終端輸入事件
struct event* ev_cmd = event_new(base, STDIN_FILENO,
EV_READ | EV_PERSIST, cmd_msg_cb,
(void*)bev);
event_add(ev_cmd, NULL);
//當socket關閉時會用到回調參數
bufferevent_setcb(bev, server_msg_cb, NULL, event_cb, (void*)ev_cmd);
bufferevent_enable(bev, EV_READ | EV_PERSIST);
event_base_dispatch(base);
printf("finished \n");
return 0;
}
void cmd_msg_cb(int fd, short events, void* arg)
{
char msg[1024];
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
struct bufferevent* bev = (struct bufferevent*)arg;
//把終端的消息發送給服務器端
bufferevent_write(bev, msg, ret);
}
void server_msg_cb(struct bufferevent* bev, void* arg)
{
char msg[1024];
size_t len = bufferevent_read(bev, msg, sizeof(msg));
msg[len] = '\0';
printf("recv %s from server\n", msg);
}
void event_cb(struct bufferevent *bev, short event, void *arg)
{
if (event & BEV_EVENT_EOF)
printf("connection closed\n");
else if (event & BEV_EVENT_ERROR)
printf("some other error\n");
//這將自動close套接字和free讀寫緩衝區
bufferevent_free(bev);
struct event *ev = (struct event*)arg;
//因爲socket已經沒有,所以這個event也沒有存在的必要了
event_free(ev);
}
typedef struct sockaddr SA;
int tcp_connect_server(const char* server_ip, int port)
{
int sockfd, status, save_errno;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr) );
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
status = inet_aton(server_ip, &server_addr.sin_addr);
if( status == 0 ) //the server_ip is not valid value
{
errno = EINVAL;
return -1;
}
sockfd = ::socket(PF_INET, SOCK_STREAM, 0);
if( sockfd == -1 )
return sockfd;
status = ::connect(sockfd, (SA*)&server_addr, sizeof(server_addr) );
if( status == -1 )
{
save_errno = errno;
::close(sockfd);
errno = save_errno; //the close may be error
return -1;
}
evutil_make_socket_nonblocking(sockfd);
return sockfd;
}
ser.cpp:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<event.h>
#include<event2/bufferevent.h>
void accept_cb(int fd, short events, void* arg);
void socket_read_cb(bufferevent* bev, void* arg);
void event_cb(struct bufferevent *bev, short event, void *arg);
int tcp_server_init(int port, int listen_num);
int main(int argc, char** argv)
{
int listener = tcp_server_init(9999, 10);
if( listener == -1 )
{
perror(" tcp_server_init error ");
return -1;
}
struct event_base* base = event_base_new();
//添加監聽客戶端請求連接事件
struct event* ev_listen = event_new(base, listener, EV_READ | EV_PERSIST,
accept_cb, base);
event_add(ev_listen, NULL);
event_base_dispatch(base);
event_base_free(base);
return 0;
}
void accept_cb(int fd, short events, void* arg)
{
evutil_socket_t sockfd;
struct sockaddr_in client;
socklen_t len = sizeof(client);
sockfd = ::accept(fd, (struct sockaddr*)&client, &len );
evutil_make_socket_nonblocking(sockfd);
printf("accept a client %d\n", sockfd);
struct event_base* base = (event_base*)arg;
bufferevent* bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, socket_read_cb, NULL, event_cb, arg);
bufferevent_enable(bev, EV_READ | EV_PERSIST);
}
void socket_read_cb(bufferevent* bev, void* arg)
{
char msg[4096];
size_t len = bufferevent_read(bev, msg, sizeof(msg));
msg[len] = '\0';
printf("recv the client msg: %s", msg);
char reply_msg[4096] = "I have recvieced the msg: ";
strcat(reply_msg + strlen(reply_msg), msg);
bufferevent_write(bev, reply_msg, strlen(reply_msg));
}
void event_cb(struct bufferevent *bev, short event, void *arg)
{
if (event & BEV_EVENT_EOF)
printf("connection closed\n");
else if (event & BEV_EVENT_ERROR)
printf("some other error\n");
//這將自動close套接字和free讀寫緩衝區
bufferevent_free(bev);
}
typedef struct sockaddr SA;
int tcp_server_init(int port, int listen_num)
{
int errno_save;
evutil_socket_t listener;
listener = ::socket(AF_INET, SOCK_STREAM, 0);
if( listener == -1 )
return -1;
//允許多次綁定同一個地址。要用在socket和bind之間
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(port);
if( ::bind(listener, (SA*)&sin, sizeof(sin)) < 0 )
goto error;
if( ::listen(listener, listen_num) < 0)
goto error;
//跨平臺統一接口,將套接字設置爲非阻塞狀態
evutil_make_socket_nonblocking(listener);
return listener;
error:
errno_save = errno;
evutil_closesocket(listener);
errno = errno_save;
return -1;
}
想了解學習更多C++後臺服務器方面的知識,請關注:
微信公衆號:C++後臺服務器開發