实现一个简单的HTTP服务器点击链接获取源码
应用层有很多协议,其中HTTP协议就是其中一个,HTTP协议(超文本传输协议)也是较为重要的一个协议。
在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输超文本(如 HTML 文档)。
HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。
说到URL大家可能不熟悉,但是说到“网址”大家就一目了然,实际上,“网址”就是URL。
HTTP做的工作:用户在网络上获取资源(图片、文本、视频等)时是在服务器端获取,所有的资源都是在服务器端。通过某种协议(如http协议或者https协议)来标识用户所需要的资源,然后返回给用户。
我们知道Linux适合做后端开发,那么这些服务器端的资源是在Linux操作系统中。而Linux操作系统一切皆文件,必须得找到对应的资源,需要通过路径来标识,因此在URL后半部分就是路径。
因此,URL就是统一资源定位符,域名+文件路径,标识互联网中唯一的文件资源。
像 / ? :
等这样的字符, 已经被 url
当做特殊意义理解了. 因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为 16 进制, 然后从右到左, 取 4 位(不足 4 位直接处理), 每 2 位做一位, 前面加上%
, 编码成%XY
格式。
HTTP请求
在HTTP请求中,格式如下:
HTTP响应
在HTTP请求中,格式如下:
HTTP中如何将报头和有效载荷进行分离(封装)? 通过空行即\r\n
如何HTTP的请求和响应读到一个完整的报头?通过换行符可以知道读到一个完整的报文。
如果请求和响应中包含了正文(DATA),如何保证读到一个完整的报头?怎么知道正文部分的长度?在之前的网络版本计算器中只有一个字段,拥有有效载荷长度的字段。在HTTP的请求和应答中报头属性中有一个公共属性叫做Content-Length: xxx
,无论是请求还是响应,如果有正文部分,那么这个字段一定包含,这样就能读到一个完整的报文。
在HTTP请求中,需要基本的属性(请求行、请求报头、空行、请求正文)以及进一步显示出具体属性(使用的方法、使用的URL、使用的HTTP版本、请求报头以KV形式显示)
class HttpRequest
{
public:
HttpRequest():_blank_line(base_sep)
{}
void Deserialize(std::string &reqstr)
{}
~HttpRequest() {}
private:
// 请求的基本格式
std::string _req_line;
std::vector<std::string> _req_handers;
std::string _blank_line;
std::string _body_text;
//具体属性
std::string _method;
std::string _url;
std::string _version;
std::unordered_map<std::string,std::string> _headers_kv;
};
在反序列化中:
Getline
从reqstr
中获取请求行do-while
循环来读取所有的请求头部信息。每次循环调用 Getline(reqstr)
获取一行数据,并将它存储在 header
中void Deserialize(std::string &reqstr)
{
// 基本的反序列化
_req_line=Getline(reqstr);
std::string header;
do
{
header=Getline(reqstr);
if(header.empty()) break;
else if(header==base_sep) break;
_req_handers.push_back(header);
}while(true);
if(!reqstr.empty())
_body_text=reqstr;
//进一步反序列化
ParseReqLine();
ParseReqHeader();
}
寻找base_sep
的位置,如果找不到说明没有分隔符,是一个空串;如果找到了base_sep
就分割从0
到base_sep
之间的字符串。然后更新reqstr
,即删除已经提取的部分。如果 line
是空字符串(即没有有效的行内容),则返回 base_sep
,表示一个空行或分隔符。否则,返回提取的有效行内容 line
。
const static std::string base_sep="\r\n";
std::string Getline(std::string &reqstr)
{
auto pos=reqstr.find(base_sep);
if(pos==std::string::npos)
return std::string();
std::string line=reqstr.substr(0,pos);
reqstr.erase(0,line.size()+base_sep.size());
return line.empty()?base_sep:line;
}
解析一个请求行,并将解析的结果存储到类的成员变量 _method
、_url
和 _version
中。
代码通过 std::stringstream
提供的流提取操作符 (>>
) 来从 ss 中依次提取数据并赋值给 _method
、_url
和 _version
。
>> _method
:将从流中提取的第一个单词(如 GET)赋值给 _method
。通常情况下,_method 代表 HTTP 请求的方法(例如 GET、POST 等)。>> _url
:提取流中的第二个单词(如 /index.html),并将其赋值给 _url
。这个字符串通常代表请求的 URL 路径。_version
:提取流中的第三个单词(如 HTTP/1.1),并将其赋值给 _version
。这个字符串代表请求的 HTTP 版本。void ParseReqLine()
{
std::stringstream ss(_req_line);
ss>>_method>>_url>>_version;
}
未解析的解析报头是以:
分割的,因此这里使用:
作为分隔符。
将每个请求头的键值对存储到 _headers_kv
容器中。它通过遍历 _req_handers
,提取出每个请求头的键和值,并将它们插入到一个键值对容器中。
const static std::string line_sep=": ";
void ParseReqHeader()
{
for(auto &header:_req_handers)
{
auto pos=header.find(line_sep);
if(pos==std::string::npos) continue;
std::string k=header.substr(0,pos); // [)
std::string v=header.substr(pos+line_sep.size());
if(k.empty()||v.empty()) continue;
_headers_kv.insert(std::make_pair(k,v));
}
}
class HttpResponse
{
public:
HttpResponse():_version(httpversion),_blank_line(base_sep)
{}
void AddCode(int code)
{
_status_code=code;
_desc="OK";
}
void AddHeader(const std::string &k,const std::string &v)
{
_headers_kv[k]=v;
}
void AddBodyText(const std::string body_text)
{
_resp_body_text=body_text;
}
std::string Serialize()
{
}
~HttpResponse()
{}
private:
// 基本属性
std::string _version;
int _status_code;
std::string _desc;
std::unordered_map<std::string,std::string> _headers_kv;
// 响应的基本格式
std::string _status_line;
std::vector<std::string> _resp_handers;
std::string _blank_line;
std::string _resp_body_text;
};
在HTTP响应序列化中:
_status_line
字符串中_headers_kv
容器,它是一个存储 HTTP 响应头的键值对容器。每一行头部字符串会被 push_back
到 _resp_handers
容器中,最终该容器保存了所有响应头的字符串 responsestr
std::string Serialize()
{
// 构建状态行
_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;
// 构建应答报头
for(auto &hander:_headers_kv)
{
std::string header_line=hander.first+line_sep+hander.second+base_sep;
_resp_handers.push_back(header_line);
}
// 空行和正文
// 空行已经初始化,正文已经保存在_resp_body_text中
// 正式序列化
std::string responsestr=_status_line;
for(auto &line:_resp_handers)
{
responsestr+=line;
}
responsestr+=_blank_line;
responsestr+=_resp_body_text;
return responsestr;
}
telnet 127.0.0.1 8888
通过这个测试,得到响应
class HttpResponse
{
public:
HttpResponse():_version(httpversion),_blank_line(base_sep)
{}
void AddCode(int code)
{
_status_code=code;
_desc="OK";
}
void AddHeader(const std::string &k,const std::string &v)
{
_headers_kv[k]=v;
}
void AddBodyText(const std::string body_text)
{
_resp_body_text=body_text;
}
std::string Serialize()
{
// 构建状态行
_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;
// 构建应答报头
for(auto &hander:_headers_kv)
{
std::string header_line=hander.first+line_sep+hander.second+base_sep;
_resp_handers.push_back(header_line);
}
// 空行和正文
// 空行已经初始化,正文已经保存在_resp_body_text中
// 正式序列化
std::string responsestr=_status_line;
for(auto &line:_resp_handers)
{
responsestr+=line;
}
responsestr+=_blank_line;
responsestr+=_resp_body_text;
return responsestr;
}
~HttpResponse()
{}
private:
// 基本属性
std::string _version;
int _status_code;
std::string _desc;
std::unordered_map<std::string,std::string> _headers_kv;
// 响应的基本格式
std::string _status_line;
std::vector<std::string> _resp_handers;
std::string _blank_line;
std::string _resp_body_text;
};
html
<!DOCTYPE html>
<html>
<head>
<title>南桥几晴秋(gwj.cn)</title>
<meta charset="UTF-8">
</head>
<body>
<div id="container" style="width:800px">
<div id="header" style="background-color:#FFA500;">
<h1 style="margin-bottom:0;">南桥几晴秋</h1></div>
<div id="menu" style="background-color:#FFD700;height:200px;width:100px;float:left;">
<b>Menu</b><br>
HTML<br>
CSS<br>
JavaScript</div>
<div id="content" style="background-color:#EEEEEE;height:200px;width:700px;float:left;">
这是一个测试</div>
<div id="footer" style="background-color:#FFA500;clear:both;text-align:center;">
Copyright © gwj.com</div>
</div>
<div>
<img src="/image/1.jpg" alt="一张图片">
</div>
</body>
</html>
在浏览器中输入:http://119.3.220.34:8888/
,得到如下界面:
获得一个完整的网页,浏览器要先得到html,根据html的标签检测出我们还要获取的其他资源,浏览器会继续发起HTTP请求。
我们在使用网站时,点击某个按钮进行跳转,实际上是在访问wwwroot
中的文件
在我自己的HTTP服务器中显示如下:
在现代Web开发中,HTML文件的内容通常属于前端部分,而通过后端调用和渲染这些内容是常见的架构设计。
HTTP
中的 Connection
字段是 HTTP
报文头的一部分, 它主要用于控制和管理客户端与服务器之间的连接状态。
HTTP/1.1
协议中, 默认使用持久连接。 当客户端和服务器都不明确指定关闭连接时, 连接将保持打开状态, 以便后续的请求和响应可以复用同一个连接。 HTTP/1.0
上实现持久连接, 需要在请求头中显式设置 Connection: keep-alive
语法格式
• Connection: keep-alive
: 表示希望保持连接以复用 TCP 连接。
• Connection: close
: 表示请求/响应完成后, 应该关闭 TCP 连接。
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定 向), 504(Bad Gateway)
什么叫做重定向?
用户浏览器想目标服务器发起请求,但是目标服务器想用户浏览器推送了一个服务器,此时用户浏览器向推送的服务器进行请求,重定向的地址在location
字段中。
所谓的永久与临时无非就是字面意思,永久即下次再请求时直接去新的服务器,会修改客户的地址,临时不对客户做任何影响。
永久重定向:当你决定将一个页面永久迁移到新地址时,使用永久重定向是正确的做法。例如,网站改版、页面URL结构优化或域名更换时。
临时重定向:当你需要在短期内将页面临时指向另一个URL时使用,比如进行系统维护、A/B测试或者临时更新页面内容时。
if(req.Path()=="wwwroot/redir")
{
std::string reire_path="https://www.qq.com";
resp.AddCode(302,_code_to_desc[302]);
resp.AddHeader("Location",reire_path);
}
GET一般用来获取静态资源,也可以通过URL向服务器传递参数
std::string Method()
{
LOG(DEBUG,"Client request method is %s\n",_method.c_str());
return _method;
}
POST方法可以通过httprequest的正文来进行参数传递
POST方法传递参数比GET方法更私密,登录信息不回显示在浏览器中。但是都不安全。要想保证安全必须对http的参数部分进行加密。
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<functional>
#include<sstream>
#include<fstream>
#include<unordered_map>
const static std::string base_sep="\r\n";
const static std::string line_sep=": ";
const static std::string prefixpath="wwwroot"; // web根目录
const static std::string homepage="index.html"; // web根目录
const static std::string httpversion="HTTP/gwj_1.0";
const static std::string spacesep=" ";
const static std::string suffixsep=".";
const static std::string html_404="404.html";
const static std::string arg_sep="?";
class HttpRequest
{
private:
std::string Getline(std::string &reqstr)
{
auto pos=reqstr.find(base_sep);
if(pos==std::string::npos)
return std::string();
std::string line=reqstr.substr(0,pos);
reqstr.erase(0,line.size()+base_sep.size());
return line.empty()?base_sep:line;
}
void ParseReqLine()
{
std::stringstream ss(_req_line);
ss>>_method>>_url>>_version;
if(strcasecmp(_method.c_str(),"GET")==0)
{
auto pos=_url.find(arg_sep);
if(pos!=std::string::npos)
{
_body_text=_url.substr(pos+arg_sep.size());
_url.resize(pos); // 只保留了?前面的字符
}
}
_path+=_url;
if(_path[_path.size()-1]=='/')
{
_path+=homepage;
}
auto pos=_path.rfind(suffixsep);
if(pos!=std::string::npos)
{
_suffix=_path.substr(pos);
}
else
{
_suffix=".default";
}
}
void ParseReqHeader()
{
for(auto &header:_req_handers)
{
auto pos=header.find(line_sep);
if(pos==std::string::npos) continue;
std::string k=header.substr(0,pos); // [)
std::string v=header.substr(pos+line_sep.size());
if(k.empty()||v.empty()) continue;
_headers_kv.insert(std::make_pair(k,v));
}
}
public:
HttpRequest():_blank_line(base_sep),_path(prefixpath)
{}
void Deserialize(std::string &reqstr)
{
// 基本的反序列化
_req_line=Getline(reqstr);
std::string header;
do
{
header=Getline(reqstr);
if(header.empty()) break;
else if(header==base_sep) break;
_req_handers.push_back(header);
}while(true);
if(!reqstr.empty())
_body_text=reqstr;
//进一步反序列化
ParseReqLine();
ParseReqHeader();
}
std::string Url()
{
LOG(DEBUG,"Client Want path %s\n",_path.c_str());
return _url;
}
std::string Path()
{
LOG(DEBUG,"Client Want %s\n",_url.c_str());
return _path;
}
std::string Suffix()
{
return _suffix;
}
std::string Method()
{
LOG(DEBUG,"Client request method is %s\n",_method.c_str());
return _method;
}
std::string GetRequestBody()
{
LOG(DEBUG,"Client request method is %s, args: %s, request path: %s\n",_method.c_str(),_body_text.c_str(),_path.c_str());
return _body_text;
}
void Print()
{
std::cout<<"-----------------------------------------------"<<std::endl;
std::cout<<"###"<<_req_line<<std::endl;
for(auto &header:_req_handers)
{
std::cout<<"@@@"<<header<<std::endl;
}
std::cout<<"***"<<_blank_line;
std::cout<<">>>"<<_body_text<<std::endl;
std::cout<<"Method: "<<_method<<std::endl;
std::cout<<"URL: "<<_url<<std::endl;
std::cout<<"Version: "<<_version<<std::endl;
for(auto header_kv:_headers_kv)
{
std::cout<<"< "<<header_kv.first<<" > : < "<<header_kv.second<<" >"<<std::endl;
}
}
~HttpRequest() {}
private:
// 请求的基本格式
std::string _req_line;
std::vector<std::string> _req_handers;
std::string _blank_line;
std::string _body_text;
//具体属性
std::string _method;
std::string _url;
std::string _path;
std::string _suffix; // 资源后缀
std::string _version;
std::unordered_map<std::string,std::string> _headers_kv;
};
class HttpResponse
{
public:
HttpResponse():_version(httpversion),_blank_line(base_sep)
{}
void AddCode(int code,const std::string &desc)
{
_status_code=code;
_desc=desc;
}
void AddHeader(const std::string &k,const std::string &v)
{
_headers_kv[k]=v;
}
void AddBodyText(const std::string body_text)
{
_resp_body_text=body_text;
}
std::string Serialize()
{
// 构建状态行
_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;
// 构建应答报头
for(auto &hander:_headers_kv)
{
std::string header_line=hander.first+line_sep+hander.second+base_sep;
_resp_handers.push_back(header_line);
}
// 空行和正文
// 空行已经初始化,正文已经保存在_resp_body_text中
// 正式序列化
std::string responsestr=_status_line;
for(auto &line:_resp_handers)
{
responsestr+=line;
}
responsestr+=_blank_line;
responsestr+=_resp_body_text;
return responsestr;
}
~HttpResponse()
{}
private:
// 基本属性
std::string _version;
int _status_code;
std::string _desc;
std::unordered_map<std::string,std::string> _headers_kv;
// 响应的基本格式
std::string _status_line;
std::vector<std::string> _resp_handers;
std::string _blank_line;
std::string _resp_body_text;
};
using func_t=std::function<HttpResponse(HttpRequest&)>;
class HttpServer
{
private:
std::string GetFileContent(const std::string path)
{
std::ifstream in(path,std::ios::binary);
if(!in.is_open()) return std::string();
in.seekg(0,in.end);
int filesize=in.tellg(); //读写偏移量,计算文件大小
in.seekg(0,in.beg);
std::string content;
content.resize(filesize);
in.read((char*)content.c_str(),filesize);
in.close();
return content;
}
public:
HttpServer()
{
_mime_type.insert(std::make_pair(".html","text/html"));
_mime_type.insert(std::make_pair(".jpg","image/jpeg"));
_mime_type.insert(std::make_pair(".png","image/png"));
_mime_type.insert(std::make_pair(".default","text/html"));
_code_to_desc.insert(std::make_pair(100,"Continue"));
_code_to_desc.insert(std::make_pair(200,"OK"));
_code_to_desc.insert(std::make_pair(201,"Created"));
_code_to_desc.insert(std::make_pair(301,"Moved Permanently"));
_code_to_desc.insert(std::make_pair(302,"Found"));
}
// #define TEST
std::string HandlerHttpRequest(std::string &reqstr) //reqstr被客户序列化过
{
#ifdef TEST
std::cout<<"------------------------------------------------------"<<std::endl;
std::cout<<reqstr;
std::string responsestr = "HTTP/1.1 200 OK\r\n";
responsestr += "Content-Type: text/html\r\n";
responsestr += "\r\n";
responsestr += "<html><h1>hello Linux, hello gwj!</h1></html>";
return responsestr;
#else
std::cout << "---------------------------------------" << std::endl;
std::cout << reqstr;
std::cout << "---------------------------------------" << std::endl;
HttpRequest req;
HttpResponse resp;
req.Deserialize(reqstr);
// req.Method();
if(req.Path()=="wwwroot/redir")
{
std::string reire_path="https://www.qq.com";
resp.AddCode(302,_code_to_desc[302]);
resp.AddHeader("Location",reire_path);
}
else if (!req.GetRequestBody().empty())
{
if(IsServiceExists(req.Path()))
{
resp=_service_lists[req.Path()](req);
}
}
else
{
std::string content = GetFileContent(req.Path());
if (content.empty())
{
content = GetFileContent("wwwroot/404.html");
resp.AddCode(404, _code_to_desc[404]);
resp.AddHeader("Content-Length", std::to_string(content.size()));
resp.AddHeader("Content-Type", _mime_type[".html"]);
resp.AddBodyText(content);
}
else
{
resp.AddCode(200, _code_to_desc[200]);
resp.AddHeader("Content-Length", std::to_string(content.size()));
resp.AddHeader("Content-Type", _mime_type[req.Suffix()]);
resp.AddBodyText(content);
}
}
return resp.Serialize();
#endif
}
void InsertService(const std::string servicename,func_t f)
{
std::string s=prefixpath+servicename;
_service_lists[s]=f;
}
bool IsServiceExists(const std::string &servicename)
{
auto iter=_service_lists.find(servicename);
if(iter==_service_lists.end()) return false;
else return true;
}
~HttpServer() {}
private:
std::unordered_map<std::string,std::string> _mime_type;
std::unordered_map<int,std::string> _code_to_desc;
std::unordered_map<std::string,func_t> _service_lists;
};