文章目录
- 1. 预备知识
- 1.1 域名
- 1.2 url
- 2. http请求和响应的格式
- 2.1 http request
- 2.2 http response
- 2.3 使用telnet看一下http response
- 2.4 使用fiddler来进行抓包,看一下http request
- 3. 写一个简单的httpserver
- 3.1 基础框架
- 3.2 web根目录
- 3.3 拿到url,更好的访问网页
- 3.4 get和post
- 3.5 404 页面
- 3.6 重定向
- 3.7 设置 Content-Type
- 3.8 设置 Cookie
1. 预备知识
1.1 域名
域名(Domain Name)是互联网上用于识别和定位网站、网络服务或其他互联网资源的字符型标识。
类似这样的https://www.baidu.com
,代替了http://220.181.38.150
。目的是为了方便记忆
当用户在浏览器中输入域名时,计算机需要通过域名系统(DNS)将域名转换为对应的 IP 地址,才能找到相应的服务器并获取网站内容。
HTTP 协议默认使用端口 80,HTTPS 协议默认使用端口 443。当你在浏览器中输入一个域名(如https://www.example.com
)来访问一个网站时,浏览器会自动尝试使用这些默认端口进行连接。当然也可以手动指定。https://www.baidu.com:80
也可以访问到百度网页
1.2 url
URL(Uniform Resource Locator),中文名称是统一资源定位符,它是用于完整地描述互联网上网页和其他资源的地址。具有唯一性
下面是一个完整的url格式
协议://主机名:端口号/路径/文件名?查询参数#片段标识符
- 主机名:也称为域名,是用于定位资源所在服务器的标识。它通过域名系统(DNS)解析为 IP 地址,从而让浏览器能够找到服务器的位置。例如,在
https://www.baidu.com
中,www.baidu.com
就是主机名。 - 端口号(可选):紧跟在主机名之后,用
:
隔开。端口号用于指定服务器上接收请求的特定端口。如果没有指定端口号,对于常见的协议会使用默认端口。例如,HTTP 默认端口是 80,HTTPS 默认端口是 443。如果要使用非默认端口访问资源,就需要在 URL 中明确写出端口号。比如http://www.example - website.com:8080
,这里的8080
就是端口号。 - 路径(可选):位于主机名或端口号之后,用
/
与前面部分隔开。路径用于指定资源在服务器中的具体位置,它可以是一个文件夹路径或者一个特定文件的路径。例如,在https://www.example - website.com/products/item1.html
中,/products/item1.html
就是路径部分,表示在服务器的products
文件夹下的item1.html
文件。 - 查询参数(可选):如果存在,位于路径之后,用
?
与前面部分隔开。查询参数用于向服务器传递额外的信息,如用户的搜索条件、筛选条件等。它由一系列参数名和参数值组成,参数名和参数值之间用=
连接,不同的参数之间用&
分隔。例如,在https://www.example - website.com/search?q = keyword&page = 2
中,?q = keyword&page = 2
是查询参数部分,其中q = keyword
表示搜索关键词为keyword
,page = 2
表示显示搜索结果的第 2 页。 - 片段标识符(可选):如果存在,位于查询参数之后,用
#
与前面部分隔开。片段标识符用于指定文档内的一个特定位置,通常用于在一个较长的网页文档中快速定位到某个部分。比如,在一个很长的网页中有一个标题为 “章节 3” 的部分,其对应的片段标识符可能是#section3
,当用户在 URL 中添加这个片段标识符后,浏览器会直接跳转到网页中该标题对应的位置。
**url和域名的关系:**域名是 URL(统一资源定位符)的一部分。URL 包含了用于定位资源所需的完整信息,而域名主要用于识别和定位资源所在的服务器。例如,在 URLhttps://www.example.com/products/item1.html
中,https://www.example.com
是域名部分。它帮助浏览器找到资源所在的服务器,而 URL 的其余部分(如 /products/item1.html
路径部分等)则用于在服务器上进一步定位具体的资源。
特殊情况处理:当 URL 中包含特殊字符(如中文、空格、非 ASCII 字符等)时,需要对这些字符进行编码,以确保 URL 能够正确地在网络中传输。URL 编码采用%
加上字符的十六进制 ASCII 码来表示特殊字符。
例如,我们在搜索框输入aaa+?&你好aaa
,此时会url会被编码成(截取了部分)
aaa%2B%3F%26你好aaa
被编码后的url会发送到服务器,服务器可以进行decode。
这个网址可以在线编码和解码UrlEncode编码和UrlDecode解码-在线URL编码解码工具
2. http请求和响应的格式
2.1 http request
- 请求行(Request Line)
- 这是 HTTP 请求的第一行,包含了请求方法、请求的 URL 以及 HTTP 协议版本。
- 请求方法:常见的有以下几种。
- GET:用于从服务器获取资源。例如,当用户在浏览器中输入一个网址或者点击一个超链接时,浏览器通常会发送一个 GET 请求来获取网页的内容。GET 请求的参数会附加在 URL 的查询字符串部分(例如
https://www.example.com/search?q=keyword
,这里的q=keyword
就是查询参数),这些参数是可见的,并且有长度限制。 - POST:用于向服务器提交数据,通常用于表单提交。例如,当用户在一个网站上填写注册信息或者登录信息并点击提交按钮时,浏览器会发送一个 POST 请求。POST 请求的数据通常包含在请求体中,相对于 GET 请求,POST 请求的数据对用户来说是不可见的,并且没有严格的长度限制。
- PUT:用于将数据上传到服务器指定的位置,以更新资源。例如,在一些支持文件上传或者数据更新的 Web 应用中会用到 PUT 请求。PUT 请求和 POST 请求有些类似,但 PUT 请求通常是幂等的,即多次执行相同的 PUT 操作,对资源的影响和一次操作是相同的。
- DELETE:用于请求服务器删除指定的资源。例如,在一些具有资源管理功能的 Web 应用中,用户可以通过发送 DELETE 请求来删除自己创建的文件或者记录。
- GET:用于从服务器获取资源。例如,当用户在浏览器中输入一个网址或者点击一个超链接时,浏览器通常会发送一个 GET 请求来获取网页的内容。GET 请求的参数会附加在 URL 的查询字符串部分(例如
- 请求的 URL:这部分明确了请求所指向的资源位置,格式和前面介绍的 URL 格式相同,包括协议、主机名、路径等部分。例如
https://www.example.com/products/item1
。 - HTTP 协议版本:常见的有 HTTP/1.0、HTTP/1.1 和 HTTP/2.0 等。例如
HTTP/1.1
表示这个请求是按照 HTTP 1.1 版本的协议规则发送的。
- 请求头部(Request Headers)
- 请求头部包含了一系列的键 - 值对,用于向服务器提供更多关于请求的信息,
- User - Agent:用于标识客户端的软件信息,包括浏览器类型、版本号等。
- Accept:用于告诉服务器客户端能够接受的内容类型。例如
Accept: text/html,application/xhtml+xml,application/xml;q = 0.9,image/avif,image/webp,image/apng,*/*;q = 0.8
,表示客户端可以接受文本 / HTML 格式、应用程序 / XHTML + XML 格式、应用程序 / XML 格式(权重为 0.9)、图像 / AVIF 格式、图像 / WEBP 格式、图像 / APNG 格式以及其他所有格式(权重为 0.8)的内容。 - Accept - Language:用于表示客户端偏好的语言。
- Cookie:如果客户端之前访问过该网站并且服务器设置了 Cookie,那么在后续的请求中,客户端会通过 Cookie 头将之前存储的 Cookie 信息发送给服务器。例如
Cookie: session_id = 123456789; user_preference = dark_mode
,这里的session_id
和user_preference
是 Cookie 中的键值对,分别代表会话 ID 和用户偏好(深色模式)。 - Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- Connection:连接的行为,可以是长连接(HTTP1.1支持),也可以是短连接(HTTP1.0支持)。短连接意味着当前的 HTTP 请求 - 响应完成后,应该立即关闭连接。长连接意味着客户端(在请求头中设置)或者服务器(在响应头中设置)希望保持连接打开,以便可以在这个连接上进行后续的 HTTP 请求和响应交互,这样可以通过同一个 TCP 连接获取网页中的多个资源,如图片、脚本文件等。
- 空行(Blank Line)
- 请求头部和请求体之间有一个空行,这个空行用于分隔请求头部和请求体,是 HTTP 请求格式的一个重要部分。它的存在使得服务器能够清楚地分辨出请求头部已经结束,接下来是请求体部分。
- 请求体(Request Body),或者说成请求正文
- 不是所有的 HTTP 请求都有请求体。如 GET 请求通常没有请求体,因为它主要是用于获取资源,参数已经在 URL 中传递。而 POST、PUT 等请求通常会有请求体。
- 请求体用于承载要发送给服务器的数据。例如,在 POST 请求中,如果是一个表单提交,请求体可能包含用户填写的表单数据,如姓名、地址、电话号码等。
图片示意如下:
2.2 http response
- 状态行(Status Line)
- 这是 HTTP 响应的第一行,包含了 HTTP 协议版本、状态码和状态消息。
- HTTP 协议版本:与请求中的协议版本相对应,常见的有 HTTP/1.0、HTTP/1.1 和 HTTP/2.0 等。例如 “HTTP/1.1”,表明服务器是按照 HTTP 1.1 版本的协议规则进行响应的。
- 状态码:是一个三位数字的代码,用于表示服务器对请求的处理结果。状态码分为五大类:
- 1xx(信息性状态码):例如 100 Continue,表示服务器已经收到了请求的部分内容,并且客户端可以继续发送其余部分。这类状态码主要用于 HTTP/1.1 协议中,在一些需要分块传输数据的场景下使用。
- 2xx(成功状态码):最常见的是 200 OK,表示服务器成功处理了请求,并返回了请求的内容。例如,当浏览器发送一个 GET 请求获取网页内容,服务器成功找到并返回网页文件时,就会返回 200 OK 状态码。
- 3xx(重定向状态码):如 301 Moved Permanently,表示被请求的资源已经永久移动到了新的位置,服务器会在响应头中给出新的位置信息(Location 字段),引导客户端访问新位置。302 Found 则表示资源临时移动。
- 4xx(客户端错误状态码):404 Not Found 是最常见的,它表示客户端请求的资源在服务器上不存在。例如,用户在浏览器中输入了一个错误的网址,服务器找不到对应的资源时就会返回 404 状态码。400 Bad Request 表示客户端发送的请求格式有误。
- 5xx(服务器错误状态码):500 Internal Server Error 表示服务器在处理请求过程中发生了内部错误,可能是服务器代码出现问题或者服务器资源不足等原因导致。
- 状态消息:是对状态码的简单文字描述,辅助理解状态码的含义。例如,对于状态码 200,状态消息是 “OK”;对于状态码 404,状态消息是 “Not Found”。
- 响应头部(Response Headers)
- 响应头部包含一系列的键 - 值对,用于向客户端提供关于响应的更多信息,如内容类型、内容长度、缓存信息等。
- Content - Type:用于告知客户端响应内容的类型。例如 “Content - Type: text/html; charset=UTF - 8”,表示响应内容是 HTML 文本,并且字符编码是 UTF - 8。如果响应内容是图片,可能是 “Content - Type: image/jpeg” 或 “Content - Type: image/png” 等。
- Content - Length:表示响应内容的长度(字节数)。例如 “Content - Length: 1024”,意味着响应内容的大小是 1024 字节。
- Cache - Control:用于控制客户端缓存行为。例如 “Cache - Control: max - age = 3600”,表示客户端可以缓存该响应内容,并且在 3600 秒内可以直接使用缓存内容,无需再次向服务器请求。
- Set - Cookie:如果服务器希望在客户端设置 Cookie,会通过 Set - Cookie 头部来实现。例如 “Set - Cookie: session_id = 123456789; Path=/; Expires=Wed, 28 - Nov - 2024 12:00:00 GMT”,这样就会在客户端设置一个名为 session_id 的 Cookie,用于后续的会话跟踪等用途。
- 空行(Blank Line)
- 响应头部和响应体之间有一个空行,用于分隔这两个部分,就像在请求格式中一样,这个空行让客户端能够清楚地分辨出响应头部已经结束,接下来是响应体部分。
- 响应体(Response Body), 或者说成相应正文
- 响应体是服务器返回给客户端的实际内容,根据请求的不同,内容也各不相同。
- 如果是 GET 请求获取网页,响应体可能是 HTML 文件的内容,包含网页的文本、图像引用、脚本引用等。
图片示意如下:
2.3 使用telnet看一下http response
GET / HTTP/1.1
Host: <目标服务器域名>
[lyf@hcss-ecs-3db9 ~]$ telnet www.baidu.com 80
Trying 180.101.50.242...
Connected to www.baidu.com.
Escape character is '^]'.
GET / HTTP/1.1
Host: www.baidu.comHTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Connection: keep-alive
Content-Length:
Content-Type: text/html
Date: Thu, 28 Nov 2024 14:13:28 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Pragma: no-cache
Server: BWS/1.1
Set-Cookie: BAIDUID=4C4DF0392EDC72CD7F87CAA8907F56F8:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=4C4DF0392EDC72CD7F87CAA8907F56F8; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1732803208; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BAIDUID=4C4DF0392EDC72CD585CEB7A5EA950FA:FG=1; max-age=31536000; expires=Fri, 28-Nov-25 14:13:28 GMT; domain=.baidu.com; path=/; version=1; comment=bd
Traceid: 173280320805907865708728492423275128693
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1
X-Xss-Protection: 1;mode=block<!DOCTYPE html>
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /><meta content="always" name="referrer" /><metaname="description"content="全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。"/><link rel="shortcut icon" href="//www.baidu.com/favicon.ico" type="image/x-icon" /><linkrel="search"type="application/opensearchdescription+xml"href="//www.baidu.com/content-search.xml"title="百度搜索"/><title>百度一下,你就知道</title>//...
</html>
2.4 使用fiddler来进行抓包,看一下http request
访问百度,没有请求体
3. 写一个简单的httpserver
3.1 基础框架
下面是HttpServer.hpp
使用了之前在自定义协议里写的Socket.hpp
#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include <thread>#define PORT 9000class HttpServer
{
public:HttpServer(uint16_t port = PORT) : _port(port) {}void startServer(){_sock.Socket();_sock.Bind(_port);_sock.Listen();for (;;) {string clientIp;uint16_t clientPort;int sockFd = _sock.Accept(&clientIp, &clientPort);log(INFO, "get a new link, fd: %d\n", sockFd);thread([=]{handlerHttp(sockFd);}).detach();}}// 获得请求,发送响应void handlerHttp(int sockFd){char buf[10240];memset(buf, 0, sizeof buf);for(;;) {ssize_t n = read(sockFd, buf, sizeof buf);if (n > 0) {// 打印一下客户端的信息buf[n] = 0;cout << buf;// 返回响应string text = "<h1>hello world</h1>";string statusLine = "HTTP/1.1 200 OK\r\n";string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);write(sockFd, response.c_str(), response.size());} else if (n==0) {cout << "read done\n";break;} else {cout << "read error!\n";break;} } close(sockFd);}
private:Sock _sock;uint16_t _port;
};
下面是HttpServer.cc
,有main函数
#include "HttpServer.hpp"
#include <memory>int main(int argc, char* argv[])
{if(argc != 2) {cerr << "usage error!\n";return -1;}uint16_t port = stoi(argv[1]);unique_ptr<HttpServer> svr(new HttpServer(port));svr->startServer();return 0;
}
启动服务器后,用浏览器访问。服务器收到request请求,向客户端发送response响应
3.2 web根目录
直接在handlerHttp()
里硬编码html代码不太好,更好的是建一个web根目录,Web 根目录是 Web 服务器用于存放可通过网络访问的文件的基本目录。它是 Web 站点文件层次结构的顶层目录,所有的 Web 资源(如 HTML 文件、CSS 文件、JavaScript 文件、图像等)都存储在这个目录或它的子目录下。建一个webroot
文件夹,再在里面放置一个index.html
文件
.
├── HttpServer.cc
├── HttpServer.hpp
├── log.hpp
├── Makefile
├── myServer
├── Socket.hpp
└── wwwroot└── index.html
index.html
就是简单的显示
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>newTitle</title>
</head>
<body><h1>hello world</h1><h2>你好世界</h2>
</body>
</html>
修改handlerHttp()
获取text
的方法,通过调用函数readFromHtml
获取,该函数进行文件操作
const static string ROOTPATH = "wwwroot";
// 获得请求,发送响应
void handlerHttp(int sockFd)
{// ...string text = readFromHtml(ROOTPATH + "/index.html");// ...
}
// 读文件
string readFromHtml(string path)
{std::ifstream file(path);if(!file) {log(FATAL, "readFromHtml() open error!\n");return "404";}std::string line = "";std::string content = "";while(getline(file, line)) {content += line;}return content;
}
此时如果修改html的代码,就算不重启服务器,也会更新页面。
3.3 拿到url,更好的访问网页
之前的测试时访问页面的url域名后的路径并没有处理,就算url随便写,也能访问到页面。
封装一个结构体HttpRequest.hpp
,该结构体的parse()
方法用来解析请求行
#pragma once
#include <string>
#include <iostream>
#include <vector>
#include <sstream>using namespace std;
const static string SEP = "\r\n";
const static string ROOTPATH = "./wwwroot";
const static string HOMEPAGE = "index.html";struct HttpRequest
{vector<string> reqHeader; // 请求行,请求报头string text; // 正文部分string method; // 方法string url; // 统一资源定位符string httpVersion; // http版本string filePath = ROOTPATH; // 最终要访问文件的路径,根据用户输入的不同进行处理// 将http的requset请求拆解。void deserialize(string req){for(;;) {size_t pos = req.find(SEP);if(pos == string::npos) {// 什么都没找到return;}string tmp = req.substr(0, pos);if(tmp == "") {// 找到了空行break;} reqHeader.push_back(tmp);// 移该已经添加到数组的部分(别忘了移除SEP)req.erase(req.begin(), req.begin() + pos + SEP.size());}// 假设这是一个完整的请求。那么剩下的就都是text了。text = req;}// 解析请求行void parseHeadLine(){istringstream iss(reqHeader[0]); iss >> method >> url >> httpVersion;if(url == "/" || url == "/index.html" || url == "/favicon.ico") {// 进入默认页面 (现在是./wwwroot)filePath += "/";filePath += HOMEPAGE;/*ps: “/favicon.ico” 是请求的资源路径。“/” 表示服务器的根目录路径,“favicon.ico” 是文件名。这个文件通常是网站的图标文件。当浏览器访问一个网站时,会自动尝试获取这个图标文件,用于在浏览器的标签页、书签栏等位置显示网站的图标。这样可以让用户更容易识别不同的网站。*/} else {// 如果用户输入了url(假设输入为/aaab,就进入它输入的那个filePath += url;}}// 用来打印信息void printInfo(){printf("====================================\n");for (const auto &e : reqHeader) {printf("%s\n\n", e.c_str());}printf("method: %s\nurl: %s\nhttpVersion: %s\nfilePath: %s\n", method.c_str(), url.c_str(), httpVersion.c_str(), filePath.c_str());printf("\ntext:%s\n", text.c_str());printf("====================================\n");}
};
稍微修改一下HttpServer.hpp
中的handlerHttp()
// 获得请求,发送响应
void handlerHttp(int sockFd)
{char buf[10240];memset(buf, 0, sizeof buf);for(;;) {ssize_t n = read(sockFd, buf, sizeof buf);if (n > 0) {// 打印一下客户端的信息buf[n] = 0;cout << buf;// 自定义协议,获取路径HttpRequest req = HttpRequest();req.deserialize(buf);req.parseHeadLine();// req.printInfo();// string path = ROOTPATH;// path += url;// 返回响应string text = readFromHtml(req.filePath);string statusLine = "HTTP/1.1 200 OK\r\n";string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);write(sockFd, response.c_str(), response.size());} else if (n==0) {cout << "read done\n";break;} else {cout << "read error!\n";break;} } close(sockFd);
}
再写一个html文件,此时目录结构如下
.
├── HttpRequest.hpp
├── HttpServer.cc
├── HttpServer.hpp
├── log.hpp
├── Makefile
├── myServer
├── Socket.hpp
└── wwwroot├── index.html└── testDir└── test.html
下面是几种访问情况。
可以在前面的html中写上超链接,可以在不同网页间进行跳转。
3.4 get和post
我们修改一下index.html
,加一个表格,修改method
为get
和post
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>newTitle</title>
</head>
<body><form action="/testDir/test.html" method="get">姓名:<br><input type="text" name='name' value="zs"><br>密码:<br><input type="password" name='password' value="123456"><br><br><input type="submit" value="Submit"></form><!-- <h1>hello!</h1> -->
</body>
</html>
get
方法通过url进行传参,post
方法通过正文部分进行传参。
下面是get
点击提交过后的url:
下面是post
点击提交过后服务器的响应(也可以通过fiddler来进行抓包查看):
3.5 404 页面
现在的404页面不好看,下面修改一下
修改一下HttpServer.hpp
,readFromHtml()
读取失败后不再直接返回404,而是返回空串,然后到hanlerHttp()
处理
// 获得请求,发送响应
void handlerHttp(int sockFd)
{char buf[10240];memset(buf, 0, sizeof buf);for(;;) {ssize_t n = read(sockFd, buf, sizeof buf);if (n > 0) {// 打印一下客户端的信息buf[n] = 0;cout << buf;// 自定义协议,获取路径HttpRequest req = HttpRequest();req.deserialize(buf);req.parseHeadLine();// 返回响应string text = "";string statusLine = "";string exitStr = readFromHtml(req.filePath);if(exitStr.empty()) {statusLine = "HTTP/1.1 404 Not Found\r\n";// 从出错页面读取string path = ROOTPATH;path += "/404Page.html";text = readFromHtml(path);} else {// 正常读取statusLine = "HTTP/1.1 200 OK\r\n";text = exitStr;}string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);write(sockFd, response.c_str(), response.size());} else if (n==0) {cout << "read done\n";break;} else {cout << "read error!\n";break;} } close(sockFd);
}// 读文件
string readFromHtml(string path)
{std::ifstream file(path);if(!file) {log(FATAL, "readFromHtml() open error!\n");return "";}std::string line = "";std::string content = "";while(getline(file, line)) {content += line;}return content;
}
现在如果404会进入到404Page.html
这个页面
3.6 重定向
让服务器指导浏览器,让浏览器访问新的地址,这就叫做重定向
修改HttpServer.hpp
的handlerHttp()
方法,让浏览器无论如何都帮我们重定向到一个新的网页,目的只是为了看看效果。
void handlerHttp(int sockFd)
{char buf[10240];memset(buf, 0, sizeof buf);for(;;) {ssize_t n = read(sockFd, buf, sizeof buf);if (n > 0) {// 打印一下客户端的信息buf[n] = 0;cout << buf;// 自定义协议,获取路径HttpRequest req = HttpRequest();req.deserialize(buf);req.parseHeadLine();// 返回响应string text = "";string statusLine = "";string exitStr = readFromHtml(req.filePath);if(exitStr.empty()) {statusLine = "HTTP/1.1 404 Not Found\r\n";// 从出错页面读取string path = ROOTPATH;path += "/404Page.html";text = readFromHtml(path);} else {// 正常读取,为了测试,这里将状态码改为302statusLine = "HTTP/1.1 302 Found\r\n";text = exitStr;}string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";header += "Lffcation: https://legacy.cplusplus.com/\r\n"; // 跳转一个新的网页string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);write(sockFd, response.c_str(), response.size());} else if (n==0) {cout << "read done\n";break;} else {cout << "read error!\n";break;} } close(sockFd);
}
当我们访问网页时,会被跳转到https://legacy.cplusplus.com/
网页
用fiddler抓包看下:
3.7 设置 Content-Type
为了让页面显示图片,设置一下相应的Content-Type
属性。
HttpRequest.hpp
如下,添加了一个属性suffix
,给parseHeadLine()
方法加了一点东西
#pragma once
#include <string>
#include <iostream>
#include <vector>
#include <sstream>using namespace std;
const static string SEP = "\r\n";
const static string ROOTPATH = "./wwwroot";
const static string HOMEPAGE = "index.html";struct HttpRequest
{vector<string> reqHeader; // 请求行,请求报头string text; // 正文部分string method; // 方法string url; // 统一资源定位符string httpVersion; // http版本string suffix; // 后缀string filePath = ROOTPATH; // 最终要访问文件的路径,根据用户输入的不同进行处理// 将http的requset请求拆解。void deserialize(string req){for(;;) {size_t pos = req.find(SEP);if(pos == string::npos) {// 什么都没找到return;}string tmp = req.substr(0, pos);if(tmp.empty()) {// 找到了空行break;} reqHeader.push_back(tmp);// 移该已经添加到数组的部分(别忘了移除SEP)req.erase(0, pos + SEP.size());}// 假设这是一个完整的请求。那么剩下的就都是text了。text = req;}// 解析请求行,得到filePath和suffixvoid parseHeadLine(){istringstream iss(reqHeader[0]); iss >> method >> url >> httpVersion;// 得到filePathif(url == "/" || url == "/index.html" || url == "/favicon.ico") {// 进入默认页面 (现在是./wwwroot)filePath += "/";filePath += HOMEPAGE;/*ps: “/favicon.ico” 是请求的资源路径。“/” 表示服务器的根目录路径,“favicon.ico” 是文件名。这个文件通常是网站的图标文件。当浏览器访问一个网站时,会自动尝试获取这个图标文件,用于在浏览器的标签页、书签栏等位置显示网站的图标。这样可以让用户更容易识别不同的网站。*/} else {// 如果用户输入了url(假设输入为/aaab,就进入它输入的那个filePath += url;}// 得到suffixauto pos = url.rfind('.');suffix = (pos == string::npos) ? ".html" : url.substr(pos); // 默认后缀是.html}// 用来打印信息void printInfo(){printf("====================================\n");// for (const auto &e : reqHeader) {// printf("%s\n\n", e.c_str());// }printf("method: %s\nurl: %s\nhttpVersion: %s\nfilePath: %s\n", method.c_str(), url.c_str(), httpVersion.c_str(), filePath.c_str());printf("\ntext:%s\n", text.c_str());printf("====================================\n");}
};
HttpServer.hpp
如下,加了一个哈希表,图片需要用二进制读取,所以也需要修改readFromHtml()
方法,改成二进制读取,重名为为readFromFile()
#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include "HttpRequest.hpp"
#include <thread>
#include <iostream>
#include <fstream>
#include <unordered_map>#define PORT 9000class HttpServer
{
public:HttpServer(uint16_t port = PORT) : _port(port){_contentType.insert({".html", "text/html"});_contentType.insert({".png", "image/png"});_contentType.insert({".jpg", "image/jpeg"});}void startServer(){_sock.Socket();_sock.Bind(_port);_sock.Listen();// log(INFO, "here\n");for (;;) {string clientIp;uint16_t clientPort;int sockFd = _sock.Accept(&clientIp, &clientPort);log(INFO, "get a new link, fd: %d\n", sockFd);thread([=]{handlerHttp(sockFd);}).detach();}}// 获得请求,发送响应void handlerHttp(int sockFd){char buf[10240];memset(buf, 0, sizeof buf);for(;;) {ssize_t n = read(sockFd, buf, sizeof buf);if (n > 0) {// 打印一下客户端的信息buf[n] = 0;cout << buf;// 自定义协议,获取路径HttpRequest req = HttpRequest();req.deserialize(buf);req.parseHeadLine();// 返回响应string text = "";string statusLine = "";string exitStr = readFromFile(req.filePath);if(exitStr.empty()) {statusLine = "HTTP/1.1 404 Not Found\r\n";// 从出错页面读取string path = ROOTPATH;path += "/404Page.html";text = readFromFile(path);} else {// 正常读取statusLine = "HTTP/1.1 200 OK\r\n";text = exitStr;}string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";header += "Content-Type: ";header += getContentTypeValue(req.suffix) + "\r\n";string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);write(sockFd, response.c_str(), response.size());} else if (n==0) {cout << "read done\n";break;} else {cout << "read error!\n";break;} } close(sockFd);}// 读文件string readFromFile(string path){std::ifstream file(path, std::ios::binary); // 二进制方式打开if(!file) {log(FATAL, "readFromHtml() open error!\n");return "";}file.seekg(0, std::ios::end);std::size_t fileSize = file.tellg(); // 得到文件的大小file.seekg(0);std::string content(fileSize, '0'); // 给content先分配大小file.read((char*)content.c_str(), fileSize); // 直接读fileSize大小file.close();return content;}// 通过哈希表来获取url后缀中的文件格式的conteTypestring getContentTypeValue(const string& suffix){auto pos = _contentType.find(suffix);return (pos == _contentType.end()) ? _contentType[".html"] : _contentType[suffix]; // 默认返回html}
private:Sock _sock;uint16_t _port;unordered_map<string, string> _contentType; // 需要在构造函数初始化
};
index.html
如下,让其显示两张图片
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>newTitle</title>
</head>
<body><h1>hello!</h1><img src="./image/cat.jpg" alt="cat"><img src="./image/nyaruko.png" alt="奈亚子">
</body>
</html>
目录结构如下
[lyf@hcss-ecs-3db9 pro24_11_29HttpServer]$ tree .
.
├── HttpRequest.hpp
├── HttpServer.cc
├── HttpServer.hpp
├── log.hpp
├── Makefile
├── myServer
├── Socket.hpp
└── wwwroot├── 404Page.html├── image│ ├── cat.jpg│ └── nyaruko.png├── index.html└── testDir└── test.html
运行服务器,用浏览器访问根目录,成功显示图片
观察服务器打印的信息,发现至少有3次请求,分别是根目录/
,/image/cat.jpg
,/image/nyaruko.png
。每次请求都要建立一个连接,这就叫做短连接。
如果只建立一次连接,就把所有的图片都拿到了,就叫做长连接。
3.8 设置 Cookie
http对登录用户的会话保持功能:
- 当用户首次登录成功后,服务器会在响应头中设置一个或多个 Cookie。Cookie 是一个包含键 - 值对的小文本信息块,会通过浏览器存储在客户端(可能是内存级别,也可能是文件级别)。例如,服务器可能会设置一个名为 “session_id” 的 Cookie,其值是一个唯一的会话标识符(如 “123456789abcdef”)。在后续的 HTTP 请求中,浏览器会自动将这些 Cookie 包含在请求头中发送给服务器。
- 服务器收到请求后,通过检查请求头中的 Cookie 来识别用户。以一个简单的 Web 应用为例,服务器可能会维护一个会话存储(可以是内存中的一个数据结构,也可以是基于数据库的存储),其中存储了每个会话 ID 对应的用户登录状态、权限等信息。当收到带有 “session_id” Cookie 的请求时,服务器会在会话存储中查找对应的记录,以确定用户是否已经登录以及相关信息。
可以修改HttpServer.hpp
中的handlerHttp()
方法,让其相应的数据加上cookie
void handlerHttp(int sockFd)
{// ...string header = "Content-Length: ";header += to_string(text.size()) + "\r\n";header += "Content-Type: ";header += getContentTypeValue(req.suffix) + "\r\n";header += "Set-Cookie: "; // 设置cookieheader += "session_id=123456789abcdef\r\n";string blankLine = "\r\n";string response = (statusLine + header + blankLine + text);// ...
}
用浏览器访问根目录,查看cookie,刷新网页,仍能看到cookie
服务器当然也可以看到: