【Linux 网络】HTTP

一、认识 HTTP
HTTP 的底层是 TCP socket ;
#pragma once
#include "Socket.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
#include <iostream>
#include <signal.h>
#include <unistd.h>
using callback_t = std::function<std::string(std::string &)>;
class TcpServer
{
public:
TcpServer(int port, callback_t cb)
: _port(port), _cb(cb), _listensocket(std::make_unique<TcpSock>())
{
_listensocket->BuildListenSocketMethod(_port);
}
void HandlerRequeset(std::shared_ptr<Socket> sockfd, InetAddr addr)
{
// 短服务
std::string inbuffer; // 字节流式的队列
ssize_t n = sockfd->Recv(&inbuffer);
if (n > 0)
{
std::string send_str = _cb(inbuffer);
sockfd->Send(send_str);
}
else if (n == 0)
{
LOG(LogLevel::DEBUG) << addr.ToString() << " quit,me too";
}
else
{
LOG(LogLevel::DEBUG) << addr.ToString() << " read error,me too";
}
sockfd->Close();
}
void Run()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
InetAddr addr;
auto sockfd = _listensocket->Accept(&addr);
if (sockfd == nullptr)
continue;
LOG(LogLevel::DEBUG) << "获取一个新连接: " << addr.ToString()
<< ", sockfd: " << sockfd->SockFd();
if (fork() == 0)
{
_listensocket->Close();
HandlerRequeset(sockfd, addr);
exit(0);
}
sockfd->Close();
}
}
~TcpServer()
{
}
private:
int _port;
std::unique_ptr<Socket> _listensocket;
callback_t _cb;
};
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << "localport" << std::endl;
}
std::string TestHttp(std::string &request)
{
std::cout << "############################" <<std::endl;
std::cout << request << std::endl;
std::cout << "############################" <<std::endl;
return "你好";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t serverport = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG_STRATEGY();
std::unique_ptr<TcpServer> tsock = std::make_unique<TcpServer>(serverport, TestHttp);
tsock->Run();
return 0;
}


HTTP 协议是一个大的字符串,以 \r\n 来分割字符串,所以 HTTP 协议是以 \r\n 来进行序列化和反序列化的,所以:HTTP 序列和反序列化的过程就是一个字符串的拼接和切分的过程,这个过程 http 没有用任何第三方库;


问题:为什么要有空行?
答:区分报头和有效载荷;
问题:什么是协议?
答:协议是一个约定;在编码层面上就是一个结构体或者类;这个类或者结构体被服务端和客户端共同约定好字段、属性、序列和反序列化;
问题:为什么要有正文?
答:人类上网的行为就两种:1.获取信息,2.上传信息,所以我们上网的过程就是:APP、客户端和服务器进行进行间通信的,本质就是 IO 操作;
二、认识 URL
URL就是网址:
新的:

老的:

这个域名会自动通过 DNS 转换成公网 IP;
我们想得到信息,那么在没有得到信息(资源)之前,这个消息在服务器上,所以你要通过这个域名找到这台服务器;不管这个资源是图片还是音频等在Linux下都是文件,因为 Linux 下一切皆文件,那么 Linux 服务器这么多的文件,怎么找到用户要的文件,所以这个文件要路径,就是上门这个 URL 域名后面那一窜;
既然有了公网 IP 和 文件路径,端口号在哪?
答:http、https 、fep 等协议的端口号是固定的,所以浏览器省略了,但是访问服务器时浏览器会自动加上;

因为路径是唯一的,域名也是唯一的,所以 URL 就能表示互联网中唯一的一个文件;所以 URL 称为统一资源定位符;
URI:统一资源标识符,URL 属于 URI 的一种,能标识唯一性的就是 URI。
HTTP 协议的 request,读取时怎么解决粘包问题?

答:1.通过访问到空行来保证读取到一个完整的报头;2.如果有正文,通过 Content-Length(自描述段) 有效载荷的长度(就是正文的字符串的长度)来读取;

状态码:这个报文的可信度,给浏览器看的,不用我们关心;例如:404(状态码):请求错误(状态码描述)
状态码描述:给人看的。
注意:http 请求,必须要有应答文,即便是错误;
uri : / 表明请求目标服务器的首页;
uri : /a/b/c.html表明请求目标服务器的 web 根目录下的指定路径下的文件;
问题:什么是资源?
答:在Linux里面资源就是文件;
问题:什么是 Web 根目录?
答:任意一个路径,按照目录组织形式,把资源文件保存起来;


结论:http 请求获取任何页面的时候,页面是有多个元素构成的,获取完整的首页不等于一次http请求,一个首页的获取,对应的可能是几十上百个 http 的请求;
http 的应答是超文本的,有自己的类型,那么客户端是如何得知正文的类型的?
答:报头有个属性是:Content-Type:,他是用来标识正文的属性的,这就要求客户端有个 Content-Type 对照表,因为类型有很多;根据资源的后缀来对应 Content-Type;

Referer:当前页面是从哪个页面跳转过来的;
Referer 的作用——1.数据统计;2.拦截;
Connection:keep-alive :期望长链接;长链接就是一次链接可以同时发送多个请求,访问端要处理这些请求,就是例如:一次链接中,客户端10次请求,服务端应答 10 次;短链接就是:1 次链接,1 次请求,1 次应答;长连接是 HTTP/1.1 版本新增加的特性;

什么是重定向?
答:因为提高访问的一方,服务的地址(url)发生改变,要求 Client 更改访问位置,去新的地址访问,这就是就是重定向;
临时重定向 VS 永久重定向
答:本质区别就是影不影响客户对地址的认识;
临时重定向:客户端去访问服务端,假设这个服务端要访问的网址是 www.a.com,此时服务端返回:301/302 的状态码和 Location: 新站点的 url:www.b.com,如果是永久重定向,客户端把 www.a.con 替换成 www.b.com 最后跳转到这个新的站点,此时如果客户端再次访问就拿着 www.b.com 去访问;如果是临时重定向客户端直接拿着 www.b.com 去访问这个站点,再次访问服务端还是拿着 www.a.com 去请求;
三、请求方法

客户端访问服务端无非两种行为:1.获取服务端的资源;2.上传数据到服务器;
99%最常见是请求方法是 GET(重点) 和 POST,其他的大部分都被禁掉了,因为防止恶意攻击服务器;
GET:既可以获取信息,又可以上传信息,通过 url 进行参数传递,如果 url 带有 ? 号,就是上传信息,注意:?号后面就是参数,就是要上传的信息;那么怎么处理的这些上传的信息,前端通过 action :处理表单的后端模块,来处理;注意:?号之前的就是处理该函数的后端模块。
结论:HTTP 可以通过链接 url 的方式对外提供服务(其实就是 url 带参);这称为 resful 风格的微服务接口,例如:下面这条网址一看就是使用的 GET 方法

POST 方法通过正文来传参;
GET VS POST:
1.GET 获取静态资源,GET 传参是 form 参数,参数在 url 的后面;
2.POST 传参,参数在正文部分;
优缺点分析:
1.GET 和 POST 都不安全,因为是明文传参,即:都可以通过报文看到参数;
2.POST 更私密一些,POST 可以用来传递参数较多的情况
3.GET 传参,回显到 url 中,不安全,不能运用于:登录、注册、付款等;
fiddler:这是一个工具,他就像一台代理服务器,可以捕捉用户 http 的请求,然后再让 fidder 发送给目标主机,目标主机的应答也是先由 fidder 捕捉,再发给我们的客户端;
我们通过 http 发送的报文经过无数台路由器,理论上这些路由器都可以获取我们的报文,这是非常不安全的,这个原理就跟 fiddler 一样;所以为了安全就有了另外一种协议:HTTPS;
http 是面向无连接的、无状态的,为什么?
答:虽然 http 的底层是 TCP ,TCP 是连接的,但是 TCP 协议不关我 http 的事,所以 http 是无连接的;无状态,在我们的理解中是:不做历史请求、用户行为等记录,但是 http 的无状态和我们的理解是相反的,例如:我们在网上看个使用,要 VIP 才能看,你注册一下你是 VIP ,如果我们把这个视频看完了,看下一个,假设无状态就是我们理解的那种,那么你就要在登录一下;这样不就会失去用户了吗?所以:HTTP 为了保证能够对用户进行一定程度的状态保持功能,服务器 + 浏览器 + HTTP( Cookie + Session);
Cookie : 浏览器向服务器发起请求 + 账号和密码,此时服务器进行认证(我们程序员代码实现认证),认证通过之后,服务器可以往属性里面添加:Set-Cookie: name = ...... 和 password = .....,返回给浏览器,此时浏览器的 Cookie 以内存或者文件的方式保存账号和密码,当浏览器再次请求时,报头里面就有账号和密码了;不用二次认证;
Cookie 技术:1.应答的报头属性之一,客户端要主动保存 cookie 信息,保存方式:1.内存级,2.文件级;2.从此往后,浏览器在向目标服务器发起 n 次 http 请求时,请求报头主动保存当前的 cookie 信息;
Set-Cookie 是由固定格式的,里面有一个:到期时间,如果这个到期时间是浏览会话结束时,Cookie 的保存信息的方式的内存级,如果是有具体的过期时间那么就是文件级;
上面 Cookie 的过程是有风险的,例如:如果有个木马获取到了 Cookie 里面的信息,hei客就能在浏览器设置 Cookie ,填写 Cookie 信息,此时就能假冒别人去访问服务器;造成两种风险:1.冒充,2.个人信息泄漏;所以为了解决这种问题,浏览器向服务器发起首次发起账号密码的登录请求时,服务器内部会给这个用户生成一个 session 文件,每个用户都有,所以每个用户的 session 文件通过 session id 来区分,当然 session 有可能不是文件而是内存,此时服务器往这个用户的 session 填写 账号和密码 ,此时给客户端应答的 Set-cookie 内容是 session id ,当客户端再次发起请求时通过 session id 来认证;所以 session 只能解决个人信息泄漏的问题,因为在中国没有人敢攻击服务器,他们受到法律保护,但是解决不了盗号的问题,但是缓解盗号问题,例如:账号的ID 地址快速跳转,1s 从广西跳到北京,这个账号绝对有问题,所以服务器把他对应的 session 文件删除掉;
四、代码实现
Http.hpp:
#pragma once
#include <string>
#include <unordered_map>
#include <sstream>
#include "Logger.hpp"
#include <iostream>
#include <functional>
#include <fstream>
static const std::string linesep = "\r\n";
static const std::string innersep1 = " ";
static const std::string innersep2 = ": ";
static const std::string webroot = "./wwwroot";
static const std::string defaulthome = "index.html";
static const std::string html_404 = "404.html";
static const std::string suffixsep = ".";
static const std::string argssep = "?";
using namespace NS_LOG_MODULE;
// http 协议
class HttpRequest
{
private:
std::string ReadOneLine(std::string &reqstr, bool *status)
{
auto pos = reqstr.find(linesep);
if (pos == std::string::npos)
{
*status = false;
return std::string();
}
*status = true;
auto line = reqstr.substr(0, pos);
reqstr.erase(0, pos + linesep.size());
return line;
}
void ParseReqLine(std::string &reqline)
{
std::stringstream ss(reqline);
ss >> _method >> _uri >> _httpversion;
}
void BuildkV(std::string &line, std::string *k, std::string *v)
{
auto pos = line.find(innersep2);
if (pos == std::string::npos)
{
*k = *v = std::string();
return;
}
*k = line.substr(0, pos);
*v = line.substr(pos + innersep2.size());
}
public:
HttpRequest() {}
~HttpRequest() {}
bool Derialize(std::string &reqstr)
{
bool status = true;
std::string reqline = ReadOneLine(reqstr, &status);
if (!status)
return false;
LOG(LogLevel::DEBUG) << reqline;
ParseReqLine(reqline);
LOG(LogLevel::DEBUG) << "_mehtod: " << _method;
LOG(LogLevel::DEBUG) << "_uri: " << _uri;
LOG(LogLevel::DEBUG) << "_httpversion:" << _httpversion;
while (true)
{
status = true;
reqline = ReadOneLine(reqstr, &status);
if (status && !reqline.empty())
{
std::string k, v;
BuildkV(reqline, &k, &v);
if (k.empty() || v.empty())
continue;
_req_headers.insert(std::make_pair(k, v));
}
else if (status)
{
_blank_line = linesep;
break;
}
else
{
// return false;
break;
}
}
_path = webroot;
_path += _uri; // ./wwwroot
if (_uri == "/")
{
_path += defaulthome; // ./wwroot/index.heml
}
LOG(LogLevel::DEBUG) << "_path: " << _path;
//处理 GET 方法带参问题,POST 正文带参问题
if (_method == "GET")
{
// 查看 _path 是否待参数
auto pos = _path.find(argssep); // GET /login?username_name=zhansan&password_name=1111 HTTP/1.1
if (pos != std::string::npos) // 带参数处理
{
_req_body = _path.substr(pos + argssep.size()); // GET 带参变成 POST 正文带参
_path = _path.substr(0, pos);
}
}
else if(_method == "POST")
{
_req_body = reqstr;
}
return true;
}
std::string Body()
{
return _req_body;
}
std::string Path()
{
return _path;
}
void SetPath(const std::string &path)
{
_path = path;
}
std::string Suffix() // index.html 提取文件后缀:.html
{
if (_path.empty())
{
return std::string();
}
else
{
auto pos = _path.rfind(suffixsep);
if (pos == std::string::npos)
{
return std::string();
}
else
return _path.substr(pos);
}
}
private:
std::string _method;
std::string _uri;
std::string _httpversion;
std::unordered_map<std::string, std::string> _req_headers;
std::string _blank_line;
std::string _req_body;
std::string _path;
};
class HttpResponse
{
private:
std::string Code2Desc(int code)
{
switch (code)
{
case 200:
return "OK";
case 400:
return "Bad Request";
case 404:
return "Not Found";
case 301:
return "Moved Permanently";
case 302:
return "See Other";
default:
return "";
}
}
public:
HttpResponse() : _httpversion("HTTP/1.1"), _blank_line("\r\n")
{
}
~HttpResponse() {}
std::string Derialize()
{
return std::string();
}
std::string Serialize()
{
std::string respstr = _httpversion + innersep1 + std::to_string(_code) +
innersep1 + _desc + linesep;
if (!_resp_body.empty())
{
std::string len = std::to_string(_resp_body.size());
SetHeader("Content-Length", len);
}
for (auto &elem : _resp_heads)
{
std::string line = elem.first + innersep2 + elem.second + linesep;
respstr += line;
}
respstr += _blank_line;
respstr += _resp_body;
return respstr;
}
bool ReadContent(const std::string &path)
{
// 打开文件,二进制模式
// 以二进制的方式读取
std::ifstream file(path, std::ios::binary);
if (!file.is_open())
{
LOG(LogLevel::WARNING) << path << " 资源不存在!";
return false;
}
LOG(LogLevel::DEBUG) << path << " 资源存在!";
// 定位到文件末尾获取文件大小
file.seekg(0, std::ios::end);
std::streampos fileSize = file.tellg(); // 获取文件大小
file.seekg(0, std::ios::beg);
// 创建足够大的字符串来保存数据
_resp_body.resize(static_cast<size_t>(fileSize));
// 读取文件内容到字符串
file.read(&_resp_body[0], fileSize);
file.close();
return true;
}
void SetCode(int code)
{
if (code >= 100 && code < 600)
{
_code = code;
_desc = Code2Desc(_code);
}
else
{
// 资源不存在
LOG(LogLevel::DEBUG) << "非法的状态码:" << code;
}
}
bool SetHeader(const std::string &key, const std::string &value)
{
_resp_heads[key] = value;
return true;
}
private:
std::string _httpversion;
int _code;
std::string _desc;
std::unordered_map<std::string, std::string> _resp_heads;
std::string _blank_line;
std::string _resp_body;
};
using func_t = std::function<HttpResponse(HttpRequest &)>;
class Http
{
private:
std::unordered_map<std::string, func_t> _handlers;
public:
std::string Suffix2Desc(const std::string &suffix)
{
if (suffix == ".html")
return "text/html";
else if (suffix == ".css")
return "text/css";
else if (suffix == ".png")
{
return "image/png";
}
return "text/html";
}
public:
Http() {}
// ./login -> ./wwwroot/login
void Register(const std::string &action, func_t hander)
{
std::string key = webroot;
key += action;
_handlers[key] = hander;
}
std::string HandlerRequest(std::string &requeststr)
{
HttpRequest req;
std::string respstr;
std::cout << requeststr;
if (req.Derialize(requeststr))
{
std::string target = req.Path();
HttpResponse resp;
// 1.交互式处理 -> 处理带参数的 url 和 纯静态网页
if (_handlers.find(target) != _handlers.end())
{
resp = _handlers[target](req);
}
else
{
// 处理静态网页
if (resp.ReadContent(target))
{
std::string suffix = req.Suffix();
std::string mime_type_value = Suffix2Desc(suffix);
resp.SetHeader("Content-Type", mime_type_value);
resp.SetCode(200);
}
else
{
// 资源不存在
resp.SetCode(301);
resp.SetHeader("Location", "/404.html");
}
}
respstr = resp.Serialize();
}
return respstr;
}
~Http() {}
};
TcpServer.hpp:
#pragma once
#include "Socket.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
#include <iostream>
#include <signal.h>
#include <unistd.h>
using callback_t = std::function<std::string(std::string &)>;
class TcpServer
{
public:
TcpServer(int port, callback_t cb)
: _port(port), _cb(cb), _listensocket(std::make_unique<TcpSock>())
{
_listensocket->BuildListenSocketMethod(_port);
}
void HandlerRequeset(std::shared_ptr<Socket> sockfd, InetAddr addr)
{
// 短服务
std::string inbuffer; // 字节流式的队列
ssize_t n = sockfd->Recv(&inbuffer);
if (n > 0)
{
std::string send_str = _cb(inbuffer);
std::cout <<"发送成功"<< std::endl;
sockfd->Send(send_str);
}
else if (n == 0)
{
LOG(LogLevel::DEBUG) << addr.ToString() << " quit,me too";
}
else
{
LOG(LogLevel::DEBUG) << addr.ToString() << " read error,me too";
}
sockfd->Close();
}
void Run()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
InetAddr addr;
auto sockfd = _listensocket->Accept(&addr);
if (sockfd == nullptr)
continue;
LOG(LogLevel::DEBUG) << "获取一个新连接: " << addr.ToString()
<< ", sockfd: " << sockfd->SockFd();
if (fork() == 0)
{
_listensocket->Close();
HandlerRequeset(sockfd, addr);
exit(0);
}
sockfd->Close();
}
}
~TcpServer()
{
}
private:
int _port;
std::unique_ptr<Socket> _listensocket;
callback_t _cb;
};
Main.cc:
#include "TcpServer.hpp"
#include "Http.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << "localport" << std::endl;
}
//处理带参的 url 的参数
HttpResponse Login(HttpRequest& req)//注册
{
}
HttpResponse Register(HttpRequest& req)//登录
{
}
HttpResponse Search(HttpRequest& req)
{
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t serverport = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG_STRATEGY();
//HTTP 协议
std::unique_ptr<Http> http = std::make_unique<Http>();
http->Register("/login",Login);
http->Register("/register",Register);
http->Register("/Search",Search);
std::unique_ptr<TcpServer> tsock = std::make_unique<TcpServer>(serverport,
[&http](std::string& reqstr)->std::string{
return http->HandlerRequest(reqstr);
});
tsock->Run();
return 0;
}
更多推荐




所有评论(0)