前言

此博客记录对于TinyWebServer项目的学习,并根据自己的理解做出些许更改。
原项目地址:https://github.com/qinguoyi/TinyWebServer

测试效果

先放一波效果图

HTTP请求

HTTP GET请求行内容。

第一行为请求行,分为三个部分:请求方法、请求地址URL和HTTP协议版本,它们之间用空格分割。
后面都是头部字段。
在所有头部字段之后,HTTP请求必须包含一个空行,以标识头部字段的结束。请求行和每个头部字段都必须以结束(回车符和换行符)﹔而空行则必须只包含一-个,不能有其他字符,甚至是空白字符。
这是解析HTTP请求的关键。
而post请求与get请求相比多了消息体,如下图所示,在处理时与get不同,需要将其中的内容提取出来。

解析HTTP请求

使用两个状态机实现,主状态机在内部调用从状态机,从状态机实现按行读取HTTP请求,如下图所示

主状态机调用从状态机读取到完整的一行请求后,按照HTTP请求的结构判断该行属于请求行还是头部字段,然后进行分别解析,直到遇到一个空行,表明得到了一个正确的http请求。

 //主状态机状态,检查请求报文中元素enum CHECK_STATE{CHECK_STATE_REQUESTLINE = 0,CHECK_STATE_HEADER,CHECK_STATE_CONTENT};//HTTP状态码enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION};//从状态机的状态,文本解析是否成功enum LINE_STATUS{LINE_OK = 0,LINE_BAD,LINE_OPEN};
//从m_read_buf读取,并处理请求报文HTTP_CODE process_read();//向m_write_buf写入响应报文数据bool process_write(HTTP_CODE ret);//主状态机解析报文中的请求行数据HTTP_CODE parse_request_line(char *text);//主状态机解析报文中的请求头数据HTTP_CODE parse_headers(char *text);//主状态机解析报文中的请求内容HTTP_CODE parse_content(char *text);//m_start_line是已经解析的字符//get_line用于将指针向后偏移,指向未处理的字符char *get_line() { return m_read_buf + m_start_line; };//从状态机读取一行LINE_STATUS parse_line();
//存储读取的请求报文数据char m_read_buf[READ_BUFFER_SIZE];//缓冲区中m_read_buf中数据的最后一个字节的下一个位置int m_read_idx;//m_read_buf读取的位置m_checked_idxint m_checked_idx;//m_read_buf中已经解析的字符个数int m_start_line;//主状态机的状态CHECK_STATE m_check_state;

主状态机逻辑

//有限状态机处理请求报文
http_conn::HTTP_CODE http_conn::process_read()
{LINE_STATUS line_status = LINE_OK;HTTP_CODE ret = NO_REQUEST;char *text = 0;//parse_line()更新m_checked_idx,从而更新textwhile ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)){//text指向m_read_buf的未处理数据的开始text = get_line();//m_checked_idx表示当前已处理的位置,这里更新m_start_line为下一个的开始m_start_line = m_checked_idx;LOG_INFO("%s", text);switch (m_check_state){//请求行case CHECK_STATE_REQUESTLINE:{ret = parse_request_line(text);if (ret == BAD_REQUEST)return BAD_REQUEST;break;}//头部字段case CHECK_STATE_HEADER:{ret = parse_headers(text);if (ret == BAD_REQUEST)return BAD_REQUEST;else if (ret == GET_REQUEST){//得到一个完整的请求就进入处理请求的程序return do_request();}break;}case CHECK_STATE_CONTENT:{ret = parse_content(text);if (ret == GET_REQUEST)return do_request();//没读完,继续读line_status = LINE_OPEN;break;}default:return INTERNAL_ERROR;}}return NO_REQUEST;
}//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{//在HTTP报文中,请求行用来说明请求类型//要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。//请求行中最先含有空格和\t任一字符的位置并返回//strpbrk在源字符串(s1)中找出最先含有搜索字符串(s2)//中任一字符的位置并返回,若找不到则返回空指针m_url = strpbrk(text, " \t");//如果没有空格或\t,则报文格式有误if (!m_url){return BAD_REQUEST;}//将该位置改为\0,用于将前面数据取出//(已读取的数据不再会匹配到\t)*m_url++ = '\0';//取出数据,并通过与GET和POST比较,以确定请求方式char *method = text;if (strcasecmp(method, "GET") == 0)m_method = GET;else if (strcasecmp(method, "POST") == 0){m_method = POST;cgi = 1;}elsereturn BAD_REQUEST;//m_url此时跳过了第一个空格或\t字符,但不知道之后是否还有//将m_url向后偏移,通过查找//继续跳过空格和\t字符,指向请求资源的第一个字符//strspn:检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标//即跳过匹配的字符串片段m_url += strspn(m_url, " \t");//相同逻辑,判断HTTP版本号m_version = strpbrk(m_url, " \t");if (!m_version)return BAD_REQUEST;*m_version++ = '\0';m_version += strspn(m_version, " \t");//仅支持HTTP/1.1if (strcasecmp(m_version, "HTTP/1.1") != 0)return BAD_REQUEST;//对请求资源前7个字符进行判断//这里主要是有些报文的请求资源中会带有http:////这里需要对这种情况进行单独处理if (strncasecmp(m_url, "http://", 7) == 0){m_url += 7;m_url = strchr(m_url, '/');}//同样增加https情况if (strncasecmp(m_url, "https://", 8) == 0){m_url += 8;m_url = strchr(m_url, '/');}//一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源if (!m_url || m_url[0] != '/')return BAD_REQUEST;//当url为/时,显示欢迎界面if (strlen(m_url) == 1)strcat(m_url, "judge.html");//请求行处理完毕,将主状态机转移处理请求头m_check_state = CHECK_STATE_HEADER;return NO_REQUEST;
}//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{//判断是空行还是请求头if (text[0] == '\0'){//判断是GET还是POST请求//!0 is POSTif (m_content_length != 0){//POST需要跳转到消息体处理状态m_check_state = CHECK_STATE_CONTENT;return NO_REQUEST;}//==0 is GETreturn GET_REQUEST;}//解析请求头部connection字段else if (strncasecmp(text, "Connection:", 11) == 0){text += 11;//跳过空格和\t字符text += strspn(text, " \t");if (strcasecmp(text, "keep-alive") == 0){//如果是长连接,则将linger标志设置为truem_linger = true;}}//解析请求头部Content-length字段else if (strncasecmp(text, "Content-length:", 15) == 0){text += 15;text += strspn(text, " \t");m_content_length = atol(text);}//解析请求头部Host字段else if (strncasecmp(text, "Host:", 5) == 0){text += 5;text += strspn(text, " \t");m_host = text;}else{LOG_INFO("oop!unknow header: %s", text);}return NO_REQUEST;
}//判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text)
{if (m_read_idx >= (m_content_length + m_checked_idx)){text[m_content_length] = '\0';//POST请求中最后为输入的用户名和密码m_string = text;return GET_REQUEST;}return NO_REQUEST;
}

从状态机

//从状态机,用于分析出一行内容
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{char temp;for (; m_checked_idx < m_read_idx; ++m_checked_idx){temp = m_read_buf[m_checked_idx];if (temp == '\r'){if ((m_checked_idx + 1) == m_read_idx)return LINE_OPEN;else if (m_read_buf[m_checked_idx + 1] == '\n'){m_read_buf[m_checked_idx++] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}return LINE_BAD;}else if (temp == '\n'){if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r'){m_read_buf[m_checked_idx - 1] = '\0';m_read_buf[m_checked_idx++] = '\0';return LINE_OK;}return LINE_BAD;}}return LINE_OPEN;
}

HTTP响应报文

响应报文结构

在linux系统中可以用curl命令测试,可以看到响应报文的前两部分,如下图所示。

生成响应报文

前面解析完成一个请求报文后,会调用do_request()函数,根据请求报文的解析结果,判断请求资源是否存在、请求动作是否正确等,将相关文件映射到内存,以便后续直接从内存中读取,填充到响应报文返回给浏览器,浏览器解析响应报文后,就生成了我们所看到的网页界面。
这里使用了IO向量机制,IO向量即struct iovec,iovec结构体中可以存放多个buffer,使用对应的readv和writev读写文件,可以按顺序读到多个buffer中,以及将多个buffer的内容写到文件描述符。
此处m_iv[0]指向m_write_buf(存放响应报文头部内容),m_iv[1]指向m_file_address(使用mmap将请求资源映射到的内存),然后可以使用writev按顺序将其写到socket中。

//添加响应报文的公共函数
bool http_conn::add_response(const char *format, ...)
{//如果写入内容超出m_write_buf大小则报错if (m_write_idx >= WRITE_BUFFER_SIZE)return false;//定义可变参数列表va_list arg_list;//将变量arg_list初始化为传入参数va_start(arg_list, format);//将数据format从可变参数列表写入缓冲区写,返回写入数据的长度int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);//如果写入的数据长度超过缓冲区剩余空间,则报错if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)){va_end(arg_list);return false;}//更新m_write_idx位置m_write_idx += len;//清空可变参列表va_end(arg_list);LOG_INFO("request:%s", m_write_buf);return true;
}//添加状态行
bool http_conn::add_status_line(int status, const char *title)
{return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}//添加消息报头,具体的添加文本长度、连接状态和空行
bool http_conn::add_headers(int content_len)
{return add_content_length(content_len) && add_linger() &&add_blank_line();
}//添加Content-Length,表示响应报文的长度
bool http_conn::add_content_length(int content_len)
{return add_response("Content-Length:%d\r\n", content_len);
}//添加文本类型,这里是html
bool http_conn::add_content_type()
{return add_response("Content-Type:%s\r\n", "text/html");
}//添加连接状态,通知浏览器端是保持连接还是关闭
bool http_conn::add_linger()
{return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}//添加空行
bool http_conn::add_blank_line()
{return add_response("%s", "\r\n");
}//添加文本content
bool http_conn::add_content(const char *content)
{return add_response("%s", content);
}//生成响应报文
bool http_conn::process_write(HTTP_CODE ret)
{switch (ret){case INTERNAL_ERROR:{add_status_line(500, error_500_title);add_headers(strlen(error_500_form));if (!add_content(error_500_form))return false;break;}case BAD_REQUEST:{add_status_line(404, error_404_title);add_headers(strlen(error_404_form));if (!add_content(error_404_form))return false;break;}case FORBIDDEN_REQUEST:{add_status_line(403, error_403_title);add_headers(strlen(error_403_form));if (!add_content(error_403_form))return false;break;}case FILE_REQUEST:{add_status_line(200, ok_200_title);if (m_file_stat.st_size != 0){add_headers(m_file_stat.st_size);m_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;m_iv[1].iov_base = m_file_address;m_iv[1].iov_len = m_file_stat.st_size;m_iv_count = 2;bytes_to_send = m_write_idx + m_file_stat.st_size;return true;}else{const char *ok_string = "<html><body></body></html>";add_headers(strlen(ok_string));if (!add_content(ok_string))return false;}}default:return false;}m_iv[0].iov_base = m_write_buf;m_iv[0].iov_len = m_write_idx;m_iv_count = 1;bytes_to_send = m_write_idx;return true;
}

处理报文请求与报文响应的逻辑

//处理http报文请求与报文响应
void http_conn::process()
{//NO_REQUEST,表示请求不完整,需要继续接收请求数据HTTP_CODE read_ret = process_read();if (read_ret == NO_REQUEST){//注册并监听读事件modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);return;}//调用process_write完成报文响应bool write_ret = process_write(read_ret);if (!write_ret){close_conn();}//注册并监听写事件modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}

使用writev发送响应报文,外部调用接口

//发送响应报文
bool http_conn::write()
{int temp = 0;//表示响应报文为空,一般不会出现这种情况if (bytes_to_send == 0){modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);init();return true;}while (1){//将响应报文的状态行、消息头、空行和响应正文发送给浏览器端temp = writev(m_sockfd, m_iv, m_iv_count);//errorif (temp < 0){//判断缓冲区是否满了if (errno == EAGAIN){modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);return true;}unmap();return false;}//更新已发送字节bytes_have_send += temp;//更新wei发送字节bytes_to_send -= temp;//第一个iovec头部信息的数据已发送完,发送第二个iovec数据if (bytes_have_send >= m_iv[0].iov_len){//不再继续发送头部信息m_iv[0].iov_len = 0;//发送完m_write_buf信息后,此处将mmap映射到内存的数据发送m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);m_iv[1].iov_len = bytes_to_send;}//继续发送第一个iovec头部信息的数据else{//m_write_buf为生成的响应报文缓存区,不包含请求文件内容m_iv[0].iov_base = m_write_buf + bytes_have_send;m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;}//判断条件,数据已全部发送完if (bytes_to_send <= 0){//如果发送失败,但不是缓冲区问题,取消映射unmap();//重新注册写事件modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);//浏览器的请求为长连接if (m_linger){//重新初始化HTTP对象**init();return true;}else{return false;}}}
}

测试

此处测试的目的仅仅是测试该部分是否能正常工作,所以不使用mysql部分功能。
测试逻辑,初始化TCP,注册epoll,保存连接用户的地址作为http_conn对象初始化的用户地址,当有可读事件时,初始化http_conn对象,然后调用read_once()读取用户请求,调用process()生成响应报文,调用write()发送响应报文以及请求的资源。

int main()
{Log::get_instance()->init("./ServerLog", 0, 2000, 800000, 0);const char* ip = "0.0.0.0";int port = 9006;sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd > 0);int ret = bind(listenfd, (sockaddr*)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);map<int, sockaddr_in> client_map;//epoll创建内核事件表epoll_event events[1024];int m_epollfd = epoll_create(5);assert(m_epollfd != -1);addfd(m_epollfd, listenfd, false, 0);//简单测试,只有一个用户连接,所以只定义了一个http_conn对象//没有初始化mysql连接,先不涉及mysql部分功能http_conn http_tmp;http_conn::m_epollfd = m_epollfd;while(1){ret = epoll_wait(m_epollfd, events, 1024, -1);if(ret < 0){printf("epoll failure\n");break;}for(int i = 0; i < ret; ++i){int sockfd = events[i].data.fd;if(sockfd == listenfd){printf("有新连接发生\n");sockaddr_in client_address;socklen_t client_addrLen = sizeof(client_address);int connfd = accept(listenfd, (sockaddr*)&client_address, &client_addrLen);addfd(m_epollfd, connfd, false, 0);client_map[connfd] = client_address; }else if(events[i].events & EPOLLIN){printf("有可读事件发生\n");//root文件夹路径char server_path[200];//获取当前工作路径getcwd(server_path, 200);char root[6] = "/root";char *m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);strcpy(m_root, server_path);strcat(m_root, root);string user = "root";string passwd = "ubuntu";string databasename = "db_test";//LT模式http_tmp.init(sockfd, client_map[sockfd], m_root, 0, 0, user, passwd, databasename);//读取请求if(http_tmp.read_once()){printf("读取成功\n");}else{//已读完close(sockfd);continue;}//处理请求,生成响应报文http_tmp.process();//发送响应报文if(http_tmp.write()){printf("发送成功\n");}}else{printf("something else happened\n");}}}http_tmp.~http_conn();//struct linger tmp = {0, 1};setsockopt(listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));close(listenfd);close(m_epollfd);return 0;
}

这里在do_request()中增加了打印请求的资源,方便观察传输的资源。

//功能逻辑单元
http_conn::HTTP_CODE http_conn::do_request()
{strcpy(m_real_file, doc_root);int len = strlen(doc_root);//打印请求的资源名printf("m_url:%s\n", m_url);const char *p = strrchr(m_url, '/');

效果图

终端显示资源访问情况

个人博客

个人博客链接

webserver之处理HTTP请求相关推荐

  1. tomcat请求数据的编码设置

    tomcat5中,为了保证get数据采用UTF8编码,在server.xml中进行了如下设置: <Connector port="8080" maxThreads=" ...

  2. [Node.js]get/post请求

    摘要 在很多情况下,我们的web服务器都需要接受客户端浏览器传递的参数或者数据.最常见的是get和post请求. 获取get请求的内容 get请求传递的参数在url中,参数部分在?后面.因此可以手动解 ...

  3. Ajax 调用webservice 解决跨域请求和发布到服务器后本地调用成功外网失败的问题...

    webservice 代码 1 /// <summary> 2 /// MESService 的摘要说明 3 /// </summary> 4 [WebService(Name ...

  4. 第四节:跨域请求的解决方案和WebApi特有的处理方式

    一. 简介 前言: 跨域问题发生在Javascript发起Ajax调用,其根本原因是因为浏览器对于这种请求,所给予的权限是较低的,通常只允许调用本域中的资源, 除非目标服务器明确地告知它允许跨域调用. ...

  5. 第十八节:跨域请求的解决方案和WebApi特有的处理方式

    一. 简介 前言: 跨域问题发生在Javascript发起Ajax调用,其根本原因是因为浏览器对于这种请求,所给予的权限是较低的,通常只允许调用本域中的资源, 除非目标服务器明确地告知它允许跨域调用. ...

  6. 动手做webserver的核心之http解析

    简介 webserver往小里说核心功能就是socket管理.url处理.http协议处理.业务dll管理等:下面简介绍一下http协议:超文本传输协议(HTTP)是一种通信协议,当时就是为web传输 ...

  7. Servlet生命周期与Web容器架构及处理请求详解

    一.Servlet对象 Servlet对象运行在Servlet容器中,Servlet的生命周期由容器管理(Servlet容器的基本思想是在服务器端使用Java来动态生成网页). (一)Servlet对 ...

  8. Net5上传文件请求大小限制设置(默认最大只能上传28.61m)

    在IIS上配置文件上传上限(在vs下使用IIS Express 调试时也可生效) 第一步:在项目工程下创建web.config,设置文件属性[复制到输出目录]为始终复制或较新复制 <?xml v ...

  9. squid 优化指南

    很多squid 优化只限于在 squid 参数和系统参数上面的调整.但是这个实在只是细枝末节的事情,只要不是太弱智的配置导致无法缓存,squid的性能不会有太大差距,也就提高10%左右,只有实际的业务 ...

最新文章

  1. linux常用文本编辑器nano/vi/vim
  2. pytorch安装教程(Windows版本)
  3. python变量定义类型_03_python的数据类型和变量的定义及使用
  4. 浮动元素与兄弟之间的关系 速记 1211
  5. 子弹短信 android,子弹短信精简版
  6. loinc编码_通用检验编码系统(Universal Laboratory Test Code System)于香港电子健康纪录计划的应用...
  7. python得安什么安装包_初学 Python 需要安装哪些软件?
  8. 3分钟tips:协方差和相关系数的异同
  9. the JAR file spring-beans-4.0.0.RELEASE.jar has no source attachment
  10. The proxy server is refusing connections 怎么解决呢?
  11. VGG16和VGG19
  12. OFDM子载波频率 知乎_SDR (软件无线电)
  13. 基于Pytorch实现的声音分类
  14. matlab模拟正态分布曲线拟合,[MATLAB数学相关] 急请教:正态分布曲线拟合问题
  15. 炉石兄弟 修复图腾师问题 by大神beebee102, 还有阴燃电鳗
  16. 【数据库】union和union all合并结果操作
  17. matlab sparse
  18. 每日学习 与 每日未知
  19. Unity之Shader基础探索
  20. 子网掩码,CIDR前缀法表示掩码

热门文章

  1. 架构设计:服务自动化部署和管理流程
  2. Xshell远程访问工具及epel-release包安装
  3. c# maiform父窗体改变动态的gridew 奇偶行变色的快捷方法
  4. 基于JS实现回到页面顶部的五种写法(从实现到增强)
  5. ORACLE 11G负载均衡测试
  6. Spring Boot使用MyBatis 3打印SQL的配置
  7. ES6 import export
  8. php之数据类型自动转换
  9. Hadoop学习总结:Map-Reduce入门
  10. redhat9.0配置apache 出现乱码