TinyWebServer源码阅读(二)

姬念
2023-12-01

本篇文章主要介绍HttpBuffer部分
Http请求和响应部分,其主要包含三个类httpConn, httpRequest, httpResponse
Buffer解决的是并发情况下的读写问题

1. http请求和响应报文

首先让我们熟悉一下请求(Request)和响应(Response)报文,这两种报文在WebServer服务器中分别对应httpRequesthttpResponse类进行处理。

1.1 http请求报文

HTTP请求报文由四部分组成:

  • 请求行
  • 请求头部
  • 空行
  • 请求数据

请求分为两种,GET和POST,让我们来看一个例子
GET

(请求行部分)
GET /search?hl=zh-CN&source=hp&q=domety&aq=f&oq= HTTP/1.1  
(请求头部分)
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */*  
Referer: <a href="http://www.google.cn/">http://www.google.cn/</a>  
Accept-Language: zh-cn  
Accept-Encoding: gzip, deflate  
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)  
Host: <a href="http://www.google.cn">www.google.cn</a>  
Connection: Keep-Alive  
Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-FxlRugatx63JLv7CWMD6UB_O_r
(空行)
(请求数据)

POST

POST / HTTP1.1
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley

1.2 http响应报文

http响应报文由四部分组成

  • 状态行
  • 消息报头
  • 空行
  • 响应正文
    一个例子如下
(状态行)
HTTP/1.1 200 OK
(消息报头)
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 122
(空行)
(响应正文)
<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>

2 httpConn

httpConn类处理的是和http连接相关的问题,它用于处理和client的连接
其成员变量有

    static bool isET; // 是否是边缘触发
    static const char* srcDir; //根路径
    static std::atomic<int> userCount; //总用户数

    int fd_; //文件描述符
    struct  sockaddr_in addr_; //连接时的socket地址
    
    bool isClose_; //是否关闭
    
    int iovCnt_; //供readv/writev使用,表示缓冲区的个数
    struct iovec iov_[2]; //供readv/writev使用,表示缓冲区地址(iov_base)和长度(iov_len)的结构体
    
    Buffer readBuff_; // 读缓冲区
    Buffer writeBuff_; // 写缓冲区

    HttpRequest request_; // httpRequest对象
    HttpResponse response_; // httpResponse对象

从成员变量中就可以看出,httpConn就是http连接的一个表示,因为成员函数就显而易见了

  • init/close: 初始化httpConn和关闭文件
  • read/write:读取数据到readBuff中/将writeBuff中写出
  • process: 解析readBuff,将相应的response写到writeBuff
  • IsKeepAlive: 是否还保持连接,通过request_isKeepAlive进行判断
  • get相关的方法: 获得地址/端口号等等

2.1 initclose

init: 初始化成员变量和静态变量,例如userCount++,设置fd, addr,初始化writeBuffreadBuff等等
close: 解除response_建立的文件映射,userCount--,关闭fd,设置isClosed,在析构函数中会调用close

2.2 read

ssize_t HttpConn::read(int* saveErrno) {
    ssize_t len = -1;
    do {
        len = readBuff_.ReadFd(fd_, saveErrno);
        if (len <= 0) {
            break;
        }
    } while (isET);
    return len;
}

2.3 write

fd注册EPOLLIN事件时,当TCP读缓冲区有数据到达时就会触发EPOLLIN事件。当fd注册EPOLLOUT事件时,当TCP写缓冲区有剩余空间时就会触发EPOLLOUT事件,此时DealWrite就是处理EPOLLOUT事件。
这里有一个问题是,为什么要放到循环中?

查看writev 的用法有提到
When using non-blocking I/O on objects, such as sockets, that are subject to flow control, write() and writev() may write fewer bytes than requested; the return value must be noted, and the remainder of the operation should be retried when possible

writev是分散写,也就是你的数据可以这里一块,那里一块,然后只要将这些数据的首地址,长度什么的写到一个iovc的结构体数组里,传递给writev,writev就帮你来写这些数据,在你看来,分散的数据就像是连成了一体。但是在非阻塞IO的情况下,如果writev返回一个大于0的值num,这个值又小于所有要传递的文件块的总长度,这意味着什么,意味着数据还没有写完。如果你还想写的话,你下一次调用writev的时候要重新整理iovc数组

ssize_t HttpConn::write(int* saveErrno) {
    ssize_t len = -1;
    do {
        len = writev(fd_, iov_, iovCnt_);
        if(len <= 0) {
            *saveErrno = errno;
            break;
        }
        if(iov_[0].iov_len + iov_[1].iov_len  == 0) { break; } /* 传输结束 */
        else if(static_cast<size_t>(len) > iov_[0].iov_len) {
            iov_[1].iov_base = (uint8_t*) iov_[1].iov_base + (len - iov_[0].iov_len);
            iov_[1].iov_len -= (len - iov_[0].iov_len);
            if(iov_[0].iov_len) {
                writeBuff_.RetrieveAll();
                iov_[0].iov_len = 0;
            }
        }
        else {
            iov_[0].iov_base = (uint8_t*)iov_[0].iov_base + len; 
            iov_[0].iov_len -= len; 
            writeBuff_.Retrieve(len);
        }
    } while(isET || ToWriteBytes() > 10240);
    return len;
}

2.4 process函数

  • 初始化request_: request_.init()
  • 解析readBuff_: request.parse(readBuff_),同时用解析的内容初始化response_
  • 生成回应内容: response_.makeResponse()
  • 更新ioviov_cnt: iov[0]存储的是响应头,iov[1]存储的是响应文件response_.File()

3 httpRequest

我们仍旧从成员变量开始分析

    PARSE_STATE state_; // 状态
    std::string method_, path_, version_, body_; //方法,路径,版本,请求体
    std::unordered_map<std::string, std::string> header_; //请求头
    std::unordered_map<std::string, std::string> post_; //post的内容

    static const std::unordered_set<std::string> DEFAULT_HTML;
    static const std::unordered_map<std::string, int> DEFAULT_HTML_TAG;

可以看出httpRequest是对socket解析到的http状态的解析和保存。最主要的是parse函数,剩下的就约等于字符串的处理,将处理的字符串保存在成员变量中。

3.1 parse函数

其利用有限状态机模型,每次解析一行,从已知状态跳转到下一个状态,可以参考1.1中的http请求报文

bool HttpRequest::parse(Buffer& buff) {
   const char CRLF[] = "\r\n";
   if(buff.ReadableBytes() <= 0) {
       return false;
   }
   while(buff.ReadableBytes() && state_ != FINISH) {
       const char* lineEnd = search(buff.Peek(), buff.BeginWriteConst(), CRLF, CRLF + 2);
       std::string line(buff.Peek(), lineEnd);
       switch(state_)
       {
       case REQUEST_LINE:
           if(!ParseRequestLine_(line)) {
               return false;
           }
           ParsePath_(); // state在其中会被置为HEADERS
           break;    
       case HEADERS:
           ParseHeader_(line); // state被置为BODY
           if(buff.ReadableBytes() <= 2) {
               state_ = FINISH;
           }
           break;
       case BODY:
           ParseBody_(line);
           break;
       default:
           break;
       }
       if(lineEnd == buff.BeginWrite()) { break; }
       buff.RetrieveUntil(lineEnd + 2);
   }
   LOG_DEBUG("[%s], [%s], [%s]", method_.c_str(), path_.c_str(), version_.c_str());
   return true;
}

3.2 parseHeader

直接采用regex字符串匹配,将陪陪的字符串保存在header

void HttpRequest::ParseHeader_(const string& line) {
    regex patten("^([^:]*): ?(.*)$");
    smatch subMatch;
    if(regex_match(line, subMatch, patten)) {
        header_[subMatch[1]] = subMatch[2];
    }
    else {
        state_ = BODY;
    }
}

4 httpResponse

成员变量如下

    int code_; // status code
    bool isKeepAlive_;

    std::string path_;
    std::string srcDir_;
    
    char* mmFile_; //
    struct stat mmFileStat_;

4.1 init

采用response_.parse()获得的src, path, code, keepalive进行初始化

void HttpResponse::Init(const string& srcDir, string& path, bool isKeepAlive, int code){
    assert(srcDir != "");
    if(mmFile_) { UnmapFile(); }
    code_ = code;
    isKeepAlive_ = isKeepAlive;
    path_ = path;
    srcDir_ = srcDir;
    mmFile_ = nullptr; 
    mmFileStat_ = { 0 };
}

4.2 makeResponse

void HttpResponse::MakeResponse(Buffer& buff) {
    /* 判断请求的资源文件 */
    if(stat((srcDir_ + path_).data(), &mmFileStat_) < 0 || S_ISDIR(mmFileStat_.st_mode)) {
        code_ = 404;
    }
    else if(!(mmFileStat_.st_mode & S_IROTH)) {
        code_ = 403;
    }
    else if(code_ == -1) { 
        code_ = 200; 
    }
    ErrorHtml_();
    AddStateLine_(buff);
    AddHeader_(buff);
    AddContent_(buff);
}

stat用法如下

int stat(const char *path, struct stat *buf);
stat() function is used to list properties of a file identified by path. It reads all file properties and dumps to buf structure. The function is defined in sys/stat.h header file.
return 0 when success, -1 when unable to get file properties.

这里先判断是否能获得文件属性,

  • 不能获得文件属性 or 文件属性为目录 -> code_ = 404
  • 其他组读权限 -> code_ = 403
  • 没有异常情况 -> code_ = 200

之后根据code_errorHtml,在这一步中如果根据错误码把mmFileStat_设置为对应的界面,如果code正常的话,其实这步会被跳过

调用AddStateLine_添加状态行

调用AddHeader_加入消息头

调用AddContent_添加消息内容

Buffer

buffer主要管理的是字符串的在并发场景下的存储和读取,从buffer的成员变量来看,buffer的组成是非常简单的

    std::vector<char> buffer_;
    std::atomic<std::size_t> readPos_;
    std::atomic<std::size_t> writePos_;
  • 一个可扩容的vector
  • atomic类型的readPos_writePos_组成,readPos_表示读操作应该开始的位置,writePos_表示写操作应该开始的位置,readPos_writePos_之间的区域则是写入了但还没有读的区域。

主要方法包括:

  • retrieve(size_t len): 将readPos_向后移动len
  • retrieveAll(): readPos_writePos都置为0
  • RetrieveAllToStr(): 将readPos_writePos读入到string
  • MakeSpace(int len): 判断能写入的区域是否能容纳下写入的长度,不能的话就resize一下buffer,否则把待写的内容移动到buffer的最前端
  • Append(const char *str, size_t len): 先确保空间可用,不够就扩容,之后用copy函数将数据写入到buffer中,更新writePos_
  • ReadFd(int fd, int *saveErrno): 使用readv函数从fd中读取内容到buffer
  • WriteFd(int fd, int *saveErrno): 使用write函数将buffer内容写到fd
 类似资料: