【网络编程项目1】HTTP服务器编程
github仓库:https://github.com/brucewayne9064/LinuxCpp_server
一、CGI程序的工作方式
静态网页:浏览器通过URL请求网页,服务器返回该网页的文件给浏览器,浏览器在本地解析渲染该文件
动态网页:浏览器通过URL请求网页,服务器返回参数,在浏览器本地产生网页
CGI程序:(common gateway interface,通用网关接口)浏览器通过URL请求后缀为cgi,服务器将该请求传给CGI程序,CGI程序通过标准IO接收,处理完后通过标准IO发给服务器,服务器再传给浏览器
二、web服务器Apache
三、HTTP原理
hyper text transfer protocol,超文本传输协议,用于从万维网服务器(www,world wide web)传输超文本到本地浏览器。基于TCP/IP传输协议,位于C/S构架上,默认端口号为80
URL的一般格式:
1 | 协议名://主机名[:端口号][/目标路径/文件名][#锚点名] |
浏览器给dns服务器发送域名,dns服务器返回ip,浏览器向对应ip请求网页
四、HTTP特点
- 支持C/S
- 简单快速
- 灵活:传输任意类型数据对象,由content-type标记
- 无连接:限制为每次连接只处理一个请求,收到客户端应答后就断开连接。节省传输时间
- 无状态:对于事务处理没有记忆能力
- 媒体独立:只要C/S端知道如何处理数据,任何类型都可以传输
五、HTTP消息结构
URI包括URL和URN两个类别,URL是URI的子集,所以URL一定是URI,而URI不一定是URL
URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。(=URL+URN)
URL = Universal Resource Locator 统一资源定位符,一种定位资源的主要访问机制的字符串,一个标准的URL必须包括:protocol、host、port、path、parameter、anchor。
URN = Universal Resource Name 统一资源名称,通过特定命名空间中的唯一名称或ID来标识资源。
HTTP使用URI进行传输数据,建立连接。一旦建立后,数据消息通过[RFC5322]或MIME[RFC2045]传输
六、客户端请求消息
HTTP请求:请求行(内含请求方法)+请求头部+空行+请求数据
常用的请求方法有:
GET(向特定资源发出查找请求,而不修改服务器状态)
带参数的GET(参数存储在URL中?的后面,以 variable=value 的形式)
POST(向指定资源提交数据进行处理请求,参数存储在HTTP请求主体中,而不是URL中)
七、服务器响应消息
HTTP响应:状态行+消息报头(响应头)+空行+响应正文
八、HTTP状态码
HTTP状态码是服务器对客户端请求的响应(server header),它们表示请求是否成功完成,以及发生了什么情况。
一些具体的HTTP状态码及其含义如下:
- 200 OK:请求成功。
- 301 Moved Permanently:请求的资源已永久移动到新位置,并且将来的请求应该使用新的URL。
- 404 Not Found:请求的资源不存在。
- 500 Internal Server Error:服务器遇到了未知的错误。
九、HTTP状态码分类
HTTP状态码有五类,每类有一百个状态码,但并不是所有的状态码都常用。
常见的HTTP状态码有以下几种:
- 1xx:信息响应,表示服务器已收到请求,正在处理中。
- 2xx:成功响应,表示请求已成功完成,服务器给出了预期的响应。
- 3xx:重定向响应,表示请求被重定向到其他地方,需要客户端进一步操作。
- 4xx:客户端错误响应,表示请求有错误,或者服务器无法处理。
- 5xx:服务器错误响应,表示服务器遇到了内部错误,无法完成请求。
十、实现HTTP服务器(tinyhttp项目)
实现fork一个子进程,子进程与父进程通过pipe进行通信(同一时刻只能实现单向通信(半双工,如果实现全双工就要两个管道了),例如父进程向子进程发送数据,如果想要实现子进程向父进程发送数据,则需要重新设置关闭和打开的文件描述符):
- 在父进程中调用pipe()函数创建一个管道,产生两个文件描述符fd[0]和fd[1],分别指向管道的读端和写端。
- 在父进程中调用fork()函数创建一个子进程,由于子进程会继承父进程打开的文件描述符,所以父子进程可以通过新创建的管道进行通信。
- 根据通信的方向,关闭不需要的文件描述符,例如如果父进程向子进程发送数据,那么父进程关闭fd[0](读),子进程关闭fd[1](写)。
- 使用write()函数向管道的写端写入数据,使用read()函数从管道的读端读取数据。
- 使用close()函数关闭管道的文件描述符。
执行cgi脚本的步骤(tinyhttp):
- web客户端对服务器发起HTTP请求,请求中包含cgi程序的路径和参数。
- 服务器收到一个http请求,建立一个线程(pthread_create)来处理(accept)
- 如果请求地址未找到,返回404,结束
- 如果请求地址为无参数GET,返回文件,结束
- 如果是带参数GET, POST,服务器根据请求中的URL找到对应的cgi程序
- 建立两个pipe,分别为cgi_input,cgi_output,并fork一个子进程来执行它。
- 服务器重定向子进程的标准输入和标准输出到管道的相应端口,并关闭不需要的端口。在子进程中,把STDOUT定位到cgi_output的写入端,把STDIN定位到cgi_input的读取端,关闭cgi_input写入端,cgi_output读取端。
- 服务器设置子进程的环境变量,包括请求方法、参数、内容类型、内容长度等,以便cgi程序获取用户数据。
- 子进程执行cgi程序,从标准输入读取用户数据,进行业务逻辑处理,然后将结果输出到标准输出。
- 父进程中,关闭cgi_input读取端,cgi_output写入端。把POST写入cgi_input,从cgi_output读取子进程的输出数据,构造HTTP响应并发送给客户端。
- 关闭连接,完成一次HTTP请求与回应。
为什么要fork一个子进程?
因为每个cgi程序只能处理一个用户请求,而服务器可能同时收到多个用户请求,所以需要fork出多个子进程来并发执行不同的cgi程序。
基本调用流程:
main() $\to$ startup()$\to$accept_request()$\to$execute_cgi()
服务器是父进程,他通过运行主线程来监听和接收新的连接请求,主线程创建子线程来处理每个连接请求。子线程是主进程中的其他线程,它们负责执行accept_request函数,解析HTTP请求,发送HTTP响应,以及fork子进程来执行cgi脚本。子进程是子线程fork出来的新的进程,它们负责运行cgi脚本,并通过管道和子线程通信。
