在跨平台網絡基礎庫中,libevent與asio近年來使用比較廣泛。asio對boost的依賴太大,個人認為發展前途堪憂,尤其asio對http沒有很好的支持也是缺點之一。
libevent對http有天生支持,含有服務與客戶兩個部分,是做web服務的好特性。
libevent隨對http支持很優秀,但並不支持html5標准的websocket,這有些與時代脫軌。如果你熟悉websocket協議,像自己擴展libevent,很遺憾,libevent的http部分並不支持邏輯層擴展。所以我想,還是通過源碼級擴展比較好。注:git上有代碼級擴展,但不是在http功能上的擴展。
正文:
libevent的http支持核心代碼都在http.c中,包含了幾個相關頭文件,包括http.h、http-internal.h、http_struct.h、http_compat.h。
libevent的主要容器是列表,由一系列宏進行操作。包括http request,callback函數,http connection,輸入頭信息,輸出頭信息均被列表容器管理。回調函數的搜索匹配,evkeyxxx相關的頭信息搜索,均需要在列表中遍歷,會有些許性能損耗。
libevent有兩個方法設置http事件回調函數:evhttp_set_cb,evhttp_set_gencb.我本計划用開關的方式來決定是否開啟websocket的連接升級(Connection: Upgrade)功能,后來覺得與libevent的原始架構有些不一致,最終決定用類似設置回調函數的方法設置哪些路徑接收WebSocket升級:evhttp_set_ws,evhttp_del_ws。
處理頭信息:
evhttp_read_header是接管websocket升級的好地方,我在EVHTTP_REQUEST case的地方添加處理代碼,根據WebSocket標准文檔,先在header中尋找升級Key(Sec-WebSocket-Key),進行Hash(SHA1->BASE64)返回給客戶端即可完成升級。至於hash代碼,在windows下可方便的用加解密相關函數(Crypt開頭)解決,在Linux就要用openssl了。
libevent默認在讀完header后會關閉bufferevent的讀取事件,這會影響之后我們websocket的通訊,為此我寫了一個新的寫緩沖函數,不停止讀取事件:evhttp_write_buffer_nostop_read。只需要復制evhttp_write_buffer函數,刪除設置緩沖cb的代碼即可。
libevent http connection有個state枚舉,用來只是當前讀取狀態,我為此枚舉添加了一個狀態:EVCON_READING_WSDATA,在提升Websocket完成后,將state設置為EVCON_READING_WSDATA,並且為evhttp_read_cb添加一個對應case,處理websocket的數據。
代碼
注:代碼按照libevent源碼風格進行編寫,除了大括號后置,基本就是本人的風格了。
先做到協議提升與協議解析,下次再討論發送的問題以及數據類型的問題。
websocket客戶端key處理代碼:
Linux需要這些頭文件<openssl/sha.h>,<openssl/bio.h>,<openssl/evp.h>,<string.h>,<openssl/buffer.h>,前提你應該有openssl的devel版被安裝。
Linux總歸會麻煩一些,忍咯!Windows需要引用crypt32.lib,Linu需要引用libcrypto.lib(gcc:lcrypto)。
Windows已測試,Linux只進行了片段代碼測試。
1 const char* 2 ws_hash(const char* client_key) { 3 static char result[50]; 4 if (strlen(client_key) > 37) 5 return NULL; 6 7 const char* uuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 8 char src[100]; 9 strcpy(src, client_key); 10 strcat(src, uuid); 11 size_t src_len = strlen(src); 12 13 #ifdef _WIN64 or _WIN32 14 HCRYPTPROV hCryptProv; 15 if (CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0)){ 16 HCRYPTHASH hHash; 17 if (CryptCreateHash(hCryptProv, CALG_SHA1, 0, 0, &hHash)){ 18 if (CryptHashData(hHash, (BYTE*)src, src_len, 0)){ 19 BYTE hash_result[50]; 20 DWORD out_len = sizeof(hash_result); 21 if (CryptGetHashParam(hHash, HP_HASHVAL, hash_result, &out_len, 0)){ 22 DWORD crypt_out_len = 100; 23 CryptBinaryToStringA(hash_result, out_len, CRYPT_STRING_BASE64, result, &crypt_out_len); 24 result[crypt_out_len - 2] = 0; 25 return result; 26 } 27 } 28 } 29 } 30 #else 31 unsigned char* value = SHA1((unsigned char*)src, src_len, out); 32 BIO *bm = NULL, *bio = NULL; 33 bio = BIO_new(BIO_f_base64()); 34 if (bio) { 35 bm = BIO_new(BIO_s_mem()); 36 if (bm) { 37 BIO_push(bio, bm); 38 BIO_write(bio, value, strlen((const char*)value)); 39 BIO_flush(bio); 40 BUF_MEM *buf; 41 BIO_get_mem_ptr(bio, &buf); 42 strcpy(result, buf->data); 43 BIO_free_all(bio); 44 } 45 } 46 #endif 47 return NULL; 48 }
websocket提升代碼:
主要處理頭信息並寫回客戶端與狀態。
1 case EVHTTP_REQUEST: { 2 /* handle the websocket upgrade key */ 3 const char* seckey = evhttp_find_header(req->input_headers, "Sec-WebSocket-Key"); 4 if (seckey) { 5 struct evhttp_wsup* wsup; 6 char* translated; 7 /* Test for different URLs */ 8 const char* path = evhttp_uri_get_path(req->uri_elems); 9 size_t offset = strlen(path); 10 if ((translated = mm_malloc(offset + 1)) == NULL) 11 return; 12 evhttp_decode_uri_internal(path, offset, translated, 13 0 /* decode_plus */); 14 TAILQ_FOREACH(wsup, &evcon->http_server->websocket_upgrades, next) { 15 if (!stricmp(wsup->what, translated)) { 16 evhttp_add_header(req->output_headers, "Connection", "Upgrade"); 17 evhttp_add_header(req->output_headers, "Upgrade", "WebSocket"); 18 evhttp_add_header(req->output_headers, "Sec-WebSocket-Accept", ws_hash(seckey)); 19 req->websocket = 1; 20 evhttp_response_code(req, 101, "SwitchProtocol"); 21 evhttp_make_header(req->evcon, req); 22 evhttp_write_buffer_nostop_read(req->evcon, NULL, NULL); 23 evcon->state = EVCON_READING_WSDATA; 24 bufferevent_enable(evcon->bufev, EV_READ); 25 break; 26 } 27 } 28 mm_free(translated); 29 return; 30 }
處理websocket協議的代碼:
1 int 2 process_buffer(unsigned char* buff, size_t data_len) 3 { 4 if (data_len < 2) 5 return 0; 6 switch (buff[0] & 0xF){ 7 case 8: 8 return 1; 9 case 9: 10 case 10:{ 11 auto len = buff[1] & 0x7F; 12 auto mask = (buff[1] & 0x80) > 0; 13 if (len > 0) 14 return -1; 15 16 if (mask) 17 return 6; 18 else 19 return 2; 20 break; 21 } 22 case 1:{ 23 auto len = buff[1] & 0x7F; 24 auto mask = (buff[1] & 0x80) > 0; 25 int head_len = 0; 26 if (len == 127) 27 head_len = mask ? 14 : 10; 28 else if (len == 126) 29 head_len = mask ? 8 : 4; 30 else 31 head_len = mask ? 6 : 2; 32 if (data_len < head_len) 33 return 0; 34 35 int tail_len = 0; 36 if (len == 127) 37 tail_len = (int)ntohll((unsigned long long)(buff + 2)); 38 else if (len == 126) 39 tail_len = ntohs((u_short)(buff + 2)); 40 else 41 tail_len = len; 42 43 if (data_len < head_len + tail_len) 44 return 0; 45 46 if (mask) 47 for (int i = head_len, j = 0; j < tail_len; i++, j++) 48 buff[i] = buff[i] ^ buff[head_len - 4 + j % 4]; 49 50 char* utf8_text = buff + head_len; 51 52 return head_len + tail_len; 53 } 54 } 55 return 0; 56 } 57 58 static void 59 evhttp_read_wsdata(struct evhttp_connection *evcon, struct evhttp_request *req) 60 { 61 struct evbuffer *buf = bufferevent_get_input(evcon->bufev); 62 63 size_t buflen = evbuffer_get_length(buf); 64 if (buflen == 0) 65 return; 66 67 size_t drain_len = 0; 68 unsigned char* data = evbuffer_pullup(buf, buflen); 69 while (buflen>0) { 70 int result = process_buffer(data, buflen); 71 if (result < 0) { 72 evhttp_connection_free(evcon); 73 return; 74 } 75 else if (result > 0) { 76 if (result > buflen) { 77 evhttp_connection_free(evcon); 78 return; 79 } 80 drain_len += result; 81 data += result; 82 buflen -= result; 83 } 84 else 85 break; 86 } 87 if(drain_len>0)evbuffer_drain(buf, drain_len); 88 89 /* Read more! */ 90 bufferevent_enable(evcon->bufev, EV_READ); 91 }