【Linux网络编程】应用层协议-----HTTP协议
文章目录
- 一、前置知识
- 1、域名
- 2、URL
- Encode 和 Decode
- 3、HTTP请求响应格式:
- HTTP请求:
- HTTP响应:
- 4、通过telnet见一见响应
- 二、编码实现简单网页请求响应:
- 1、封装的Socket.hpp
- 2、封装的log.hpp
- 3、Makefile自动化编译脚本
- 4、主函数逻辑:
- 5、封装的HttpServer服务
- Start接口:
- 线程方法:
- HanderHttp:
- 运行结果:
- 返回响应:
- 小细节:
- 网页
- 封装请求:
- 路径处理:
- 三、Http周边知识:
- 1、Http方法:
- 2、表单:
- 3、状态码
- 4、重定向:
- 5、长短链接
- 6、cookie:
一、前置知识
1、域名
如下,当我们访问一个网站的时候,往往是在浏览器中输入对应的网址就能够直接访问对应的网站
我们输入的网址就叫做域名,如上述的https://www.baidu.com
实际上,也可以使用IP地址+端口号的方式进行访问,事实上浏览器进行解释的时候会将域名解释成IP地址+端口号
当我们自己做好一个网站的时候,如果想把这个网站能够经常被别人访问,就需要申请域名、证书然后部署成功之后就能够成功访问了
在进行访问网站的时候,我们使用的默认协议是http和https,默认端口号分别为80和443
那么关于这个端口号,对于客户端是不确定的,自动分配,只要保证唯一性即可;但是对于服务端,如果随便修改就会导致客户端找不到对应的服务端
2、URL
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,也就是说网络上的所有资源,都可以使用唯一的字符串标识进行获取
一个URL的结构如下
在我们访问浏览器想要上网的时候,一般就会进行输入网址然后就能够访问到想要访问的资源
所以我网络行为分为两种:1、把别人的东西资源拿下来 2、把自己的东西传上去
Encode 和 Decode
当我们在进行浏览器输入的时候,就是将自己的资源传上去,如下当进行输入helloWorld的时候,在上方的URL中就能够看到自己输入的参数了
我们仔细观察就会发现,协议,域名,参数之间都是由特定的符号进行分开的,那么如果我们也输入特定的符号,那么浏览器会不会解析成功呢?
如下,当然会解析成功,会将那些符号解析为%XX 这样就能够与原来URL中的特殊字符进行区分
将特定的字符转化为计算机能够认识的语言我们叫做编码Encode;反过来,将编码后的代码还原为原始字符就是Decode解码
我们不需要主动进行编码解码,因为浏览器为我们做了,也不需要主动去记,因为有在线编码/解码工具
在线解码编码工具
3、HTTP请求响应格式:
HTTP请求:
请求格式如下,分为三个部分,请求行,请求报头,请求正文,还有一个空行分开请求报头和请求正文
其中,前三部分都是HTTP协议自带的,请求正文就是用户所提交的信息,可以为空
请求行
请求行由如下三个部分组成,分别是请求方法,URL,HTTP协议版本还有一个换行符标志
其中请求方法一般是GET或者是POST,其中GET使用频率极大
URL用来进行定位资源
HTTP版本,常见有1.0,1.1,2.0
最后有一个换行符来区分请求行和请求报头
请求报头:
请求报头由许多行组成,里面有许多字段,其中大多都是KV式的,每一行都是HTTP请求的属性
正文部分:
为了区分正文部分和报头部分,我们将正文部分和报头部分进行空行的分割
那么如何读到一个完整的请求正文呢?------在请求报头中有一个属性叫:Content-Length,这个属性记录了正文的长度,当我们读取到报文中的这个属性,就能够拿到正文的长度,然后就能够准确地读取正文的长度了
这里的正文部分可以没有,那么就表示只是想获取资源,不想上传资源
所以对于HTTP请求来说,一个完整的请求是如下这样的:
其中每一行都是通过\r\n进行分割开的
那么如何进行 序列化与反序列?
- 序列化:使用 \r\n 进行拼接
- 反序列化:根据 \r\n 进行读取
HTTP响应:
HTTP响应和HTTP请求的格式是类似的
响应格式如下,分为三个部分,状态行,响应报头,响应正文,还有一个空行分开响应报头和响应正文
状态行:
请求响应的HTTP版本可以一样,也可以不一样,二者在进行请求的时候进行交换协商,看看服务端为客户端提供哪些功能
响应报头:
这里同样是KV结构
响应报头是服务器向客户端传递响应元信息,相较于请求报头:关键报头的种类和作用有显著差异,但部分通用报头(如Date、Cache-Control)在两者中均会出现
响应正文
响应正文和响应报头之间也是使用空行分割开的,在响应正文中存放的就是我们所请求的资源(如HTML,png,jpg等等)
4、通过telnet见一见响应
在网络通信中,我们发起的请求就是上述中的请求结构Request,得到响应的时候,拿到的就是Response,一来一回就拿到一次的HTTP请求和响应
如下是通过telnet进行请求百度网页,然后得到响应
二、编码实现简单网页请求响应:
首先准备如下文件
其中Socket.hpp和log.hpp是之前封装的两个文件,分别是TCP套接字的核心功能和日志功能
1、封装的Socket.hpp
TCP套接字核心功能
#include <iostream>
#include <unistd.h>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include "log.hpp"enum{SOCKET_ERR = 2,BIND_ERR,LISTEN_ERR
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM,0);if(_sockfd < 0){lg(FATAL,"socket err,%s:%d",strerror(errno),errno);exit(SOCKET_ERR);}}void Bind(uint16_t port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;socklen_t len = sizeof(local);int n = bind(_sockfd,(struct sockaddr*)&local,len);if(n < 0){lg(FATAL,"bind err,%s,%d",strerror(errno),errno);exit(BIND_ERR);}}void Listen(){int n = listen(_sockfd,backlog);if(n < 0){lg(FATAL,"listen err,%s,%d",strerror(errno),errno);exit(LISTEN_ERR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t len = sizeof(client);int n = accept(_sockfd,(struct sockaddr*)&client,&len);if(n < 0){lg(WARNING, "accept error, %s: %d", strerror(errno), errno);return -1;}char in_buffer[64];inet_ntop(AF_INET,&client,in_buffer,sizeof(in_buffer));*clientip = in_buffer;*clientport = ntohs(client.sin_port);return n;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));socklen_t len = sizeof(server);int n = connect(_sockfd,(struct sockaddr*)&server,len);if(n < 0){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(_sockfd);}int Getsockfd(){return _sockfd;}public:int _sockfd;
};
2、封装的log.hpp
日志功能
#pragma once#include <iostream>
#include <ctime>
#include <cstdarg>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3#define SIZE 1024
#define logname "log.txt"using namespace std;class Log
{
public:Log():printstyle(SCREEN),path("./log/")// 默认路径是当前路径下的log文件夹{// mkdir(path.c_str(),0765);}void change(int style){printstyle = style;}string leveltostring(int level){switch (level){case INFO:return "INFO";case DEBUG:return "DEBUG";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "NON";}}void operator()(int level, const char *format, ...){// 处理时间time_t now = time(nullptr);// 将时间戳转为本地时间struct tm *local_time = localtime(&now);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min, local_time->tm_sec);// 处理可变参数va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 将两个消息组合起来成为一个完整的日志消息// 默认部分+自定义部分char logbuffer[SIZE * 2];snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);printlog(level, logbuffer);}// void logmessage(int level, const char *format, ...)// {// // 处理时间// time_t now = time(nullptr);// // 将时间戳转为本地时间// struct tm *local_time = localtime(&now);// char leftbuffer[SIZE];// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),// local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,// local_time->tm_hour, local_time->tm_min, local_time->tm_sec);// // 处理可变参数// va_list s;// va_start(s, format);// char rightbuffer[SIZE];// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);// va_end(s);// // 将两个消息组合起来成为一个完整的日志消息// // 默认部分+自定义部分// char logbuffer[SIZE * 2];// snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);// printlog(level, logbuffer);// }void printlog(int level, const string &logbuffer) // 这里引用避免大型字符串的拷贝开销,优化性能{switch (printstyle){case SCREEN:cout << logbuffer << endl;break;case ONEFILE:printonefile(logname, logbuffer);break;case MOREFILE:printmorefile(level, logbuffer);break;}}void printonefile(const string &_logname, const string &logbuffer){string __logname = path + _logname;int fd = open(__logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;write(fd, logbuffer.c_str(), logbuffer.size());close(fd);}void printmorefile(int level, const string &logbuffer){// 思路:通过不同的文件名进行区分string _logname = logname;_logname += ".";_logname += leveltostring(level);printonefile(_logname, logbuffer);}~Log(){}private:int printstyle;string path;
};Log lg;
3、Makefile自动化编译脚本
Makefile自动化编译脚本
httpserver:HttpServer.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f httpserver
接着实现HTTP服务,这里我们将HttpServer进行封装
如下是主函数逻辑,首先判断命令行,我们想通过 ./httpserver+端口号 的形式进行启动,接着构建服务器对象,然后调用Start接口,启动服务器
4、主函数逻辑:
HttpServer.cc
#include <memory>
#include "HttpServer.hpp"void Usage(char* args)
{printf("\n\t");cout<<args << " port"<<endl<<endl;
}int main(int argc,char* args[])
{// 判断命令行if(argc != 2){Usage(args[0]);exit(1);}// 构建服务器对象uint16_t port = std::stoi(args[1]);std::unique_ptr<HttpServer> ptr(new HttpServer(port));// 调用Start,启动服务器ptr->Start();return 0;
}
5、封装的HttpServer服务
接着是重点,封装的HTTP服务:
首先是封装的HttpServer这个类:
其中使用缺省参数传进来端口号,通过命令行进行传入端口号
class HttpServer
{
public:// 构造析构HttpServer(uint16_t port) : _port(port){}// Start接口bool Start(){}~HttpServer(){}private:// 监听套接字和服务端的端口号Sock _listenfd;// 端口号从命令行中一步步传参过来uint16_t _port;
};
接着就是实现一个个接口:
Start接口:
思路:
- 首先就是创建TCP套接字:创建、绑定、监听
- 接着在一个循环中进行多线程的调用
- 然后Start接口是服务端调用的,所以就需要在循环中接收客户端发送过来的请求
- 最后创建线程,通过线程方法进行请求的处理
bool Start(){// 创建套接字,绑定,监听_listenfd.Socket();_listenfd.Bind(_port);_listenfd.Listen();// 循环while (true){// 利用多线程进行调用// 这里是服务端,所以就需要先接收客户端的IP和portstring clientip;uint16_t clientport;int sockfd = _listenfd.Accept(&clientip, &clientport);if (sockfd < 0){lg(FATAL, "accept error,error:%d errorstring:%s", errno, strerror(errno));continue;}lg(INFO, "get a new link sockfd = %d", sockfd);// 接着创建线程(四个参数)pthread_t tid;ThreadDate *td = new ThreadDate(sockfd);pthread_create(&tid, nullptr, ThreadRun, td);}}
线程方法:
线程方法中:
- 首先进行线程分离,其目的是让处理客户端请求的短期线程,在执行结束时自动释放内核资源,避免僵尸线程和资源泄漏,同时不阻塞主线程的连接接收逻辑,确保服务器长期稳定运行
- 接着拿到线程中所传的参数
- 将HTTP服务响应的核心代码封装成HanderHttp方法,进行调用
- 进行后续工作,避免内存泄漏
static void *ThreadRun(void *args){// 线程分离pthread_detach(pthread_self());// 拿到创建线程中所传的参数ThreadDate *td = static_cast<ThreadDate *>(args);// 调用HanderHttp方法HanderHttp(td->_sockfd);// 后续工作delete td;return nullptr;}
HanderHttp:
- 这里首先从sockfd中通过recv读取客户端的请求
- 如果读取成功就将请求打印出来看看
- 将读上来的当做字符串打印出来
static void HanderHttp(int sockfd){// 从sockfd套接字中读取数据,recvchar buffer[10240];ssize_t n = recv(sockfd,buffer,sizeof(buffer)-1,0);// 如果读取成功将读取得到的数据打印出来if(n > 0){buffer[n] = 0; // 把读上来的数据当做字符串cout<<buffer;}close(sockfd);}
运行结果:
启动服务后,在浏览器中输入服务端的IP地址+端口号就能够进行请求了
之后在服务端中就能够看到请求了,这就说明浏览器已经能够访问到我们创建的HTTP服务了
返回响应:
在HanderHttp函数中,我们得到了请求之后,就可以进行响应处理,然后发送回给客户端
以下是代码完善:
static void HanderHttp(int sockfd){// 从sockfd套接字中读取数据,recvchar buffer[10240];ssize_t n = recv(sockfd,buffer,sizeof(buffer)-1,0);// 如果读取成功将读取得到的数据打印出来,然后返回响应过程,得到响应后通过send发送给请求if(n > 0){buffer[n] = 0; // 把读上来的数据当做字符串cout<<buffer;// 在返回响应过程中,其中的主体是需要进行网页文件的,那么读取网页文件封装一个函数//正文内容string text = "Hello World";//状态行string response_line = "HTTP/1.1 200 OK\r\n";//报头string response_header = "Content-Length:";response_header += to_string(text.size());response_header += "\r\n";//空行string response_blank = "\r\n";//融合请求string response;response += response_line;response += response_header;response += response_blank;response += text;//发送send(sockfd,response.c_str(),response.size(),0);}close(sockfd);}
接着我们重新启动服务端,在浏览器中重新进行请求
这样我们就能实现一个简单的处理http请求的服务了
小细节:
在这里:ThreadRun和HanderHttp被声明为静态成员函数,是为了适配pthread库的线程入口函数要求 ——消除this指针带来的函数签名不匹配问题,同时保证静态函数ThreadRun可以正常调用请求处理逻辑HanderHttp
也就是说,ThreadRun这里是静态的原因是因为存在this指针,如果ThreadRun是非静态成员函数,其实际会包含this指针,与pthread_create要求的函数指针类型不兼容,无法通过编译,因此,ThreadRun必须声明为静态成员函数 ——静态成员函数没有this指针
而HanderHttp是因为在ThreadRun中被调用,因为静态成员函数无法直接调用非静态成员函数,如果HanderHttp是非静态的,静态函数ThreadRun无法获取this指针,也就无法调用它。因此,HanderHttp也必须声明为静态的,才能在ThreadRun中直接调用
网页
将正文部分的helloworld换成前端的网页语法,这样就能够请求到一个网页了
重新请求后如下:
但是实际上肯定不是通过字符串这样写网页的,而是通过一个文件进行前端的编码,然后在正文部分进行拼接的,并且我们在访问别人网页资源的时候,是带有路径的,常规的IP地址+端口号访问的是“/目录”,也就是所谓的根目录,访问的是根目录下的所有资源,在编码中就是根文件夹以及这个文件夹下面的所有文件夹和文件资源
在我们访问的/目录的后面加上其他路径,这种就被叫做web目录,访问的就是这个web路径下的全部资源
那么我们接下来就实现上述
首先进行根目录的定义
const string wwwroot = "./wwwroot";
然后在当前路径下创建对应的文件夹
在这个文件夹中存在一个html文件,这个文件就是所谓的首页网站,在这个文件中进行编码
<html><body><h1>Hello World</h1>
</body></html>
接下来在Http服务中实现读取文件的功能:
设计思路:
首先将文件打开,并进行打开检查
接着一行一行地读取文件内容
最后关闭打开的流
返回得到的文件内容
总的来说,这个函数就是接收文件路径,返回该路径下的文件内容
string ReadHtmlContent(const string& path){// 文件打开ifstream in(path);// 文件打开检查if(!in.is_open()) return "404";// 读取文件内容string content;string line;while(getline(in,line)){content += line;}// 关闭文件并返回结果in.close();return content; }
这样,我们进行拿到文件内容的时候,就通过上述函数即可
那么问题来了,这个路径是什么呢?从哪来的呢?-----那当然是客户端请求的,客户端请求的请求行中,存在一个URL,URL里面就有客户端所请求的路径
所以接下来就处理客户端发送过来的请求即可,我们是不是在如下的时候拿到了客户端的请求资源,当时我们是将拿到的请求只是打印出来了,接下来就需要对拿到的请求进行处理
封装请求:
为了对拿到的请求进行处理,那么就需要封装一个请求类,通过反序列化对拿到的请求字符串进行处理
const string sep = "\r\n";class Request
{
public:void Deserialize(string req){int pos = 0;// 切割字符串while(true){pos = req.find(sep); // 通过\r\n分隔符进行查找if(pos == string::npos) break; // 这里如果没找到就证明已到达字符串末尾,没有更多分隔符string tmp = req.substr(0,pos); // 找到后就进行字符串截取if(tmp.empty()) break; // 截取为空证明遇到了连续的\r\n分隔符,即报头部分已结束(空行是报头与正文的分隔标志)_req_header.push_back(tmp); // 当截取后不为空就push到报头部分req.erase(0,pos+sep.size()); // 将查找后并push进报头部分的字符串进行删除}// 切割完报头部分后,剩下的都是正文部分_text = req;DebugPrint(); // Debug进行打印观察}void DebugPrint(){cout<<"--------------------------------------------------------------------"<<endl;for(auto& ch : _req_header){cout<<ch<<endl<<endl;}cout<<_text<<endl;}public:vector<string> _req_header; // 存放报头string _text; // 存放正文
};
接下来构建请求类并进行反序列化,在反序列化中进行了DebugPrint打印观看是否反序列化成功
如下是结果,发现反序列化成功
这样,我们就能够拿到客户端传来的URL了
如下,新增成员,里面准备放请求行中的三个属性:方法,URL,版本
接着实现一个解析函数,将请求行中的方法,URL,版本进行解析,放在成员变量中
void parse(){// 字符串流(stringstream)的格式化提取特性stringstream ss(_req_header[0]);ss>>_method>>_url>>_http_version;}
上述两行代码的原理:
- 自动分割:>>默认以空白字符(空格、换行等)作为分隔符,自动跳过连续的空白
- 顺序提取:按顺序从流中读取内容,依次存入后续变量
因此,对于请求行"GET /index.html HTTP/1.1"
第一个>>会读取到第一个空格为止,将"GET"存入_method
第二个>>会跳过空格,读取到下一个空格为止,将"/index.html"存入_url
第三个>>会跳过空格,读取剩余内容,将"HTTP/1.1"存入_http_version
路径处理:
当我们访问根目录的时候,并不是访问根目录下的全部资源,而是访问的一个首页资源(也就是该文件夹下的一个文件资源),所以还要对路径进行处理
首先定义首页资源名
在成员变量中新增_path,存入访问路径
接着在请求类的解析函数中进行处理
// 解析字符串void parse(){// 字符串流(stringstream)的格式化提取特性stringstream ss(_req_header[0]);ss>>_method>>_url>>_http_version;// 处理路径问题_path = wwwroot; // "./wwwroot"if(_url == "/" || _url == "/index.html"){_path += "/";_path += homepage;}else {_path += _url;}}
这样,就能够得到客户端请求的URL中的路径了,这样就能够访问对应的文件资源了,所以在对应的文件中进行html前端编码,就能够申请到对应的资源网页了
这样,在HanderHttp服务中就能够构建请求类,然后通过这之前写的ReadHtmlContent进行读取文件,这里传参就是请求中构建的路径,这样就能够准确地请求对应的资源了
启动服务器,通过浏览器请求首页
并且这个时候只需修改对应的html文件,然后点击刷新,重新申请即可
还能通过前端代码,能够实现网页的跳转
首页实现代码:
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><h1>这是首页这是首页这是首页这是首页这是首页这是首页</h1><a href="http://113.44.34.125:8888/a/b/c/2.html">到第二张网页</a><a href="http://113.44.34.125:8888/x/y/3.html">到第三张网页</a></body></html>
第二张网页实现代码
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>这是第二张网页</h1><h1>这是第二张网页</h1><h1>这是第二张网页</h1><a href="http://113.44.34.125:8888">回到首页</a><a href="http://113.44.34.125:8888/x/y/3.html">到第三张网页</a>
</body>
</html>
第三张网页实现代码
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>这是第3张网页</h1><h1>这是第3张网页</h1><h1>这是第3张网页</h1><h1>这是第3张网页</h1><h1>这是第3张网页</h1><h1>这是第3张网页</h1><a href="http://113.44.34.125:8888">回到首页</a><a href="http://113.44.34.125:8888/a/b/c/2.html">到第二张网页</a>
</body>
</html>
这样就能够实现网页跳转了:
全部代码
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <errno.h>
#include <fstream>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>#include "Socket.hpp"
#include "log.hpp"using namespace std;const string wwwroot = "./wwwroot";
const string sep = "\r\n";
const string homepage = "index.html";// 封装ThreadData
class ThreadData
{
public:ThreadData(int sockfd) : _sockfd(sockfd){}~ThreadData(){}public:int _sockfd;
};class Request
{
public:void Deserialize(string req){int pos = 0;// 切割字符串while(true){pos = req.find(sep); // 通过\r\n分隔符进行查找if(pos == string::npos) break; // 这里如果没找到就证明已到达字符串末尾,没有更多分隔符string tmp = req.substr(0,pos); // 找到后就进行字符串截取if(tmp.empty()) break; // 截取为空证明遇到了连续的\r\n分隔符,即报头部分已结束(空行是报头与正文的分隔标志)_req_header.push_back(tmp); // 当截取后不为空就push到报头部分req.erase(0,pos+sep.size()); // 将查找后并push进报头部分的字符串进行删除,还要删除分割字符串}// 切割完报头部分后,剩下的都是正文部分_text = req;// DebugPrint(); // Debug进行打印观察}// 解析字符串void parse(){// 字符串流(stringstream)的格式化提取特性stringstream ss(_req_header[0]);ss>>_method>>_url>>_http_version;// 处理路径问题_path = wwwroot; // "./wwwroot"if(_url == "/" || _url == "/index.html"){_path += "/";_path += homepage;}else {_path += _url;}}void DebugPrint(){cout<<"--------------------------------------------------------------------"<<endl;for(auto& ch : _req_header){cout<<ch<<endl<<endl;}cout<<_text<<endl;}public:vector<string> _req_header;string _text;// 请求行三个:方法,URL,版本string _method;string _url;string _http_version;// URL路径string _path;
};class HttpServer
{
public:// 构造析构HttpServer(uint16_t port) : _port(port){}// Start接口bool Start(){// 创建套接字,绑定,监听_listenfd.Socket();_listenfd.Bind(_port);_listenfd.Listen();// 循环while (true){// 利用多线程进行调用// 这里是服务端,所以就需要先接收客户端的IP和portstring clientip;uint16_t clientport;int sockfd = _listenfd.Accept(&clientip, &clientport);if (sockfd < 0){lg(FATAL, "accept error,error:%d errorstring:%s", errno, strerror(errno));continue;}lg(INFO, "get a new link sockfd = %d", sockfd);// 接着创建线程(四个参数)pthread_t tid;ThreadData *td = new ThreadData(sockfd);pthread_create(&tid, nullptr, ThreadRun, td);}}static string ReadHtmlContent(const string& path){// 文件打开ifstream in(path);// 文件打开检查if(!in.is_open()) return "404";// 读取文件内容string content;string line;while(getline(in,line)){content += line;}// 关闭文件并返回结果in.close();return content; } static void HanderHttp(int sockfd){// 从sockfd套接字中读取数据,recvchar buffer[10240];ssize_t n = recv(sockfd,buffer,sizeof(buffer)-1,0);// 如果读取成功将读取得到的数据打印出来,然后返回响应过程,得到响应后通过send发送给请求if(n > 0){buffer[n] = 0; // 把读上来的数据当做字符串cout<<buffer;// 构建请求类Request rq;rq.Deserialize(buffer);rq.parse();// 在返回响应过程中,其中的主体是需要进行网页文件的,那么读取网页文件封装一个函数//正文内容string text = ReadHtmlContent(rq._path);//状态行string response_line = "HTTP/1.1 200 OK\r\n";//报头string response_header = "Content-Length:";response_header += to_string(text.size());response_header += "\r\n";//空行string response_blank = "\r\n";//融合请求string response;response += response_line;response += response_header;response += response_blank;response += text;//发送send(sockfd,response.c_str(),response.size(),0);}close(sockfd);}static void *ThreadRun(void *args){// 线程分离pthread_detach(pthread_self());// 拿到创建线程中所传的参数ThreadData *td = static_cast<ThreadData *>(args);// 调用HanderHttp方法HanderHttp(td->_sockfd);// 后续工作delete td;return nullptr;}~HttpServer(){}private:// 监听套接字和服务端的端口号Sock _listenfd;// 端口号从命令行中一步步传参过来uint16_t _port;
};
三、Http周边知识:
1、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方法:
GET 方法:
- 核心作用:从服务器获取资源(如网页、图片、数据列表),是一种 “只读” 请求,不应该对服务器上的资源产生任何修改(即 “幂等性”)
- 本质特性:请求的目的是 “查询”,而非 “提交”,服务器仅需返回客户端所需的资源即可
POST 方法:
- 核心作用:向服务器提交数据,用于创建、修改或删除服务器上的资源(如提交表单、上传文件、创建用户), 可能会改变服务器的状态(非幂等性,除非特殊设计)
- 本质特性:请求的目的是 “提交”,数据会被服务器处理并用于更新资源,而非单纯查询
2、表单:
在网站将数据传输给服务端的时候,通过表单进行提交的,是用来提交数据的
比如一些登录界面,像如下这些就是通过表单进行提交的数据的
提交的方法可以用POST或者是GET
POST方法采用请求到正文提交参数
GET方法通过url进行提交参数,参数数量是受限的,并且会回显,不私秘,POST方法私密
但是GET和POST都是不安全的。想要安全,就要加密
3、状态码
状态码表示请求资源是不是成功的,最典型的如404
状态码类别 | 类别名称 | 原因短语(说明) |
---|---|---|
1XX | Informational(信息性) | 接收的请求正在处理 |
2XX | Success(成功) | 请求正常处理完毕 |
3XX | Redirection(重定向) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误) | 服务器无法处理请求(客户端请求有误) |
5XX | Server Error(服务器错误) | 服务器处理请求出错(服务端自身问题) |
4、重定向:
HTTP 重定向的核心原理,是服务器通过特定的 HTTP 响应(包含状态码和新目标地址),主动引导客户端(如浏览器、APP)放弃原请求目标,转而向新 URL 发起请求的过程。整个流程完全基于 HTTP 协议规范,是客户端与服务器之间一次 “请求 - 引导 - 再请求” 的互动闭环
通俗的说就是当客户端向服务器发送请求时,服务器返回一个特殊的响应,指示客户端去访问另一个URL,也就是说是服务器让浏览器(客户端)去访问新的地址
重定向分两种临时重定向和永久重定向
其中临时重定向是只改变当前访问,下次访问还是原来的网站
永久重定向是永久改变,下次访问就是重定向后的网站
5、长短链接
在如下这个请求中有许多属性:
- Host 指定了请求的目标服务器的主机地址和端口号
- Connection表示希望与服务器保持长连接,这样在后续的请求中可以复用当前的 TCP 连接,减少建立连接的开销
- Upgrade-Insecure-Requests:1告诉服务器,客户端希望将所有的不安全的 HTTP 请求(如 http://)升级为安全的 HTTPS 请求
- User-Agent 包含了客户端的信息
- Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
表示客户端能够接受的响应内容类型,q 是权重值,范围 0 - 1,值越大表示越希望得到该类型的内容。 - Accept-Encoding: gzip, deflate
说明客户端支持的内容编码方式,这里支持 gzip 和 deflate 压缩方式,服务器可以用这些方式压缩响应内容,减少传输的数据量 - Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
表示客户端希望的语言偏好,zh-CN(简体中文)的权重最高,其次是 zh(中文),然后是 en(英语)等,服务器可以根据这个信息返回对应语言的内容
这里我们理解理解Connection
作为一个网页,是由多个元素组成的,每一个元素就是一个资源
以前的网页是基于HTTP协议的,HTTP协议又是基于TCP套接字的,TCP是基于字节流的,服务端和客户端之间是需要建立链接的,这个必须经过TCP的connect
那么假如有100个资源,那么就需要请求100次,那么就需要建立100次链接
也就是说先发送请求,然后建立链接,完成请求,断开链接…
像上述这样的就叫做短连接
长连接和短连接不同
双方建立TCP连接,服务器响应数据后不关闭连接,将连接 “保持” 在闲置状态
客户端后续请求(如加载同一网页的其他资源)直接复用该连接
连接保持时间由服务器配置(如 Nginx 默认 keepalive_timeout 65s),闲置超时后自动关闭;或客户端主动发送 Connection: close关闭连接
像上述这样的我们叫做长连接
维度 | 短连接 | 长连接 |
---|---|---|
连接生命周期 | 一次请求对应一次连接:请求发起→建立连接→传输数据→连接关闭 | 一次连接复用多次请求:建立连接→多次传输数据→按需关闭 |
技术依赖 | 基于 HTTP/1.0 默认实现(HTTP/1.1 需手动配置关闭) | 基于 HTTP/1.1 默认实现(需显式声明 Connection: keep-alive) |
核心目标 | 简化连接管理,避免闲置连接占用资源 | 减少连接建立 / 关闭的开销,提升高频请求效率 |
类型 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
短连接 | 连接即时释放,不占用服务器资源 | 频繁建立 / 关闭连接,耗时(TCP 握手开销) | 低频请求场景(如静态网页访问、单次 API 调用) |
长连接 | 减少连接开销,提升高频请求效率 | 长期闲置连接占用服务器内存、端口资源 | 高频交互场景(如电商购物车、APP 实时消息、视频弹幕) |
6、cookie:
HTTP是无状态协议,即服务器每次接收客户端请求时,都无法识别这个请求是否来自之前访问过的用户,当我们访问一个网页的时候,比如说当我们访问CSDN的时候,每一次的访问和请求是没有任何关系的,也就是说当每一次访问的时候都要传数据上去,然后进行登录,但是实际情况却不是这样,我们每次进行访问的时候是自动登录的
这就是因为cookie
Cookie 的本质是 客户端本地存储的键值对文本(类似一张 “身份卡片”),大小通常限制在 4KB 以内(仅能存储少量关键信息,无法存图片、视频等大文件)其核心作用是解决 HTTP 协议的 “无状态性” 问题
cookie分为文件级和内存级
其区别如下:
对比维度 | 文件级 cookie(持久化) | 内存级 cookie(会话级) |
---|---|---|
存储位置 | 本地文件系统(硬盘 / 设备存储) | 浏览器内存(RAM) |
生命周期 | 由 Expires/Max-Age 定义(可长期) | 浏览器会话期间(关闭窗口即失效) |
持久化能力 | 支持持久化(重启设备 / 浏览器仍存在) | 不支持(仅临时存在) |
典型用途 | 记住登录状态、保存用户偏好(如主题) | 临时会话标识(如未登录时的购物车) |
手动清除方式 | 需通过浏览器设置手动删除(或过期自动删) | 关闭浏览器自动清除,无需手动操作 |
优点
- 轻量高效:仅 4KB 大小,不占用服务器存储(数据存在客户端),对网络传输和服务器性能影响极小
- 兼容性强:所有主流浏览器(Chrome、Firefox、Safari 等)均原生支持,无需额外插件,是最通用的客户端存储方案之一。
- 功能核心:支撑登录状态保持、购物车、个性化推荐等关键功能,是现代网站不可替代的基础技术。
缺点
- 存储容量有限:4KB 限制导致无法存储大量数据(如完整的用户画像、历史订单记录)。
- 跨域限制严格:受 Domain 和 Path 属性限制,A 网站的 Cookie 无法被 B 网站读取(虽保证安全,但也限制了跨域数据共享)。
- 易被篡改 / 窃取:若未设置 HttpOnly、Secure 等属性,Cookie 可能被恶意脚本(XSS 攻击)窃取,或通过 HTTP 明文传输被拦截。