基于HTTP协议的小型Web服务器——日期计算器

HTTP协议——超文本传输协议:是互联网上应用最广泛的网络协议。它是应用层的协议,底层是基于TCP通信的。

HTTP协议的工作过程:客户通过浏览器向服务器发送文档请求,服务器将请求的资源返回给浏览器,然后关闭连接。即:连接—>请求—>响应—>关闭连接。

通用的HTTP服务器框架

通用——与业务无关                      框架——二次开发

基于HTTP协议的小型Web服务器——日期计算器_第1张图片

HTTP协议的请求与响应

基于HTTP协议的小型Web服务器——日期计算器_第2张图片

服务器的特点

  • 客户/服务器模式(C/S)
  • 简单快速,程序规模小,通信速度快
  • 灵活,可以传输任意数据
  • 无连接,每次连接只处理一个请求
  • 无状态,不会保存之前的请求和响应

服务器的基本流程

启动服务器——>基于TCP的初始化(创建、绑定、监听);使用线程进入事件循环(创建线程、线程分离)——>进入线程入口——>处理请求过程

处理请求过程

1、解析请求

  • 从socket中读取出首行

HTTP请求中的换行符:把未知问题转换成已知问题,将\r和\r\n转换成\n

  1.  一个字符一个字符的从socket中读取数据
  2. 判定当前字符是不是\r
  3. 如果当前字符是\r,就尝试读取下一个字符                                                                                                                                  a)如果下一个字符是\n                                                                                                                                                            b)如果下一个字符不是\n                                                                                                                                                          针对这两种情况,都把当前字符转换成\n
  4. 如果当前字符是\n直接结束函数(这一行已经读完了)
  5. 如果当前字符是普通字符,直接追加到输出结果中
  • 解析首行,获取url和method

首行格式:GET http://www.baidu.com/index.html?aa=10&b=20 HTTP/1.1

基于HTTP协议的小型Web服务器——日期计算器_第3张图片

  • 解析url,获取url_path和query_string

url的格式:/index.html?aa=10&b=20

以?为分隔符,?前面的是url_path,?后面的是query_string

  • 解析header,丢弃大部分header,只保留content-Length

2、根据请求计算响应并写回客户端

  • 处理静态页面(GET+无参数)
  1. 根据url_path获取文件路径:若以“/”结尾的直接追加index.html                                                                                                                                             若不是以“/”结尾的利用文件属性来判断其是否是目录——若是则追加index.html
  2. 打开文件,读取文件内容,并将其写入socket中

                  (1)调用open打开文件

                  (2)构造HTTP响应报文(首行、空行),并调用send将构造号的报文发送

                  (3)读文件内容(调用stat获取文件大小)并将其写到socket中

                  (4)关闭文件

  • 处理动态页面(GET+参数或POST)
  1. 创建一对匿名管道
  2. 创建子进程fork
  3. 父进程核心流程                                                                                                                                                                         a) 如果是POST请求,把body部分的数据读出来写到管道中,剩下的动态生成页面的过程都交给子进程来完成                     b) 构造HTTP响应中的首行,header,空行                                                                                                                             c) 从管道中读取数据(子进程动态生成的页面),把这个数据也写到socket中                                                                           d) 进程等待,回收子进程的资源
  4. 子进程核心流程                                                                                                                                                                       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 
  2     
  3         
  4     
  5     
  6         
7 起始时间:
8 9
10 结束时间:
11 12

13 14
15 16
  • 先获取参数(方法、query_string、body)放入buf中
  1. 先从环境变量中获取到方法
  2. 如果是 GET 方法,就是直接从环境变量中获取到QUERY_STRING
  3. 如果是POST方法,先通过环境变量获取到 CONTENT_LENGTH ,再从标准输入中读取 body
  • 解析buf中的参数
  • 根据业务的具体需求,完成计算
  1. 判断日期是否合法——>获取当月的天数
  2. 计算日期之间间隔的天数

                (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 }
  • 把计算出的结果构造成页面返回给浏览器

你可能感兴趣的:(基于HTTP协议的小型Web服务器——日期计算器)