目录
2.2 URL中的?与urldecode\urlencode
tips:简单介绍一下wwwroot以及index.html
Build--------------Response不可缺少的一环
书接上回,虽说应用层在一定意义上是可以被我们自己实现的,但其实应用层依然有很多行业规范。其中最出名的当然属HTTP
1. 初识HTTP
HyperText Transfer,超文本传输协议,是一种规定了客户端如何访问服务器的协议,以保证超文本可以被交换和传输(如 HTML 文档)。
客户端通过 HTTP 协议向服务器发送 请求,服务器收到请求后处理并返回响应。
Http有以下的一些特点:
无连接、无状态,即服务器不会保存客户端的信息,每次访问都需要重新建立连接。比如,在OAuth2协议中,Token需要一直被放在URL中表示具有访问权限。
http是基于Tcp实现的:
应用层协议:HTTP
HTTP基于TCP协议实现
采用TCP意味着继承了其全双工通信、序列化传输和报文交换等特性
不同应用层协议有各自独特的需求,因此协议种类繁多
但所有应用层协议都存在一个共同点:依赖流式服务确保报文的完整性
tips:网络中的一些新名词(AI生成):
超文本(Hypertext)
超文本是一种文本形式,它包含可以链接到其他文本的超链接。这些链接可以是文本、图片、视频等其他媒体形式。超链接使得用户可以通过点击来跳转到同一文档的不同部分或完全不同的文档。超文本是万维网(World Wide Web)的基础,它允许用户在不同网页之间导航。
HTML(HyperText Markup Language)
HTML 是一种标记语言,用于创建网页和网页应用程序中的内容结构。它定义了网页的结构和内容,但不涉及样式和行为。HTML 文档由一系列的元素(elements)组成,这些元素通过标签(tags)来定义。例如,
<html>
定义了整个网页的开始和结束,<body>
定义了网页的主体内容,<h1>
到<h6>
定义了不同级别的标题,<p>
定义了段落。HTML 5 是 HTML 的最新版本,它增加了对多媒体内容的支持,如音频和视频,以及图形和动画等。
XML(eXtensible Markup Language)
XML 是一种标记语言,用于存储和传输数据。它与 HTML 相似,但 XML 被设计为一种数据描述语言,而不是一种用于显示数据的语言。XML 允许用户定义自己的标签,这使得它非常灵活,可以用于各种不同的数据表示需求。
XML 的一些关键特性包括:
可扩展性:用户可以定义自己的标签来描述数据。
自描述性:XML 文档包含了足够的信息来描述其内容,不需要外部的元数据。
层级结构:XML 数据可以组织成树状结构,这使得它非常适合表示复杂的数据关系
正式学习http是什么之前,再来了解一下什么是URL
2. URL
2.1 基本结构
平时我们俗称的 " 网址 " 其实就是说的 URL![]()
URL还有另一个名字:Uniform Resource Locator,简称 URL)是互联网上用来标识某一处资源的地址。URL 提供了一种方式,通过它可以访问互联网上的各种资源,如网页、图片、视频、音频文件等。
对于一个URL,首先是协议名称(https表示是加密状态)
然后的news.qq.com是域名。域名(Domain Name)是互联网上用于标识和定位计算机或计算机组(通常是指网站)的一个文本字符串。域名系统(Domain Name System,简称DNS)将域名转换为数字IP地址,这样人们就可以通过易于记忆的域名来访问网站,而不必记住复杂的数字地址。由以上内容不难看出,其实域名是IP地址的另一种表现形式,会被体系结构解释成目标服务器的IP地址。
最后的rain/a/xxxxxx是路劲,其实是目标文件在服务器上的被访问资源的地址(有没有觉得这个地址看着很像是文件地址?)。被访问的资源就是一种文件。
在URL(统一资源定位符)中,以斜杠
/
开始的部分(比如上述图片中的就是/rain/...)并不总是代表文件系统的根目录,而是通常被称为“web根目录”。web根目录是Web服务器上一个特定的目录,它通常是Web服务器提供服务的起始点,所有的Web内容都是从这个目录开始组织的。
所以,HTTP请求的资源本质是文件。
什么是上网
对于我们程序员来说,用户上网的一些基础背景概念:
1.我的数据给别人。别人的数据给我 -> IO->上网的所有行为,都是在IO
2. 什么是资源?图片、音频、视频、文本
3. 客户访问的资源一定是在世界上的某台机器上放着的(通过IP确定)。并且为了获取资源,需要确定系统的路径->这两个信息被URL合在一起确定下来了。
现在,假设我们已经通过URL打开了一个资源。刚刚说到,http是不记录访问者信息的,请问如何把资源推送回去呢?
打开文件之后,用哪个端口号推回去呢?如何把资源送回去呢?
因为指明协议,就相当于明确了回来的时候是要回到哪个端口(比如1024以内的端口,都被比较出名的协议给占了)
这也是为什么URL中只标注了域名,没有标注端口号
成熟的应用层协议通常与特定的端口号强烈关联。这种关联是通过互联网号码分配机构(IANA,Internet Assigned Numbers Authority)来管理和分配的,以确保不同服务和应用程序之间的通信不会发生冲突。
换句话说,像http这样的协议,端口号都是写死了的。
一些知名端口(Well-Known Ports):从0到1023,这些端口号被IANA分配给了特定的服务。例如:
端口 80 通常用于 HTTP(超文本传输协议)。
端口 443 通常用于 HTTPS(安全的超文本传输协议)。
端口 22 通常用于 SSH(安全外壳协议)。
端口 25 通常用于 SMTP(简单邮件传输协议)
2.2 URL中的?与urldecode\urlencode
?的右边都是资源或者参数
URL(Uniform Resource Locator,统一资源定位符)的
?
右边部分被称为查询字符串(query string)。查询字符串用于向服务器提供额外的信息,这些信息通常用于指定对资源的某些操作或过滤条件。查询字符串由一个或多个参数组成,每个参数由一个名称和一个值组成,参数之间用&
符号分隔。比如 搜qinghua,wd参数(world,关键字)值就是qinghua,最后一排还有一个prefixsug,可能是某种前缀,表示还要搜索以qinghua为前缀的其他信息。
urlencode:就像C语言中的/ % 等特殊转义符,在搜索的时候会被加码成其他样子。
包括不限于 : // ? / = &都是被转义了的。
转义的规则如下 :将需要转码的字符转为 16 进制,然后从右到左,取 4 位 ( 不足 4 位直接处理 ) ,每 2 位做一位,前面加上 % ,编码成 %XY 格式![]()
再比如,搜索这几个转移字符 //?=&/
这是一个可以URL解码编码的工具:
易混淆:URL和HTTP传输请求两者是什么关系?
先来看一眼,http的请求到底是长什么样的
HTTP是一种用于在网络上传输数据的协议,就像TCP一样,只不过http是基于TCP的协议
URL才是日常所说的”网址“,是用于指定网络上的资源位置,是一个基于万维网的‘‘文件地址’’
关于http协议,是如何做到如何保证面向字节流的完整性的?
HTTP的宏观结构
要想进一步理解HTTP,必须手搓一个,手搓之前,一起看一下宏观结构
首先是请求行,请求行依次包含三个数据内容:请求方法(最常用的只有GET和POST)、URL、HTTP协议版本,三个内容之间用空格隔开。以下是常见的http方法,具体的使用会在以后详细讲。
然后是http的请求报头:
包含多行内容,每一行都有自己固定的格式,也都是以换行符代表结束
Key一般是请求相关的属性,Value一般是该属性的内容,后会接一个空行,空行的本质就是\r\n
HTTP RESPONSE同理,状态行内容有相应的变化。
名字上,RESPONSE主要是状态行 响应报头 空行 响应正文
状态码,比如404,描述就是not found
很明显,这样的request或者response是需要被OS管理的
可以分别构造两个class来管理两种属性。请求正文、报头等不过就是里面的成员,甚至还能大胆推测,就是string类的
在把这个类丢进tcp的缓冲区之前,一定需要进行序列化,序列化之后再让TCP去传输,传输也就不需要刻意再去管,交给TCP即可。
现在再看这是不是更能理解为什么HTTP是应用层协议了?
说白了,其本职工作只是结构化、序列化,真正的传输(在当前视角下),只要丢进TCP层次的缓冲区即可。
如果有看过上一文的读者可能会发现,如果HTTP更多的功能是用于序列化,那其实非常像网络计算器中的Parse函数。
大概思考如何进行序列化:
序列化,就是把前面的内容全部缩成一个长字符串
空行可以用作分割,空行之前就是报头,空行之后就是正文
报头里面有描述正文长度的字段
反序列化的时候就可以按行读取,一定也会读到空行。读到空行之后,一定可以在请求报头中获得一个Content_length字段,再次读正文的时候就比较方便了。
http本身作为一个协议,序列化等都是自己实现的,没有依赖其他的比如JSON库。
REQUEST和RESPONSE很像,所以他们的序列化和反序列化做法可以说是几乎完全一样的。
理论部分暂时结束,下面会基于以前的部分代码先进行一次封装,封装好了就构建HTTP,读者朋友可以先跟着之前的敲一遍,也可以直接跟着以下代码写。【LINUX网络】使用TCP简易通信_liunx 不启用中转连接tcp服务-CSDN博客
3. DEMO CODE
-----------------------记录完成demo代码中的过程与遇到的困难
写代码才能真正手撕清楚这个过程,不过这个写和调试的过程注定是费时和痛苦的。
对于初学者来说,代码量确实有点大,共勉。
我的构思中,大概分层为:Socket层(采用模板方法模式,将Socket作为基类,TcpSocket作为派生类,读者可以自行实现UdpServer),TcpServer层(复用Socket层)
先谈套接字封装,封装完了就可以实现一个简单的http_server
1. 封装Socket的模块
TcpServer大概分为socket,bind,listen,accept四步,并不是直接就能用的。我们希望能封装一个类来完成以上工作,让我们可以直接在TcpServer中使用
这次的封装采用 模板方法模式
首先完成一个Socket的类:
注意:
除了几个到派生类中去具体实现的虚方法,可以先刻画好一个BuildSocket方法,到时候可以直接调。
根据socket\bind\listen\accept四步依次实现,不过应当考虑到accept是需要在服务器运行的主逻辑中去执行的
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
// 网络四件套
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 基类,规定创建socket的方法
// 提供若干个、固定模式的方法
class Socket
{
public:
virtual ~Socket() = 0;
virtual int SocketOrDie() = 0;
virtual bool BindOrDie() = 0;
virtual bool ListenOrDie() = 0;
virtual int Accept() = 0;
virtual void Close();
#ifdef _WIN
BuildTcpSocket()
{
SocketOrDie();
BindOrDie();
ListenOrDie();
}
#else //LINUX版本
void BuildSocket()
{
SocketOrDie();
BindOrDie();
ListenOrDie();
}
#endif
private:
};
开始实现派生类:private中暂时构思的是加一个_sock_fd即可,初始赋值为-1,检测到值为-1就是暂时还不能使用的。
virtual int SocketOrDie() override
{
_sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(_sock_fd < 0)
{
LOG(LogLevel::ERROR) << "Tcp socket fail";
DIE(SOCK_ERR);
}
LOG(LogLevel::DEBUG)<<"listen socket success";
return _sock_fd;
}
Bind的时候要注意结合之前我们自己实现的封装网络地址的类。Bind的时候需要把端口号传进去,
用传进去的port构造一个Inet_Addr的对象
注意,这只是个TCP的socket的接口,不是TCP服务器本身。这个类只是用于服务于我们在TCPServer中去构建一个个的Socket
class TcpSocket : public Socket { public: TcpSocket() :_sock_fd(gsockfd) {} virtual int SocketOrDie() { _sock_fd = socket(AF_INET,SOCK_STREAM,0); if(_sock_fd < 0) { LOG(LogLevel::ERROR) << "Tcp socket fail"; DIE(SOCK_ERR); } LOG(LogLevel::DEBUG)<<"listen socket success"; return _sock_fd; } virtual bool BindOrDie(int port) { if(_sock_fd==gsockfd) { return false; } InetAddr inet_addr(port); int n = ::bind(_sock_fd,inet_addr.NetAddr(),inet_addr.NetAddrLen()); if (n < 0) { LOG(LogLevel::ERROR) << "bind error"; DIE(BIND_ERR); } LOG(LogLevel::DEBUG)<<"bind success"; return true; } virtual bool ListenOrDie() { if(_sock_fd==gsockfd) { return false; } int n = ::listen(_sock_fd, BACKLOG); if(n<0) { LOG(LogLevel::ERROR)<<"listen false"; DIE(LISTEN_ERR); } LOG(LogLevel::DEBUG)<<"listen success"; return true; } virtual int Accept() { } virtual void Close() { ::close(_sock_fd); } virtual ~TcpSocket() { } private: int _sock_fd; };
http是基于TCP协议的,所以http是需要基于TcpServer的,一层一层往上搭。
TcpServer:
loop模块,核心逻辑
loop模块有两个任务,accept和IO
如何设计这个Accept模块呢?
以下是博主自己的思考(思考怎么设计参数、如何传参也是开发中最重要的一步)
在TcpServer层,肯定希望直接能通过我们封装好的Ptr调一个Accept来获得一个可以直接用的tcpsocket(多态情况下肯定是需要被基类去控制的)。不过此处的Socket类被我们实现成了一个虚类,最多使用指针去实现。
那么TcpSocket层次的Accept方法就可以返回一个构造好的Socket的智能指针。值得注意的是,此处的返回的这个智能指针需要是shared的而非unique。
相当于用父类指针指向了一个子类的实例化对象
再来看Sockegt层次中Accept的参数——说白了,我们封装的Accept函数就是处理好 真正的accept 的两类返回值:打开的 ''打工人sockfd'' 以及了解客户端信息的struct sockaddr_in。
结合上述逻辑,我觉得可以在上层调用时传一个Client的InetAddr*类型参数,作为输出型参数,保留::accept的两个输出型参数的sockaddr_in的信息。另外用accept的返回值构建一个shared实例,返回其指针到TcpServer层
现在的Accept实际拿到的是一个Socket基类指向的TcpSocket派生类的对象,其中的成员变量_sock_fd就是刚刚accept的newsockfd
所以需要在Socket类中再去封装对应的读写方法,这样在TcpServer层次才能直接去读写这个被封装起来并且被传回到TcpServer的Socket。(这样设计,可以方便TcpServer层次直接使用读写接口)
我们希望的:
HttpServer
别忘了,尽管我们已经可以在Tcp层次直接调Recv和Send,但是我们为了保证字节流的完整性,还需要进行对应的序列化与反序列化。先大概实现一个recv与send叭。
然后就是调用其他的进程、线程或线程池来把具体的处理函数调起来,此处我们选择进程
子进程没有必要继续继承这个listen_fd。
既然任务都交给孙子进程了,父进程就等待并回收子进程即可。并且父进程也可以关闭他accept到的文件描述符
HttpServer需要遵守一定的规则,所以还需要一个HttpProtocol,最后调用HttpProtocol的也该命名为Http.cc。
现在需要进一步把文件描述符丢给创建的孙子进程,也就是上面注释掉的handler。
除了sockfd丢给handler,刚刚在Accept中的输出型Client也可以用起来:
处理方法丢给上层,直接调用一个_handler就可以了。
因为IO的具体逻辑包括解析、是否读完整等。所以,所谓解析、与完整性的判断,就是交给Http层次就可以了。
HTTP一定都是基于Tcp协议的:
关于文件描述符是如何传进来:
class HttpServer : public nocopy { public: HttpServer(int port = gport) :_port(port) ,_tsvr(std::make_unique<TcpServer>(_port)) {} void Start() { _tsvr->InitServer([this](SockPtr sockptr, InetAddr client){ return this->HandlerHttpRequest(sockptr,client); }); _tsvr->Loop(); } //处理核心逻辑的函数 bool HandlerHttpRequest(SockPtr sockptr, InetAddr client) { }
考虑在TcpServer中加一个Init模块来单独初始化handler,这样在HTTP层次,构建TcpServer的时候才能更方便的去传lambda
语法tips:
lambda表达式返回的是一个临时的函数对象,临时变量具有常性,如果用一个&接受会导致权限的放大,所以可以选择赋值或者const&(&会放大权限,const保证权限不被放大)
初代版本(DEMO 0.0)
目前为止,整个程序已经可以跑起来了。
直接用浏览器访问当前服务器,就会被accept。
先贴一下调过后的代码:从顶向下,依次是HTTP、TCP、SOCKET
#pragma once
#include <iostream>
#include <string>
#include "TcpServer.hpp"
#include "HttpProtocol.hpp"//现在这还是一个空的头文件
const uint16_t gport = 8080;
using namespace Tcpserver;
class HttpServer : public nocopy
{
public:
HttpServer(uint16_t port = gport)
:_port(port)
,_tsvr(std::make_unique<TcpServer>(_port))
{}
void Start()
{
_tsvr->InitServer([this](SockPtr sockptr, InetAddr client){
return this->HandlerHttpRequest(sockptr,client);
});
_tsvr->Loop();
}
//处理核心逻辑的函数
bool HandlerHttpRequest(SockPtr sockptr, InetAddr client)
{
LOG(LogLevel::DEBUG)<<"get a new client fd is "<<sockptr->Fd()<<" the addr is "\
<<client.NetName();
return true;
}
~HttpServer()
{}
private:
uint16_t _port;
std::unique_ptr<TcpServer> _tsvr;
};
#pragma once
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include "Socket.hpp"
#include "Common.hpp"
using namespace SocketModule;
namespace Tcpserver
{
// 子进程调用该方法,把处理任务交给上层
using handler_t = std::function<bool(SockPtr, InetAddr)>;
class TcpServer : public nocopy
{
public:
TcpServer(uint16_t port)
: _ListenSockPtr(std::make_unique<TcpSocket>()), _port(port)
{
_ListenSockPtr->BuildTcpSocketMethod(_port);
}
void InitServer(const handler_t &handler)
{
_handler = handler;
}
void Loop()
{
_running = true;
while (_running)
{
// 1.Accept
InetAddr Client;
// Clinet在当前语境下,只是用来获得一个客户端的信息,
// 获得客户端信息之后如果不使用,这个变量可以直接释放,
// 所以建议直接当作栈变量而非去堆上new
SockPtr newsock = _ListenSockPtr->Accept(&Client);
LOG(LogLevel::DEBUG) << newsock->Fd();
if (!newsock->IsAcceptlegal() || nullptr == newsock)
{
// 拉客失败就重新再拉
// if(nullptr == newsock)
// LOG(LogLevel::DEBUG)<<"nullptr == newsock";
// if(!newsock->IsAcceptlegal())
// {
// LOG(LogLevel::DEBUG)<<"IsAcceptlegal";
// //exit(9);
// }
continue;
}
LOG(LogLevel::DEBUG) << Client.NetName();
// 2.进行IO
// 创建进程处理
pid_t _pid;
_pid = fork();
if (_pid == 0)
{
_ListenSockPtr->Close();
// 避免父进程wait,交给孙子进程,被OS管理
if (fork() > 0)
exit(0);
// 交给上层
_handler(newsock, Client);
exit(0);
}
newsock->Close();
waitpid(_pid, nullptr, 0);
}
_running = false;
}
~TcpServer()
{
_ListenSockPtr->Close();
}
private:
std::unique_ptr<Socket> _ListenSockPtr;
uint16_t _port;
bool _running;
handler_t _handler;
};
}
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
// 网络四件套
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using namespace Inet_Addr;
namespace SocketModule
{
class Socket;
using SockPtr = std::shared_ptr<Socket>;
// 基类,规定创建socket的方法
// 提供若干个、固定模式的方法
class Socket
{
public:
virtual ~Socket() = default;
virtual int SocketOrDie() = 0;
virtual bool BindOrDie(uint16_t port) = 0;
virtual bool ListenOrDie() = 0;
virtual SockPtr Accept(InetAddr *client) = 0;
virtual bool Recv(std::string *out) = 0; // Recv的参数是一个输出型参数
virtual bool Send(std::string &in) = 0; // Send的参数是一个输入型参数
virtual void Close() = 0;
virtual bool IsAcceptlegal() = 0;
virtual int Fd() = 0;
#ifdef _WIN
void BuildTcpSocketMethod()
{
SocketOrDie();
BindOrDie();
ListenOrDie();
}
#else
void BuildTcpSocketMethod(uint16_t port)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie();
}
void BuildUdpSocket() {}
#endif
private:
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sock_fd = gsockfd)
: _sock_fd(sock_fd)
{
}
virtual int SocketOrDie() override
{
_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_sock_fd < 0)
{
LOG(LogLevel::ERROR) << "Tcp socket fail";
DIE(SOCK_ERR);
}
LOG(LogLevel::DEBUG) << "listen socket success";
return _sock_fd;
}
virtual bool BindOrDie(uint16_t port) override
{
if (_sock_fd == gsockfd)
{
return false;
}
InetAddr inet_addr(port);
int n = ::bind(_sock_fd, inet_addr.NetAddr(), inet_addr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
DIE(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind success";
return true;
}
virtual bool ListenOrDie() override
{
if (_sock_fd == gsockfd)
{
return false;
}
int n = ::listen(_sock_fd, BACKLOG);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen false";
DIE(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen success";
return true;
}
virtual SockPtr Accept(InetAddr *client) override
{
if (nullptr == client)
{
return nullptr;
}
sockaddr_in *peer_p = new sockaddr_in();
socklen_t peer_len = sizeof(*peer_p);
LOG(LogLevel::DEBUG)<<"Ready to accept";
int newsockfd = ::accept(_sock_fd, CONV(peer_p), &peer_len);
LOG(LogLevel::DEBUG)<<"accept success";
// client.Set TODO!!!!!!!!
client->SetSockAddr(*peer_p, peer_len);
return std::make_shared<TcpSocket>(newsockfd);
}
inline bool IsAcceptlegal() override // Accept中返回的newsockfd可能是accept失败了的,所以需要检查
{
if (_sock_fd < 0)
return false;
return true;
}
virtual bool Recv(std::string *out) override
{
char in_buffer[SIZE];
int in_size = ::recv(_sock_fd, in_buffer, SIZE - 1, 0);
if (in_size > 0)
{
in_buffer[in_size] = 0;
*out = in_buffer;
return true;
}
return false;
}
virtual bool Send(std::string &in) override
{
int _size = ::send(_sock_fd, in.c_str(), in.size(), 0);
if (_size > 0)
return true;
return false;
}
virtual void Close()
{
if (_sock_fd < 0)
return;
::close(_sock_fd);
}
virtual ~TcpSocket() override
{
}
virtual int Fd() override { return _sock_fd; }
private:
int _sock_fd;
};
// class UdpSocket : public Socket
// {
// public:
// virtual ~UdpSocket()
// {
// }
// virtual int SocketOrDie() = 0;
// virtual bool BindOrDie() = 0;
// virtual bool ListenOrDie() = 0;
// virtual int Accept() = 0;
// virtual void Close();
// private:
// };
}
在浏览器(客户端)中直接访问当前我的服务器,发现在尝试连接四次之后卡住了。可能是连接超时等问题。毕竟现在的服务器没有返回任何的资源
如果我们打印一下收到的http request:
再仔细看看第一排的请求行
其实真正的http请求只有第一排
其他的都是请求报头,都是k-v形式的
很有趣的发现,在打印完第一次之后,空格空了两行,其中的一个空格是LOG自带的,说明整个http请求还自带一个空格,第一排都是GET+空的URL+版本号 满足最开始的宏观理解。
连接成功!
DEMO 1.0
尝试手写一个返回的消息,被send到对应的fd中去。
再次观察Response的结构:
在HTTP(HyperText Transfer Protocol,超文本传输协议)中,
status-line
(状态行)是HTTP响应消息中的第一个行,它提供了关于服务器响应的基本信息。状态行由三个部分组成,分别是:
HTTP版本:指示服务器使用的HTTP协议的版本,例如
HTTP/1.1
。状态码(Status Code):一个三位数的代码,用来说明请求的处理结果。状态码分为五类:
1xx
:指示信息——请求已接收,继续处理。
2xx
:成功——请求已成功被服务器接收、理解,并接受。
3xx
:重定向——需要后续操作才能完成请求。
4xx
:客户端错误——请求包含语法错误或无法完成请求。
5xx
:服务器错误——服务器在处理请求的过程中发生了错误。原因短语(Reason Phrase):一个简短的文本描述,用来提供状态码的额外信息。例如,对于状态码
404
,原因短语通常是Not Found
。一个典型的状态行示例如下:
HTTP/1.1 200 OK
这表示服务器使用的是HTTP/1.1版本,状态码是
200
,表示请求成功,而OK
是对应的原因短语,进一步说明请求已经成功处理。状态行是HTTP响应消息的开始,它后面跟着响应头部(headers)和可选的响应主体(body)。状态行为客户端提供了关于请求结果的快速概览,使得客户端能够根据状态码和原因短语来决定如何进一步处理响应。
说白了,现在就是假装返回一个假的、已经被反序列化的"Response",现在只要status_line以及Body两部分组成即可,暂时不用对应的响应报头。
首先写第一排的内容,string status_line HTTP/1.1 200 OK
//demo 1.0 硬编码来返回 //sep是状态行的\r\n,Backline是空行,本质也是一个\r\n std::string status_line = "HTTP/1.1 200 OK" + Sep + Backline;
再让AI生成一个html的body,作为Body
//demo 1.0 硬编码来返回 //sep是状态行的\r\n,Backline是空行,本质也是一个\r\n std::string status_line = "HTTP/1.1 200 OK" + Sep + Backline; std::string body = "<!DOCTYPE html>\ <html>\ <head>\ <meta charset = \"UTF-8\">\ <title> Hello World</title>\ </head>\ <body>\ <p> LOVE YOU Ronin007 回家吃饭饭</ p>\ </body> </html>"; std::string http_response = status_line+body; sockptr->Send(http_response); //可以用这个弱智的硬编码去逗一逗女朋友的傻笑,如果你有女朋友的话
出来的效果类似于:
DEMO 2.0
--------------------------------------加入HttpProtocol部分
现在就可以考虑读取请求,对请求进行分析处理了!
如何对http请求进行序列化和反序列化呢(对http进行协议化)?
要确定两个事情:
1.读取请求的完整性
2. 完整http反序列化,http response序列化
现在,对http进行协议化。
直接利用两张图来完成
因为两个报头中都可能包含多个string,所以选择用vector来记录
观察这个Http请求,思考如何取出各个数据:
GET /favicon.ico HTTP/1.1 Host: 81.70.12.246:8080 Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0 Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 Referer: http://81.70.12.246:8080/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
首先,提取出request的整个请求行(就是提取一个子串),提取了子串之后还要想办法获得几个子结构。
第二排表示请求发到谁去了,发到哪一个主机的哪一个端口。
第三排 长连接(暂时不理会),
第五排 User-Agent(下一个块中分享)
Accept:表示能接受以下的内容,也就是正文到底是accept后的哪一种类型。
刚刚提到的UserAgent:
User-Agent 是一个 HTTP 请求头部字段,它用于识别请求发起者的信息,比如浏览器类型、操作系统、浏览器版本以及它所运行的设备或软件。这个字段通常由客户端(如 Web 浏览器)自动设置,并且可以在服务器端的 HTTP 响应中看到。
User-Agent 头部的主要目的是帮助服务器确定请求的设备和软件环境,以便它可以返回最适合该环境的内容或进行特定的优化。例如,一个移动设备的浏览器可能会在 User-Agent 字符串中包含设备型号和浏览器版本信息,这样服务器就可以根据这些信息提供适合移动设备显示的页面布局和资源。
比如,以上两张图就能看出发出请求方的操作系统,一个是win,一个是基于Linux的安卓。
因为request或者response都需要提取出第一排的内容,所以这个ParseFirstLine应该写到Common.hpp中去
直接实现即可:
在HttpServer中调用这个内容
做好相应的debug,发现我们已经成功分离request的请求行
void Deserilize(std::string &str) { LOG(LogLevel::DEBUG)<<"##################################"; LOG(LogLevel::DEBUG)<<"before\n"<<str; // 获取请求行 if (ParseFirstLine(str, &_req_line, Sep)) { // 获得请求行之后,对请求行进行细化 ParseReqLine(_req_line); } LOG(LogLevel::DEBUG)<<"afetr\n"; Print(); LOG(LogLevel::DEBUG)<<"##################################"; }
可以看到,请求的url是/
如果换个位置做访问测试:
请求到的url就是/a/b/c/d.html
tips:简单介绍一下wwwroot以及index.html
wwwroot
是一个常见的术语,通常用于描述网站的根目录(root directory)。
根目录:
wwwroot
是一个文件夹,通常用于存放网站的所有文件和资源。它是网站的“家目录”,所有网页、图片、脚本、样式表等文件都存储在这个目录下。Web服务器的默认目录:在许多Web服务器(如Apache、Nginx、IIS等)中,
wwwroot
是默认的网站根目录。当用户通过浏览器访问网站时,服务器会从这个目录中查找并提供相应的文件。示例
假设你的网站地址是
https://example.com
,那么:
https://example.com
默认会访问wwwroot
目录下的默认文件(通常是index.html
)。如果用户访问
https://example.com/about.html
,服务器会从wwwroot
目录中查找about.html
文件。文件结构
一个典型的
wwwroot
目录结构可能如下:wwwroot/ ├── index.html ├── about.html ├── contact.html ├── images/ │ ├── logo.png │ └── background.jpg ├── css/ │ └── styles.css └── js/ └── script.js
index.html
是一个HTML文件,通常被用作网站的默认首页.css是一种描述HTML的文件,让页面更好看
现在分离 请求报头:简单的字符串处理任务
void ParseHeader(std::string &str) { /* 调用ParseFirstLine去解决问题, 可能有三种情况: out正常;return false&&out为空;return true&&out空串 分别对应:正常给出来、已经找完了、找到了空行 */ while (true) { std::string headler_line; bool flage = ParseFirstLine(str, &headler_line, ::Sep); if (flage && !headler_line.empty()) { _req_head.push_back(headler_line); } else if(!flage && headler_line.empty()) { break; } else if(flage && headler_line.empty()) { continue; } } }
现在发下left str之后已经没有内容了(因为现在body为空)
现在的整个报头还是字符串,想把它的KV属性都拆出来:
DEMO 3.0
-------------------------------------使用html在wwwroot中完成code=404和code=200的情况描述
家目录wwwroot
刚才提到,如果想有一个http服务,必须要构造一个http的家目录,文件名,比如:wwwroot
后端服务如果想被访问,决定了站点必须要有一个“默认首页”
web根目录名字都是被隐藏了的。
www.baidu.com和www.baidu.com/index.html是一个东西,可以在当前环境中使用mkdir指令和touch指令创建这两个文件。
用户真正想访问的是被放到http请求的uri部分。
tips:
URL(Uniform Resource Locator,统一资源定位符)和 URI(Uniform Resource Identifier,统一资源标识符)都是用于标识资源的字符串,但它们的含义和用途有所不同。
URI可以进一步被分成URL或者URN。URI不一定标识清楚了资源具体在哪里。
所以,URI就是去获得用户想访问的资源的地址,并且这个路径都是建立在默认路径之上的,作为服务器,我们需要加上这个路径
浏览器的uri是默认忽略了这个根目录,服务器需要自动加这个前置路径,才能找到用户希望找到的资源。
uri知道了用户想用什么,需要一个HttpResponse,把这个文件塞进去。但是uri是在Request的字段内,所以在Request中,还需要一个GetContent方法,在request中去读文件。
把客户请求的文件拿出来:
Public中再设计一个对应的调用方法即可。
class Response
Build--------------Response不可缺少的一环
不同于Request是从浏览器处拿到然后手动反序列化出来,Response的构建过程需要我们使用各种信息来Build。比如当前服务器的版本号,对于发起的请求的状态码、状态描述等。
如何根据用户想要的内容构建回复呢?这就是Build模块
对于任何HTTP,任何请求,都必有应答。
如果请求的资源不存在该怎么办?这就是状态码存在的意义,也是人们常说的404 Not Found
今天只了解404和200即可
关于为什么需要一个HTTP版本在报头,笔者借助AI生成了一个小tips:
HTTP协议的版本信息在Request和Response报文中扮演着重要角色。具体来说:
- Request中的HTTP版本(如HTTP/1.1)表示:
- 客户端(通常是浏览器)支持的HTTP协议版本
- 决定了客户端会发送哪些请求头字段
- 影响客户端对持久连接、分块传输等特性的支持 示例:HTTP/1.1请求会自动包含Host头字段,而HTTP/1.0则不会
- Response中的HTTP版本(如HTTP/1.1)表示:
- 服务器实际使用的HTTP协议版本
- 决定了服务器会返回哪些响应头字段
- 影响服务器对特性(如Keep-Alive)的支持程度 示例:HTTP/1.1服务器会默认保持连接,而HTTP/1.0服务器需要显式设置Connection: keep-alive
版本差异的应用场景:
- 当客户端发送HTTP/2.0请求但服务器只支持HTTP/1.1时,服务器会降级响应
- 不同版本支持的压缩算法可能不同(如HTTP/2支持HPACK头部压缩)
- 缓存控制机制在不同版本间有差异(HTTP/1.1引入更多缓存控制指令)
在实际通信中,最终使用的HTTP版本是客户端和服务器都支持的最高共同版本。
private:
std::string Code2Desc(int status_code)
{
std::string ret;
switch (status_code)
{
case 404:
ret = "Not Found";
break;
case 200:
ret = "OK";
break;
default:
break;
}
return ret;
}
public:
void Build(HttpRequest& req)
{
std::string& content = req.GetContent();
if(content.empty())
{
_status_code = 404;
_status_desc = Code2Desc(_status_code);
}
else
{
_status_code = 200;
_status_desc = Code2Desc(_status_code);
}
}
如果用户要访问一个不存在的页面,我们需要返回404对应的文件资源(此处可以让AI生成一个前端的html文件),那么就需要去更改一下用户想访问的URI:
public:
void Build(HttpRequest& req)
{
std::string& content = req.GetContent();
if(content.empty())
{
_status_code = 404;
req.SetURI(Page404);
content = req.GetContent();
}
else
{
_status_code = 200;
}
_status_desc = Code2Desc(_status_code);
}
然后是Serilize:把要返回的内容都进行序列化
Deserilize是把一个字符串给解析出来内容,是输入型参数
Serilize是一个输出型参数,应该用指针。
可以试着直接用指令连接:
一个空行,然后就是网页的内容
还可以试试从浏览器去访问,浏览器拿到这些标签性质的文件之后还会解析,生成一个小小的前端页面。
index.html作为默认访问是需要特殊处理的
不过,直接访问的/也应该把根目录放出来,可以特殊处理一下:
现在直接输入ip+port:比如81.70.12.246:8080
快拿去逗逗你的宠物。
前端代码直接让AI生成即可。
如果访问一个不存在的网址(比如/a/b/c/d.html):
DEMO 4.0
---------------------------------------------结合前端,加入图片
前端开发其实就是写wwwroot里的内容,wwwroot外面的内容才是后端完成的。
读者朋友看了这么久,学个入门前端来放松下:
借助教程来看一个简单的HTML标签——a标签。换句话说,就是前端中各种链接的点击
让AI生成一个简单的电商前端html代码(所有的代码都在文末的git链接上)
a标签表示这个链接会发起的html请求,只要把各个界面的a标签都设置到希望跳转到的a标签即可。
终于想起来还没有设置Response的header,依然是用一个unorderedmap去实现,今天只加两个值,一个是content_type,一个是content_length
176排,设置header内容;181排,将unordered_map中的内容全部加在HttpResponse里去
只不过目前为止,我们只加了一个属性,就是Content_Length
再了解一个属性,content_type
HTTP作为超文本传输协议,不仅可以传输文本,还能传输图片、视频等“超文本”,比如上述的html里,只要标明图片名称,并且在wwwroot的image文件夹下是真的有这些图片,就能完成任务了。
对照刚刚的图片为什么没显示出来,是因为图片内容传输失败了。
简单说明是如何去获得图片的:
浏览器会去自动进行一个请求图片的任务。
浏览器对发过去的html进行解析,发现还需要去找三张图片,于是回到对应的image文件夹下去找。
但是,原来的SetContent方法是用ifstream直接去读,这是不正确的。因为图片是以二进制形式传输,二进制中可能遇到很多的/0等,直接读到一半的时候就失败了,所以需要一个标准的GET方法来重新传资源。
GET方法
int SetContent() { std::ifstream in(_url,std::ios::binary); if(!in.is_open()) return OPEN_ERR; in.seekg(0,in.end); int filesize = in.tellg(); in.seekg(0,in.beg); _content.resize(filesize); in.read((char*)_content.c_str(),filesize); in.close(); return 0; // std::ifstream file(_url); // if (!file.is_open()) // { // LOG(LogLevel::ERROR) << "OPEN FAIL"; // return OPEN_ERR; // } // std::string line; // while (std::getline(file, line)) // { // _content += line; // } // file.close(); return 0; }
下面是我们之前的SetContent的方法,现在是新版本的,通过控制文件大小和文件偏移量解决问题,一次性把整个文件都读进去。
这下就能看到了所有的图片了:
理解浏览器的强大
如果打印出我们的HttpResponse
会发现全是乱码,但是浏览器能正确识别这些内容,形成我们的网页。
content_type:
content_type是响应报头的另一个字段,用于告诉浏览器数据的类型。浏览器需要知道一个文件的类型,是根据分析后缀来的,会将分析得到的后悔写成一个MIME类型的样子供浏览器识别。
后缀会转换成对应的字段,赋值到content_type里去
找后缀(suffix):
把对应的content-type信息加到映射表以及vector中去。
文章内容太多,分两次发,下一篇会重新介绍如何GET POST ,客户端如何call整个调用过程等内容..............