cpp-httplib 解析


前言

cpphttplib的简单解析


header

http常用状态码含义:

  • 2xx: 成功
  • 3xx: 重定向
  • 4xx: 客户端请求错误
  • 5xx: 服务器内部错误

request-response

client:
需要有content-length告知发送的数据量; 最好要求

POST /index.html?query=ok&query2=none HTTP/1.1\r\n
Content-Length: 4\r\n
Content-Type: text/plain\r\n
Connection: Keep-Alive\r\n
\r\n
body

server:

响应后关闭本次连接, 若Connection: close,则发送数据后,不关闭连接,等待下次请求(需有超时)

cpphttplib

只有解析请求头

阻塞式accept 单线程只能处理单个单个请求

增加header的解析加入处理的handler机制

762e7938fd2f12eac3f36151d419b7d70385648a 加入client
fgets请求行 for-fgets解析请头 根据Content-Length-fgets(buf,len, fp) 调用对应的handler 写入response 最后关闭链接
只能处理纯文本, http头少\r\n会阻塞,如果Content-Length长了会导致fgets阻塞

6062ea592bca2fcb908e31c01db382a3138bf2ec
奠定模块基础, httplib为总, 开始为 Request Response Server Client声明
namespace detail 中为 inline函数 最后为类的定义
method=POST Content-Type:application/x-www-form-urlencoded 则将body(content)解析为url的query并加入requst.params

eef74af19be78ef65479f1cf8ce39bc9d6582994
添加对静态小文件的支持, GET /filename HTTP/1.1前提得设basedir

write_socket,可发送httpbody为二进制,但line,header只能是字符,否则会出问题

17年加入openssl支持 增加封装继承动态多态特性

修复read_content, 有len,有chunk,两者都没有
Content-Length : 显示body的大小,根据这个读取内容
Transfer-Encoding : chunked
对端关闭接着write会收到SIGPIPE信号

MultipartFile存储下面解析的内容; Progress 本意为显示读取body的进度
method is “POST” and Content-Type:“application/x-www-form-urlencoded” 将body作为url的query解析放入req.params
“Content-Type”, “multipart/form-data; boundary=----WebKitFormBoundarysBREP3G013oUrLB4\r\n”
“------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=“text1”\r\n\r\ntext default\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=“text2”\r\n\r\naωb\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=“file1”; filename=“hello.txt”\r\nContent-Type: text/plain\r\n\r\nh\ne\n\nl\nl\no\n\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=“file2”; filename=“world.json”\r\nContent-Type: application/json\r\n\r\n{\n “world”, true\n}\n\r\n------WebKitFormBoundarysBREP3G013oUrLB4\r\nContent-Disposition: form-data; name=“file3”; filename=”“\r\nContent-Type: application/octet-stream\r\n\r\n\r\n------WebKitFormBoundarysBREP3G013oUrLB4–\r\n”

95b22a980a74083d78320ea18b828a884f660763
加入多线程支持,建立用户链接,将该socket丢给新创建的线程

增加Keep-Alive支持: while(count > 1 && detail::select(…) > 0) {…process_request…(keep read and write)}
Connection:close
Content-Type:
Content-Length:100

添加gzip支持
Transfer-Encoding:
Accept-Encoding: gzip gzip;q=0

getpeername getsockname getnameinfo
getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error, &len)

添加PUT DELETE OPTIONS方法
PUT用于上传文件或更新资源; DELETE用于删除服务器上的资源; OPTIONS用于获取目标资源所支持的通信选项;

服务端支持发送chunked数据,数据有用户通过std::function<string (uint64_t offset)> streamcb 生成

添加PATCH方法. 适用于只需要更新资源的某些属性或字段的场景,而不是替换整个资源

3f42804a4f1ba8169d8db1e4e792f336b304e050
进一步增加SSLClient对TLS的支持 Wildcard support for verifying server certificate. fix #87

v0.2.0添加cmake支持

v0.2.1 修复了xxx问题

9785cd47f20eed14f9cbfaa8833727f51dfaec01
添加 线程池, 可选择,自定义线程池

v0.2.4 为Client添加重定向

47bc7456d25e01a1c6b3efce71f90a42028dd9ad
添加poll支持

v0.2.5 添加CONNECT TRACE PRI 请求方法支持

Content receiver support on server

v0.2.6 添加文件上传服务器案例 在此之后基本就很难把控到每行代码了

Client添加auth功能

69a28d50f68afc7a1bdf6f25f665ee39fd11d46f
添加split.py

Client 添加使用特定网络接口的功能 getaddrinfo(interface, “0”, &hints, &result)
cli.set_interface(“eth0”); // Interface name, IP address or host name
将cliend sock与特定网口绑定,从而使用特定网口发送请求

添加 Proxy-Authenticate" : "WWW-Authenticate
eb4fcb5003a4bccb0fe8d39b23db484800089756

v0.5.7 修复regex的相关问题

v0.5.8 修复了一些问题

添加DELETE方法, Server::routing

添加url解析,通过regex

v0.5.12 Appled HANDLE_EINTR to send and select system calls

v0.6.1 5465
实现: 按照行读取line和header (如何读取?:recv(fd, &byte, 1)),读取一行解析一行. 根据header读取content,可能是一次读取recv(ptr, len)/recv(ptr, BUFSIZ)/ read_line

添加compressor类

压缩部分的代码越来越复杂,为了更好的解耦,特将该功能以类实现

v0.7.2 添加谷歌brotli压缩算法支持

v0.7.3 server添加compressor基类

v0.7.4 为客户端添加错误处理,返回 class Result {shared_ptr; Error;};

SSL_connect and SSL_accept in non-blocking mode (#728)
SSL connection is performed in two steps:
First, a regular socket connection is established.
Then, SSL_connect/SSL_accept is called to establish SSL handshake.
If a network problem occurs during the second stage, SSL_connect on the client may hang indefinitely.
The non-blocking mode solves this problem.

Add exception handler (#845)

operator"“_ replaced by operator”"_t

增强 split.py

add content_type == “application/protobuf”

MultipartFormDataParser 的性能提升

通过 constexpr 将string运行期比较转化为 编译期计算静态string hash结果从而运行期比较整形字,提高程序运行期效率

23 Jul 2022 将所有的const char* 替换为 string

添加ZOS支持

#if !defined(_AIX) && !defined(__MVS__) 
#include <ifaddrs.h> 
#endif
#ifdef __MVS__
#include <strings.h>
#ifndef NI_MAXHOST
#define NI_MAXHOST 1025
#endif

Add named path parameters parsing (Implements #1587) (#1608)

v0.15.0 Fix Issue

结构解析

有意思的trick

利用对象的生命周期

// This is based on
// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189".

struct scope_exit {
  explicit scope_exit(std::function<void(void)> &&f)
      : exit_function_(std::move(f)), execute_on_destruction_{true} {}
  scope_exit(scope_exit &&rhs) noexcept
      : exit_function_(std::move(rhs.exit_function_)),
        execute_on_destruction_{rhs.execute_on_destruction_}
  {
    rhs.release();
  }
  ~scope_exit() 
  {
    if (execute_on_destruction_) { this->exit_function_(); }
  }
  void release() { this->execute_on_destruction_ = false; }

private:
  scope_exit(const scope_exit &) = delete;
  void operator=(const scope_exit &) = delete;
  scope_exit &operator=(scope_exit &&) = delete;

  std::function<void(void)> exit_function_;
  bool execute_on_destruction_;
};

// use example
{
    auto se =  scope_exit([&]() { someclass::is_running_ = false; });
    //...
    当se的生命周期结束时会自动调用 该匿名函数
}

判断fd是否健在

inline bool is_socket_alive(socket_t sock) {
  const auto val = detail::select_read(sock, 0, 0);  // 判断 select调用 的返回值是否错误
  if (val == 0) {
    return true;
  } else if (val < 0 && errno == EBADF) {
    return false;
  }
  char buf[1];
//   MSG_PEEK
//     This flag causes the receive operation to return data from the beginning of the receive queue without
//     removing that data from the queue.  Thus, a subsequent receive call will return the same data.
  return 判断是否健在 recv(sock, buf, 1, MSG_PEEK)
}

阻塞式读写防止阻塞的方法

// 当接收缓冲区超时未收到数据后会, recv会返回
// setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

// 发送超时仅在发送缓冲区满且等待发送时没有可用空间的这段时间内触发,
// 保证阻塞操作能返回,发送缓冲区满后的最长阻塞时间
setsockopt(client_socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

listen, accept

// listen会创建两个队列,1正在进程三次握手 2握手完成连接已建立  1+2不会比5大
// 如果2队列无数据则accept阻塞,有则取出一个,获取到该连接的fd(源ip:port 目标ip:port 协议, 这5个标识唯一fd)

cpp-httplib解析

1.创建server_socket_fd

host = “0.0.0.0”, port = 8080, socket_flag = 0;

struct addrinfo hints;
struct addrinfo *result;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_protocol = IPPROTO_IP;
hints.ai_family = AF_UNSPEC = 0;
hists.ai_flags = socket_flag;

getaddrinfo(node = host.c_str(), service = to_string(port).c_str(), &hints, result);
auto se = scope_exit([&] { freeaddrinfo(result); });
for (auto rp = result; rp; rp = rp->ai_next)
{
  auto sock = socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol);
  if (sock is invalid) continue;
  setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
  if (rp->ai_family == AF_INET6) {
    setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0);
  }
//   set_socket_opt//callback  
  setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, 1);
//   try_bind_or_connnect
  ::bind(sock, rp.ai_addr, rp.ai_addrlen);
  ::listen(sock, 5);
}
于是获得 svr_sock_
上面获得的可能是ipv4也可能是ipv6
getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr), &addr_len)
ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);

2. 监听事件循环

// select
fd_set fds;
FD_ZERO(&fds);
FD_SET(server_sock, &fds);
timeval tv = {0, 0};

while true:
    // select
    rv = select(server_sock+1, &fds, nullptr = wfds, nullptr=errfds, &tv);
    // poll : poll(&pfd, 1, timeout)
    if (rv < 0 && errno == EINTR) {
        std::this_thread::sleep_for(std::chrono::microseconds{1});
        continue;
    }
    break;

client connect to server means TCP 三次握手已完成,链接已经建立,下一步可获取client_sock
socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC);
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, 5s);
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, 5s);

将关于该client sock的处理放入线程池中处理,包括读写数据,处理用户请求
该架构为 reactor 模式, client sock为阻塞式读写

然后该线程重新开始事件循环,等待新的连接

3. 处理单用户请求的函数 process_and_close_socket

该函数返回前,会关闭本次连接

4. 获取client sock的一些信息

struct sockaddr_storage addr;
getpeername(client_sock, (struct sockaddr *)(&addr), &addr_len);
getsockname(client_sock, (struct sockaddr *)(&addr), &addr_len);

5. 一些列令人窒息的callback操作拆解

inline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
                       time_t keep_alive_timeout_sec) {
  using namespace std::chrono;

  const auto interval_usec =
      CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND;

  // Avoid expensive `steady_clock::now()` call for the first time
  if (select_read(sock, 0, interval_usec) > 0) { return true; }

  const auto start = steady_clock::now() - microseconds{interval_usec};
  const auto timeout = seconds{keep_alive_timeout_sec};

  while (true) {
    if (svr_sock == INVALID_SOCKET) {
      break; // Server socket is closed
    }

    auto val = select_read(sock, 0, interval_usec);
    if (val < 0) {
      break; // Ssocket error
    } else if (val == 0) {
      if (steady_clock::now() - start > timeout) {
        break; // Timeout
      }
    } else {
      return true; // Ready for read
    }
  }

  return false;
}

bool rewrite_process_and_close_socket(socket_t sock)
{
    std::string remote_addr;
    int remote_port = 0;
    detail::get_remote_ip_and_port(sock, remote_addr, remote_port);

    std::string local_addr;
    int local_port = 0;
    detail::get_local_ip_and_port(sock, local_addr, local_port);

    auto ret = false;
    assert(keep_alive_max_count_ > 0);
    auto count = keep_alive_max_count_;
    // keep_alive在规定的时间内fd必须可读,否则关闭它
    while (count > 0 && keep_alive(svr_sock_, sock, keep_alive_timeout_sec_)) {
        auto close_connection = count == 1;
        auto connection_closed = false;
        // ssl ?
        SocketStream strm(sock, read_timeout_sec_, read_timeout_usec_,
                        write_timeout_sec_, write_timeout_usec_);
        // 包含读取并解析一次完整的http请求,
        ret = process_request(strm, remote_addr, remote_port, local_addr,
                        local_port, close_connection, connection_closed,
                        nullptr);
        if (!ret || connection_closed) { break; }
        count--;  // 处理完一次请求后-1
    }

    detail::shutdown_socket(sock);
    detail::close_socket(sock);

    return ret;
}

6. 处理单用户请求

假设用户可能一次会发送任意长度的数据,可能发送后会在任意时刻阻塞任意长的时间

在每次通过recv读取用户数据之前需要,需先通过select判断数据是否可读,可读则一次读取缓冲区大小

之后逐个字节获取并解析读取到的数据,若读取到\r\n则返回

率先读取,解析请求行,紧接着读取(以新行\r\n判定头结束)和处理请求头,然后处理解析结果: parse_request_line, detail::read_headers

如果 Connection: close 则bool connection_closed = true;该次请求结束后关闭本次连接

read_content, 获取请求body, 每次recv前需判断是否可读,以防止阻塞, 将读取到的body放入request.body中(解压后),如果发送方没按http协议来则会检测到错误

通过一些列机制调用用户业务代码

生成完整的response, 最后通过调用detail::write_data发送数据

在调用每次在调用send之前会先判断1.发送缓冲区是否可写.2.该fd是否该健在

在发送整个response时,先发送line+header,再发送body部分

至此,一次完整的用户响应完成

references

https://github.com/yhirose/cpp-httplib

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值