基於libevent和unix domain socket的本地server

https://www.pacificsimplicity.ca/blog/libevent-echo-server-tutorialhtml

根據這一篇寫一個最簡單的demo。而後開始寫client。git

client調優

client最初的代碼以下:github

 1 #include <sys/socket.h>
 2 #include <sys/un.h>
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #include <sys/socket.h>
 7 #include <fcntl.h>
 8 #include <errno.h>
 9 
10 int main(int argc, char *argv[]) {
11     struct sockaddr_un addr;
12     int fd,rc;
13 
14     if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
15         perror("socket error");
16         exit(-1);
17     }
18 
19     const char *socket_path = "/tmp/mysocket";
20     memset(&addr, 0, sizeof(addr));
21     addr.sun_family = AF_UNIX;
22     strcpy(addr.sun_path, socket_path);
23 
24     if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
25         perror("connect error");
26         exit(-1);
27     }
28 
29     char sendbuf[8145] = {0};
30     rc = 8145;
31     
32     {
33         if (write(fd, sendbuf, rc) != rc) {
34             if (rc > 0) fprintf(stderr,"partial write");
35             else {
36                 perror("write error");
37                 exit(-1);
38             }
39         }        
40     }
41     
42 
43     char buf[1024] = {0};
44 
45     while ((rc = read(fd, buf, 1024)) > 0) {
46         buf[rc] = '\0';
47                 printf("%s\n", buf);
48     }
49 
50     close(fd);
51 
52     return 0;
53 }            

代碼很簡單,會發現有個問題,read這裏會阻塞住不退出。網絡

由於這是阻塞IO,讀不到數據時會阻塞。有沒辦法能夠知道服務端已經寫完了呢?若是用非阻塞的是否是有不同的返回碼呢。又試了下非阻塞版。socket

 1 int val = fcntl(fd, F_GETFL, 0);
 2 fcntl(fd, F_SETFL, val|O_NONBLOCK);// 設置爲非阻塞
 3 
 4 //...
 5 
 6 char buf[1024] = {0};
 7 while (true) {
 8         rc = read(fd, buf, 1024);
 9         if (rc > 0) {
10             buf[rc] = '\0';
11             printf("recv:%s\n", buf);
12         } else if (rc == 0) {
13             break;
14         } else if (rc < 0 && (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)) {
15             //printf("errno %d\n", errno);
16             continue;
17         } else {
18             break;
19         }
20     }

這時就會出現一直跑到第15行這裏,errno一直是EWOULDBLOCK/EAGAIN。函數

非阻塞模式下返回值 <0時而且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的狀況下認爲鏈接是正常的, 繼續發送。spa

https://blog.csdn.net/qq_14821541/article/details/52028924.net

好吧,問題一樣沒有解決。實際上網絡通訊server端可能會出現不少狀況,寫得慢、網絡慢或者server掛了等,爲了魯棒性,一個比較通用的策略就是超時。若是超了時間就直接退出。code

1 struct timeval tv;
2 tv.tv_sec = 3;
3 tv.tv_usec = 0;
4 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));

用阻塞+超時,這樣就能夠正常退出了。不過仍是沒解決正常狀況下的退出。server

一個簡單的思路就是服務端寫完了數據,在數據的最終加上一個mark,標識已經寫完了,client讀到這個mark,就直接退出。

 1 #include <sys/socket.h>
 2 #include <sys/un.h>
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #include <sys/socket.h>
 7 #include <fcntl.h>
 8 #include <errno.h>
 9 
10 int main(int argc, char *argv[]) {
11     struct sockaddr_un addr;
12     int fd,rc;
13 
14     if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
15         perror("socket error");
16         exit(-1);
17     }
18 
19     struct timeval tv;
20     tv.tv_sec = 3;
21     tv.tv_usec = 0;
22     setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
23 
24     const char *socket_path = "/tmp/mysocket";
25     memset(&addr, 0, sizeof(addr));
26     addr.sun_family = AF_UNIX;
27     strcpy(addr.sun_path, socket_path);
28 
29     if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
30         perror("connect error");
31         exit(-1);
32     }
33 
34     const char *end_mark = "$@%^&~";
35     int end_mark_len = strlen(end_mark);
36 
37     char sendbuf[8145] = {0};
38     rc = 8145;
39     memcpy(sendbuf + rc - end_mark_len, end_mark, end_mark_len);
40     
41     printf("%s %d\n", sendbuf, rc);
42 
43     {
44         if (write(fd, sendbuf, rc) != rc) {
45             if (rc > 0) fprintf(stderr,"partial write");
46             else {
47                 perror("write error");
48                 exit(-1);
49             }
50         }    
51     }
52     
53 
54     char buf[1024] = {0};    
55     while ((rc = read(fd, buf, 1024)) > 0) {
56         buf[rc] = '\0';
57         if (rc < end_mark_len) break;
58         if (strncmp(buf + rc - end_mark_len, end_mark, end_mark_len) == 0) {
59             printf("%s\n", buf);
60             break;
61         }
62     }
63 
64     close(fd);
65 
66     return 0;
67 }

server調優

https://www.pacificsimplicity.ca/blog/libevent-echo-server-tutorial

前面咱們用bufferevent_setcb來設置回調函數,libevent的回調觸發時機是這樣的:

  1. 當輸入緩衝區的數據大於或等於輸入低水位時,讀取回調就會被調用。默認狀況下,輸入低水位的值是 0,也就是說,只要 socket 變得可讀,就會調用讀取回調。
  2. 當輸出緩衝區的數據小於或等於輸出低水位時,寫入回調就會被調用。默認狀況下,輸出低水位的值是 0,也就是說,只有當輸出緩衝區的數據都發送完了,纔會調用寫入回調。所以,默認狀況下的寫入回調也能夠理解成爲 write complete callback。
  3. 當鏈接創建、鏈接關閉、鏈接超時或者鏈接發生錯誤時,則會調用事件回調。

參考:http://senlinzhan.github.io/2017/08/20/libevent-buffer/

http://www.wangafu.net/~nickm/libevent-book/Ref6_bufferevent.html

這裏提到的demo,會有幾個問題:

  1. client提早退出時,server端繼續寫數據會收到sigpipe信號,而後直接退出。
  2. echo_read_cb讀回調,若是讀的數據比較大,可能會觸發屢次,然而咱們須要在數據結束時再同時處理,這裏一樣須要判斷一下數據是否已經讀取結束;
  3. 若是client提早退出,即便忽略了sigpipe信號 ,可是連接依舊不會關閉;

 第一個問題,是由於鏈接創建,若某一端關閉鏈接,而另外一端仍然向它寫數據,第一次寫數據後會收到RST響應,此後再寫數據,內核將向進程發出SIGPIPE信號,通知進程此鏈接已經斷開。而SIGPIPE信號的默認處理是終止程序。解決方案就是直接忽略SIGPIPE信號。

1 signal(SIGPIPE, SIG_IGN);

第二個問題,一樣用一個mark來標記讀取結束。這裏用到evbuffer_peek來獲取整個buffer內存而不是copy出來再查。

http://www.wangafu.net/~nickm/libevent-book/Ref7_evbuffer.html

 1 bool CheckReadFinished(struct evbuffer *input) {
 2     const int limit_vec = 10;
 3 
 4     struct evbuffer_iovec v[limit_vec];
 5     int n = evbuffer_peek(input, -1, NULL, v, limit_vec);
 6     if (n <= 0) {
 7         return false;
 8     }
 9 
10     int end_mark_len = strlen(end_mark);
11     for (unsigned i = n - 1; i >= 0; --i) {
12         size_t len = v[i].iov_len;
13         if (len >= end_mark_len) {
14             return strncmp((char*)(v[i].iov_base) + (len - end_mark_len), end_mark, end_mark_len) == 0;
15         } else {
16             if (strncmp((char*)(v[i].iov_base), end_mark + (end_mark_len - len), len) != 0) {
17                 return false;
18             }
19             end_mark_len -= len;
20         }
21     }
22     return false;
23 }

這裏直接用了limit_vec來限制大小,若是超出buff大小就認爲是錯誤的。

 1 static void echo_read_cb(struct bufferevent *bev, void *ctx) {
 2     struct evbuffer *input = bufferevent_get_input(bev);
 3     struct evbuffer *output = bufferevent_get_output(bev);
 4     
 5     if (CheckReadFinished(input)) {
 6         size_t len = evbuffer_get_length(input);
 7         printf("we got some data: %d\n", len);
 8         evbuffer_add_printf(output, end_mark);
 9     }
10 }

第三個問題,client異常退出是避免不了的,因此要有容錯機制,一樣是採用超時來容錯。

 1 static void
 2 accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) {
 3     evutil_make_socket_nonblocking(fd);
 4 
 5     struct event_base *base = evconnlistener_get_base(listener);
 6     struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
 7     bufferevent_setcb(bev, echo_read_cb, echo_write_cb, echo_event_cb, NULL);
 8 
 9     // 設置超時,而後斷開連接
10     struct timeval read_tv = {2, 0}, write_tv = {3, 0};
11     bufferevent_set_timeouts(bev, &read_tv, &write_tv);
12 
13     bufferevent_enable(bev, EV_READ | EV_WRITE);
14 }

而後在BEV_EVENT_TIMEOUT事件觸發時free掉evbuff。由於咱們指定了BEV_OPT_CLOSE_ON_FREE,因此這時候就會斷掉鏈接。

1 static void echo_event_cb(struct bufferevent *bev, short events, void *ctx) {
2     if (events & BEV_EVENT_ERROR)
3         perror("Error from bufferevent");
4     if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR | BEV_EVENT_TIMEOUT)) {
5         printf("free event\n");
6         bufferevent_free(bev);
7     }
8 }

這裏咱們也能夠看到,正常狀況下,當client讀取結束以後會close(fd),這時就會觸發BEV_EVENT_EOF事件,一樣是會關掉服務端的鏈接。

相關文章
相關標籤/搜索