基于libmicrohttpd的HTTP服务器初探

  最近写一个小项目,需要用到HTTP Server来支持一个API操作,正如同现在如火如荼的云API泛滥时代,感觉你不弄个开放接口都不好意思说自己是互联网公司。要求是用POST方式以及JSON数据格式发一个请求,然后服务端返回一个JSON格式的结果。当然用HTTP格式而非Socket方式的好处是显而易见的,HTTP是建立在socket之上的一个应用层协议,虽然性能可能不是最好的,但意味着你不用定义数据包的协议格式、容错以及通信过程中的各种繁琐的细节,只需要关注用户发送的有效负载信息及自己的处理逻辑设计实现就可以了。
  说到HTTP可能最先想到的是Apache和Nginx,然而寡人不是做前端的,也不懂全世界最好的语言(PHP),更不晓得RESTful API,HTTP收到的JSON数据还不晓得怎么和后台的CGI结合起来。于是,这次另辟蹊径找到了libmicrohttpd,话说这个libmicrohttpd是用C写的,GNU旗下产品,支持Linux、Windows、以及andriod、symbian等平台(其定位就是作为一个嵌入式的HTTP库,方便集成到各个程序中去),支持SSL、session等机制,不过每个部分都比较简单,算是简单实现了一个HTTP的框架吧。

libmicrohttpd

一、HTTP协议 - libmicrohttpd

  libmicrohttpd的文档算是比较的详细,但是用起来还是有点需要注意的:

1.1 处理POST JSON数据格式的请求

  libmicrohttpd本身提供了POST数据的处理接口,在新连接到时候使用MHD_create_post_processor创建POST处理器,收到数据后,调用MHD_post_process处理,只不过得到的数据表示为带=的键值对形式,这对我们这种JSON数据显然是不行的。
  如果需要自己收取处理数据,然后自己用JSON库来解析处理,那么我们需要做的是,将完整的数据收上来,然后自己手动处理数据。其思路为:
  (1) 在create_response函数中,调用MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_LENGTH);,这会解析HTTP头部得到数据的长度,依据这个长度创建接受缓冲区;
  (2) 不断地收数据,直到接受不到数据为止,然后检查收到的数据长度和之前头部报告的长度是否相等;
  (3) 调用自己的数据处理接口。

  代码的样式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const char* param = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_LENGTH);
content_len = atoi(param);

// URL: http://localhost:8080/api
if (0 == strcmp (method, "POST") && !strncasecmp(url, "/api", 4)) {
if (NULL == posthandler) { // new connection
posthandler = (P_MHD_Data)malloc (sizeof (MHD_Data));
if (NULL == posthandler)
return MHD_NO;

posthandler->len = 0; //actual data load
posthandler->data = malloc(content_len + 1);
memset(posthandler->data, 0, content_len+1);
*con_cls = (void *) posthandler;

return MHD_YES;
}
else {
if (*upload_data_size != 0){
memcpy (posthandler->data + posthandler->len, upload_data, *upload_data_size);
posthandler->len = posthandler->len + (*upload_data_size);
((char *)(posthandler->data))[posthandler->len] = '\0';
/* indicate that we have processed */
*upload_data_size = 0;

return MHD_YES;
}
else if (NULL != posthandler->data) {
/* done with POST data, serve response */
if (content_len != posthandler->len)
goto ERROR_RT;

if (process_post_data(posthandler) != 0)
goto ERROR_RT;

return send_page (connection, (const char*)posthandler->data);
}
}
}

1.2 服务端运行模式

  在启动libmicrohttpd服务MHD_start_daemon的时候有很多选项,其中最重要的是指定服务器的线程工作模式,大体来说分为以下几类:
(1) MHD_USE_THREAD_PER_CONNECTION
  MHD会启动一个线程来进行侦听,当得到一个连接的时候,会生成一个新的线程来处理这个连接,由于连接之间没有通信机制,所以适用于无状态的连接,但是如果创建线程的开销比较大的时候,显然效率不高;
(2) MHD_USE_SELECT_INTERNALLY
  创建一个线程来处理侦听和处理,这种情况要被认为响应是不会阻塞,而且能够快速返回的,不然这个线程处于忙等待,系统的吞吐量会被严重影响。
(3) MHD_USE_POLL
  包括派生的EPOLL等形式,是I/O+线程池的最佳服务器模式,当使用这些模式的时候,需要使用MHD_OPTION_THREAD_POOL_SIZE来设置使用线程池的大小。然后你可以在处理函数中,打印当前threadid来验证线程池是否工作正常。
  我个人用的是终极MHD_USE_EPOLL_INTERNALLY_LINUX_ONLY模式,然后MHD_OPTION_THREAD_POOL_SIZE线程池设置为6个线程。

1
2
3
4
5
6
7
8
daemon = MHD_start_daemon (MHD_USE_EPOLL_INTERNALLY_LINUX_ONLY|MHD_USE_DEBUG,
global_options.http_option.port,
NULL, NULL,
&create_response, NULL,
MHD_OPTION_NOTIFY_COMPLETED, &request_completed,
NULL,
MHD_OPTION_THREAD_POOL_SIZE, 6,
MHD_OPTION_END);

1.3 返回文件系统的资源文件

  这个部分已经在哥的业务需求之外了。在前面的create_response之中,等于做了个钩子——对于请求的url是/api的时候,就接受数据并进行JSON解析,其实对于其他URL访问,可以返回文件系统中对应文件给客户,就是普通的WebServer模式,于是添加了send_page_from_file(connection, url);这么个函数来处理其他URL请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int
send_page_from_file (struct MHD_Connection *connection, const char *filename){
int ret;
struct MHD_Response *response;
struct stat st;
char filepath[PATH_MAX];
strcpy(filepath, global_options.http_option.ROOT_DIR);
strcat(filepath, filename);

stat(filepath, &st);
if ( S_ISDIR(st.st_mode) && strlen(global_options.http_option.PAGE_INDEX)) {
strcat(filepath,"/");
strcat(filepath, global_options.http_option.PAGE_INDEX);
}

int fd = open(filepath, O_RDONLY);
if(fd == -1)
return send_page(connection, err404page);

FILE *fp = fdopen(fd, "r");
fseek(fp, 0, SEEK_END);
unsigned long len = ftell(fp); //文件大小
response = MHD_create_response_from_fd(len, fd);

if (!response)
return MHD_NO;

if(strstr(filename, ".htm"))
MHD_add_response_header(response,"Content-Type","text/html; charset=utf-8");
else if (strstr(filename, ".js"))
MHD_add_response_header(response,"Content-Type","application/x-javascript");

ret = MHD_queue_response (connection, MHD_HTTP_OK, response);
MHD_destroy_response (response);

//close(fd); //此处不能关闭
return ret;
}

  其实上面的函数核心就是MHD_create_response_from_fd,当然你也可以把整个文件读出来,然后调用MHD_create_response_from_data来发送,官方的手册上说data函数对于小的可以放到内存上的数据或者文件(比如静态页面)比较合适,后者适用于不知道发送数据的具体大小或者数据过大不能放到内存的情况,不过觉得上面这样用也挺方便的。(不知道是不是要保存这个handle,然后在request_completed中关闭,如果后续发现描述符泄露再改吧。)
  然后顺便模拟了DocumentRoot、Indexes等特性,配置过apache的应当很熟悉。

二、JSON数据处理 - json-c

  JSON库网络一搜一大把,甚至有个专门评测的列出几十个JSON库测试支持的标准以及性能。由于是用C写的,那么C++的库就被PASS掉了,目前用的json-c库,感觉比较清爽,而且时间比较久了也究竟考验,用起来不错。
  如果后面使用C++开发程序,推荐使用json11,但是如果环境比较的老旧不支持C++11,那么可以使用RapidJson也是不错的。
  配置文件也用json的方式设置,然后程序启动的时候,用这个库就可以加载配置文件了。大名鼎鼎的Sublime编辑器就是这么干的哦!

三、数据库 - MySQL C API

  MySQL的C API算是最底层的比较裸的数据库操作接口了吧。当编译连接的时候有线程安全的版本(mysqlclient_r),官方说明在多线程的情况下可以保证线程安全的,只是需要遵守一些初始化和使用约定,比如在mysql_query()和mysql_store_result() 两个调用之间,不允许其他线程使用该连接,而如果涉及到事物、回滚等机制,问题可能会更加的复杂。
  其实,在附录的pdf文件中也提到,对于MySQL数据库,在服务端也有线程池的概念,也是一个连接一个线程处理的,在客户端如果多个线程公用一个连接,除了要考虑额外的同步问题,性能也不会有所提高,所以对于客户端,最好的解决方案是使用连接池。
  自己使用C简单做了个连接池,放到st_utils库当中去了。

  由于涉及到公司业务,这里就不开源了。这个简单的HTTP服务器我在http://mail.freesign.net上部署了,资源占用极少,原来静态的GitHub Pages显示没有什么问题哦,感兴趣的同学可以压一压!

本文完!

参考