0x14-套接字编程-HTTP服务器(2)

HTTP服务器的结构

  • HTTP服务器 本质上就是一个 TCP的接收端 程序
  • 但凡一个正常的 TCP 接收端程序,都逃不过那几个流程:
    • 创建监听socket -> 绑定端口,IP -> 监听socket -> 接受新连接 -> 处理读写… -> 关闭完成的连接
    • 其中前三步比较固定,最多对这个监听用的socket,进行一些优化处理,设置一些属性之类的,但那都是固定模式,想想就能明白。硬要说重要的地方,也就是在于是否把socket设为非阻塞(non-blocking)了。
    • 后面几步,每个都是很重要的环节,需要细细设计才行

所谓非阻塞,我还是不班门弄斧了,请移步 UNIX网络编程-卷1-中文·第三版 127页(英文版160页) 的图6-6,清楚的对比了,阻塞,非阻塞,异步,I/O复用的区别和含义。十分建议写网络程序之前,去把这本书的某些章节大致过一遍。

  • 对于这章节需要写的这个服务器而言,采用的是经典且流行的 I/O复用+非阻塞套接字(socket)+多线程(线程池) 结构。
  • 呐,又出现一个新的知识点,I/O复用,这是什么鬼。

I/O复用

  • 我给一个不太严密的解释,那就是 将你这个程序需要等待的地方,集中起来
  • 打个比方:
    • 假设有100个新连接,被你的监听套接字给成功接受(accept())了
    • 这时候并不是所有新连接都立刻有数据可以读,那此时你有两种选择:阻塞,非阻塞。但不论是哪一种都会导致同一个结果
      • 阻塞: 那假设你只有一个线程处理这100个连接,万一要是正好处理到这个暂时没有数据的连接,就要一直等待它的数据到来,后面的几十个连接都要闲着;假设有多个线程同时处理,理由还是一样,换汤不换药,而且难道你还能开100个线程去处理吗?那如果更多的连接呢?
      • 非阻塞: 比阻塞看起来稍微好一些,因为如果没有数据到来的话,那就直接跳过这个连接,直接去处理下一个连接了,但是你想想,这不就是遍历了吗?万一连接量一大,假设上万,而且只有少数的几个连接有数据活跃,这无用功做的是不是太多了?多开几个线程去平摊压力?那么要开多少比较合适?
    • 这时候喜欢偷懒的程序员,自然就不愿意了,于是考虑是否可以有一个,让我们可以在单个线程的情况下还能够只处理那些活跃的连接?
    • 这时候出现了所谓的 I/O复用 技术,说是技术,因为它使用的还是同步型的操作(read, write),只不过套接字设为非阻塞的了。
    • Linux平台下的epoll, Unix(包括Mac)平台下的kqueue, Windows平台下的IOCP,各平台通用的select, poll,还有几个历史实现就不赘述了。
    • 最后这两个select, poll活跃连接明显少于总连接数的情况下,性能比前三个要差许多,故本章使用的是epoll,(当然还有资料比较多的原因啦
  • 说说 epoll 的工作

    • 首先它帮我们管理着所有的套接字,用来监听这些套接字哪些有了数据,就返回谁。
    • 将所有等待,阻塞都集中在了一个地方,那就是epoll_wait()调用上
    • 而且可以针对不同的事件进行不同的监听,这就是事件驱动这种模式的由来
  • 事件驱动

    • 简单来说,就是针对某种事件进行触发的一种编程模式
    • 具体来说,假设你在网络编程,正在处理一个套接字,由于TCP是全双工的,意味着这个TCP套接字是可读可写,问题来了,什么时候可读,什么时候可写呢?这就延伸出了事件,读事件,写事件,错误事件等
    • 可以通过epoll_clt()来设置要监听的事件,当然也可以同时监听多个事件,看你的设计了。
  • 具体的epoll接口的详细介绍,可以直接在Linux上,使用man epoll进行查看手册,这是基本功。

    • epoll_create, epoll_ctl, epoll_wait

服务器结构

  • 继续回到服务器结构
  • 上面简单的讲述了一下什么是 I/O复用,以及将会用到的具体实现epoll。那具体说一下,整个程序的流程
  • 还是老规矩,写程序之前要先构思,自己在纸上画一画,大概的流程是什么
  • 问题: 想要完整处理一个HTTP请求,需要哪些步骤?
    1. 解析HTTP请求报文
    2. 创建HTTP回复报文
  • 逻辑就这么简单啊,但是加上细节部分,就会稍微麻烦一些了:
    • 完整地 从套接字中,读取 HTTP请求报文
    • 解析 HTTP请求报文,并判断其有效性
    • 生成 HTTP回复报文
    • 完整地 通过对应套接字,发送给请求者。
  • 在这里我假设,你已经对TCP编程的模型很熟悉了,不熟的可以去顶部看看再回来
    • 并发服务器的关键点就在于
      • 高效且正确地接收尽可能多的连接
      • 高效且正确地处理尽可能多的连接
      • 以上忽略了安全性
    • 该如何设计?
      • 让某个epoll用来服务于接收新连接这个环节(accept)
      • 让某些epoll用来处理这些新连接的事务。
      • 这样理论上我们既发挥了单核的极限(epoll),又用上了多核的优势(多个epoll)
    • 更具体的呢?
      • 在主线程里使用单个epoll来处理,监听套接字的读事件,也就是接受新连接
      • 再开几个线程epoll,用来平分处理这些新连接。
    • 这样也就是网络编程的一整个流程,如果看到这里你已经大概有了一个程序思路,实际上就已经达到目的了,接下来就是直接上手代码就行
    • 还是迷迷糊糊的,就一步一步跟着我,写出这个服务器,会大有脾益。

小经验,在编程中,读往往比写要复杂许多。在网络编程里面亦是。

  • 有图有真相,希望能够自己画。
    HTTP流程图

  • 现在大致有了思路,可以整理整理自己接下来该干什么了

环境准备

  • 99%的中国大学学生的操作系统,应该都是 Windows或者Max OS(maxOS),那么建议你直接使用虚拟机进行环境的搭建,可以选择开源免费的Visual Box,Windows下也可以使用商业版的VMware,Mac下有一个更棒的商业版选择Paralelle Desktop,但是这都是软件,算是无关紧要的。
  • 选择一个Linux发行版,由于我用的是 Debian系列的Ubuntu 16.04 LTS,所以我也推荐这个发行版,其他的发行版也许略有差异,不再多说。
  • 装好之后,直接进入开发阶段吧。
    • IDE可以选择Clion或者Kdevelop
    • 当然你要用Vim我也不会阻拦,但是请装好两个插件NerdtreeYouCompleteMe,配合好另一个软件tmux(简单使用),不然你会想死。
    • 除了Vim,你也可以选择 Visual Studio Code加装一个C/C++ tools也是不错的。
    • 作为时尚的我,自然选择Clion了,简单明了,且还是使用CMake作为构建工具。
  • 想要进行这么底层的网络编程,请准备好GoogleUnix网络编程卷1,如果你两个都没有的话,不说了,再见。建议准备一个那玩意儿去访问Google