【Linux网络篇】:HTTP协议深度解析---基础概念与简单的HTTP服务器实现
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客
文章目录
- 一.三个预备知识
- 认识域名
- 认识URL
- 认识URL编码和解码
- 二.http请求和响应的格式
- 三.实现一个简单的HTTP服务器
- HTTP服务器实现-version 1
- HTTP服务器实现-version 2
- 四.HTTP的细节字段
- HTTP的请求方法
- HTTP的状态码
- HTTP的常见Header
一.三个预备知识
认识域名
1.定义:
- 域名是互联网上用于标识和定位网站的一串字符串,比如
baidu.com
。他相当于网站的“门牌号”,方便用于记忆和访问。
2.结构:
域名通常由多个部分组成,从右到左依次是:
- 顶级域名:比如
.com
,org
,.net
,.cn
等。 - 二级域名:比如
baidu
在baidu.com
中。 - 三级域名:比如
www
在www.baidu.com
中。
3.作用:
- 域名将用户可读的地址转换为机器可以识别的IP地址(比如
1.111.11.1:8888
)。 - 通过DNS(域名系统)实现域名到IP地址的映射。
我们平常访问各种网页使用的域名,首先会被解析为IP地址,在网络通信时,正真用到的其实是IP地址。
使用一个IP地址在浏览器访问时,默认采用的协议就是http
或者https
,这种知名的协议绑定的端口号一般都是固定的。
http
协议绑定的端口号是80
;https
协议绑定的端口号是443
。
认识URL
1.定义:
URL
是用于标识和定位互联网上资源的完整地址,比如http://www.example.com/page?param=value
。
平常我们访问的网址其实就是URL
——统一资源定位符
所有网络上的资源(图片,视频,文章等),都可以用唯一的一个”字符串“标识,并且可以获取到。
2.结构:
URL
通常由以下部分组成:
- 协议:比如
http://
或者https://
,表示访问资源的方式。 - 域名:比如
www.example.com
,表示资源所在的服务器(IP地址标识)。 - 端口:比如
80
或443
,表示服务器上的服务端口(默认可以省略)。 - 路径:比如
/path
,表示资源所在服务器上的具体位置。 - 查询参数:比如
?pararm=value
,表示传递给服务器的额外信息。 - 锚点:比如
#section
,表示页面内的特定位置。
认识URL编码和解码
像/ ? : #
等这样的字符,已经被URL
当作特殊意义理解了,因此这些字符不能随意出现。
少量的情况下,提交或者获取的资源地址本身可能包含和URL
中特殊字符冲突的字符,所以BS
双方(浏览器和服务器)要进行编码(urlencode
)和解码(urldecode
)的工作。
URL编码(urlencode)
-
定义:
URL编码是将URL中的特殊字符(比如
/ ? : #
等)转换为%
后跟十六位进制ASCLL
码的形式,以便在互联网上安全传输。 -
常见编码规则:
字母,数字,
- _ . ~
这些字符不编码;其他字符编码为%xx
,其中XX
是字符的十六进制ASCLL
码。
URL解码(urldecode)
-
定义:
URL解码是将URL编码后的字符串转换为原始字符。
实际应用
-
浏览器自动编码:
用户在浏览器输入URL时,浏览器会自动对特殊字符进行URL编码;比如,输入
https://example.com?page?name=张三
,浏览器会自动编码为https://example.com/page?name=%E5%BC%A0%E4%b8%89
。 -
服务器解码:
服务器收到请求后,会先解码URL,再解析参数;比如服务器收到
name=%E5%BC%A0%E4%b8%89
,解码后得到name=张三
。
最后总结上面三点:
域名:是网站的“门牌号”,方便用户记忆,通过DNS转换为IP地址。
URL:是完整的资源地址,包含协议,域名,路径,参数等信息,用于定位和访问互联网上的资源。
URL编码:将特殊字符转换为
%xx
格式,确保URL的合法性和安全性。URL解码:将
%xx
格式转换为原始字符,便于服务器解析。
二.http请求和响应的格式
先大概了解格式是什么样子,后面会讲解每个细节字段。
三.实现一个简单的HTTP服务器
主程序
HttpServer.cc
:用于启动服务器
#include <iostream>
#include "HttpServer.hpp"static void Usage(const std::string &proc){std::cout << "/r/nUsage: " << proc << "serverport/r/n";
}int main(int argc, char *argv[]){if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);HttpServer *httpsvr = new HttpServer(port);httpsvr->InitServer();httpsvr->StartServer();return 0;
}
HTTP服务器实现-version 1
HttpServer.hpp
:
#pragma once#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include <sstream>
#include <fstream>
#include "Socket.hpp"
#include "Log.hpp"// 注意:我这里使用的Sokcet.hpp文件是我自己封装的Socket套接字类,具体实现可以看我上一篇文章extern Log log;class HttpServer;class ThreadData{
public:ThreadData(const int &sockfd, HttpServer *svr): _sockfd(sockfd), httpsvr(svr){}public:int _sockfd;HttpServer *httpsvr;
};class HttpServer{
public:HttpServer(const uint16_t port):_port(port){}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();log(INFO, "HttpServer Init ... Done");}// 启动服务器void StartServer(){while(true){std::string clientip;uint16_t clientport;int sockfd = _listensock.Accept(&clientip, &clientport);if (sockfd < 0){continue;}log(INFO, "httpserver get a connect, sockfd: %d", sockfd);ThreadData *td = new ThreadData(sockfd, this);pthread_t tid;pthread_create(&tid, nullptr, ThreadRun, td);}}private:void HttpHandler(const int &sockfd){char buffer[1024];ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl; // 假设读取到的是一个完整的,独立的http请求std::string text ="hello world!"; // 响应正文部分std::string response_line = "HTTP/1.0 200 OK\r\n"; // 状态行std::string response_header = "Content-Length: ";response_header += std::to_string(text.size());response_header += "\r\n"; // 响应报头std::string blank_line = "\r\n"; // 空行分隔符// 构建响应字符串std::string response = response_line;response += response_header;response += blank_line;response += text;// 发送响应ssize_t k = 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);td->httpsvr->HttpHandler(td->_sockfd);delete td; // 释放线程信息对象return nullptr;}~HttpServer(){}
private:uint16_t _port;MySocket _listensock;
};
测试结果:
服务器收到的请求:
HTTP服务器实现-version 2
在实现version2
版本的服务器之前,先来理解两个点:
1.HTTP请求行中的URL作用
-
定义:
HTTP请求行中的URL是客户端请求的资源路径,比如
GET /index.html HTTP/1.1
;这里的/index.html
就是URL。 -
作用:
- 定位资源:URL告诉服务器客户端向要访问的具体资源(比如网页,图片,文件等)。
- 传递参数:URL可以包含查询参数(比如
?name=张三
),用于向服务器传递额外的信息。 - 区分请求:不同的URL对应不同的资源,服务器根据URL返回不同的内容。
2.web根目录/
-
定义:
web根目录是服务器上存放网站文件的根文件,所有HTTP请求的URL都是相对于这个目录的。
-
作用:
- 文件组织:web根目录下通常包含网站的所有文件,比如
HTML,CSS,JavaScript,图片
等。 - 安全隔离:服务器只允许访问web根目录下的文件,防止用户访问服务器上的其他敏感文件。
- URL映射:URL路径直接映射到web根目录下的文件路径。
- 文件组织:web根目录下通常包含网站的所有文件,比如
-
示例:
假设web根目录是
/wwwroot
:- 请求
GET /index.html
:服务器返回/wwwroot/index.html
文件。 - 请求
GET /a/b/hello.html
:服务器返回/wwwroot/a/b/hello.html
文件。
- 请求
明白了上面两点后,再来修改自己写的服务器,也实现通过不同的URL地址访问不同的网页资源。
#pragma once#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include <sstream>
#include <fstream>
#include "Socket.hpp"
#include "Log.hpp"extern Log log;const std::string wwwroot = "./wwwroot"; // web根目录
const std::string seq = "\r\n"; // 分隔符
const std::string homepage = "index.html"; // 首页class HttpServer;class ThreadData{
public:ThreadData(const int &sockfd, HttpServer *svr): _sockfd(sockfd), httpsvr(svr){}public:int _sockfd;HttpServer *httpsvr;
};class HttpRequest{
public:// 反序列化void Deserialize(std::string req){while(true){std::size_t pos = req.find(seq);if (pos == std::string::npos){break;}std::string cur = req.substr(0, pos);if (cur.empty()){break;}req_header.push_back(cur);req.erase(0, pos + seq.size());}text = req;}void Parse(){std::stringstream ss(req_header[0]);ss >> method >> url >> http_version;file_path = wwwroot; // ./wwwrootif (url == "/" || url == "/index.html"){file_path += "/";file_path += homepage; // ./wwwroot/index.html}else{file_path += url; // ./wwwroot/...}}void DebugPrint(){for(auto &line : req_header){std::cout << "-----------------------" << std::endl;std::cout << line << std ::endl<< std::endl;}std::cout << "method: " << method << std::endl;std::cout << "url: " << url << std::endl;std::cout << "http_version: " << http_version << std::endl;std::cout << "file_path: " << file_path << std::endl;std::cout << text << std::endl;}public:std::vector<std::string> req_header; // 请求报头的每一行存放到数组中std::string text; // 请求正文// 请求行解析之后的结果std::string method;std::string url;std::string http_version;std::string file_path;
};class HttpServer{
public:HttpServer(const uint16_t port):_port(port){}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();log(INFO, "HttpServer Init ... Done");}// 启动服务器void StartServer(){while(true){std::string clientip;uint16_t clientport;int sockfd = _listensock.Accept(&clientip, &clientport);if (sockfd < 0){continue;}log(INFO, "httpserver get a connect, sockfd: %d", sockfd);ThreadData *td = new ThreadData(sockfd, this);pthread_t tid;pthread_create(&tid, nullptr, ThreadRun, td);}}private:// 根据路径路径打开对应的的网页文件 获取响应正文部分static std::string ReadHtmlContent(const std::string &htmlpath){std::ifstream in(htmlpath);if (!in.is_open()){return "404";}std::string line;std::string content;while (std::getline(in, line)){content += line;}in.close();return content;}void HttpHandler(const int &sockfd){char buffer[1024];ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl; // 假设读取到的是一个完整的,独立的http请求HttpRequest req;req.Deserialize(buffer); // 先反序列化req.Parse(); // 解析//req.DebugPrint(); // 测试打印std::string text; // 响应正文部分text = ReadHtmlContent(req.file_path);std::string response_line = "HTTP/1.0 200 OK\r\n"; // 状态行std::string response_header = "Content-Length: ";response_header += std::to_string(text.size());response_header += "\r\n"; // 响应报头std::string blank_line = "\r\n"; // 空行分隔符// 构建响应字符串std::string response = response_line;response += response_header;response += blank_line;response += text;// 发送响应ssize_t k = 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);td->httpsvr->HttpHandler(td->_sockfd);delete td; // 释放线程信息对象return nullptr;}~HttpServer(){}
private:uint16_t _port;MySocket _listensock;
};
网页一
index.html
:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>hello world</title>
</head>
<body><h1>Hello world!</h1><h1>第一张网页</h1><h1>第一张网页</h1><h1>第一张网页</h1><h1>第一张网页</h1><h1>第一张网页</h1><h1>第一张网页</h1><a href="http://1.117.74.41:28080/a/b/hello.html">到第二张网页</a><a href="http://1.117.74.41:28080/x/world.html">到第三张网页</a>
</body>
</html>
网页二
hello.html
:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>hello world</title>
</head>
<body><h1>第二张网页</h1><h1>第二张网页</h1><h1>第二张网页</h1><h1>第二张网页</h1><h1>第二张网页</h1><h1>第二张网页</h1><h1>第二张网页</h1><a href="http://1.117.74.41:28080">回到首页</a><a href="http://1.117.74.41:28080/x/world.html">到第三张网页</a>
</body>
</html>
网页三
world.html
:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>hello world</title>
</head>
<body><h1>第三张网页</h1><h1>第三张网页</h1><h1>第三张网页</h1><h1>第三张网页</h1><h1>第三张网页</h1><h1>第三张网页</h1><h1>第三张网页</h1><a href="http://1.117.74.41:28080">回到首页</a><a href="http://1.117.74.41:28080/a/b/hello.html">到第二张网页</a>
</body>
</html>
网页资源目录结构:
测试结果:
访问第一张网页资源:
服务器收到的请求:
访问第二张网页资源:
服务器收到的请求:
访问第三张网页资源:
服务器收到的请求:
四.HTTP的细节字段
HTTP的请求方法
我们在日常使用网站时,比如登录时需要提交我们的用户名和密码等数据,而数据提交给服务器最常见的方式就是表单(属于web前端方面的知识,了解即可)。
当用户填写表单后,输入用户名和密码后,点击提交按钮,就会触发表单的submit
事件,然后由浏览器发送请求,我们输入的数据就会作为参数提交,具体如何提交参数,不同的请求方法有不同的方式,最常用的就是GET
和POST
方法。
GET方法和POST方法都支持参数的提交
- 如果使用
GET
方法时,提交的参数是通过URL
提交的,以?
为起始符号,多个参数之间使用&
符号分割;参数数量有效,并且不私秘。
修改第一张网页为登陆界面进行测试:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="/a/b/hello.html" method="get">name: <input type="text" name="name"><br>password: <input type="password" name="passwd"><br><input type="submit" value="提交"></form>
</body>
</html>
测试结果:
输入的参数name=zmh
以及passwd=123456
通过URL
传参
服务器收到的请求:
- 如果使用
post
方法时,提交的参数是通过请求正文提交的。
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="/a/b/hello.html" method="post">name: <input type="text" name="name"><br>password: <input type="password" name="passwd"><br><input type="submit" value="提交"></form>
</body>
</html>
测试结果:
服务器收到的请求:
输入的参数通过请求正文提交
HTTP的状态码
1. 2xx(成功)
-
200 OK
请求成功,服务器已返回请求的数据。这是最常见的状态码,表示一切正常。
-
201 Created
请求成功,并且服务器创建了新的资源(比如 POST 请求创建了一个新用户)。
-
204 No Content
请求成功,但服务器没有返回任何内容(比如 DELETE 请求后,资源已删除)。
2. 3xx(重定向)
-
301 Moved Permanently(永久重定向)
请求的资源已永久移动到新的 URL,浏览器会自动跳转到新地址。
-
302 Found(临时重定向)
请求的资源临时移动到新的 URL,浏览器会跳转到新地址,但下次可能还会请求原地址。
3. 4xx(客户端错误)
-
400 Bad Request
请求语法错误,服务器无法理解(比如参数格式不对)。
-
401 Unauthorized
请求需要身份验证,客户端未提供有效的认证信息。
-
403 Forbidden
服务器理解请求,但拒绝执行(比如权限不足)。
-
404 Not Found
请求的资源不存在,服务器找不到对应的文件或页面。
-
405 Method Not Allowed
请求的 HTTP 方法(如 GET、POST)不被服务器支持。
4. 5xx(服务器错误)
-
500 Internal Server Error
服务器内部错误,无法完成请求(比如代码异常)。
-
502 Bad Gateway
服务器作为网关或代理,从上游服务器收到无效响应。
-
503 Service Unavailable
服务器暂时不可用(比如过载或维护)。
-
504 Gateway Timeout
服务器作为网关或代理,等待上游服务器响应超时。
补充内容:
1.实现一个自己的404错误界面:
err.html
:错误文件
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>hello world</title>
</head>
<body><h1>你要访问的文件是: 404</h1>
</body>
</html>
修改一下源代码:
如果访问一个不存在的资源,就访问错误文件err.html
,显示错误界面:
测试结果:
查看服务器发送的响应:
(这里使用的抓包工具是Fiddler
)
2.什么是临时重定向和永久重定向
临时重定向:
- 服务器告诉浏览器:”资源暂时在另一个地方,请去哪里找,但下次可能还会变。“
- 浏览器会跳转到新地址,但下一次请求时仍会尝试访问源地址。
- 搜索引擎不会更新索引,仍会保留原地址。
永久重定向:
- 服务器告诉浏览器:”资源已永久移动到新地址,以后请直接访问新地址。“
- 浏览器会跳转到新地址,并记住新地址,下一次直接访问新地址。
- 搜索引擎会更新索引,将原地址的权重转移到新地址。
重定向响应报头:
Location
字段- 当服务器返回301(永久重定向)或302(临时重定向)时,必须在响应报头中包含
Location
字段。 - 这个字段的值是新地址的URL,浏览器会根据这个地址跳转。
- 当服务器返回301(永久重定向)或302(临时重定向)时,必须在响应报头中包含
示例:
修改一下服务器源代码,如果访问一个不存在的资源,不再是访问上面的错误文件err.html
,而是临时重定向到新地址http://www.qq.com
,由浏览器二次发送请求。
输入一个不存在的资源路径进行访问,比如:1.117.74.41:28080/q
,就会自动跳转到新地址:
查看服务器发送的响应:
HTTP的常见Header
1.请求头(Request Header)
Host
:- 作用:指定请求的目标服务器的域名和端口号
- 示例:
Host: www.example.com:80
- 说明:HTTP/1.1版本协议要求必须包含该字段,用于区分同一IP上的多个虚拟机。
User-Agent
:- 作用:表示客户端(浏览器,爬虫)的信息
- 示例:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
- 说明:服务器可以根据该字段返回适配的页面(比如移动端或者PC端)
Referer
:- 作用:标识请求来源的URL
- 示例:
Referer: https://www.example.com/page
- 说明:服务器可以根据该字段判断请求是否来自合法来源,防止CSRF攻击。
Cookie
:- 作用:携带客户端存储的Cookie信息。
- 示例:
Cookie: session=abc123456
- 说明:用于维持用户会话状态,比如登录信息,购物车数据等(这个字段后面会重点讲)。
2. 响应头(Response Headers)
-
Content-Type
:- 作用:指定响应内容的 MIME 类型。
- 示例:
Content-Type: text/html; charset=UTF-8
- 说明:浏览器根据此字段解析响应内容(如 HTML、JSON、图片等)。
-
Content-Length
- 作用:指定响应内容的字节数。
- 示例:
Content-Length: 1234
- 说明:浏览器根据此字段判断响应是否接收完整。
-
Location
- 作用:指定重定向的目标 URL。
- 示例:
Location: https://www.example.com/new-page
- 说明:当服务器返回 301(永久重定向)或 302(临时重定向)时,浏览器会根据此字段跳转到新地址(这个字段在前面已经提到过了)。
-
Set-Cookie
- 作用:服务器要求客户端存储的 Cookie 信息。
- 示例:
Set-Cookie: session=abc123; Path=/; HttpOnly
- 说明:用于维持用户会话状态,如登录信息、购物车数据等(这个后面重点讲)。
补充内容:
1.长连接和短连接
长连接
-
定义:
长连接是指客户端和服务端建立连接后,保持连续不关闭,后续请求可以复用这个连接。
-
特点:
- 复用连接:多个HTTP请求可以共用一个TCP连接,避免重复建立连接的开销
- 减少延迟:避免每次请求建立连接都重新握手和挥手,减少网络延迟
- 提高性能:适合需要频繁请求的场景
-
实现方式:
通过响应头
Connection:keep-alive
字段实现;HTTP/1.1默认支持长连接。
短连接
-
定义:
短连接是指客户端和服务器建立连接后,发送完请求并收到响应后立即关闭连接。
-
特点:
- 每次请求都建立新连接:每次HTTP请求都需要重新建立TCP连接,请求完后立即关闭
- 增加延迟:每次请求建立连接,都需要握手和挥手,增加网络延迟
- 资源消耗:频繁建立和关闭连接会消耗服务器和客户端的资源(比如CPU,内存,网络带宽)
-
实现方式:
通过
Connection:close
字段实现,服务器响应后立即关闭连接;HTTP/1.0默认使用短连接,除非设置长连接响应头字段。
一个巨大的网页是包含非常多的资源的(比如图片等资源),请求获取网页资源时也要请求获取网页中包含的其他资源。
网页加载的流程:
-
初始请求:
当用户访问一个网页时,浏览器会先发送一个HTTP请求,获取该网页的HTML文件。
-
解析HTML文件:
浏览器解析网页的HTML文件,发现其中包含多个资源(比如图片,CSS,JavaScript等),例如:
<img src="/images/logo.png" /> <img src="/images/banner.jpg" />
-
长连接复用:
如果服务器支持长连接,浏览器就会复用同一个TCP连接,依次发送多个HTTP请求获取HTML文件中包含的资源,然后服务器依次处理并返回响应。
1.请求
GET /images/logo.png
2.请求
GET /images/banner.jpg
-
连接关闭:
当所有资源加载完后,或者连接超时,浏览器就会关闭连接;如果后续需要加载新资源,浏览器就会重新建立连接。
在实际效果中,采用长连接减少了连接建立和关闭的开销,网页加载速度更快,现代网页大多都是采用长连接。
修改上面的服务器代码以及在网站首页中添加几张图片,模拟实现网页加载的效果:
HttpServer.hpp
修改:
#pragma once#include <iostream>
#include <string>
#include <pthread.h>
#include <vector>
#include <sstream>
#include <fstream>
#include <unordered_map>
#include "Socket.hpp"
#include "Log.hpp"extern Log log;const std::string wwwroot = "./wwwroot"; // web根目录
const std::string seq = "\r\n"; // 分隔符
const std::string homepage = "index.html"; // 首页class HttpServer;class ThreadData{
public:ThreadData(const int &sockfd, HttpServer *svr): _sockfd(sockfd), httpsvr(svr){}public:int _sockfd;HttpServer *httpsvr;
};class HttpRequest{
public:// 反序列化void Deserialize(std::string req){while(true){std::size_t pos = req.find(seq);if (pos == std::string::npos){break;}std::string cur = req.substr(0, pos);if (cur.empty()){break;}req_header.push_back(cur);req.erase(0, pos + seq.size());}text = req;}void DebugPrint(){for(auto &line : req_header){ std::cout << "-----------------------" << std::endl;std::cout << line << std ::endl<< std::endl;}std::cout << "method: " << method << std::endl;std::cout << "url: " << url << std::endl;std::cout << "http_version: " << http_version << std::endl;std::cout << "file_path: " << file_path << std::endl;std::cout << text << std::endl;}void Parse(){std::stringstream ss(req_header[0]);ss >> method >> url >> http_version;file_path = wwwroot; // ./wwwrootif (url == "/" || url == "/index.html"){file_path += "/";file_path += homepage; // ./wwwroot/index.html}else{file_path += url; // ./wwwroot/...}// 修改点一:std::size_t pos = file_path.rfind(".");if (pos == std::string::npos){suffix = ".html";}else{suffix = file_path.substr(pos);}}public:std::vector<std::string> req_header; // 请求报头的每一行存放到数组中std::string text; // 请求正文// 请求行解析之后的结果std::string method;std::string url;std::string http_version;std::string file_path;std::string suffix; // 后缀字符串
};class HttpServer{
public:HttpServer(const uint16_t port):_port(port){// 修改点二:content_type.insert({".html", "text/html"});content_type.insert({".png", "image/png"});content_type.insert({"jpg", "image/jpg"});}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();log(INFO, "HttpServer Init ... Done");}// 启动服务器void StartServer(){while(true){std::string clientip;uint16_t clientport;int sockfd = _listensock.Accept(&clientip, &clientport);if (sockfd < 0){continue;}log(INFO, "httpserver get a connect, sockfd: %d", sockfd);ThreadData *td = new ThreadData(sockfd, this);pthread_t tid;pthread_create(&tid, nullptr, ThreadRun, td);}}private:// 根据路径路径打开对应的的网页文件 获取响应正文部分static std::string ReadHtmlContent(const std::string &htmlpath){// 修改点三:std::ifstream in(htmlpath, std::ios::binary);if(!in.is_open()){return "404";}in.seekg(0, std::ios_base::end);auto len = in.tellg();in.seekg(0, std::ios_base::beg);std::string content;content.resize(len);in.read((char *)content.c_str(), content.size());in.close();return content;}std::string SuffixToDesc(const std::string &suffix){auto iter = content_type.find(suffix);if(iter==content_type.end()){return content_type[".html"];}else{return content_type[suffix];}}void HttpHandler(const int &sockfd){char buffer[1024];ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl; // 假设读取到的是一个完整的,独立的http请求HttpRequest req;req.Deserialize(buffer); // 先反序列化req.Parse(); // 解析//req.DebugPrint(); // 测试打印std::string text; // 响应正文部分text = ReadHtmlContent(req.file_path);bool ok = true;if (text == "404"){ok = false;std::string err_file_path = wwwroot;err_file_path += "/err.html";text = ReadHtmlContent(err_file_path);}std::string response_line; // 状态行if(ok){response_line = "HTTP/1.1 200 OK\r\n"; }else{//response_line = "HTTP/1.0 404 Not Found\r\n"; response_line = "HTTP/1.0 302 Found\r\n"; // 临时重定向}std::string response_header = "Content-Length: "; // 响应报头response_header += std::to_string(text.size());response_header += "\r\n"; // if(!ok){// response_header += "Location: http://www.qq.com\r\n"; // 新地址的URL// }// 修改点四:response_header += "Content-Type: ";response_header += SuffixToDesc(req.suffix);response_header += "\r\n";std::string blank_line = "\r\n"; // 空行分隔符// 构建响应字符串std::string response = response_line;response += response_header;response += blank_line;response += text;// 发送响应ssize_t k = 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);td->httpsvr->HttpHandler(td->_sockfd);delete td; // 释放线程信息对象return nullptr;}~HttpServer(){}
private:uint16_t _port;MySocket _listensock;std::unordered_map<std::string, std::string> content_type;
};
index.html
修改:
<!DOCTYPE html>
<html lang="zh-CN">
<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><img src="/images/1.png" /><img src="/images/2.jpg" />
</body>
</html>
网页资源目录结构:
测试结果:
服务器收到的请求:先是收到网页资源请求,然后是两张图片资源的请求
2.HTTP对登陆用户的会话保持功能
我们在日常使用网站时,比如B站,第一次使用时会让我们先进行用户登录,当登录之后关闭网页,再打开该网页通常都是直接显示已登录状态,不用再次登录,这就是登录用户的会话保持功能。
具体实现则是通过Cookie
和Session
机制
Cookie机制
-
定义:
Cookie是服务器存储在客户端(浏览器)中的一小段文本信息,用于标识用户身份或记录用户状态。
-
工作流程:
1.用户登录:用户输入用户名和密码,提交登录表单。
2.服务器验证:服务器验证用户名和密码,如果正确,生成一个唯一的会话ID(
Session ID
)。3.设置Cookie:服务器在响应头中设置
Set-Cookie
字段,将会话ID发送给浏览器。4.浏览器存储:浏览器将Cookie存储在本地后续请求时自动携带。
5.会话保持:用户访问其他页面时,浏览器会自动在请求报头中携带Cookie。
6.服务器验证:服务器根据Cookie中的会话ID识别用户身份,保持会话状态。
Session机制
-
定义:
Session是服务器存储用户会话信息的机制,通常与会话ID关联。
-
工作流程:
1.用户登录:用户输入用户名和密码,提交登录表单。
2.服务器验证:服务器验证用户名和密码,如果正确,生成一个唯一的会话ID(
Session ID
)。3.存储会话信息:服务器将用户信息(比如用户ID,权限等)存储在会话中,会话ID通过Cookie发送给浏览器。
4.会话保持:用户访问其他页面时,浏览器会自动在请求报头中携带Cookie。
6.服务器验证:服务器根据Cookie中的会话ID识别用户身份,保持会话状态。
-
会话存储方式:
- 内存存储:会话信息存储在服务器内存中,适合单机部署。
- 数据库存储:会话信息存储在数据库中,适合分布式部署。
- Redis存储:会话信息存储在Redis中,适合高并发场景。
接下来先在我们自己的服务器发送的响应报头中添加一个Set-Cookie
字段,模拟实现Cookie机制看看效果
修改inde.html
首页为登陆界面:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="/a/b/hello.html" method="post">name: <input type="text" name="name"><br>password: <input type="password" name="passwd"><br><input type="submit" value="提交"></form>
</body>
</html>
测试结果:
从登录界面进入到第二张网页后,点击刷新旁边的三角,找到Cookie点击,就能看到当前存储的Cookie文件
服务器收到的请求:
当从登录界面进入第二张网页,再次发送请求时就会将Cookie文件中的session=abc123456
发送过去。
注意事项:
1.核心机制
-
Cookie 和 Session 本质上相同:都是通过会话 ID 关联客户端和服务器端的会话数据。
-
客户端存储会话 ID:会话 ID 存储在客户端(Cookie),客户端每次请求时携带。
-
服务器存储会话数据:会话数据(如用户信息、权限、购物车等)存储在服务器。
2. 区别
-
Cookie 机制
- 重点:强调会话 ID 由客户端存储。
- 适用场景:适合存储少量数据(如会话 ID、用户偏好设置)。
-
Session 机制
- 重点:强调会话数据由服务器存储。
- 适用场景:适合存储大量数据(如用户信息、权限、购物车等)。
总结:
Cookie和Session本质上相同:
都是将会话ID存储在客户端中,会话数据存储在服务器上;客户端只能访问会话ID,无法直接修改会话数据。
区别在于概念和实现:Cookie强调客户端存储会话ID,Session强调服务器存储会话数据。
实际应用中:Session通常依赖Cookie机制实现,但是Session更灵活,安全,适合存储大量数据。
以上就是关于应用层协议HTTP的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!