0x17-套接字编程-HTTP服务器(5)

  • 让我们停下来,回想一下之前的内容

    1. 首先读取配置文件,并凭此打开服务器套接字
    2. 确定一切完备的情况下(listen),开启事务循环handle_loop
    3. 准备好各项资源prepare_worker,开启两种线程就真正开始工作了
  • string_t 不打算详细讲解,因为并不是什么好的设计,但是只需要将接口,改成C风格的就不错,但是有一个致命的缺点,就是这不是二进制字符串

    • 什么意思?就是这是一个C风格的字符串,无法很好的存储二进制数据,例如无法存储\0这个字符,实际上要设计就需要重新设计。
    • 但这个小程序绰绰有余,因为只是作为一个静态资源HTTP服务器在使用。
    • 在本章最后,会将源代码地址贴上,仅供参考,写的不够严谨,但还是有意义的练习。

2016-08-28 修复上述问题,具体可以参看源码,现在支持二进制数据

万事开头难,当你在键盘上打下第一句代码的时候你就成功了。看永远都只能是谈谈兵,虽说谈兵也需要技术

生成一个响应报文

  • 实际上客户端对你怎么处理这些数据一点都不感兴趣,他们感兴趣的不就是你的响应报文是什么吗
  • 所以说到了这一步就要看看这个报文的组成,但这并不是我们的重点,简单讲一下哪些属性比较重要。
  • 还记得开头的时候,给出了一个报文实例,实际上最明了的莫过于在浏览器中摁F12后自己查看交互报文,再专业一些使用Wireshark这类专业抓包软件也未尝不可,以浏览器为例:
  • 并发HTTP服务器(5) - 图1
    • 这是个人博客上的一个背景图的请求交互,重点看Response Headers
    • 这么一长串,实际上真正必不可少的还是那么两行
      • HTTP/1.1 200 OK
      • Content-Length: 377710
    • 前者告诉你这个球球的结果,后者告诉你请求的结果的内容在哪里,即在报文中空行后多少个字节都是请求的结果。
  • 那在C语言中,或者说在任何语言中,都没什么特别好的办法,就是用字符串构造报文了。作为一个标准库比较贫瘠的语言,这就要我们多做一点工作,这也是为什么要自己写一个字符串结构体的原因所在。

    • 当然如果你为了兼容二进制数据,那么甚至连标准库中的字符串函数都不能使用了,包括Linux提供的扩展gnu99字符串函数,原因是因为C-Style字符串是以\0作为结束符的。
  • 现在我们规定一下,我们这个服务器的响应报文会包含的部分

    1. 状态行是必要的 HTTP/VER STATUS_CODE STATUS_MESSAGE\r\n
    2. 服务器时间 Date: xxx\r\n
      • 用的是UTC格式,实际上此处也可以有点小讲究,后面提一下
    3. 资源类型 Content-Type: xxx\r\n
    4. 资源长度 Content-Length: xxx\r\
    5. 连接状态 Connection: xxx\r\n
    6. 空行\r\n
    7. 资源
  • 在进入生成报文的环节中,其实还有很多工作要做,例如判断是什么请求方法是否是恶意请求获取资源的各种信息 等,直接进入最核心的阶段make_response中的write_to_buf

  • 也就是构造报文阶段

    1. __thread char local_write_buf[CONN_BUF_SIZE] = {0};
    2. static int write_to_buf(conn_client * restrict client, // connection client message
    3. const char * const * restrict status, int rsource_size) {
    4. #define STATUS_CODE 0
    5. #define STATUS_TITLE 1
    6. #define STATUS_CONTENT 2
    7. char * write_buf = &local_write_buf[0]; /* Local write buffer */
    8. string_t resource = client->conn_res.requ_res_path; /* Resource that peer request */
    9. string_t w_buf = client->w_buf; /* Real data buffer */
    10. int w_count = 0;
    11. struct tm * utc; /* Get GMT time Format */
    12. time_t now;
    13. time(&now);
    14. utc = gmtime(&now);/* Same As before */

    utc此时并不是标准的格式字符串,但这个变量里面有我们需要的资源

    1. /* Construct the HTTP head */
    2. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "%s %s %s\r\n",
    3. http_ver[client->conn_res.request_http_v],
    4. status[STATUS_CODE], status[STATUS_TITLE]);
    5. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Date: %s, %02d %s %d %02d:%02d:%02d GMT\r\n",
    6. date_week[utc->tm_wday], utc->tm_mday,
    7. date_month[utc->tm_mon], 1900+utc->tm_year,
    8. utc->tm_hour, utc->tm_min, utc->tm_sec);
    9. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Content-Type: %s\r\n", content_type[client->conn_res.content_type]);
    10. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Content-Length: %u\r\n", 0 == rsource_size
    11. ? (unsigned int)strlen(status[2]):(unsigned int)rsource_size);
    12. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "Connection: close\r\n");
    13. w_count += snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, "\r\n");
    14. write_buf[w_count] = '\0';

    从上往下依次是刚才我在上面介绍的顺序,使用的是snprintf函数,其实此处可以将这些语句合并起来写,而不是分别调用,十分浪费。但这么写比较清晰

    其中在生成时间的时候,我使用的是预定义好的静态字符串数组来帮助我,可很好的猜到这些date_xxx数组里放的都是些什么,无非就是一些时间的缩写。

    1. /* 写入缓冲区 */
    2. append_string(w_buf, STRING(write_buf));
    3. client->w_buf_offset = w_count;
    4. /* If Server do not wanna to sent local file */
    5. if (0 == rsource_size) { /* GET Method */
    6. append_string(w_buf, STRING(status[STATUS_CONTENT]));
    7. snprintf(write_buf+w_count, CONN_BUF_SIZE-w_count, status[2]);
    8. return 0;
    9. } else if (-1 == rsource_size) { /* HEAD Method */
    10. return 0;
    11. }
    12. /* 如果需要服务器上的实体资源,那就找到它 */
    13. int fd = open(resource->str, O_RDONLY);
    14. if (fd < 0) {
    15. return -1; /* Write again */
    16. }
    17. /* 将资源文件映射到内存里,这样就能很好的操作 */
    18. char *file_map = mmap(NULL, (size_t)rsource_size, PROT_READ, MAP_PRIVATE, fd, 0);
    19. if (NULL == file_map) {
    20. assert(file_map != NULL);
    21. }
    22. close(fd);
    23. /* 存入缓冲区 */
    24. append_string(w_buf, file_map, rsource_size);
    25. client->w_buf_offset += rsource_size;
    26. munmap(file_map, (unsigned int)rsource_size);
    27. return 0;
    28. }
  • 上面有几个函数调用open, mmap, munmap,学过Linux系统编程的人肯定知道,这是共享内存的一种最简单高效的方式。

  • 看不太懂的可以去查询APUE或者网上资源很多,这是很重要的一个知识点。大致的功能就是将一个文件打开,并映射到内存中,这个内存可以在多个进程间共享MAP_SHARED也可以不共享MAP_PRIVATE,这样我们就能像数组一样对其进行读取操作了。

  • 至于make_response_page的代码就不贴源码了,因为代码几乎都是在做检测的工作,例如安全之类的事情,以及方法分配,只需要扫一眼就能够很清楚的理解了。

  • 在构造完成报文之后,下一步自然就是发送它了,那我们又回到了worker_thread中去

发送报文

  • 那这个就简单很多了,直接贴上代码

    1. HANDLE_STATUS handle_write(conn_client * client) {
    2. /* String Version */
    3. char* w_buf = client->w_buf->str;
    4. int w_offset = client->w_buf_offset;
    5. int nbyte = w_offset;
    6. int count = 0;
    7. int fd = client->file_dsp;
    8. while (nbyte > 0) {
    9. w_buf += count;
    10. count = write(fd, w_buf, nbyte);
    11. if (count < 0) {
    12. if (EAGAIN == errno || EWOULDBLOCK == errno) {
    13. /* 如果发送缓冲区不够容纳所有的,那就下次再发 */
    14. memcpy(client->w_buf->str, w_buf, strlen(w_buf));
    15. client->w_buf_offset = nbyte;
    16. return HANDLE_WRITE_AGAIN;
    17. }
    18. /* 在这个地方就是前面所说的那个EPIPE错误 */
    19. else /* if (EPIPE == errno) */
    20. /* 对端关闭了连接 */
    21. return HANDLE_WRITE_FAILURE;
    22. }
    23. else if (0 == count)
    24. return HANDLE_WRITE_FAILURE;
    25. nbyte -= count;
    26. }
    27. return HANDLE_WRITE_SUCCESS;
    28. }
  • 就是这么简单,因为实在是没有其他工作可以做了
    • 尝试发送所有,直到发送完全部数据,或者发送缓冲区不够,那就等待下次发送,这个通过epoll很容易就实现了。
    • 如果发现对面的不在了,直接关闭就好啦。

附加

小结

  • 其实也是拖拖拉拉地在不断地写这些东西
  • 也还是因为时间不多的原因,一直想抽一个连贯的时间,结果一拖就是半年,所以做事一定要当机立断,当然要经过脑子。看起来挺矛盾
  • 写到这里,算是给自己的求学之路一个挺好的交代,因为至少将自己知道的都写了出来,对我也好,对其他人也好,至少挺安心的。
  • 无论如何都要感谢一下互联网,学校图书馆的馆藏和荐购权限。
  • 不知道我这些东西有多少能帮助到看的人,但我知道一定会有影响,也一定有不好的地方,但是我不怕,就怕没人和我说我错在哪里。
  • 接下来我想做的事就是用剩下的一年里去互联网,IT的各个大领域实习,见见世面,心中还是有鸿鹄之志的。
  • 这本书也就到此为止了

题外话

  • 实际上也是构思了三个月左右,我打算附加一章,用来实现一个数据库系统,在上一节也提到过。
  • 大致的想法是实现 : SQL编译器,数据库存储引擎,数据库管理系统。至于事务的话,看看吧。觉得如果我写下来就一定会和大家分享。谢谢给我支持的那些人。