0x17-套接字编程-HTTP服务器(5)
让我们停下来,回想一下之前的内容
- 首先读取配置文件,并凭此打开服务器套接字
- 确定一切完备的情况下(
listen
),开启事务循环handle_loop
- 准备好各项资源
prepare_worker
,开启两种线程就真正开始工作了
string_t
不打算详细讲解,因为并不是什么好的设计,但是只需要将接口,改成C风格的就不错,但是有一个致命的缺点,就是这不是二进制字符串- 什么意思?就是这是一个C风格的字符串,无法很好的存储二进制数据,例如无法存储
\0
这个字符,实际上要设计就需要重新设计。 - 但这个小程序绰绰有余,因为只是作为一个静态资源HTTP服务器在使用。
- 在本章最后,会将源代码地址贴上,仅供参考,写的不够严谨,但还是有意义的练习。
- 什么意思?就是这是一个C风格的字符串,无法很好的存储二进制数据,例如无法存储
2016-08-28 修复上述问题,具体可以参看源码,现在支持二进制数据
万事开头难,当你在键盘上打下第一句代码的时候你就成功了。看永远都只能是谈谈兵,虽说谈兵也需要技术
生成一个响应报文
- 实际上客户端对你怎么处理这些数据一点都不感兴趣,他们感兴趣的不就是你的响应报文是什么吗
- 所以说到了这一步就要看看这个报文的组成,但这并不是我们的重点,简单讲一下哪些属性比较重要。
- 还记得开头的时候,给出了一个报文实例,实际上最明了的莫过于在浏览器中摁
F12
后自己查看交互报文,再专业一些使用Wireshark
这类专业抓包软件也未尝不可,以浏览器为例: - 这是个人博客上的一个背景图的请求交互,重点看
Response Headers
- 这么一长串,实际上真正必不可少的还是那么两行
HTTP/1.1 200 OK
Content-Length: 377710
- 前者告诉你这个球球的结果,后者告诉你请求的结果的内容在哪里,即在报文中空行后多少个字节都是请求的结果。
- 这是个人博客上的一个背景图的请求交互,重点看
那在C语言中,或者说在任何语言中,都没什么特别好的办法,就是用字符串构造报文了。作为一个标准库比较贫瘠的语言,这就要我们多做一点工作,这也是为什么要自己写一个字符串结构体的原因所在。
- 当然如果你为了兼容二进制数据,那么甚至连标准库中的字符串函数都不能使用了,包括
Linux
提供的扩展gnu99
字符串函数,原因是因为C-Style字符串是以\0
作为结束符的。
- 当然如果你为了兼容二进制数据,那么甚至连标准库中的字符串函数都不能使用了,包括
现在我们规定一下,我们这个服务器的响应报文会包含的部分
- 状态行是必要的
HTTP/VER STATUS_CODE STATUS_MESSAGE\r\n
- 服务器时间
Date: xxx\r\n
- 用的是UTC格式,实际上此处也可以有点小讲究,后面提一下
- 资源类型
Content-Type: xxx\r\n
- 资源长度
Content-Length: xxx\r\
- 连接状态
Connection: xxx\r\n
- 空行
\r\n
- 资源
- 状态行是必要的
在进入生成报文的环节中,其实还有很多工作要做,例如判断是什么请求方法,是否是恶意请求, 获取资源的各种信息 等,直接进入最核心的阶段
make_response
中的write_to_buf
也就是构造报文阶段
__thread char local_write_buf[CONN_BUF_SIZE] = {0};
static int write_to_buf(conn_client * restrict client, // connection client message
const char * const * restrict status, int rsource_size) {
#define STATUS_CODE 0
#define STATUS_TITLE 1
#define STATUS_CONTENT 2
char * write_buf = &local_write_buf[0]; /* Local write buffer */
string_t resource = client->conn_res.requ_res_path; /* Resource that peer request */
string_t w_buf = client->w_buf; /* Real data buffer */
int w_count = 0;
struct tm * utc; /* Get GMT time Format */
time_t now;
time(&now);
utc = gmtime(&now);/* Same As before */
utc
此时并不是标准的格式字符串,但这个变量里面有我们需要的资源/* Construct the HTTP head */
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "%s %s %s\r\n",
http_ver[client->conn_res.request_http_v],
status[STATUS_CODE], status[STATUS_TITLE]);
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Date: %s, %02d %s %d %02d:%02d:%02d GMT\r\n",
date_week[utc->tm_wday], utc->tm_mday,
date_month[utc->tm_mon], 1900+utc->tm_year,
utc->tm_hour, utc->tm_min, utc->tm_sec);
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Content-Type: %s\r\n", content_type[client->conn_res.content_type]);
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Content-Length: %u\r\n", 0 == rsource_size
? (unsigned int)strlen(status[2]):(unsigned int)rsource_size);
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Connection: close\r\n");
w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "\r\n");
write_buf[w_count] = '\0';
从上往下依次是刚才我在上面介绍的顺序,使用的是
snprintf
函数,其实此处可以将这些语句合并起来写,而不是分别调用,十分浪费。但这么写比较清晰其中在生成时间的时候,我使用的是预定义好的静态字符串数组来帮助我,可很好的猜到这些
date_xxx
数组里放的都是些什么,无非就是一些时间的缩写。/* 写入缓冲区 */
append_string(w_buf, STRING(write_buf));
client->w_buf_offset = w_count;
/* If Server do not wanna to sent local file */
if (0 == rsource_size) { /* GET Method */
append_string(w_buf, STRING(status[STATUS_CONTENT]));
snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, status[2]);
return 0;
} else if (-1 == rsource_size) { /* HEAD Method */
return 0;
}
/* 如果需要服务器上的实体资源,那就找到它 */
int fd = open(resource->str, O_RDONLY);
if (fd < 0) {
return -1; /* Write again */
}
/* 将资源文件映射到内存里,这样就能很好的操作 */
char *file_map = mmap(NULL, (size_t)rsource_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (NULL == file_map) {
assert(file_map != NULL);
}
close(fd);
/* 存入缓冲区 */
append_string(w_buf, file_map, rsource_size);
client->w_buf_offset += rsource_size;
munmap(file_map, (unsigned int)rsource_size);
return 0;
}
上面有几个函数调用
open
,mmap
,munmap
,学过Linux
系统编程的人肯定知道,这是共享内存的一种最简单高效的方式。看不太懂的可以去查询
APUE
或者网上资源很多,这是很重要的一个知识点。大致的功能就是将一个文件打开,并映射到内存中,这个内存可以在多个进程间共享MAP_SHARED
也可以不共享MAP_PRIVATE
,这样我们就能像数组一样对其进行读取操作了。至于
make_response_page
的代码就不贴源码了,因为代码几乎都是在做检测的工作,例如安全之类的事情,以及方法分配,只需要扫一眼就能够很清楚的理解了。在构造完成报文之后,下一步自然就是发送它了,那我们又回到了
worker_thread
中去
发送报文
那这个就简单很多了,直接贴上代码
HANDLE_STATUS handle_write(conn_client * client) {
/* String Version */
char* w_buf = client->w_buf->str;
int w_offset = client->w_buf_offset;
int nbyte = w_offset;
int count = 0;
int fd = client->file_dsp;
while (nbyte > 0) {
w_buf += count;
count = write(fd, w_buf, nbyte);
if (count < 0) {
if (EAGAIN == errno || EWOULDBLOCK == errno) {
/* 如果发送缓冲区不够容纳所有的,那就下次再发 */
memcpy(client->w_buf->str, w_buf, strlen(w_buf));
client->w_buf_offset = nbyte;
return HANDLE_WRITE_AGAIN;
}
/* 在这个地方就是前面所说的那个EPIPE错误 */
else /* if (EPIPE == errno) */
/* 对端关闭了连接 */
return HANDLE_WRITE_FAILURE;
}
else if (0 == count)
return HANDLE_WRITE_FAILURE;
nbyte -= count;
}
return HANDLE_WRITE_SUCCESS;
}
- 就是这么简单,因为实在是没有其他工作可以做了
- 尝试发送所有,直到发送完全部数据,或者发送缓冲区不够,那就等待下次发送,这个通过
epoll
很容易就实现了。 - 如果发现对面的不在了,直接关闭就好啦。
- 尝试发送所有,直到发送完全部数据,或者发送缓冲区不够,那就等待下次发送,这个通过
附加
小结
- 其实也是拖拖拉拉地在不断地写这些东西
- 也还是因为时间不多的原因,一直想抽一个连贯的时间,结果一拖就是半年,所以做事一定要当机立断,当然要经过脑子。看起来挺矛盾
- 写到这里,算是给自己的求学之路一个挺好的交代,因为至少将自己知道的都写了出来,对我也好,对其他人也好,至少挺安心的。
- 无论如何都要感谢一下互联网,学校图书馆的馆藏和荐购权限。
- 不知道我这些东西有多少能帮助到看的人,但我知道一定会有影响,也一定有不好的地方,但是我不怕,就怕没人和我说我错在哪里。
- 接下来我想做的事就是用剩下的一年里去互联网,IT的各个大领域实习,见见世面,心中还是有鸿鹄之志的。
- 这本书也就到此为止了
题外话
- 实际上也是构思了三个月左右,我打算附加一章,用来实现一个数据库系统,在上一节也提到过。
- 大致的想法是实现 : SQL编译器,数据库存储引擎,数据库管理系统。至于事务的话,看看吧。觉得如果我写下来就一定会和大家分享。谢谢给我支持的那些人。