1.HTTP协议
虽然我们说,应⽤层协议是我们程序猿⾃⼰定的.但实际上,已经有⼤佬们定义了⼀些现成的,⼜⾮常好⽤的应⽤层协议,供我们直接参考使⽤.HTTP(超⽂本传输协议)就是其中之⼀。
在互联⽹世界中,HTTP(HyperText Transfer Protocol,超⽂本传输协议) 是⼀个⾄关重要的协议。
它定义了客⼾端(如浏览器)与服务器之间如何通信,以交换或传输超⽂本(如HTML⽂档)。HTTP协议是客⼾端与服务器之间通信的基础。客⼾端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是⼀个⽆连接、⽆状态的协议,即每次请求都需要建⽴新的连接,且服务器不会保存客⼾端的状态信息。
3.HTTP回应—Response
4.HTTP request----------------客户端如何打开想要访问的资源
- 左边就是http协议规定的传输的数据类型,右边则是各个主机中存储的数据
- 这里只用请求做说明-------说白了就是,
左边到右边就是反序列化,右边到左边就是序列化!!!!!!
!!!!
5.HTTP状态码
- 从客户端读取之后,需要设置状态码!!!!!!!
- 404就是典型的客户端错误码,指客户访问了服务器端没有存储的网页,会显示这个错误
以淘宝·网页举例,淘宝的服务端没有存储a.html 所以会显示无法访问!!!!!
6.HTTP常⻅⽅法
1.GET方法
⽤途:⽤于请求URL指定的资源。
⽰例: GET /index.html HTTP/1.1
特性:指定资源经服务器端解析后返回响应内容。
2.POST⽅法
⽤途:⽤于传输实体的主体,通常⽤于提交表单数据。
⽰例: POST /submit.cgi HTTP/1.1
特性:可以发送⼤量的数据给服务器,并且数据包含在请求体中。
二者对比------以login.html为例
那如果把post改为get呢?
- 使用get的话,服务端就能拿到登录的数据了,联系数据库,就能做客户注册了!!!!!!!!!!!
- 使用?做分割符!!!!!!!!
7.代码全览
先看一下格式:
Http.hpp:
// 防止头文件被重复包含
#pragma once
// 包含必要的头文件
#include "Socket.hpp" // Socket相关功能
#include "TcpServer.hpp" // TCP服务器实现
#include "Util.hpp" // 工具函数
#include "Log.hpp" // 日志模块
#include <iostream> // 标准输入输出
#include <string> // 字符串处理
#include <memory> // 智能指针
#include <sstream> // 字符串流
#include <functional> // 函数对象
#include <vector> // 动态数组
#include <unordered_map> // 哈希表
// 使用命名空间
using namespace SocketModule; // Socket模块命名空间
using namespace LogModule; // 日志模块命名空间
// 定义常量字符串
const std::string gspace = " "; // 空格
const std::string glinespace = "\r\n"; // HTTP换行符
const std::string glinesep = ": "; // 头部字段分隔符
// 定义Web根目录和默认页面
const std::string webroot = "./wwwroot"; // 网站根目录
const std::string homepage = "index.html"; // 默认首页
const std::string page_404 = "/404.html"; // 404页面路径
// HTTP请求类
class HttpRequest
{
public:
// 构造函数,初始化交互标志为false
HttpRequest() : _is_interact(false)
{
}
// 序列化方法(暂未实现)
std::string Serialize()
{
return std::string();
}
// 解析请求行(如 GET / HTTP/1.1)
void ParseReqLine(std::string &reqline)
{
// 使用字符串流分割请求行
std::stringstream ss(reqline);
ss >> _method >> _uri >> _version; // 分别提取方法、URI和版本--------以空格为分隔符--------这里method是GET,uri是/--但会被自动翻译为/下的第一个.html文件,verson则是HTTP/1.1
}
// 反序列化HTTP请求
bool Deserialize(std::string &reqstr)
{
// 1. 提取请求行
std::string reqline;
bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);//把第一行读入reqline,glinespace是\r\n-----作为一句的尾部
LOG(LogLevel::DEBUG) << reqline; // 记录请求行日志
// 2. 解析请求行
ParseReqLine(reqline);
// 处理URI
if (_uri == "/")
_uri = webroot + _uri + homepage; // 默认首页路径
else
_uri = webroot + _uri; // 其他资源路径
// 记录解析结果日志
LOG(LogLevel::DEBUG) << "_method: " << _method;
LOG(LogLevel::DEBUG) << "_uri: " << _uri;
LOG(LogLevel::DEBUG) << "_version: " << _version;
// 检查URI中是否包含参数
const std::string temp = "?";
auto pos = _uri.find(temp);
if (pos == std::string::npos)
{
return true; // 无参数直接返回//----------访问.html,png等静态内容时就会直接返回!!!!!!!!!
}
// 分离参数和URI
_args = _uri.substr(pos + temp.size()); // 提取参数部分
_uri = _uri.substr(0, pos); // 提取纯URI部分
_is_interact = true; // 标记为交互请求
return true;
}
// 获取URI
std::string Uri()
{
return _uri;
}
// 检查是否为交互请求
bool isInteract()
{
return _is_interact;
}
// 获取参数
std::string Args()
{
return _args;
}
// 析构函数
~HttpRequest()
{
}
private:
std::string _method; // HTTP方法(GET/POST等)
std::string _uri; // 请求资源路径
std::string _version; // HTTP版本
std::unordered_map<std::string, std::string> _headers; // 请求头
std::string _blankline; // 空行
std::string _text; // 请求体
std::string _args; // 请求参数
bool _is_interact; // 是否为交互请求标志
};
// HTTP响应类
class HttpResponse
{
public:
// 构造函数,初始化空行和HTTP版本
HttpResponse() : _blankline(glinespace), _version("HTTP/1.0")
{
}
// 序列化HTTP响应
std::string Serialize()
{
// 构建状态行
std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
// 构建响应头
std::string resp_header;
for (auto &header : _headers)
{
std::string line = header.first + glinesep + header.second + glinespace;
resp_header += line;
}
// 组合状态行、响应头、空行和响应体
return status_line + resp_header + _blankline + _text;
}
// 设置目标文件
void SetTargetFile(const std::string &target)
{
_targetfile = target;
}
// 设置状态码和描述
void SetCode(int code)
{
_code = code;
switch (_code)
{
case 200:
_desc = "OK";
break;
case 404:
_desc = "Not Found";
break;
case 301:
_desc = "Moved Permanently";
break;
case 302:
_desc = "See Other";
break;
default:
break;
}
}
// 添加响应头
void SetHeader(const std::string &key, const std::string &value)
{
auto iter = _headers.find(key);
if (iter != _headers.end())
return;
_headers.insert(std::make_pair(key, value));
}
// 根据文件后缀确定Content-Type!!!!!!!!//如果要访问的网页中还有其他资源如图片,音频。。。。。。就需要设置content-type-------可查找mine表!!!!
std::string Uri2Suffix(const std::string &targetfile)
{
// 查找最后一个点号
auto pos = targetfile.rfind(".");
if (pos == std::string::npos)
{
return "text/html"; // 默认返回HTML类型
}
std::string suffix = targetfile.substr(pos);
if (suffix == ".html" || suffix == ".htm")
return "text/html";
else if (suffix == ".jpg")
return "image/jpeg";
else if (suffix == "png")
return "image/png";
else
return "";
}
// 构建HTTP响应
bool MakeResponse()
{
// 忽略favicon.ico请求
if (_targetfile == "./wwwroot/favicon.ico")
{
LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
return false;
}
// 处理重定向测试
if (_targetfile == "./wwwroot/redir_test")
{
SetCode(301);
SetHeader("Location", "https://www.qq.com/");
return true;
}
// 读取文件内容
int filesize = 0;
bool res = Util::ReadFileContent(_targetfile, &_text);
if (!res) // 文件不存在
{
_text = "";
LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
SetCode(404); // 设置404状态码------客户端访问了不存在的网页!!!!!!!
_targetfile = webroot + page_404; // filetarget指向404页面!!!!!!!!
filesize = Util::FileSize(_targetfile);
Util::ReadFileContent(_targetfile, &_text); // 读取404页面内容
std::string suffix = Uri2Suffix(_targetfile);
SetHeader("Content-Type", suffix); // 设置Content-Type---------------注意这个一定要有,不然没办法链接到网页
SetHeader("Content-Length", std::to_string(filesize)); // 设置内容长度
}
else // 文件存在
{
LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;
SetCode(200); // 设置200状态码
filesize = Util::FileSize(_targetfile);
std::string suffix = Uri2Suffix(_targetfile);
SetHeader("Conent-Type", suffix);// 设置Content-Type---------------注意这个内容类型一定要有,不然没办法链接到网页
SetHeader("Content-Length", std::to_string(filesize));
SetHeader("Set-Cookie", "username=zhangsan;"); // 设置Cookie
}
return true;
}
// 设置响应体文本
void SetText(const std::string &t)
{
_text = t;
}
// 反序列化方法(暂未实现)
bool Deserialize(std::string &reqstr)
{
return true;
}
// 析构函数
~HttpResponse() {}
// 公有成员变量
public:
std::string _version; // HTTP版本
int _code; // 状态码
std::string _desc; // 状态描述
std::unordered_map<std::string, std::string> _headers; // 响应头
std::vector<std::string> cookie; // Cookie集合
std::string _blankline; // 空行
std::string _text; // 响应体
std::string _targetfile; // 目标文件路径
};
// 定义HTTP处理函数类型
using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;
// HTTP服务器类
class Http
{
public:
// 构造函数,初始化TCP服务器
Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))//《2》进一步使用port初始化TcpServer类并返回其指针---------->tcpseerver.hpp
{
}
// 处理HTTP请求
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)//-----------<13>这个才是业务函数!
{
// 接收HTTP请求
std::string httpreqstr;
int n = sock->Recv(&httpreqstr); // 接收请求数据--------<14>把客户端发来的内容(需要反序列化)存入httpreqstr
if (n > 0) // 接收成功
{
std::cout << "##########################" << std::endl;
std::cout << httpreqstr; // 打印原始请求
std::cout << "##########################" << std::endl;
// 解析请求
HttpRequest req;
HttpResponse resp;
req.Deserialize(httpreqstr);
// 处理交互请求
if (req.isInteract())
{
// 检查路由是否存在------
if (_route.find(req.Uri()) == _route.end())//查看uri是否存在于<6>中建立的_route
{
// 可添加重定向逻辑
}
else//如果存在------本文中,如果你在网页中点击login界面时会走这一条路!!!!!!!
{
// 调用注册的处理函数
_route[req.Uri()](req, resp);//-----------------------------<15>调用<6>传递的键值对的函数--即业务函数----见main.cc的Login(HttpRequest &req, HttpResponse &resp)函数
std::string response_str = resp.Serialize();//序列化,准备返回给服务器
sock->Send(response_str); // 发送响应-----------------------<16>最后一步,返回给服务器!!!!!!
}
}
else // 处理静态资源请求-----例如.html/.png文件
{
resp.SetTargetFile(req.Uri());
if (resp.MakeResponse()) // 构建响应成功
{
std::string response_str = resp.Serialize();
sock->Send(response_str); // 发送响应
}
}
}
// 调试模式下的处理
#ifdef DEBUG
std::string httpreqstr;
sock->Recv(&httpreqstr);
std::cout << httpreqstr;
// 构建简单响应
HttpResponse resp;
resp._version = "HTTP/1.1";
resp._code = 200;
resp._desc = "OK";
std::string filename = webroot + homepage;
bool res = Util::ReadFileContent(filename, &(resp._text));
(void)res;
std::string response_str = resp.Serialize();
sock->Send(response_str);
#endif
}
// 启动HTTP服务器
void Start()//---------------------<8>调用TcpServer的start,并传递参数是提供服务时用的fd建立的sock类,和客户传递过来的主机地址--------->tcpserver.hpp
{
tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
{ this->HandlerHttpRquest(sock, client); });//业务函数在这呢!!!!!!
}
// 注册服务路由
void RegisterService(const std::string name, http_func_t h)//<7>注册服务路由
{
std::string key = webroot + name; // 构建完整路径
auto iter = _route.find(key);
if (iter == _route.end()) // 防止重复注册
{
_route.insert(std::make_pair(key, h));
}
}
// 析构函数
~Http()
{
}
private:
std::unique_ptr<TcpServer> tsvrp; // TCP服务器实例
std::unordered_map<std::string, http_func_t> _route; // 路由表
};
Main.cc:
#include"Http.hpp"
void Login(HttpRequest &req, HttpResponse &resp)
{
// req.Args();
LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
std::string text = "hello: " + req.Args(); // username=zhangsan&passwd=123456
// 登录认证
resp.SetCode(200);
resp.SetHeader("Content-Type","text/plain");
resp.SetHeader("Content-Length", std::to_string(text.size()));
resp.SetText(text);
}
// void Register(HttpRequest &req, HttpResponse &resp)
// {
// LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
// std::string text = "hello: " + req.Args();
// resp.SetCode(200);
// resp.SetHeader("Content-Type","text/plain");
// resp.SetHeader("Content-Length", std::to_string(text.size()));
// resp.SetText(text);
// }
// void VipCheck(HttpRequest &req, HttpResponse &resp)
// {
// LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
// std::string text = "hello: " + req.Args();
// resp.SetCode(200);
// resp.SetHeader("Content-Type","text/plain");
// resp.SetHeader("Content-Length", std::to_string(text.size()));
// resp.SetText(text);
// }
// void Search(HttpRequest &req, HttpResponse &resp)
// {
// }
// http port
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cout << "Usage: " << argv[0] << " port" << std::endl;
exit(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);//《1》初始化一个HTTP类,并返回其指针---------》http.hpp
httpsvr->RegisterService("/login", Login); // <6> 注册服务路由-------->http.hpp,再回到main.cc
// httpsvr->RegisterService("/register", Register);
// httpsvr->RegisterService("/vip_check", VipCheck);
// httpsvr->RegisterService("/s", Search);
// httpsvr->RegisterService("/", Login);
httpsvr->Start();//<7>开始接收服务--------->http.hpp
return 0;
}
Util.hpp:
// 防止头文件重复包含的编译指令
#pragma once
// 包含标准输入输出流库
#include <iostream>
// 包含文件流操作库
#include <fstream>
// 包含字符串处理库
#include <string>
// 工具类声明
class Util
{
public:
// 静态方法:读取文件内容到字符串中,传递一个文件路径,和一个string,把文件的内容读取到string中
// 参数:filename - 文件名,out - 输出字符串指针
// 返回值:成功返回true,失败返回false
static bool ReadFileContent(const std::string &filename /*std::vector<char>*/, std::string *out)
{
// 获取文件大小
int filesize = FileSize(filename); // FileSize函数用于获取指定文件的大小(以字节为单位)。如果没打开就返回-1!!!!
// 检查文件大小是否有效
if (filesize > 0)
{
// 创建输入文件流对象
std::ifstream in(filename); // ifstream#include <fstream>
// 方式1:先声明后打开
// std::ifstream in; // 创建未关联文件的流对象
// in.open("example.txt"); // 打开文件
// 方式2:声明时直接打开(推荐)
// std::ifstream in("example.txt"); // 创建并立即打开文件,不用显示调用open函数打开文件
// 检查文件是否成功打开
if (!in.is_open())
return false;
// 调整输出字符串大小以容纳文件内容
out->resize(filesize);
// 将文件内容读取到字符串中
// 注意:这里使用了c_str()获取字符串底层指针,并进行强制类型转换
in.read((char *)(out->c_str()), filesize);
// istream& read(char* s, streamsize n);
// 在 C++ 中,out->c_str() 返回的是 const char* 类型指针,而 std::ifstream::read() 需要的是 char* 类型指针,因此需要进行强制类型转换。
// 关闭文件流
in.close(); // 记得要关闭!!!
}
else
{
// 文件大小为0或获取失败时返回false
return false;
}
// 读取成功返回true
return true;
}
// 静态方法:从大字符串中读取一行
// 参数:bigstr - 输入大字符串,out - 输出行字符串指针,sep - 行分隔符
// 返回值:成功返回true,失败返回false
static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep /*\r\n*/)
{
// 查找分隔符位置
auto pos = bigstr.find(sep);
// 如果没有找到分隔符则返回false
if (pos == std::string::npos) // std::string::npos 是 C++ 标准库中 std::string 类的一个静态常量成员,表示"未找到"或"无效位置"的特殊值。它是字符串操作中非常重要的一个标记值。
return false;
// 提取分隔符前的内容作为一行
*out = bigstr.substr(0, pos); // 起始永远是0
// 从原字符串中删除已读取的行和分隔符
bigstr.erase(0, pos + sep.size()); // 起始必须是0
// 读取成功返回true
return true;
}
// 静态方法:获取文件大小
// 参数:filename - 文件名
// 返回值:成功返回文件大小(字节),失败返回-1
static int FileSize(const std::string &filename)
{
// 以二进制模式打开文件
std::ifstream in(filename, std::ios::binary); // std::ios::binary 是 C++ 中文件打开模式的一个标志,它的作用是告诉文件流以二进制模式而非文本模式打开文件
// 文本模式(默认):
// 在某些系统(如 Windows)上,会进行换行符转换:
// 读取时,\r\n(Windows 换行)会被转换为 \n(C++ 标准换行)。
// 写入时,\n 会被转换为 \r\n。
// 可能在某些平台上处理特殊字符(如 EOF)时会有额外行为。
// 二进制模式:
// 完全按原样读写数据!!!!!,不做任何转换。
// 适合处理非文本文件(如图片!!!!!!!、音频、视频、压缩包等)。!!!!!!!!!!!!!!!!!!!!!!!
// 也适合需要精确控制文件内容的场景(如跨平台数据交换)。
// 检查文件是否成功打开
if (!in.is_open())
return -1;
// 将文件指针移动到文件末尾--------seekg移动函数!!!!
in.seekg(0, in.end);//in.end:基准位置(seek direction),这里是文件末尾(std::ios::end)。
// 获取当前指针位置(即文件大小)
int filesize = in.tellg();
// 将文件指针移回文件开头
in.seekg(0, in.beg);
// 关闭文件流
in.close();
// 返回文件大小
return filesize;
}
}; // 类定义结束
效果演示
- 在浏览器中输入115.120.238.130:8081
- 别忘了要先去云服务器官网开启安全组,这样浏览器才能访问服务端