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

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

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

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

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

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

生成一个响应报文

  • 实际上客户端对你怎么处理这些数据一点都不感兴趣,他们感兴趣的不就是你的响应报文是什么吗

  • 所以说到了这一步就要看看这个报文的组成,但这并不是我们的重点,简单讲一下哪些属性比较重要。

  • 还记得开头的时候,给出了一个报文实例,实际上最明了的莫过于在浏览器中摁F12后自己查看交互报文,再专业一些使用Wireshark这类专业抓包软件也未尝不可,以浏览器为例:

    • 这是个人博客上的一个背景图的请求交互,重点看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

  • 也就是构造报文阶段

      __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编译器,数据库存储引擎,数据库管理系统。至于事务的话,看看吧。觉得如果我写下来就一定会和大家分享。谢谢给我支持的那些人。

书籍推荐