基于CGI搭建网络数据采集站

基于CGI搭建网络数据采集站

http是被应用最为广泛的协议,不管是从学习的角度还是实用程度,做一个http服务器都是很好的选择。

项目整体思路

  1. 编写目标是编写一个小型http服务器,能正确处理来自浏览器的请求。开发环境是Linux下的C语言编写。
  2. 其次需要服务器支持CGI技术,可以让用户执行服务器上的CGI程序,实现搜索引擎的类似功能。
  3. 从底层编写的服务器自然追求比集成框架更高的单页响应速度和高并发性能。

开发记录

第一部分,完成服务器的基本功能

这里需要了解http协议格式,服务器运行的原理,TCP/IP 和套接字编程的知识

这里实现的服务器支持 GET 和POST 请求,基本能实现大部分浏览器功能。实现的是http 1.0版本,和1.1最大的一个区别是 1.1有 keep-alive 选项,这个实现需要服务器保证处理完一个请求不会关闭套接字,并且需要做一些后续工作降低不活跃连接的资源占用。

流程图
这里写图片描述

CGI示意图
这里写图片描述

第二部分,对服务器进行测试

这部分踩了很多坑。

  1. 能显示html页面,不能显示图片或者css。
  2. 发送响应报文之前,必须清空TCP接收缓冲区内容,不然会有粘包问题
  3. 对项目根目录的理解不正确
  4. 服务器自动退出

拿Chrome浏览器来说,它会首先请求URL的资源,假如是一个网页的话,浏览器会进一步分析展示这个html页面的其他元素,图片,js,css,然后继续向服务器发送请求。等拿到所有的资源就可以显示一个完整的页面了。

所以服务器的响应报文Content-Type 字段就很重要了,我实现的是分析请求URL的资源后缀,根据对照表返回浏览器需要的资源类型。

html页面中标签有 href 或者 src 这个选项,如果是以 / 开头,那么这个请求到了服务器,就会被自动拼接上资源文件夹目录。所以这个/ 并不是服务器根目录,而是特定的资源目录。

服务器自动退出是因为主线程收到了 SIGPIPE 信号, 对对端关闭的套接字进行了写操作。

这里总结一下遇到错误的处理(血的教训)

首先尽可能缩小错误范围,确保网络环境好(NAT),使用简单页面进行测试(telnet发送可控报文信息)。

第三部分,对提高服务器性能的思考

https://coolshell.cn/articles/7490.html

首先测试环境最好是在局域网内,网络环境对结果影响巨大,第一次在虚拟机里测试只能达到预期效果的一半。我使用两台阿里云服务器进行内网测试,都是1核2G内存。

服务器框架

第一个版本,对每一个连接上的客户创建线程,分离线程交给1号进程回收。这样做让线程间切换占用了大量系统资源,并且连接建立等待数据这段期间线程是阻塞等待的,占用着文件描述符。4000并发时每分钟处理请求13000+。

第二个版本,基于Reactor 事件处理模式,主线程负责等待数据就绪和监听新连接,当有文件描述符就绪后,把IO事件处理交给新线程去处理。这里用epoll和pthread库实现。每分钟处理请求可达19000+.

各单元性能

为了从方方面面提升服务器性能,其中还使用了一些高效的接口。

sendfile是linux特有的系统调用。如果用read + write的方式进行两个文件描述符的数据拷贝,那么会经历多次用户态到内核态的切换。硬件驱动把数据交给内核缓冲区,read调用复制内核缓冲区内容到程序地址空间,write调用再次切换内核态,并复制用户地址空间的数据到内核缓冲区。sendfile只需要切换到内核态一次,剩下拷贝数据的过程全部交给内核完成。

第四部分,加入CGI程序,管理Mysql

服务器搭建好了CGI平台,接下来编写合适的CGI程序让服务器发挥效力。

我的想法是做一个网络数据采集站,前端页面使用开源框架hexo渲染生成静态页面,这个页面类似于个人博客是利用markdown文件生成的,方便随时增删页面内容。主页包含许多爬虫页面入口,客户点击需求页面即可使用服务器上搭建好的爬虫程序。服务器将目标数据抓取后存入MySql数据库,并发送响应报文展示给客户。

FAQ

利用glog精确到微秒级的日志,测试服务器单元性能。主要浪费在CGI的fork替换,还有多线程的切换。利用webbench模拟高并发时,由于服务器上行带宽实在太小,网络也成为瓶颈。

CGI机制缺陷

CGI机制反复加载,调用fork-exec是性能缺陷。还可以利用FastCGI机制,管理着多个已经运行的CGI进程。当服务器有CGI请求时,将环境变量和标准输入发送到这些CGI进程里。这样一个类似于进程池的机制能大大提高CGI机制的效率。

服务器框架

这个服务器是epoll监听新链接事件和读事件,将读事件处理交给新线程去处理,解决了部分不活跃连接的问题。在每一个线程里,可能会遇到http报文未发送完毕的情况,所以就阻塞在read。线程阻塞是没问题的,若是epoll单执行流需要设置套接字非阻塞。
因为TCP是基于字节流的,所以并不是写多少字节数据对端就能完整收到,如果遇到报文没发送完的情况要判断出来,可以根据GET请求的空行或者POST请求的ContentLength字段判断,如果数据没收完整,需要一个数据结构保存已经收到的buf。
更加完善的服务器框架应该是主线程监听连接事件,线程中调用epoll监听每个连接的读事件。或者像Nginx,master进程监听端口,其余线程调用epoll_wait去accept处理新的连接。

尚未完善部分

服务器框架

<<深入理解Nginx>> <<24小时365天不间断服务>>