【计算机网络】应用层协议Http——构建Http服务服务器
🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:计算机网络
🌹往期回顾🌹: 【Linux笔记】——进程间关系与守护进程
🔖流水不争,争的是滔滔不息
- 一、Http协议
- URL
- urlencode 和 urldecode
- HTTP协议请求与响应格式
- HTTP常见方法
- Http状态码
- 关于重定向
- HTTP常见Header
- 二、构建http服务器
- Util.hpp 工具类
- Http.hpp应用层Http协议的代码
- **HttpRequest 处理客户端http请求**
- HttpRespose.hpp服务端做应答
- Http类最核心调度逻辑
- **main.cc**
- 三、Cookie和Session
一、Http协议
HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。它是万维网(WWW)数据通信的基础,设计用于客户端与服务器之间的请求-响应交互。
HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。
URL
http://www.example.com:8080/index.html
我们平常所俗称的“网址”就是说的URL。上面的url,https是采用的协议,www.example.com是域名也就是ip,端口一般是:后面的,index.html是路径,也就是用户要访问的超文本文件所处在目标主机的位置。域名是ip地址具有唯一性,路径是目标主机上特定路径的一个文件,这两个就表示了全网内唯一的文件。
但是我们发现好多URL没有端口号啊
http://www.example.com/index.html
实际上就是访问:http://www.example.com/index.html,因为有些协议有默认的端口号如http默认端口号是80.https默认端口号是443。
urlencode 和 urldecode
像 / ? : 等这样的字符, 已经被 url 当做特殊意义理解了,因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
将需要转码的字符转为 16 进制, 然后从右到左, 取 4 位(不足 4 位直接处理), 每 2 位做一位, 前面加上%, 编码成%XY 格式。
HTTP协议请求与响应格式
HTTP请求
首行:协议+url+协议版本
请求报头(Header):请求的属性,冒号分割的键值对;每组属性之间用\r\n分割,遇到空行表示Header部分结束了,如上图。
请求正文(Body):空行后面的内容都是Body,Body允许为空字符串,如果Body存在,则在Header 中会有一个 Content-Length 属性来标识 Body 的长度。
换行符和空行等特殊字符,是http能够做到报头和有效载荷的分离。http协议,序列化和反序列化用的是特殊字符进行子串拼接,不依赖第三方库
HTTP响应
首行: [版本号] + [状态码] + [状态码解释]。
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行表示 Header 部分结束。
Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在Header 中会有一个 Content-Length 属性来标识 Body 的长度; 如果服务器返回了一个 html 页面, 那么 html 页面内容就是在 body 中。
HTTP常见方法
GET方法
你访问网址的时候,其实就是发了个 GET 请求。
用于请求URL指定的资源。
POST方法
用于传输实体的主体, 通常用于提交表单数据。
就是提交数据,参数放在请求正文中。
PUT 方法
用于传输文件, 将请求报文主体中的文件保存到请求 URL 指定的位置。
HEAD 方法
与 GET 方法类似, 但不返回报文主体部分, 仅返回响应头。
DELETE 方法
用于删除文件, 是 PUT 的相反方法
OPTIONS 方法
用于查询针对请求 URL 指定的资源支持的方法。
Http状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)。
关于重定向
HTTP 状态码 301(永久重定向) 和 302(临时重定向) 都依赖 Location 选项。
HTTP 状态码 301(永久重定向)
当服务器返回 HTTP 301 状态码时, 表示请求的资源已经被永久移动到新的位置。
在这种情况下, 服务器会在响应中添加一个 Location 头部, 用于指定资源的新位置。 这个 Location 头部包含了新的 URL 地址, 浏览器会自动重定向到该地址。
例如, 在 HTTP 响应中, 可能会看到类似于以下的头部信息
HTTP/1.1 301 Moved Permanently\r\n Location: https://www.new-url.com\r\n
HTTP 状态码 302(临时重定向)
当服务器返回 HTTP 302 状态码时, 表示请求的资源临时被移动到新的位置。
同样地, 服务器也会在响应中添加一个 Location 头部来指定资源的新位置。 浏览器会暂时使用新的 URL 进行后续的请求, 但不会缓存这个重定向。
例如, 在 HTTP 响应中, 可能会看到类似于以下的头部信息
HTTP/1.1 302 Found\r\n Location: https://www.new-url.com\r\n
无论是 HTTP 301 还是 HTTP 302 重定向, 都需要依赖 Location 选项来指定资源的新位置。 这个 Location 选项是一个标准的 HTTP 响应头部, 用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。
临时重定向不是永久的,用在比如登录调整语言切换。永久重定向是永久的,用在域名变更。理解一下这两个用途的不同,就可以窥见两个重定向的不同了。
HTTP常见Header
Content-Type: 数据类型(text/html 等);
Content-Length: Body 的长度;
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
关于 connection 报头
HTTP 中的 Connection 字段是 HTTP 报文头的一部分, 它主要用于控制和管理客户端与服务器之间的连接状态
核心作用
- 管理持久连接: Connection 字段还用于管理持久连接(也称为长连接) 。 持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接, 以便在同一个连接上发送多个请求和接收多个响应。
持久连接(长连接)
- HTTP/1.1: 在 HTTP/1.1 协议中, 默认使用持久连接。 当客户端和服务器都不明确指定关闭连接时, 连接将保持打开状态, 以便后续的请求和响应可以复用同一个连接。
- HTTP/1.0: 在 HTTP/1.0 协议中, 默认连接是非持久的。 如果希望在 HTTP/1.0上实现持久连接, 需要在请求头中显式设置 Connection: keep-alive。
语法格式
• Connection: keep-alive: 表示希望保持连接以复用 TCP 连接。
• Connection: close: 表示请求/响应完成后, 应该关闭 TCP 连接。
二、构建http服务器
基于TCP通信,服务器的传输层是用之前写的模版方法进行构造。
Util.hpp 工具类
#pragma once #include #include #include using namespace std; class Util { public: static bool Getoneline(string& in,string* out,const string& sep) //获取报文请求行 { auto pos=in.find(sep); if(pos==string::npos) { return false; } *out+=in.substr(0,pos); in.erase(0, pos + sep.size()); return true; } static bool ReadFileConet(string &filename, string *out) // 把html文件写入应答正文 { // version2 : 以二进制方式进行读取 int filesize = FileSize(filename); if (filesize > 0) { std::ifstream in(filename); if (!in.is_open()) return false; out->resize(filesize); in.read((char *)(out->c_str()), filesize); in.close(); } else { return false; } return true; } static int FileSize(const std::string &filename) //获取正文长度 { std::ifstream in(filename, std::ios::binary); if (!in.is_open()) return -1; in.seekg(0, in.end); int filesize = in.tellg(); in.seekg(0, in.beg); in.close(); return filesize; } };
Getoneline,是解析一行字符串,比如提取请求行。
ReadFileConet,读取HTML、图片等资源文件。
FileSize,获取文件大小,设置Content-Length。
下面具体用到再具体问题具体分析。
Http.hpp应用层Http协议的代码
#pragma once #include "Tcpserver.hpp" #include "Util.hpp" #include "Log.hpp" #include #include using namespace std; using namespace SocketModule; using namespace LogModule; const std::string gspace = " "; const std::string glinespace = "\r\n"; const std::string glinesep = ": "; const string wwwroot = "./wwwroot"; const string homepage = "index.html"; const string page_404 = "/404.html"; class HttpRequest //处理客户端http请求 { public: HttpRequest() : _is_interact(false) { } string Serialize() { return string(); } void ParseReqLin(string &req_line) { stringstream ss(req_line); ss >> _method >> _uri >> _version; } bool Deserialize(string &in) //反序列化 { string req_line; bool r = Util::Getoneline(in, &req_line, glinespace); //获取请求行 ParseReqLin(req_line); if (_uri == "/") { _uri = wwwroot + _uri + homepage; } else { _uri = wwwroot + _uri; } // _uri: ./wwwroot/login?username=zhangsan&password=123456 LOG(LogLevel::DEBUG) return true; } _args = _uri.substr(pos + temp.size()); _uri = _uri.substr(0, pos); _is_interact = true; return true; } string Uri() { return _uri; } bool isInteract() { return _is_interact; } std::string Args() { return _args; } ~HttpRequest() { } public: string _method; //请求方法 string _uri; //URI string _version; //http 版本 unordered_map public: HttpRespose() : _blankline(glinespace) { } string Serialize() //序列化 { string status_line = _version + gspace + to_string(_code) + gspace + _desc + glinespace; string kv_line; for (auto &head : _headers) { string head_line = head.first + glinesep + gspace + head.second + glinespace; kv_line += head_line; } string resstr = status_line + kv_line + _blankline + _text; return resstr; } bool Deserialize(string &in) { return true; } void SetTargetFile(const string &target) { _targetfile = target; } void SetCode(int code) { _code = code; switch (_code) { case 200: _desc = "OK"; break; case 404: _desc = "Not Found"; break; case 302: _desc = "See Other"; break; default: break; } } void SetHeader(const string &key, const string &value) { auto iter = _headers.find(key); if (iter != _headers.end()) { return; } _headers.insert(make_pair(key, value)); } string Uri2Suffix(const string &targetfile) { auto pos = targetfile.rfind('.'); if (pos == string::npos) { return "text/html"; } 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 ""; } bool MakeResponse() { _version = "HTTP/1.1"; if (_targetfile == "./wwwroot/redir_test") // 测试重定向 { SetCode(302); SetHeader("Location", "https://www.qq.com/"); return true; } int filesize; string suffix; bool re = Util::ReadFileConet(_targetfile, &_text);//读取目标文件的内容 if (!re) { // SetCode(302); // SetHeader("Location","http://120.46.84.37:8080/404.html"); //通过重定向方式访问404页面 SetCode(404); _targetfile = wwwroot + page_404; Util::ReadFileConet(_targetfile, &_text); filesize = Util::FileSize(_targetfile); // 拿到正文大小 suffix = Uri2Suffix(_targetfile); SetHeader("Content-Length", to_string(filesize)); SetHeader("Content-Type", suffix); } else { SetCode(200); filesize = Util::FileSize(_targetfile); // 拿到正文大小 suffix = Uri2Suffix(_targetfile); SetHeader("Content-Length", to_string(filesize)); SetHeader("Content-Type", suffix); } return true; } void SetText(const std::string &t) { _text = t; } ~HttpRespose() { } public: string _version; int _code; string _desc; unordered_map public: Http(uint16_t port) : _tserver(make_unique } void HttpRequestserver(shared_ptr string reqstr; int n = sockfd-Recv(&reqstr); if (n 0) { cout if (_route.find(req.Uri()) == _route.end()) { } else { _route[req.Uri()](req, resq); string response_str = resq.Serialize(); sockfd-Send(response_str); } } else // 静态 { resq.SetTargetFile(req.Uri()); if (resq.MakeResponse()) // 封装报文 { string resq_str = resq.Serialize(); sockfd-Send(resq_str); } } } } void Start() { _tserver-Start([this](shared_ptr this-HttpRequestserver(sockfd, client); }); } void RegisterService(const string name, http_func_t h) { string key = wwwroot + name; auto iter = _route.find(key); if (iter == _route.end()) { _route.insert(make_pair(key, h)); } } ~Http() { } private: unique_ptr public: HttpRequest() : _is_interact(false) { } string Serialize() { return string(); } void ParseReqLin(string &req_line) { stringstream ss(req_line); ss _method _uri >> _version; } bool Deserialize(string &in) //反序列化 { string req_line; bool r = Util::Getoneline(in, &req_line, glinespace); //获取请求行 ParseReqLin(req_line); if (_uri == "/") { _uri = wwwroot + _uri + homepage; } else { _uri = wwwroot + _uri; } // _uri: ./wwwroot/login?username=zhangsan&password=123456 LOG(LogLevel::DEBUG) return true; } _args = _uri.substr(pos + temp.size()); _uri = _uri.substr(0, pos); _is_interact = true; return true; } string Uri() { return _uri; } bool isInteract() { return _is_interact; } std::string Args() { return _args; } ~HttpRequest() { } public: string _method; //请求方法 string _uri; //URI string _version; //http 版本 unordered_map public: HttpRespose() : _blankline(glinespace) { } string Serialize() //序列化 { string status_line = _version + gspace + to_string(_code) + gspace + _desc + glinespace; string kv_line; for (auto &head : _headers) { string head_line = head.first + glinesep + gspace + head.second + glinespace; kv_line += head_line; } string resstr = status_line + kv_line + _blankline + _text; return resstr; } bool Deserialize(string &in) { return true; } void SetTargetFile(const string &target) { _targetfile = target; } void SetCode(int code) { _code = code; switch (_code) { case 200: _desc = "OK"; break; case 404: _desc = "Not Found"; break; case 302: _desc = "See Other"; break; default: break; } } void SetHeader(const string &key, const string &value) { auto iter = _headers.find(key); if (iter != _headers.end()) { return; } _headers.insert(make_pair(key, value)); } string Uri2Suffix(const string &targetfile) { auto pos = targetfile.rfind('.'); if (pos == string::npos) { return "text/html"; } 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 ""; } bool MakeResponse() { _version = "HTTP/1.1"; if (_targetfile == "./wwwroot/redir_test") // 测试重定向 { SetCode(302); SetHeader("Location", "https://www.qq.com/"); return true; } int filesize; string suffix; bool re = Util::ReadFileConet(_targetfile, &_text);//读取目标文件的内容 if (!re) { // SetCode(302); // SetHeader("Location","http://120.46.84.37:8080/404.html"); //通过重定向方式访问404页面 SetCode(404); _targetfile = wwwroot + page_404; Util::ReadFileConet(_targetfile, &_text); filesize = Util::FileSize(_targetfile); // 拿到正文大小 suffix = Uri2Suffix(_targetfile); SetHeader("Content-Length", to_string(filesize)); SetHeader("Content-Type", suffix); } else { SetCode(200); filesize = Util::FileSize(_targetfile); // 拿到正文大小 suffix = Uri2Suffix(_targetfile); SetHeader("Content-Length", to_string(filesize)); SetHeader("Content-Type", suffix); } return true; } void SetText(const std::string &t) { _text = t; } ~HttpRespose() { } public: string _version; int _code; string _desc; unordered_map public: Http(uint16_t port) : _tserver(make_unique } void HttpRequestserver(shared_ptr string reqstr; int n = sockfd-Recv(&reqstr); if (n 0) { cout if (_route.find(req.Uri()) == _route.end()) { } else { _route[req.Uri()](req, resq); string response_str = resq.Serialize(); sockfd-Send(response_str); } } else // 静态 { resq.SetTargetFile(req.Uri()); if (resq.MakeResponse()) // 封装报文 { string resq_str = resq.Serialize(); sockfd-Send(resq_str); } } } } void Start() { _tserver-Start([this](shared_ptr this-HttpRequestserver(sockfd, client); }); } void RegisterService(const string name, http_func_t h) { string key = wwwroot + name; auto iter = _route.find(key); if (iter == _route.end()) { _route.insert(make_pair(key, h)); } } ~Http() { } private: unique_ptr _tserver-Start([this](shared_ptr this-HttpRequestserver(sockfd, client); }); } LOG(LogLevel::DEBUG) uint16_t port = std::stoi(argv[1]); Enable_Console_Log_Strategy(); // 启用控制台输出 std::unique_ptr LOG(LogLevel::DEBUG) string key = wwwroot + name; auto iter = _route.find(key); if (iter == _route.end()) { _route.insert(make_pair(key, h)); } }
- 管理持久连接: Connection 字段还用于管理持久连接(也称为长连接) 。 持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接, 以便在同一个连接上发送多个请求和接收多个响应。