HTTP协议——超文本传输协议:是互联网上应用最广泛的网络协议。它是应用层的协议,底层是基于TCP通信的。
HTTP协议的工作过程:客户通过浏览器向服务器发送文档请求,服务器将请求的资源返回给浏览器,然后关闭连接。即:连接—>请求—>响应—>关闭连接。
通用的HTTP服务器框架
通用——与业务无关 框架——二次开发
HTTP协议的请求与响应
服务器的特点
- 客户/服务器模式(C/S)
- 简单快速,程序规模小,通信速度快
- 灵活,可以传输任意数据
- 无连接,每次连接只处理一个请求
- 无状态,不会保存之前的请求和响应
服务器的基本流程
启动服务器——>基于TCP的初始化(创建、绑定、监听);使用线程进入事件循环(创建线程、线程分离)——>进入线程入口——>处理请求过程
处理请求过程
1、解析请求
- 从socket中读取出首行
HTTP请求中的换行符:把未知问题转换成已知问题,将\r和\r\n转换成\n
- 一个字符一个字符的从socket中读取数据
- 判定当前字符是不是\r
- 如果当前字符是\r,就尝试读取下一个字符 a)如果下一个字符是\n b)如果下一个字符不是\n 针对这两种情况,都把当前字符转换成\n
- 如果当前字符是\n直接结束函数(这一行已经读完了)
- 如果当前字符是普通字符,直接追加到输出结果中
- 解析首行,获取url和method
首行格式:GET http://www.baidu.com/index.html?aa=10&b=20 HTTP/1.1
- 解析url,获取url_path和query_string
url的格式:/index.html?aa=10&b=20
以?为分隔符,?前面的是url_path,?后面的是query_string
- 解析header,丢弃大部分header,只保留content-Length
2、根据请求计算响应并写回客户端
- 处理静态页面(GET+无参数)
- 根据url_path获取文件路径:若以“/”结尾的直接追加index.html 若不是以“/”结尾的利用文件属性来判断其是否是目录——若是则追加index.html
- 打开文件,读取文件内容,并将其写入socket中
(1)调用open打开文件
(2)构造HTTP响应报文(首行、空行),并调用send将构造号的报文发送
(3)读文件内容(调用stat获取文件大小)并将其写到socket中
(4)关闭文件
- 处理动态页面(GET+参数或POST)
- 创建一对匿名管道
- 创建子进程fork
- 父进程核心流程 a) 如果是POST请求,把body部分的数据读出来写到管道中,剩下的动态生成页面的过程都交给子进程来完成 b) 构造HTTP响应中的首行,header,空行 c) 从管道中读取数据(子进程动态生成的页面),把这个数据也写到socket中 d) 进程等待,回收子进程的资源
- 子进程核心流程 a) 设置环境变量(METHOD,QUERY_STRING,CONTENT_LENGTH),如果把上面这几个信息通过管道来告知替换之后的程序,也是完全可行的。但是此处我们要遵守CGI标准,所以必须使用环境变量传递以上信息 b) 把标准输入和标准输出重定向到管道上。此时CGI程序读写标准输入输出就相当于读写管道 c) 子进程进行程序替换(需要先找到是哪个CGI可执行程序,然后在使用exec函数进行替换)。替换成功之后,动态页面完全交给CGI程序进行计算生成 d)替换失败的错误处理
3、错误处理:一旦触发错误处理逻辑,就直接返回一个404
- 设置错误页面(首行、空行、提示)
- 调用send将页面发送
缺点:应根据不同的错误原因返回不同的数据段
351 void HandlerRequst(int new_sock)//处理请求
352 {
353 int err_Code = 200;//状态码
354 HttpRequest req;//定义了一个缓冲区
355 memset(&req,0,sizeof(req));//初始化缓冲区
356 //1.解析请求
357 //a) 从socket中读取出首行
358 if(ReadLine(new_sock,req.first_line,sizeof(req.first_line)-1) < 0){
359 //TODO错误处理
360 err_Code = 404;
361 goto END;
362 }
363 printf("first_line:%s\n",req.first_line);//打印日志
364 //b) 解析首行,获取url和method
365 if(ParseFirstLine(req.first_line,&req.method,&req.url) < 0){
366 //TODO错误处理
367 err_Code = 404;
368 goto END;
369 }
370 //c) 解析url,获取url_path和query_string
371 if(ParseUrl(req.url,&req.url_path,&req.query_string) < 0){
372 //TODO错误处理
373 err_Code = 404;
374 goto END;
375 }
376 //d) 解析header,丢弃大部分header,只保留content-Length
377 if(ParseHeader(new_sock,&req.content_length)){
378 //TODO错误处理
379 err_Code = 404;
380 goto END;
381 }
382 //2.根据请求计算响应并写回客户端
383 if(strcasecmp(req.method,"GET") == 0 && req.query_string == NULL){//strcasestrcmp忽略大小写的比较
384 //a) 处理静态页面(GET+无参数)
385 err_Code = HandlerStaticFile(new_sock,&req);
386 }else if(strcasecmp(req.method,"GET") == 0 && req.query_string != NULL){
387 //b) 处理动态页面(GET+参数)
388 err_Code = HandlerCGI(new_sock,&req);
389 }else if(strcasecmp(req.method,"POST") == 0){
390 //b) 处理动态页面(POST)
391 err_Code = HandlerCGI(new_sock,&req);
392 }else{
393 //TODO错误处理
394 err_Code = 404;
395 goto END;
396 }
397 END:
398 //收尾工作,主动关闭socket,会进入TIME_WAIT(谁主动断开,谁等待)
399 if(err_Code != 200){
400 Handler404(new_sock);
401 }
402 close(new_sock);//如果不关闭,文件描述符泄漏
403 }
服务器的测试——天数计算器
- 利用HTML表单设置输入的界面
1 <html>
2 <head>
3 <meta http-equiv="content-type" content="text/html;charset=utf-8">
4 </head>
5 <body>
6 <form action="action_page.php" method="POST">
7 起始时间:<br>
8 <input type="text" name="firstname">
9 <br>
10 结束时间:<br>
11 <input type="text" name="lastname">
12 <br><br>
13 <input type="submit" value="计算">
14 </form>
15 </body>
16 </html>
- 先获取参数(方法、query_string、body)放入buf中
- 先从环境变量中获取到方法
- 如果是 GET 方法,就是直接从环境变量中获取到QUERY_STRING
- 如果是POST方法,先通过环境变量获取到 CONTENT_LENGTH ,再从标准输入中读取 body
- 解析buf中的参数
- 根据业务的具体需求,完成计算
- 判断日期是否合法——>获取当月的天数
- 计算日期之间间隔的天数
(1)让日期1大于等于日期2
(2)让日期2++直到与日期1相等
58 int CalculateDayApart(int year1,int month1,int day1,
59 int year2,int month2,int day2)//计算两个日期之间相隔多少天
60 {
61 //1.先让year1大于等于year2
62 if (year1 < year2)
63 {
64 int tmp = year1;
65 year1 = year2;
66 year2 = tmp;
67 }
68 if (month1 < month2){
69 int tmp = month1;
70 month1 = month2;
71 month2 = tmp;
72 }
73 if(day1 < day2)
74 {
75 int tmp = day1;
76 day1 = day2;
77 day2 = tmp;
78 }
79 //2.让日期2++直到与日期1相等
80 int count= 0;
81 while (year1 == year2 && month1 == month2 && day1 == day2){
82 int ret = day2 + 1;
83 if (ret > GetMonthDay(year2, month2)){
84 int monthday = GetMonthDay(year2, month2);
85 ret -= monthday;
86 month2++;
87 if (month2 == 13){
88 month2 = 1;
89 year2++;
90 }
91 }
92 day2 = ret;
93 count++;
94 }
95 return count;
96 }
- 把计算出的结果构造成页面返回给浏览器