Linux网络-------3.应⽤层协议HTTP
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
特性:指定资源经服务器端解析后返回响应内容。
GET方法详解
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:// 构造函数,初始化交互标志为falseHttpRequest() : _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);// 处理URIif (_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;}// 获取URIstd::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";elsereturn "";}// 构建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>把客户端发来的内容(需要反序列化)存入httpreqstrif (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 DEBUGstd::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.hpphttpsvr->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.hppreturn 0;
}
Util.hpp:
// 防止头文件重复包含的编译指令
#pragma once// 包含标准输入输出流库
#include <iostream>
// 包含文件流操作库
#include <fstream>
// 包含字符串处理库
#include <string>// 工具类声明
class Util
{
public:// 静态方法:读取文件内容到字符串中,传递一个文件路径,和一个string,把文件的内容读取到string中// 参数:filename - 文件名,out - 输出字符串指针// 返回值:成功返回true,失败返回falsestatic 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或获取失败时返回falsereturn false;}// 读取成功返回truereturn true;}// 静态方法:从大字符串中读取一行// 参数:bigstr - 输入大字符串,out - 输出行字符串指针,sep - 行分隔符// 返回值:成功返回true,失败返回falsestatic bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep /*\r\n*/){// 查找分隔符位置auto pos = bigstr.find(sep);// 如果没有找到分隔符则返回falseif (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// 读取成功返回truereturn true;}// 静态方法:获取文件大小// 参数:filename - 文件名// 返回值:成功返回文件大小(字节),失败返回-1static 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
- 别忘了要先去云服务器官网开启安全组,这样浏览器才能访问服务端