3.1 原理与基础知识
HTTP报文格式
HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。
请求报文
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
其中,请求分为两种,GET和POST,具体的:
- GET
- POST
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。 GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
-
- HOST,给出请求资源所在服务器的域名。
- User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
- Accept,说明用户代理可处理的媒体类型。
- Accept-Encoding,说明用户代理支持的内容编码。
- Accept-Language,说明用户代理能够处理的自然语言集。
- Content-Type,说明实现主体的媒体类型。
- Content-Length,说明实现主体的大小。
- Connection,连接管理,可以是Keep-Alive或close。
- referer
-
其中当浏览器(或者模拟浏览器行为)向web 服务器发送请求的时候,头信息里有包含 Referer 。
Referer 的正确英语拼法是referrer 。由于早期HTTP规范的拼写错误,为了保持向后兼容就将错就错了。其它网络技术的规范企图修正此问题,使用正确拼法,所以目前拼法不统一。还有它第一个字母是大写。
Referer的作用:
-
防盗链
将某个http请求发给服务器后,如果服务器要求必须是某个地址或者某几个地址才能访问,而你发送的referer不符合他的要求,就会拦截或者跳转到他要求的地址,然后再通过这个地址进行访问。
-
防止恶意请求
比如静态请求是 .html结尾的,动态请求是.shtml,那么由此可以这么用,所有的*.shtml请求,必须 Referer 为我自己的网站。
-
- 空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
- 请求数据也叫主体,可以添加任意的其他数据。
3.2 解析请求报文
状态机
什么是(有限)状态机
状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个LED等,就有亮和灭两种状态。我们通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如LED灯的状态就是两个亮和灭。
状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。
解析http请求报文(本项目)
从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。
(图片出处:@两猿社)
主状态机的三种状态:
- CHECK_STATE_REQUESTLINE,解析请求行
- CHECK_STATE_HEADER,解析请求头
- CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求
从状态机的三种状态:
- LINE_OK,完整读取一行
- LINE_BAD,报文语法有误
- LINE_OPEN,读取的行不完整
整体流程
浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。
各子线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。
在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。
从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。
主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。
解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。
3.3 服务端响应请求
响应执行类型
浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。
其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once和http_conn::write完成数据的读取与发送。
process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果.该函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。
归纳上述所说,一共分为8种情形:
- NO_REQUEST
-
- 请求不完整,需要继续读取请求报文数据
- 跳转主线程继续监测读事件
- GET_REQUEST
-
- 获得了完整的HTTP请求
- 调用do_request完成请求资源映射
- NO_RESOURCE
-
- 请求资源不存在
- 跳转process_write完成响应报文
- BAD_REQUEST
-
- HTTP请求报文有语法错误或请求资源为目录
- 跳转process_write完成响应报文
- FORBIDDEN_REQUEST
-
- 请求资源禁止访问,没有读取权限
- 跳转process_write完成响应报文
- FILE_REQUEST
-
- 请求资源可以正常访问
- 跳转process_write完成响应报文
- INTERNAL_ERROR
-
- 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发
为了更好的理解请求资源的访问流程,这里对各种各页面跳转机制进行简要介绍。其中,浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxx,xxx通过html文件的action属性进行设置。
m_url为请求报文中解析出的请求资源,以/开头,也就是/xxx,项目中解析后的m_url有8种情况。
- /
-
- GET请求,跳转到judge.html,即欢迎访问页面
- /0
-
- POST请求,跳转到register.html,即注册页面
- /1
-
- POST请求,跳转到log.html,即登录页面
- /2CGISQL.cgi
-
- POST请求,进行登录校验
- 验证成功跳转到welcome.html,即资源请求成功页面
- 验证失败跳转到logError.html,即登录失败页面
- /3CGISQL.cgi
-
- POST请求,进行注册校验
- 注册成功跳转到log.html,即登录页面
- 注册失败跳转到registerError.html,即注册失败页面
- /5
-
- POST请求,跳转到picture.html,即图片请求页面
- /6
-
- POST请求,跳转到video.html,即视频请求页面
- /7
-
- POST请求,跳转到fans.html,即关注页面
使用mmap
一般来说,修改一个文件的内容需要如下3个步骤:
- 把文件内容读入到内存中。
- 修改内存中的内容。
- 把内存的数据写入到文件中。
从传统读写文件的过程中,我们可以发现有个地方可以优化:如果可以直接在用户空间读写 页缓存,那么就可以免去将 页缓存 的数据复制到用户空间缓冲区的过程。
那么,有没有这样的技术能实现上面所说的方式呢?答案是肯定的,就是 mmap。
使用 mmap 系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。原理如图 所示:
(图片来源:知乎@linux)
前面我们介绍过,读写文件都需要经过 页缓存,所以 mmap 映射的正是文件的 页缓存,而非磁盘中的文件本身。由于 mmap 映射的是文件的 页缓存,所以就涉及到同步的问题,即 页缓存 会在什么时候把数据同步到磁盘。
Linux 内核并不会主动把 mmap 映射的 页缓存 同步到磁盘,而是需要用户主动触发。同步 mmap 映射的内存到磁盘有 4 个时机:
- 调用
msync函数主动进行数据同步(主动)。 - 调用
munmap函数对文件进行解除映射关系时(主动)。 - 进程退出时(被动)。
- 系统关机时(被动)。
资源响应
报文拼接流程:
-
往响应报文中添加状态行 :
http/1.1 [状态码] [状态消息] -
往响应报文中佳田消息报头:
content-length:... connection: ...其中:
响应报文长度 用于浏览器端判断服务器是否发送完数据;
记录连接状态 用于告诉浏览器端保持长连接
-
添加空行
-
响应报文分为两种,一种是请求文件的存在;一种是请求出错。
所以在请求成功的情况下,还需返回具体的请求内容。
响应流程:
-
先判断是何种事件(登录?访问资源?失败?)
-
拼接响应报文
-
注册 epollout 事件。(这里详解看【杂记】epollout)
-
主线程检测写事件,并调用一个写函数将响应报文发送给客户端(浏览器)
-
“一个写函数” 详解:
在生成响应报文时初始化byte_to_send,包括头部信息和文件数据大小。通过writev函数循环发送响应报文数据,根据返回值更新byte_have_send和iovec结构体的指针和长度,并判断响应报文整体是否发送成功。
- 若writev单次发送成功,更新byte_to_send和byte_have_send的大小,若响应报文整体发送成功,则取消mmap映射,并判断是否是长连接.
-
- 长连接重置http类实例,注册读事件,不关闭连接,
- 短连接直接关闭连接
- 若writev单次发送不成功,判断是否是写缓冲区满了。
-
- 若不是因为缓冲区满了而失败,取消mmap映射,关闭连接
- 若eagain则满了,更新iovec结构体的指针和长度,并注册写事件,等待下一次写事件触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。