目录
HTTP服务器的内部实现原理主要用到了进程和线程的概念,利用多个进程或者线程同时监听 监听socket上的连接事件.
一 HTTP服务的建立
1. HTTP服务器的建立首先需要建立HTTP所需要的HTTP服务(HttpService变量),该变量中主要存储了各种客户端发来的get,post等请求的响应内容(以GET方法为例),建立方法如下所示:
HttpService router;
router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) {
return resp->String("pong");
});
该方法主要是在内部调用了AddApi()函数,该函数主要是将GET等方法与其对应的响应进行绑定,如下看看该函数是怎么实现的:
建立shared_ptr指针指向http_method_handlers变量(data值),并且将其存放在对应path(key值)下面的unordered_map中,并且将http_method_handlers的method方法赋予对应的"GET","POST"等方法值,handler赋予相应的用户定义的响应函数(如HttpService的GET方法的第二个参数).
void HttpService::AddApi(const char* path, http_method method, const http_handler& handler) {
std::shared_ptr<http_method_handlers> method_handlers = NULL;
auto iter = api_handlers.find(path);
if (iter == api_handlers.end()) {
// add path
method_handlers = std::shared_ptr<http_method_handlers>(new http_method_handlers);
api_handlers[path] = method_handlers;
}
else {
method_handlers = iter->second;
}
for (auto iter = method_handlers->begin(); iter != method_handlers->end(); ++iter) {
if (iter->method == method) {
// update
iter->handler = handler;
return;
}
}
// add
method_handlers->push_back(http_method_handler(method, handler));
}
2.建立http_server_t类型的服务器变量,给该服务器赋予相应的端口值,并且将上文提到的HttpService变量作为该服务器的service进行赋值:
http_server_t server;
server.port = 8080;
server.service = &router;
二 服务器的使用
调用http_server_run()函数,则该服务器就可以运行了:
http_server_run(&server);
对于该函数内部如何实现的,就比较复杂了,由于本人也没有完全搞明白内部的运行原理,因此我简单说一下我个人的理解:
1.该服务器判断我们调用的是http服务还是https服务,在此我们先以http为例进行简单讲解:
如果是http服务,就先调用Listen函数对于该服务器的端口号和主机名建立监听描述符并且进行返回.
if (server->port > 0) {
server->listenfd[0] = Listen(server->port, server->host);
if (server->listenfd[0] < 0) return server->listenfd[0];
hlogi("http server listening on %s:%d", server->host, server->port);
}
2.紧接着,判断是进程方式运行还是线程方式运行,在此以线程为例:
判断期待的建立的线程数目是多少,如果是0,则强制建立一个线程,否则建立worker_threads个线程对监听socket进行监听并且每个线程都会处理从该监听socket中获得的连接socket上的I/O事件.
if (server->worker_threads == 0) server->worker_threads = 1;
for (int i = wait ? 1 : 0; i < server->worker_threads; ++i) {
hthread_t thrd = hthread_create((hthread_routine)loop_thread, server);
privdata->threads.push_back(thrd);
}
3.对于线程的执行,主要是:
a.调用haccept函数对该监听socket建立accept事件(将其加入loop回环事件中),并且返回该监听socket对应的I/O事件,然后将该I/O事件与之前初始化阶段建立的http_server_t变量进行绑定.
hio_t* listenio = haccept(hloop, server->listenfd[0], on_accept);
hevent_set_userdata(listenio, server);
b.运行该loop回环事件(之前讲过,于是就开始进行监听socket连接以及等待读写事件等).
loop->run();
4.最后再说一下我对于这些事件的如何响应的理解,以用户定义回调函数on_accept()为例:
这里需要强调一下,该函数输入参数io表示从之前监听I/O事件调用accept()函数获得的连接描述符对应的I/O事件.然后对该I//O事件绑定接收和关闭事件,以及HTTP的KEPPALIVE超时事件.
static void on_accept(hio_t* io) {
http_server_t* server = (http_server_t*)hevent_userdata(io);
HttpService* service = server->service;
hio_setcb_close(io, on_close);
hio_setcb_read(io, on_recv);
hio_read(io);
hio_set_keepalive_timeout(io, service->keepalive_timeout);
// new HttpHandler, delete on_close
HttpHandler* handler = new HttpHandler;
// ssl
handler->ssl = hio_is_ssl(io);
// ip
sockaddr_ip((sockaddr_u*)hio_peeraddr(io), handler->ip, sizeof(handler->ip));
// port
handler->port = sockaddr_port((sockaddr_u*)hio_peeraddr(io));
// service
handler->service = service;
// ws
handler->ws_service = server->ws;
// FileCache
handler->files = default_filecache();
hevent_set_userdata(io, handler);
}
三.服务器程序的编写
在此主要展示如何编写服务器回显客户端回调函数以及如何给客户返回大文件:
3.1 回显客户端
这里,首先我想说一下,在http_server_run()函数主要建立了多个(用户不设置的话一般是1个)线程来对同一个监听描述符建立accept事件(这样子可能对于高访问量的服务器比较有用),然后每一个线程再对各自accept()的连接描述符建立读写I/O事件从而进行I/O复用.
我主要讲三种回显客户端发送的数据的方法:
1.该方法的回调函数主要是运行在之前所说的I/O线程中:
下面的代码主要是向HTTP服务中注册当用户发来POST报文且请求URL为"/echo"时的回调函数,回调函数为将客户端发来的请求报文的报文体赋值给服务器的响应报文的报文体.
router.POST("/echo", [](HttpRequest* req, HttpResponse* resp) {
resp->content_type = req->content_type;
resp->body = req->body;
return 200;
});
2.该方法的回调函数是在hv::async全局线程池中执行,即不占用I/O线程的处理时间.
router.POST("/echo", [](const HttpRequestPtr& req, const HttpResponseWriterPtr& writer) {
writer->Begin();
writer->WriteStatus(HTTP_STATUS_OK);
writer->WriteHeader("Content-Type", req->GetHeader("Content-Type"));
writer->WriteBody(req->body);
writer->End();
});
3.该方法的回调函数可以运行在I/O线程,hv::async全局线程池或者用户自己定义的回调函数中,在此展示以下如何放在hv::async全局线程池中:
router.POST("/echo", [](const HttpContextPtr& ctx) {
// demo演示丢到hv::async全局线程池处理,实际使用推荐丢到自己的消费者线程/线程池
hv::async([ctx]() {
ctx->send(ctx->body(), ctx->type());
});
return 0;
});
最后,展示一下对于第三种方法的运行结果截图:
1.运行该服务器:
2.利用curl指令向该服务器发送HTTP请求(向服务器发送hello,world):
3.查看服务器返回的响应报文(最后一行服务器返回的响应报文中出现了之前发送的hello,world):
3.2 发送大文件
该方法主要是建立用户自己建立一个线程循环读出客户端请求URL中服务器里面的文件,然后将其发送.其中,最主要的就是如何获得客户请求的文件:
std::string filepath = ctx->service->document_root + ctx->request->Path();
其中document_root通过查找http_service头文件可以知道,这是存放http服务器响应客户所需html文件的文件夹:var/www/html,ctx->request->Path()是客户端发送的HTTP请求报文的URL请求(如,以上说到的"/echo").
如下展示一下该回调函数代码:
对于大文件的发送需要做好流量控制,如果客户端就收速度过慢会导致发送缓存大量积压,耗费内存,所以需要std::this_thread::sleep_until(end_time)函数来将线程进行睡眠等待从而实现流量控制,具体睡眠周期是由sleep_ms_per_send(每次发送需要的时间)变量决定的.
int largefilehandler(const HttpContextPtr& ctx)
{
std::thread([ctx](){
ctx->writer->Begin();
std::string filepath = ctx->service->document_root + ctx->request->Path();//查找文件路径
HFile file;
if (file.open(filepath.c_str(), "r") != 0) {
ctx->writer->WriteStatus(HTTP_STATUS_NOT_FOUND);
ctx->writer->WriteHeader("Content-Type", "text/html");
ctx->writer->WriteBody("<center><h1>404 Not Found</h1></center>");
ctx->writer->End();
return;
}
//判断文件是否存在
http_content_type content_type = CONTENT_TYPE_NONE;
const char* suffix = hv_suffixname(filepath.c_str());
if (suffix) {
content_type = http_content_type_enum_by_suffix(suffix);
}
if (content_type == CONTENT_TYPE_NONE || content_type == CONTENT_TYPE_UNDEFINED) {
content_type = APPLICATION_OCTET_STREAM;
}
//HTTP报文首部
size_t filesize = file.size();
ctx->writer->WriteHeader("Content-Type", http_content_type_str(content_type));
ctx->writer->WriteHeader("Content-Length", filesize);
ctx->writer->EndHeaders();
//HTTP报文主体
char* buf = NULL;
int len = 4096; // 4K
SAFE_ALLOC(buf, len);
size_t total_readbytes = 0;
int last_progress = 0;
int sendbytes_per_ms = 1024; // 1KB/ms = 1MB/s = 8Mbps
int sleep_ms_per_send = len / sendbytes_per_ms; // 4ms
int sleep_ms = sleep_ms_per_send;
auto start_time = std::chrono::steady_clock::now();
auto end_time = start_time;
while (total_readbytes < filesize) {
size_t readbytes = file.read(buf, len);
if (readbytes <= 0) {
ctx->writer->close();
break;
}
int nwrite = ctx->writer->WriteBody(buf, readbytes);
if (nwrite < 0) {
// 未连接
break;
} else if (nwrite == 0) {
sleep_ms *= 2;
} else {
sleep_ms = sleep_ms_per_send;
}
total_readbytes += readbytes;
int cur_progress = total_readbytes * 100 / filesize;
if (cur_progress > last_progress) {
last_progress = cur_progress;
}
//控制发送流量
end_time += std::chrono::milliseconds(sleep_ms);
std::this_thread::sleep_until(end_time);
}
ctx->writer->End();
SAFE_FREE(buf);
}
).detach();
return 0;//未发送完毕
}
具体HTML文件代码如下所示:
测试步骤:
1.运行服务器,方法同上;
2.在浏览器上面输入地址:http://127.0.0.1:8080/first_html.html;
3.浏览器显示: