http服务设计
对于静态页面服务器来说,其工作流程如下:
- 接收客户端消息
- 解析出http请求报文
- 业务逻辑,拼装响应报文
- 发送给客户端结果
http连接类
-
设计目标:
- 将客户端唯一文件描述符封装,表示成一个连接实体.
- 从socket读报文–>buffer存储
- 给socket写报文–>buffer存储
- 解析逻辑-->进一步划分給子类去做 requests类
- 响应逻辑-->进一步划分給子类去做 response类
-
头文件
#ifndef HTTP_CONN_H
#define HTTP_CONN_H
#include <sys/types.h>
#include <sys/uio.h> // readv/writev
#include <arpa/inet.h> // sockaddr_in
#include <stdlib.h> // atoi()
#include <errno.h>
#include "../log/log.h"
#include "../buffer/buffer.h"
#include "httprequest.h"
#include "httpresponse.h"
class HttpConn {
public:
HttpConn();
~HttpConn();
void init(int sockFd, const sockaddr_in& addr);
ssize_t read(int* saveErrno);
ssize_t write(int* saveErrno);
void Close();
int GetFd() const;
int GetPort() const;
const char* GetIP() const;
sockaddr_in GetAddr() const;
bool process();
int ToWriteBytes() {
return iov_[0].iov_len + iov_[1].iov_len;
}
bool IsKeepAlive() const {
return request_.IsKeepAlive();
}
static bool isET;
static const char* srcDir;
static std::atomic<int> userCount;
private:
int fd_;
struct sockaddr_in addr_;
bool isClose_;
int iovCnt_;
struct iovec iov_[2];
Buffer readBuff_; // 读缓冲区
Buffer writeBuff_; // 写缓冲区
HttpRequest request_;
HttpResponse response_;
};
#endif //HTTP_CONN_H
- 源文件
#include "httpconn.h"
using namespace std;
const char* HttpConn::srcDir;
std::atomic<int> HttpConn::userCount;
bool HttpConn::isET;
HttpConn::HttpConn() {
fd_ = -1;
addr_ = { 0 };
isClose_ = true;
};
HttpConn::~HttpConn() {
Close();
};
void HttpConn::init(int fd, const sockaddr_in& addr) {
assert(fd > 0);
userCount++;
addr_ = addr;
fd_ = fd;
writeBuff_.RetrieveAll();
readBuff_.RetrieveAll();
isClose_ = false;
LOG_INFO("Client[%d](%s:%d) in, userCount:%d", fd_, GetIP(), GetPort(), (int)userCount);
}
void HttpConn::Close() {
response_.UnmapFile();
if(isClose_ == false){
isClose_ = true;
userCount--;
close(fd_);
LOG_INFO("Client[%d](%s:%d) quit, UserCount:%d", fd_, GetIP(), GetPort(), (int)userCount);
}
}
int HttpConn::GetFd() const {
return fd_;
};
struct sockaddr_in HttpConn::GetAddr() const {
return addr_;
}
const char* HttpConn::GetIP() const {
return inet_ntoa(addr_.sin_addr);
}
int HttpConn::GetPort() const {
return addr_.sin_port;
}
ssize_t HttpConn::read(int* saveErrno) {
ssize_t len = -1;
do {
len = readBuff_.ReadFd(fd_, saveErrno);
if (len <= 0) {
break;
}
} while (isET);
return len;
}
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;
}
bool HttpConn::process() {
request_.Init();
if(readBuff_.ReadableBytes() <= 0) {
return false;
}
else if(request_.parse(readBuff_)) {
LOG_DEBUG("%s", request_.path().c_str());
response_.Init(srcDir, request_.path(), request_.IsKeepAlive(), 200);
} else {
response_.Init(srcDir, request_.path(), false, 400);
}
response_.MakeResponse(writeBuff_);
/* 响应头 */
iov_[0].iov_base = const_cast<char*>(writeBuff_.Peek());
iov_[0].iov_len = writeBuff_.ReadableBytes();
iovCnt_ = 1;
/* 文件 */
if(response_.FileLen() > 0 && response_.File()) {
iov_[1].iov_base = response_.File();
iov_[1].iov_len = response_.FileLen();
iovCnt_ = 2;
}
LOG_DEBUG("filesize:%d, %d to %d", response_.FileLen() , iovCnt_, ToWriteBytes());
return true;
}
http request类
-
设计目标:解析请求报文
-
设计方案:正则表达式匹配+有限状态机
-
头文件
#ifndef HTTP_REQUEST_H
#define HTTP_REQUEST_H
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <regex>
#include <errno.h>
#include "../buffer/buffer.h"
#include "../log/log.h"
class HttpRequest {
public:
enum PARSE_STATE {
REQUEST_LINE,
HEADERS,
BODY,
FINISH,
};
enum HTTP_CODE {
NO_REQUEST = 0,
GET_REQUEST,
BAD_REQUEST,
NO_RESOURSE,
FORBIDDENT_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR,
CLOSED_CONNECTION,
};
HttpRequest() { Init(); }
~HttpRequest() = default;
void Init();
bool parse(Buffer& buff);
std::string path() const;
std::string& path();
std::string method() const;
std::string version() const;
std::string GetPost(const std::string& key) const;
std::string GetPost(const char* key) const;
bool IsKeepAlive() const;
/*
todo
void HttpConn::ParseFormData() {}
void HttpConn::ParseJson() {}
*/
private:
bool ParseRequestLine_(const std::string& line);
void ParseHeader_(const std::string& line);
void ParseBody_(const std::string& line);
void ParsePath_();
void ParsePost_();
void ParseFromUrlencoded_();
// static bool UserVerify(const std::string& name, const std::string& pwd, bool isLogin);
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_;
static const std::unordered_set<std::string> DEFAULT_HTML;
static const std::unordered_map<std::string, int> DEFAULT_HTML_TAG;
static int ConverHex(char ch);
};
#endif //HTTP_REQUEST_H
- 源文件
#include "httprequest.h"
using namespace std;
const unordered_set<string> HttpRequest::DEFAULT_HTML{
"/index", "/register", "/login",
"/welcome", "/video", "/picture", };
const unordered_map<string, int> HttpRequest::DEFAULT_HTML_TAG {
{"/register.html", 0}, {"/login.html", 1}, };
void HttpRequest::Init() {
method_ = path_ = version_ = body_ = "";
state_ = REQUEST_LINE;
header_.clear();
post_.clear();
}
bool HttpRequest::IsKeepAlive() const {
if(header_.count("Connection") == 1) {
return header_.find("Connection")->second == "keep-alive" && version_ == "1.1";
}
return false;
}
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_();
break;
case HEADERS:
ParseHeader_(line);
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;
}
void HttpRequest::ParsePath_() {
if(path_ == "/") {
path_ = "/index.html";
}
else {
for(auto &item: DEFAULT_HTML) {
if(item == path_) {
path_ += ".html";
break;
}
}
}
}
bool HttpRequest::ParseRequestLine_(const string& line) {
regex patten("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");
smatch subMatch;
if(regex_match(line, subMatch, patten)) {
method_ = subMatch[1];
path_ = subMatch[2];
version_ = subMatch[3];
state_ = HEADERS;
return true;
}
LOG_ERROR("RequestLine Error");
return false;
}
void HttpRequest::ParseHeader_(const string& line) {
regex patten("^([^:]*): ?(.*)$");
smatch subMatch;
if(regex_match(line, subMatch, patten)) {
header_[subMatch[1]] = subMatch[2];
}
else {
state_ = BODY;
}
}
void HttpRequest::ParseBody_(const string& line) {
body_ = line;
ParsePost_();
state_ = FINISH;
LOG_DEBUG("Body:%s, len:%d", line.c_str(), line.size());
}
int HttpRequest::ConverHex(char ch) {
if(ch >= 'A' && ch <= 'F') return ch -'A' + 10;
if(ch >= 'a' && ch <= 'f') return ch -'a' + 10;
return ch;
}
void HttpRequest::ParsePost_() {
if(method_ == "POST" && header_["Content-Type"] == "application/x-www-form-urlencoded") {
ParseFromUrlencoded_();
if(DEFAULT_HTML_TAG.count(path_)) {
int tag = DEFAULT_HTML_TAG.find(path_)->second;
LOG_DEBUG("Tag:%d", tag);
if(tag == 0 || tag == 1) {
path_ = "/welcome.html";
}
}
}
}
void HttpRequest::ParseFromUrlencoded_() {
if(body_.size() == 0) { return; }
string key, value;
int num = 0;
int n = body_.size();
int i = 0, j = 0;
for(; i < n; i++) {
char ch = body_[i];
switch (ch) {
case '=':
key = body_.substr(j, i - j);
j = i + 1;
break;
case '+':
body_[i] = ' ';
break;
case '%':
num = ConverHex(body_[i + 1]) * 16 + ConverHex(body_[i + 2]);
body_[i + 2] = num % 10 + '0';
body_[i + 1] = num / 10 + '0';
i += 2;
break;
case '&':
value = body_.substr(j, i - j);
j = i + 1;
post_[key] = value;
LOG_DEBUG("%s = %s", key.c_str(), value.c_str());
break;
default:
break;
}
}
assert(j <= i);
if(post_.count(key) == 0 && j < i) {
value = body_.substr(j, i - j);
post_[key] = value;
}
}
std::string HttpRequest::path() const{
return path_;
}
std::string& HttpRequest::path(){
return path_;
}
std::string HttpRequest::method() const {
return method_;
}
std::string HttpRequest::version() const {
return version_;
}
std::string HttpRequest::GetPost(const std::string& key) const {
assert(key != "");
if(post_.count(key) == 1) {
return post_.find(key)->second;
}
return "";
}
std::string HttpRequest::GetPost(const char* key) const {
assert(key != nullptr);
if(post_.count(key) == 1) {
return post_.find(key)->second;
}
return "";
}
http response类
-
设计目标:根据业务逻辑设计响应报文,对于静态服务器来说,仅需要解析请求资源路径,返回相对应的资源即可
-
头文件
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H
#include <unordered_map>
#include <fcntl.h> // open
#include <unistd.h> // close
#include <sys/stat.h> // stat
#include <sys/mman.h> // mmap, munmap
#include "../buffer/buffer.h"
#include "../log/log.h"
class HttpResponse {
public:
HttpResponse();
~HttpResponse();
void Init(const std::string& srcDir, std::string& path, bool isKeepAlive = false, int code = -1);
void MakeResponse(Buffer& buff);
void UnmapFile();
char* File();
size_t FileLen() const;
void ErrorContent(Buffer& buff, std::string message);
int Code() const { return code_; }
private:
void AddStateLine_(Buffer &buff);
void AddHeader_(Buffer &buff);
void AddContent_(Buffer &buff);
void ErrorHtml_();
std::string GetFileType_();
int code_;
bool isKeepAlive_;
std::string path_;
std::string srcDir_;
char* mmFile_;
struct stat mmFileStat_;
static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;
static const std::unordered_map<int, std::string> CODE_STATUS;
static const std::unordered_map<int, std::string> CODE_PATH;
};
#endif //HTTP_RESPONSE_H
- 源文件
#include "httpresponse.h"
using namespace std;
const unordered_map<string, string> HttpResponse::SUFFIX_TYPE = {
{ ".html", "text/html" },
{ ".xml", "text/xml" },
{ ".xhtml", "application/xhtml+xml" },
{ ".txt", "text/plain" },
{ ".rtf", "application/rtf" },
{ ".pdf", "application/pdf" },
{ ".word", "application/nsword" },
{ ".png", "image/png" },
{ ".gif", "image/gif" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".au", "audio/basic" },
{ ".mpeg", "video/mpeg" },
{ ".mpg", "video/mpeg" },
{ ".avi", "video/x-msvideo" },
{ ".gz", "application/x-gzip" },
{ ".tar", "application/x-tar" },
{ ".css", "text/css "},
{ ".js", "text/javascript "},
};
const unordered_map<int, string> HttpResponse::CODE_STATUS = {
{ 200, "OK" },
{ 400, "Bad Request" },
{ 403, "Forbidden" },
{ 404, "Not Found" },
};
const unordered_map<int, string> HttpResponse::CODE_PATH = {
{ 400, "/400.html" },
{ 403, "/403.html" },
{ 404, "/404.html" },
};
HttpResponse::HttpResponse() {
code_ = -1;
path_ = srcDir_ = "";
isKeepAlive_ = false;
mmFile_ = nullptr;
mmFileStat_ = { 0 };
};
HttpResponse::~HttpResponse() {
UnmapFile();
}
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 };
}
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);
}
char* HttpResponse::File() {
return mmFile_;
}
size_t HttpResponse::FileLen() const {
return mmFileStat_.st_size;
}
void HttpResponse::ErrorHtml_() {
if(CODE_PATH.count(code_) == 1) {
path_ = CODE_PATH.find(code_)->second;
stat((srcDir_ + path_).data(), &mmFileStat_);
}
}
void HttpResponse::AddStateLine_(Buffer& buff) {
string status;
if(CODE_STATUS.count(code_) == 1) {
status = CODE_STATUS.find(code_)->second;
}
else {
code_ = 400;
status = CODE_STATUS.find(400)->second;
}
buff.Append("HTTP/1.1 " + to_string(code_) + " " + status + "\r\n");
}
void HttpResponse::AddHeader_(Buffer& buff) {
buff.Append("Connection: ");
if(isKeepAlive_) {
buff.Append("keep-alive\r\n");
buff.Append("keep-alive: max=6, timeout=120\r\n");
} else{
buff.Append("close\r\n");
}
buff.Append("Content-type: " + GetFileType_() + "\r\n");
}
void HttpResponse::AddContent_(Buffer& buff) {
int srcFd = open((srcDir_ + path_).data(), O_RDONLY);
if(srcFd < 0) {
ErrorContent(buff, "File NotFound!");
return;
}
/* 将文件映射到内存提高文件的访问速度
MAP_PRIVATE 建立一个写入时拷贝的私有映射*/
LOG_DEBUG("file path %s", (srcDir_ + path_).data());
int* mmRet = (int*)mmap(0, mmFileStat_.st_size, PROT_READ, MAP_PRIVATE, srcFd, 0);
if(*mmRet == -1) {
ErrorContent(buff, "File NotFound!");
return;
}
mmFile_ = (char*)mmRet;
close(srcFd);
buff.Append("Content-length: " + to_string(mmFileStat_.st_size) + "\r\n\r\n");
}
void HttpResponse::UnmapFile() {
if(mmFile_) {
munmap(mmFile_, mmFileStat_.st_size);
mmFile_ = nullptr;
}
}
string HttpResponse::GetFileType_() {
/* 判断文件类型 */
string::size_type idx = path_.find_last_of('.');
if(idx == string::npos) {
return "text/plain";
}
string suffix = path_.substr(idx);
if(SUFFIX_TYPE.count(suffix) == 1) {
return SUFFIX_TYPE.find(suffix)->second;
}
return "text/plain";
}
void HttpResponse::ErrorContent(Buffer& buff, string message)
{
string body;
string status;
body += "<html><title>Error</title>";
body += "<body bgcolor=\"ffffff\">";
if(CODE_STATUS.count(code_) == 1) {
status = CODE_STATUS.find(code_)->second;
} else {
status = "Bad Request";
}
body += to_string(code_) + " : " + status + "\n";
body += "<p>" + message + "</p>";
body += "<hr><em>TinyWebServer</em></body></html>";
buff.Append("Content-length: " + to_string(body.size()) + "\r\n\r\n");
buff.Append(body);
}
#### 引用
- https://www.agedcat.com/programming_language/cpp/526.html
- https://github.com/Aged-cat/WebServer