首页 > 代码库 > 为libevent添加websocket支持(上)
为libevent添加websocket支持(上)
在跨平台网络基础库中,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 _WIN3214 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 #else31 unsigned char* value = http://www.mamicode.com/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 #endif47 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 else19 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 else31 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 else41 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 void59 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 =http://www.mamicode.com/ 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 else85 break;86 }87 if(drain_len>0)evbuffer_drain(buf, drain_len);88 89 /* Read more! */90 bufferevent_enable(evcon->bufev, EV_READ);91 }
为libevent添加websocket支持(上)