应用层协议中的 HTTP(超文本传输协议)。在互联网中,HTTP 协议是一个至关重要的一个协议,它定义了客户端与服务器之间如何进行通信,以交换或传输超文本。
本篇介绍了有关 URL 的相关知识,http 的报文格式,http 报头中的对应的方法以及 http 中的状态码。最后还泄漏一份关于 http 的网页代码(若想使用该代码成功的在浏览器中访问,需要将自己的 ip 和端口开放)。
HTTP 协议是客户端与服务器之间通信的基础,客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 协议是一个无连接(http 协议是基于连接的 tcp 协议,http 被称为无连接是因为在客户端和服务端不需要再次建立连接,直接忽略了向下的传输层)、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客服端的信息状态信息。
目录
URL
urlencode urldecode
HTTP报文格式
1. 请求报文
2. 响应报文
HTTP方法
1. GET方法(常用)
2. POST方法(常用)
3. PUT方法
4. HEAD方法
5. DELETE方法
6. OPTIONS
HTTP状态码
1. 信息性状态码 1XX
2. 成功状态码 2XX
3. 重定向状态码 3XX
4. 客户端错误状态码 4XX
5. 服务器错误状态码 5XX
HTTP常见报头(header)
1. connection报头
HTTP Code
Http.hpp 代码思路
1. HttpRequest
2. HttpResponse
3. HTTP报文发送
URL
URL 就是我们平时所指的网址,也被 称为统一资源定位符,如下:
如上所示,对于一个 url 由如上部分组成,首先的是协议名称;接着是域名,域名会自动的被解析为 ip 地址;还有隐藏起来的服务器端口号,一般不会显示,因为识别到协议的时候会自动添加上对应的端口号(比较出名的协议都有着对立独立的端口号);
其中最难理解的是带层次的文件路径,通常我们做服务器的是 Linux 系统,而在 Linux 系统下一切皆文件,想要把对应的资源传输到对应的客户端,就需要在对应的 Linux 系统下找到对应的文件目录,所以会有带层次从文件路径。接着是查询字符串,其实就是本地向对应的服务器提供的一些参数。
通过 URL 中的域名找到对应的 ip,协议找到对应的端口号,就可以定位到唯一的一台主机,接着通过文件路径就可以找到互联网中的唯一一个文件,所以 URL 也被称为统一资源定位符。
urlencode urldecode
我们在上文中已经说过,在带层次的文件路径后也就是 “ ?” 的后面提交的是我们用户的参数,对于用户的参数会用 && 符号进行连接,但是当我们需要提交的参数中就存在着对应的一些会起冲突的一些符号呢?比如 &=:\ 等等。
这个时候就会将对应特殊符号通过编码转换为对应的十六进制,如下图:
如上的特殊符号就是通过 urlencode 进行的加密,当我们的浏览器想要处理的时候就会通过 urldecode 进行解码(urldecode 就是 urlencode 的逆向过程)。
HTTP报文格式
HTTP 报文格式分为请求报文格式和响应报文格式。
1. 请求报文
对于 HTTP 请求报文格式如下:
其中首行:方法 + url + 版本;
请求报头:请求的各种属性,冒号分割的键值对,每组属性之间使用 \r\n 分隔,遇到空行表示请求报头结束;
请求正文:空行后面的内柔都是正文,正文允许为空字符串,若正文存在,则在请求报头中会有一个 Content-Length 属性来标识正文的长度。
如下为我使用代码捕捉的一个请求报文,如下:
如上所示,该报文的正文部分为空。
2. 响应报文
对于 HTTP 响应报文的格式如下:
如上所示,响应报文的格式和请求报文的格式基本一致。
其中,首航:版本 + 状态码 + 状态码解释
响应报头:响应的属性,冒号分割的键值对,每组属性之间使用 \r\n 进行分隔,遇到空行标识响应报头结束
正文:空行后面的内柔都是正文,正文允许为空字符串,若正文存在,则在报头中一定存在一个 Content-Length 属性标识正文的长度,通常对于服务前返回的报文的正文部分,可以说图片,可以是音频也可以是文本内柔。
其实不管是请求报文还是响应报文,都是由一个字符串所组成(只不过经过客户端或者服务端进行了序列化),虽然将其打印出来显示的是一行一行的,但是都是由一整个字符串所组成
HTTP方法
对于 HTTP 的方法而言就是在 HTTP 请求报文中请求行中的请求方法。对于 HTTP 的方法如下:
方法 说明 支持的HTTP版本
GET 获取资源 1.0、1.1
POST 传输实体主体 1.0、1.1
PUT 传输文件 1.0、1.1
HEAD 获得报文首部 1.0、1.1
DELETE 删除文件 1.0、1.1
OPTIONS 询问支持的方法 1.1
TRACE 追踪路径 1.1
CONNECT 要求用隧道协议连接代理 1.1
LINK 建立和资源之间的联系 1.0
UNLINK 断开连接关系 1.0
对于如上的方法,其中用得最多的就是 GET、POST 方法,对于各种方法的作用如下:
1. GET方法(常用)
作用:用于请求 URL 指定的资源
示例:GET /index.html HTTP/1.1
特性:将指定资源经过服务器端解析后返回相应内容
2. POST方法(常用)
作用:用于传输实体的主体,通常用于提交表单数据
示例:POST /submit.cgi HTTP/1.1
特性:可以传输大量的数据给服务器,并且将数据包含在请求体当中
对于 GET 方法和 POST 方法而言,GET 方法一般获取静态资源,可以通过 URL 来向服务器传递参数,而对于 POST 方法而言,可以通过请求的正文来进行传递参数,如下:
如上所示,当我们使用 GET 方法的时候,当我们使用一个密码登陆页面的时候,就会导致账户账号和密码直接显示在 URL 中,而当我们使用 POST 方法的时候,则不会将参数显示在 URL 中。同时也说明当我们想要传递参数,我们可以使用 POST 方法传递参数,因为 GET 方法使用 URL 传递参数,传递的参数量一定不大,而使用正文传递参数则可以很大。
3. PUT方法
作用:用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置
示例:可以发送大量的数据给服务器, 并且数据包含在请求体中
特性:可以发送大量的数据给服务器,并且数据包含在请求体当中
4. HEAD方法
作用:与 GET 方法类似,但不返回报文主体部分,仅返回响应头
示例:HEAD /index.html HTTP/1.1
特性:用于确认 URL 的有效性集资源更新的日期时间等
5. DELETE方法
作用:用于删除文件,是 PUT 的相反方法
示例:DELETE /example.html HTTP/1.1
特性:按请求 URL 删除指定的资源
6. OPTIONS
作用:用于查询针对请求 URL 指定的资源支持的方法
示例:OPTIONS * HTTP/1.1
特性:返沪允许的方法,如 GET、POST 等等
HTTP状态码
HTTP 的状态码是在响应报文中的状态行中包含的信息,对于不同的状态码对应着不同的状态表示,如下:
1. 信息性状态码 1XX
对于信息性状态码只有一个,为:100 含义为:Continue 应用场景为:上传大文件的时候,服务器会告诉客户端可以继续上传。
2. 成功状态码 2XX
成功状态码是以 2 开头的状态码,主要有三个,如下:
200 含义为:OK 应用场景为:访问网站首页,服务器返回网页内容
201 含义为:Created 应用场景为:发布新文章,服务器返回文章创建成功信息
204 含义为:No Content 应用场景:删除文章之后,服务器返回 “无内容” 表示操作成功
3. 重定向状态码 3XX
301 含义为:Moved Permanently 应用场景:网站更换域名之后,自动跳转到新域名;搜索引擎更新网站链接时使用 只要使用该状态码,则表示的是永久重定向
302 含义为:Found 或 See Other 应用场景:用户登陆成功后,重定向到用户首页 只要使用该状态码,则表示的是临时重定向
304 含义为:Not Modified 应用场景:浏览前的缓存机制,对未修改的资源返回 304 状态码 只要使用该状态码,则表示的是临时重定向
307 含义为:Temporary Redirect 应用场景:临时重定向资源到新的位置(临时重定向)
308 含义为:Permanent Redirect 应用场景:永久重定向资源到新的位置(永久重定向)
对于临时重定向和永久重定向,临时重定向的网站是临时的,而永久重定向的网站则是永久都切换到该网站了。
对于永久重定向以及临时重定向都依赖于报头中的 Location 选项,其中以 301 与 302 选项为例:
HTTP 状态码为 301 时,表示请求的资源以及被永久移动到新的位置,在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含新的 URL 地址,浏览器会自动重定向到该地址,同时缓存该重定向。
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n
HTTP 状态码为 302 时,表示请求的资源被临时的移动到新的位置,服务器同样会在响应中添加一个 Location 头部来指定资源的新位置,浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n
4. 客户端错误状态码 4XX
400 含义为:Bad Request 应用场景:填写表单时,格式不正确导致提交失败
401 含义为:Unauthorized 应用场景:访问需要登陆的页面的时候,未登陆或认证失败
403 含义为:Forbidden 应用场景:尝试访问没有查看权限的页面
404 含义为:Not Found 应用场景:访问不存在的网页链接
5. 服务器错误状态码 5XX
500 含义为:Internal Server Error 应用场景:服务器崩溃或数据错误导致页面无法加载
502 含义为:Bad Gateway 应用场景:使用代理服务器时,代理服务器无法从上游服务器获取有效响应
503 含义为:Service Unavailable 应用场景:服务器维护或者过载,暂时无法处理请求
HTTP常见报头(header)
在 HTTP 的请求报头以及响应报头之中都存在相应的报头,其中常见报头如下:
Content-Type: 数据类型(text/html/application/json等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
User-Agent: 声明用户的操作系统和浏览器版本信息
referer: 当前页面是从哪个页面跳转过来的
Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
Connection:请求完之后是关闭还是保持连接
Accept-Encoding:客户端支持的数据压缩格式,只要客户端服务器相适应,在传输过程中可以压缩传输的数据
Accept-Language:客户端可接受的语言类型
1. connection报头
对于 Connection 报头而言,主要作用是控制和管理客户端和服务器端之间的连接状态。
管理持久连接:Connection 字段还用于管理持久连接(长连接)。持久连接允许客户端和服务器在请求/响应完成之后不立即关闭 TCP 连接,以便于在同一个连接上发送多个请求和接收多个响应。(该作用对于需要长时间传输数据的连接效率较高)
协议版本:对于 HTTP/1.1 版本协议,默认使用持久连接。对于 HTTP/1.0 版本协议,默认连接是非持久的。
语法格式:
Connection: keep-alive: 表示希望保持连接以复用 TCP 连接。
Connection: close: 表示请求/响应完成后, 应该关闭 TCP 连接
对于其他的报头都是一些不怎么重要的报头,由于其内柔较多,本篇便不一一列举了,可参考这篇文章:HTTP协议格式详解之报头(HTTP header)、请求正文(body)_前端请求区分请求头和body、https://blog.csdn.net/m0_74209411/article/details/137247093#:~:text=HTTP%20%E8%AF%B7%E6%B1%82%E6%8A%A5%E5%A4%B4%E8%AF%A6
HTTP Code
以下为使用 http 协议写的一个网页代码,如下:
Sercer.cc
#include "TcpServer.hpp" // 通信管理,负责建立和断开通信 -> 会话层
#include "Http.hpp"
#include <memory>
#include "Log.hpp"using namespace log_ns;HttpResponce Login(HttpRequest& req) {// 则执行这个任务HttpResponce resp;std::cout << "Have got external news" << std::endl;req.GetRequestBody();std::cout << "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" << std::endl;resp.AddStatuscode(200, "OK");resp.AddContent("<html><h1>result done!</h1></html>");// 在这里可以进行程序替换、重定向,执行任务// pipe -> dup2 -> fork -> exec*(python/java/php)return resp;
}// 自己建立端口号
int main(int argc, char* argv[]) {if (argc != 2) {LOG(ERROR, "please input: ./server port\n");return 1;}uint16_t port = std::stoi(argv[1]);HttpServer http_server;http_server.AddService("/login", Login);service_t task = std::bind(&HttpServer::HttpServerHandler, &http_server, std::placeholders::_1);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(task, port);tsvr->Init();tsvr->Start();return 0;
}
Http.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <sstream>
#include <fstream>const static std::string base_sep = "\r\n";
const static std::string head_sep = ": ";
const static std::string space_sep = " ";
const static std::string httpversion = "HTTP/1.0";
const static std::string webrootdir = "wwwroot"; // web根目录
const static std::string suffix_sep = ".";
const static std::string defaultsuffix = ".default";
const static std::string homepages = "index.html";
const static std::string arg_sep = "?";class HttpRequest {
private:std::string Decode(std::string& packagestream) {// 对数据逐渐的减包auto pos = packagestream.find(base_sep);if (pos == std::string::npos) return std::string();std::string line = packagestream.substr(0, pos);// 将line从packagestream中删除packagestream.erase(0, line.size() + base_sep.size());return line.empty() ? base_sep : line;}void ParseRequestLine() {std::stringstream ss(_req_line);ss >> _method >> _url >> _version;ChangeGet();_path += _url;if (_path.back() == '/') {_path += homepages;}auto pos = _path.rfind(suffix_sep);if (pos == std::string::npos) {_suffix = defaultsuffix;} else {_suffix = _path.substr(pos);}}void ParseHeaderLine() {for (auto& head : _req_headers) {// 现在从中取出数据auto pos = head.find(head_sep);if (pos == std::string::npos) continue;std::string k = head.substr(0, pos);std::string v = head.substr(pos + head_sep.size());if (k.empty() || v.empty()) continue;_headers_kv[k] = v;}}// 若当前的方法是 GET,则将后面的参数都给放到正文中void ChangeGet() {if (strcasecmp(_method.c_str(), "GET") == 0) {auto pos = _url.find(arg_sep);if (pos != std::string::npos) {std::string arg_str = _url.substr(pos + arg_sep.size());_url.resize(pos);arg_str += base_sep;_body_text += arg_str;// std::cout << "body text: " << _body_text << std::endl;}}}public:HttpRequest() : _blank_line(base_sep),_path(webrootdir){}void Deserialize(std::string& packagestream) {_req_line = Decode(packagestream);do {std::string line = Decode(packagestream);if (line.empty()) break;else if (line == base_sep) break;_req_headers.push_back(line);} while (true);if (!packagestream.empty()) {_body_text = packagestream;}ParseRequestLine();ParseHeaderLine();}std::string Suffix() {return _suffix;}void GetRequestBody() {std::cout << "body text: " << _body_text << std::endl;}std::string Path() {return _path;}void PrintRequest() {// std::cout << _req_line << std::endl;// for (auto& head : _req_headers) {// std::cout << head << std::endl;// }// std::cout << _blank_line;// std::cout << _body_text << std::endl;std::cout << _method << " " << _url << " " << _version << std::endl;for (auto& it : _headers_kv) {std::cout << it.first << head_sep << it.second << std::endl;}std::cout << _blank_line;std::cout << _body_text << std::endl;}~HttpRequest() {}
private:std::string _req_line;std::vector<std::string> _req_headers;std::string _blank_line;std::string _body_text;// 更将详细的解析std::string _method;std::string _url;std::string _version;std::unordered_map<std::string, std::string> _headers_kv;std::string _path;std::string _suffix;
};class HttpResponce {
private:public:HttpResponce(): _version(httpversion),_blank_line(base_sep){}void AddStatuscode(int code, const std::string& desc) {_status_code = code;_desc_code = desc;}void AddHead(const std::string& key, const std::string& value) {_headers_kv[key] = value;}void AddContent(const std::string& content) {_body_text += content;}std::string Serialize() {_status_line = _version + space_sep + std::to_string(_status_code) + space_sep + _desc_code + base_sep;for (auto& head : _headers_kv) {std::string line = head.first + head_sep + head.second + base_sep;_resp_headers.emplace_back(line);}std::string responcepackage = _status_line;for (auto& line : _resp_headers) {responcepackage += line;}responcepackage += _blank_line;responcepackage += _body_text;return responcepackage;}~HttpResponce() {}
private:std::string _status_line;std::vector<std::string> _resp_headers;std::string _blank_line;std::string _body_text;// 真正的状态的std::string _version;int _status_code;std::string _desc_code;std::unordered_map<std::string, std::string> _headers_kv;
};using func_t = std::function<HttpResponce(HttpRequest&)>;class HttpServer {
private:// 从目录中读取信息std::string ReadContentFromRootdir(const std::string& path) {// 将文件以二进制形式打开// std::cout << "------------" << std::endl;std::ifstream in(path, std::ios::binary);if (!in.is_open()) return std::string();std::string content;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);content.resize(filesize);in.read((char*)content.c_str(), filesize);// std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;in.close();return content;}public:HttpServer() {_code_to_desc[100] = "Continue";_code_to_desc[200] = "OK";_code_to_desc[201] = "Created";_code_to_desc[204] = "No Content";_code_to_desc[301] = "Moved Permanently";_code_to_desc[302] = "Found";_code_to_desc[304] = "Not Modified";_code_to_desc[400] = "Bad Request";_code_to_desc[401] = "Unauthorized";_code_to_desc[403] = "Forbidden";_code_to_desc[404] = "Not Found";_code_to_desc[500] = "Internal Server Error";_code_to_desc[502] = "Bad Gateway";_code_to_desc[503] = "Service Unavailable";_mini_type[".html"] = "text/html";_mini_type[".jpg"] = "image/jpeg";_mini_type[".png"] = "image/png";_mini_type[".default"] = "text/html";}// #define TEST 1// 将数据进行转发std::string HttpServerHandler(std::string& reqstr) {
#ifdef TESTstd::cout << "-----------------------------" << std::endl;std::cout << reqstr;std::string responsestr = "HTTP/1.1 200 OK\r\n";responsestr += "Content-Type: text/html\r\n";responsestr += "\r\n";responsestr += "<html><h1>hello Linux, hello fans!</h1></html>";// Content-Length// return responsestr;return reqstr;
#elsestd::cout << "----------------------------------------" << std::endl;std::cout << reqstr << std::endl;HttpRequest Req;Req.Deserialize(reqstr);// 将正文消息打印出来// Req.GetRequestBody();// 将消息打印出来// std::cout << content << std::endl;HttpResponce Resp;// 要在这里测试重定向 --> 进入的我的主页面if (Req.Path() == "wwwroot/redir") {Resp.AddStatuscode(301, _code_to_desc[301]);std::string path = "https://blog.csdn.net/m0_74830524?spm=1000.2115.3001.5343";Resp.AddHead("Location", path);Resp.AddHead("Content-Type", defaultsuffix);return Resp.Serialize();}std::string content = ReadContentFromRootdir(Req.Path());if (content.empty()) {// 为空,为 404 not findif (!_service_list.count(Req.Path())) {Resp.AddStatuscode(404, _code_to_desc[404]);content = ReadContentFromRootdir("wwwroot/404.html");Resp.AddHead("Content-Length", std::to_string(content.size()));std::string suffix = Req.Suffix();Resp.AddHead("Content-Type", _mini_type[suffix]);Resp.AddHead("Set-Cookie", "username=zhangsan");Resp.AddContent(content);} else {// 存在我们则执行servicelist中的任务Resp = _service_list[Req.Path()](Req);}} else {// Resp.AddStatuscode(200, _code_to_desc[200]);Resp.AddHead("Content-Length", std::to_string(content.size()));Resp.AddHead("Content-Type", "text/html");Resp.AddContent(content);}return Resp.Serialize();
#endif}void AddService(const std::string& name, func_t service) {std::string servicename = webrootdir + name;_service_list[servicename] = service;}~HttpServer() {}private:std::unordered_map<int, std::string> _code_to_desc;std::unordered_map<std::string, std::string> _mini_type;std::unordered_map<std::string, func_t> _service_list;
};
InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>class InetAdrr {void ToHost(const struct sockaddr_in& addr) {// inet_ntoa 函数不是线程安全的函数,推荐使用 inet_ntop 函数// _ip = inet_ntoa(addr.sin_addr);char ip_buff[32];// 该函数是网络序列转主机序列 :network to processinet_ntop(AF_INET, &addr.sin_addr, ip_buff, sizeof(ip_buff));// 若想要将主机序列转换成网络序列使用函数 :// inet_pton(AF_INET, _ip.c_str(), (void*)&addr.sin_addr.s_addr); _ip = ip_buff;_port = ntohs(addr.sin_port);}
public:InetAdrr() {}InetAdrr(const struct sockaddr_in& addr) : _addr(addr){ToHost(_addr);}InetAdrr& operator=(const struct sockaddr_in& addr) {_addr = addr;return *this;}std::string Ip() const {return _ip;}bool operator==(const InetAdrr& addr) {return (_port == addr._port && _ip == addr._ip);}struct sockaddr_in Addr() const {return _addr;}std::string AddrString() const {return _ip + ":" + std::to_string(_port);}uint16_t Port() const {return _port;}~InetAdrr() {}
private:uint16_t _port;std::string _ip;struct sockaddr_in _addr;
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <cstring>
#include <fstream>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>namespace log_ns {enum { DEBUG = 1, INFO, WARNING, ERROR, FATAL };// 定义日子真正需要记录的信息struct LogMessage {std::string _level;int _id;std::string _filename;int _filenumber;std::string _curtime;std::string _log_message;};#define SCREEN_TYPE 1#define FILE_TYPE 2const std::string defaultlogfile = "./log.txt";pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER;class Log {private:std::string LevelToString(int level) {switch(level) {case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string CurTime() {// 获取当前的时间戳time_t curtime = time(nullptr);// 将当前时间戳转换成结构体struct tm* now = localtime(&curtime);char buff[128];snprintf(buff, sizeof(buff), "%d-%02d-%02d %02d:%02d:%02d", now->tm_year + 1900,now->tm_mon + 1,now->tm_mday,now->tm_hour,now->tm_min,now->tm_sec);return buff;}void Flush(const LogMessage& lg) {// 打印日志的时候可能存在线程安全,使用锁lock住pthread_mutex_lock(&log_lock);switch(_type) {case SCREEN_TYPE:FlushToScreen(lg);break;case FILE_TYPE:FlushToFile(lg);break;}pthread_mutex_unlock(&log_lock);}void FlushToFile(const LogMessage& lg) {std::ofstream out;out.open(_logfile, std::ios::app); // 文件的操作使用追加if (!out.is_open()) return;char buff[2024];snprintf(buff ,sizeof(buff), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curtime.c_str(),lg._log_message.c_str()); out.write(buff, strlen(buff));out.close();}void FlushToScreen(const LogMessage& lg) {printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curtime.c_str(),lg._log_message.c_str());}public:Log(std::string logfile = defaultlogfile): _type(SCREEN_TYPE),_logfile(logfile){}void Enable(int type) {_type = type;}void LoadMessage(std::string filename, int filenumber, int level, const char* format, ...) {LogMessage lg;lg._level = LevelToString(level);lg._filename = filename;lg._filenumber = filenumber;// 获取当前时间lg._curtime = CurTime();// std::cout << lg._curtime << std::endl;lg._id = getpid();// 获取可变参数va_list ap;va_start(ap, format);char buff[2048];vsnprintf(buff, sizeof(buff), format, ap);va_end(ap);lg._log_message = buff;// std::cout << lg._log_message;Flush(lg);}void ClearOurFile() {std::ofstream out;out.open(_logfile);out.close();}~Log() {}private:int _type;std::string _logfile;};Log lg;// LOG 宏
#define LOG(level, format, ...) \do \{ \lg.LoadMessage(__FILE__, __LINE__, level, format, ##__VA_ARGS__); \} while (0)#define EnableToScreen() \do \{ \lg.Enable(SCREEN_TYPE); \} while (0)#define EnableToFile() \do \{ \lg.Enable(FILE_TYPE); \} while (0)// 清理文件
#define ClearFile() \do \{ \lg.ClearOurFile(); \} while (0)
}
Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"namespace socket_ns {using namespace log_ns;enum {SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR};const int gblcklog = 8;class TcpSocket;class Socket;using ScokSPtr = std::shared_ptr<Socket>;// 模板方法模式class Socket {public:Socket() {}~Socket() {}virtual int CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int blcklog = gblcklog) = 0;virtual int CreateAccepte(InetAdrr* addr) = 0;virtual bool CreateConnector(uint16_t server_port, std::string server_ip) = 0;virtual ssize_t Recv(std::string* out) = 0;virtual ssize_t Send(std::string& in) = 0;virtual int GetSockfd() = 0;virtual void Close() = 0;public:void BuildListenSocket(uint16_t port, int blcklog = gblcklog) {// 分别是创建sockfd,然后将其绑定,然后listenCreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie(blcklog);}bool BuildCilentSocket(uint16_t server_port, std::string server_ip) {// 分别是创建sockfd,然后绑定,然后connnectCreateSocketOrDie();return CreateConnector(server_port, server_ip);}};class TcpSocket : public Socket {public:TcpSocket() {}TcpSocket(int sockfd): _sockfd(sockfd){}// 创建 sockfdint CreateSocketOrDie() override {_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) {LOG(FATAL, "create sockfd fail\n");exit(SOCKET_ERROR);}LOG(INFO, "get listensockfd success, sockfd: %d\n", _sockfd);return _sockfd;}// 绑定void CreateBindOrDie(uint16_t port) override {struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;int bind_n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if (bind_n < 0) {LOG(FATAL, "bind listensockfd fail, the reason: %s\n", strerror(errno));exit(BIND_ERROR);}LOG(INFO, "bind success\n");}// 绑定之后listenvoid CreateListenOrDie(int blcklog = gblcklog) override {int n = listen(_sockfd, blcklog);if (n < 0) {LOG(FATAL, "listen socket fail\n");exit(LISTEN_ERROR);}LOG(INFO, "listen sucess\n"); }int CreateAccepte(InetAdrr* addr) override {struct sockaddr_in peer;socklen_t len = sizeof(peer);// std::cout << "start accept" << std::endl;int sockfd = accept(_sockfd, (struct sockaddr*)&peer, &len);*addr = peer;// std::cout << "accept done" << std::endl;return sockfd;}bool CreateConnector(uint16_t server_port, std::string server_ip) override {struct sockaddr_in server;memset(&server, 0, sizeof(server));socklen_t len = sizeof(server);server.sin_family = AF_INET;server.sin_port = htons(server_port);// server.sin_addr.s_addr = inet_addr(server_ip.c_str());inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);int n = connect(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0) {return false; }return true;}ssize_t Recv(std::string* out) override {// 收消息,将收到的信息char buff[4096];ssize_t n = recv(_sockfd, buff, sizeof(buff), 0);if (n <= 0) return n;buff[n] = 0;*out += buff;return n;}ssize_t Send(std::string& in) override {ssize_t n = send(_sockfd, in.c_str(), in.size(), 0);return n;}int GetSockfd() override {return _sockfd;}void Close() override {close(_sockfd);}~TcpSocket() {// if (_sockfd < 0) close(_sockfd);}private:int _sockfd;};
}
TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"using namespace log_ns;
using namespace socket_ns;enum {SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR
};const int glistensockfd = -1;
const int gblcklog = 8;using service_t = std::function<std::string(std::string&)>;class TcpServer {
private:// 创建一个内部类struct ThreadData {ScokSPtr _tcp_socket;InetAdrr _addr;TcpServer* _tcp_point;ThreadData(const ScokSPtr& tcpsocket, const InetAdrr& addr, TcpServer* tcp): _tcp_socket(tcpsocket),_addr(addr),_tcp_point(tcp){}};static void* runServer(void* args) {// 将线程分离,就不用阻塞的join线程pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);// LOG(INFO, "the sockfd: %d\n", td->_sockfd);// 在这里接收消息,然后将消息放入到任务中std::string packagestream;td->_tcp_socket->Recv(&packagestream);// 对接收的消息进行处理,然后将处理结束的数据发送回来std::string package = td->_tcp_point->_task(packagestream);// 现在将package发送出去td->_tcp_socket->Send(package);td->_tcp_socket->Close();// close(); // 将其转化为tcpsocket变量则不需要显示的close了,因为已经析构了delete td;return nullptr;}public:// TcpServer(){}TcpServer(service_t task, uint16_t port): _task(task),_port(port),_isrunning(false),_tcp_socket(std::make_shared<TcpSocket>()){}void Init() {_tcp_socket->BuildListenSocket(_port);}void Start() {_isrunning = true;while (_isrunning) {// std::cout << "start run" << std::endl;InetAdrr addr;int sockfd = _tcp_socket->CreateAccepte(&addr);if (sockfd < 0) {LOG(ERROR, "%s get sockfd fail, the reason is %s\n", addr.AddrString().c_str(), strerror(errno));continue;}LOG(INFO, "get sockfd success, sockfd: %d\n", sockfd);// 为accept建立一个tcpsocket变量ScokSPtr tcp_accept = std::make_shared<TcpSocket>(sockfd);// 2. 多线程pthread_t tid;ThreadData* data = new ThreadData(tcp_accept, addr, this);pthread_create(&tid, nullptr, runServer, (void*)data);}_isrunning = false;}~TcpServer() {}
private:uint16_t _port;// int _listensocked;bool _isrunning;service_t _task;ScokSPtr _tcp_socket;
};
Thread.hpp
#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <cerrno>
#include "Log.hpp"// using func_t = std::function<void(const std::string& name, pthread_mutex_t* lock)>;
using func_t = std::function<void(const std::string& name)>;
using namespace log_ns;
// typedef void*(*func_t)(void*);const pthread_t ctid = -1;class Thread {
private:void excute() {// std::cout << _name << " begin to run" << std::endl;// LOG(INFO, "%s begin to run\n", _name.c_str());_isrunning = true;_func(_name);_isrunning = false;}static void* ThreadRoutine(void* args) {Thread* self = static_cast<Thread*>(args);self->excute();return nullptr;}
public:Thread(func_t func, const std::string& name) : _func(func),_isrunning(false),_tid(ctid),_name(name){}~Thread() {}void Start() {// 创建之后就开始运行了int n = pthread_create(&_tid, nullptr, ThreadRoutine, (void*)this);if (n != 0) {std::cout << "thread create failed!!!" << std::endl;exit(1);}}void Stop() {// 将线程暂停,使用if (_isrunning == false) return;// std::cout << _name << " stop " << std::endl;int n = ::pthread_cancel(_tid);if (n != 0) {std::cout << "thread stop failed" << std::endl;}_isrunning = false;}void Join() {// 线程等待,if (_isrunning) return;int n = pthread_join(_tid, nullptr);if (n != 0) {std::cout << "thread wait failed!!!" << strerror(errno) << std::endl;}// std::cout << _name << " join " << std::endl;}std::string Status() {if (_isrunning) return "running";else return "sleep";}
private:pthread_t _tid;func_t _func;bool _isrunning;std::string _name;
};
makefile
.PHONY:all
all:server server:Server.ccg++ -o $@ $^ -std=c++14 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f server
404.html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>(404) The page you were looking for doesn't exist.</title><link rel="stylesheet" type="text/css" href="//cloud.typography.com/746852/739588/css/fonts.css" /><style type="text/css">html,body {margin: 0;padding: 0;height: 100%;}body {font-family: "Whitney SSm A", "Whitney SSm B", "Helvetica Neue", Helvetica, Arial, Sans-Serif;background-color: #2D72D9;color: #fff;-moz-font-smoothing: antialiased;-webkit-font-smoothing: antialiased;}.error-container {text-align: center;height: 100%;}@media (max-width: 480px) {.error-container {position: relative;top: 50%;height: initial;-webkit-transform: translateY(-50%);-ms-transform: translateY(-50%);transform: translateY(-50%);}}.error-container h1 {margin: 0;font-size: 130px;font-weight: 300;}@media (min-width: 480px) {.error-container h1 {position: relative;top: 50%;-webkit-transform: translateY(-50%);-ms-transform: translateY(-50%);transform: translateY(-50%);}}@media (min-width: 768px) {.error-container h1 {font-size: 220px;}}.return {color: rgba(255, 255, 255, 0.6);font-weight: 400;letter-spacing: -0.04em;margin: 0;}@media (min-width: 480px) {.return {position: absolute;width: 100%;bottom: 30px;}}.return a {padding-bottom: 1px;color: #fff;text-decoration: none;border-bottom: 1px solid rgba(255, 255, 255, 0.6);-webkit-transition: border-color 0.1s ease-in;transition: border-color 0.1s ease-in;}.return a:hover {border-bottom-color: #fff;}</style>
</head><body><div class="error-container"><h1>404</h1><p class="return">Take me back to <a href="/">designernews.co</a></p>
</div></body>
</html>
content.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>内容页面</title>
</head>
<body><h1>内容页面</h1><a href="/register.html">进入注册页面</a>
</body>
</html>
index.html
<!DOCTYPE html>
<html>
<head><title>桀桀桀桀桀桀</title> <meta charset="UTF-8">
</head>
<body><div id="container" style="width:800px"><div id="header" style="background-color:#FFA500;"><h1 style="margin-bottom:0;">我的网站</h1></div><div id="menu" style="background-color:#FFD700;height:200px;width:100px;float:left;"><b>Menu</b><br>HTML<br>CSS<br>JavaScript</div><div id="content" style="background-color:#EEEEEE;height:200px;width:700px;float:left;">内容就在这里</div><div id="footer" style="background-color:#ffa500;clear:both;text-align:center;">Copyright © 桀桀桀桀桀桀</div></div><a href="/login.html">点击测试: 登陆页面</a><div><img src="/image/1.png" alt="一张图片"><!-- <img src="/image/2.jpg" alt="第二张图片"> --></div><div></div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登陆页面</title>
</head><body><h1>登陆页面</h1><a href="/content.html">进入内容页面</a><br><a href="/a/b/c.html">测试404</a><br><a href="/redir">测试重定向</a><br><div><!-- 默认就是GET --><form action="/login" method="POST">用户名: <input type="text" name="username" value=""><br>密码: <input type="password" name="userpasswd" value=""><br><input type="submit" value="提交"></form></div></body></html>
register.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>注册页面</title>
</head>
<body><h1>注册页面</h1><a href="/">回到首页</a>
</body>
</html>
当前目录树状图如下:
对于如上的 wwwroot 目录下的 image 目录中的图片是啥都可以,但是想要成功的让图片显示在网页中,命名一定要一致,测试结果如下:
如上在浏览器中填入的 ip 和 port(端口),是我运行程序的 ip,port 为自己设定的,若想要使用如上代码成功在浏览器中访问到对应的网页资源,需要将自己机器的 ip 和 port 开放(我的为华为云服务器,在华为云官方中开放的端口,想要开放自己的 ip 和 端口可在网上搜索。)
Http.hpp 代码思路
http 协议代码实现的原理本质就是由 http 请求报文和 http 响应报文以及数据发送三个模块组成的。
1. HttpRequest
按照请求报文的思路将发送过来的请求报文给解析,先获取请求行,然后获取请求报头的键值和关键值,接着获取请求正文。
但是对于进行如上的步骤之前,我们需要先将在网络中传输的序列化的数据进行反序列化,反序列化就只需要按照 http 报文格式进行拆解获取即可,因为网络中传输的序列化的数据其实就是字符串。
所以只需要将报文中的各种信息抽象为对应的变量即可。代码如下,对于每个模块的代码都有对应的注释
const static std::string base_sep = "\r\n";
const static std::string head_sep = ": ";
const static std::string space_sep = " ";
const static std::string httpversion = "HTTP/1.0";
const static std::string webrootdir = "wwwroot"; // web根目录
const static std::string suffix_sep = ".";
const static std::string defaultsuffix = ".default";
const static std::string homepages = "index.html";
const static std::string arg_sep = "?";class HttpRequest {
private:// 解包一行的http报文std::string Decode(std::string& packagestream) {// 对数据逐渐的解包auto pos = packagestream.find(base_sep);if (pos == std::string::npos) return std::string();std::string line = packagestream.substr(0, pos);// 将line从packagestream中删除packagestream.erase(0, line.size() + base_sep.size());return line.empty() ? base_sep : line;}// 解析请求行void ParseRequestLine() {std::stringstream ss(_req_line);ss >> _method >> _url >> _version;ChangeGet();_path += _url;// 若当前请求的资源为根目录下的资源,直接让其跳转到对应的主页面if (_path.back() == '/') {_path += homepages;}// 获取对应资源文件的后缀auto pos = _path.rfind(suffix_sep);if (pos == std::string::npos) {_suffix = defaultsuffix;} else {_suffix = _path.substr(pos);}}// 解析请求报头void ParseHeaderLine() {for (auto& head : _req_headers) {// 现在从中取出数据auto pos = head.find(head_sep);if (pos == std::string::npos) continue;std::string k = head.substr(0, pos);std::string v = head.substr(pos + head_sep.size());if (k.empty() || v.empty()) continue;_headers_kv[k] = v;}}// 若当前的方法是 GET,则将后面的参数都给放到正文中void ChangeGet() {if (strcasecmp(_method.c_str(), "GET") == 0) {auto pos = _url.find(arg_sep);if (pos != std::string::npos) {std::string arg_str = _url.substr(pos + arg_sep.size());_url.resize(pos);arg_str += base_sep;_body_text += arg_str;// std::cout << "body text: " << _body_text << std::endl;}}}public:HttpRequest() : _blank_line(base_sep),_path(webrootdir){}// 将请求报文进行反序列化void Deserialize(std::string& packagestream) {_req_line = Decode(packagestream);// 获取请求报头行do {std::string line = Decode(packagestream);if (line.empty()) break;else if (line == base_sep) break;_req_headers.push_back(line);} while (true);if (!packagestream.empty()) {_body_text = packagestream;}// 解析请求行和请求报头ParseRequestLine();ParseHeaderLine();}std::string Suffix() {return _suffix;}void GetRequestBody() {std::cout << "body text: " << _body_text << std::endl;}std::string Path() {return _path;}// DEBUG 打印解析出来的请求报文void PrintRequest() {// std::cout << _req_line << std::endl;// for (auto& head : _req_headers) {// std::cout << head << std::endl;// }// std::cout << _blank_line;// std::cout << _body_text << std::endl;std::cout << _method << " " << _url << " " << _version << std::endl;for (auto& it : _headers_kv) {std::cout << it.first << head_sep << it.second << std::endl;}std::cout << _blank_line;std::cout << _body_text << std::endl;}~HttpRequest() {}
private:std::string _req_line; // 请求行std::vector<std::string> _req_headers; // 请求报头std::string _blank_line; // 空行std::string _body_text; // 请求正文// 更将详细的解析,将如上的四种变量解析如下std::string _method; // 获取方法:GET、POST、DELETE……std::string _url; // 获取urlstd::string _version; // http版本std::unordered_map<std::string, std::string> _headers_kv; // 获取http报头std::string _path; // 解析url中的资源路径std::string _suffix; // 获取资源文件的后缀
};
2. HttpResponse
对于 http 的响应报文而言,同样和请求报文的处理方式一样,不过是相反的步骤,我们需要将需要发送出去的报文给序列化,首先需要按照响应报文的格式将对应的状态行,响应报头,空行以及正文按照对应的格式组装在一起,然后在返回即可。
同时将对应的报文格式中的各种信息抽象为具体的变量,代码如下:
class HttpResponce {
private:public:HttpResponce(): _version(httpversion),_blank_line(base_sep){}// 增加状态码以及对应的状态描述void AddStatuscode(int code, const std::string& desc) {_status_code = code;_desc_code = desc;}// 添加对应的响应报头void AddHead(const std::string& key, const std::string& value) {_headers_kv[key] = value;}// 添加对应的文本信息void AddContent(const std::string& content) {_body_text += content;}// 将报文序列化,然后返回std::string Serialize() {// 序列化状态行_status_line = _version + space_sep + std::to_string(_status_code) + space_sep + _desc_code + base_sep;for (auto& head : _headers_kv) {std::string line = head.first + head_sep + head.second + base_sep;_resp_headers.emplace_back(line);}// 序列换响应报头std::string responcepackage = _status_line;for (auto& line : _resp_headers) {responcepackage += line;}responcepackage += _blank_line;responcepackage += _body_text;return responcepackage;}~HttpResponce() {}
private:std::string _status_line; // 状态行std::vector<std::string> _resp_headers; // 响应报头std::string _blank_line; // 空行std::string _body_text; // 正文// 真正的状态的std::string _version; // http版本int _status_code; // 状态码std::string _desc_code; // 状态描述std::unordered_map<std::string, std::string> _headers_kv; // 响应报头的键值和关键值
};
3. HTTP报文发送
对于 http 报文转发,我们只需要调用 HttpRequest 中的反序列化,然后获取对应请求报文中的资源路径,通过路径获取对应的资源,然后在使用 HttpResponce 将资源和对应的报头组合在一起,将其发送出去。
还在对应的代码中加入了重定向,测试 404,以及 Cookie 等操作,代码如下:
using func_t = std::function<HttpResponce(HttpRequest&)>;class HttpServer {
private:// 从目录中读取信息std::string ReadContentFromRootdir(const std::string& path) {// 将文件以二进制形式打开// std::cout << "------------" << std::endl;std::ifstream in(path, std::ios::binary);if (!in.is_open()) return std::string();std::string content;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);content.resize(filesize);in.read((char*)content.c_str(), filesize);// std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;in.close();return content;}public:// 构造函数将状态码与状态描述 文件后缀与网页类型初始化HttpServer() {_code_to_desc[100] = "Continue";_code_to_desc[200] = "OK";_code_to_desc[201] = "Created";_code_to_desc[204] = "No Content";_code_to_desc[301] = "Moved Permanently";_code_to_desc[302] = "Found";_code_to_desc[304] = "Not Modified";_code_to_desc[400] = "Bad Request";_code_to_desc[401] = "Unauthorized";_code_to_desc[403] = "Forbidden";_code_to_desc[404] = "Not Found";_code_to_desc[500] = "Internal Server Error";_code_to_desc[502] = "Bad Gateway";_code_to_desc[503] = "Service Unavailable";_mini_type[".html"] = "text/html";_mini_type[".jpg"] = "image/jpeg";_mini_type[".png"] = "image/png";_mini_type[".default"] = "text/html";}// #define TEST 1// 将数据进行转发std::string HttpServerHandler(std::string& reqstr) {
#ifdef TESTstd::cout << "-----------------------------" << std::endl;std::cout << reqstr;std::string responsestr = "HTTP/1.1 200 OK\r\n";responsestr += "Content-Type: text/html\r\n";responsestr += "\r\n";responsestr += "<html><h1>hello Linux, hello fans!</h1></html>";// Content-Length// return responsestr;return reqstr;
#elsestd::cout << "----------------------------------------" << std::endl;std::cout << reqstr << std::endl;// 将对应的请求报文给反序列化HttpRequest Req;Req.Deserialize(reqstr);// 将正文消息打印出来// Req.GetRequestBody();// 将消息打印出来// std::cout << content << std::endl;HttpResponce Resp;// 这里测试重定向 --> 进入的我的主页面if (Req.Path() == "wwwroot/redir") {// 301 表示永久重定向Resp.AddStatuscode(301, _code_to_desc[301]);std::string path = "https://blog.csdn.net/m0_74830524?spm=1000.2115.3001.5343";Resp.AddHead("Location", path);Resp.AddHead("Content-Type", defaultsuffix);return Resp.Serialize();}std::string content = ReadContentFromRootdir(Req.Path());if (content.empty()) {// 为空,为 404 not findif (!_service_list.count(Req.Path())) {Resp.AddStatuscode(404, _code_to_desc[404]);content = ReadContentFromRootdir("wwwroot/404.html");Resp.AddHead("Content-Length", std::to_string(content.size()));std::string suffix = Req.Suffix();Resp.AddHead("Content-Type", _mini_type[suffix]);Resp.AddHead("Set-Cookie", "username=zhangsan");Resp.AddContent(content);} else {// 存在我们则执行servicelist中的任务Resp = _service_list[Req.Path()](Req);}} else {// 正文内容存在,直接获取Resp.AddStatuscode(200, _code_to_desc[200]);Resp.AddHead("Content-Length", std::to_string(content.size()));Resp.AddHead("Content-Type", "text/html");Resp.AddContent(content);}return Resp.Serialize();
#endif}// 增加任务void AddService(const std::string& name, func_t service) {std::string servicename = webrootdir + name;_service_list[servicename] = service;}~HttpServer() {}private:std::unordered_map<int, std::string> _code_to_desc; // 状态码和状态描述的映射std::unordered_map<std::string, std::string> _mini_type; // 文件后缀与网页类型的映射std::unordered_map<std::string, func_t> _service_list; // 任务类型的映射
};