文章目录
- [1. 预备知识](#1. 预备知识)
-
- [1.1 域名](#1.1 域名)
- [1.2 url](#1.2 url)
- [2. http请求和响应的格式](#2. http请求和响应的格式)
-
- [2.1 http request](#2.1 http request)
- [2.2 http response](#2.2 http response)
- [2.3 使用telnet看一下http response](#2.3 使用telnet看一下http response)
- [2.4 使用fiddler来进行抓包,看一下http request](#2.4 使用fiddler来进行抓包,看一下http request)
- [3. 写一个简单的httpserver](#3. 写一个简单的httpserver)
-
- [3.1 基础框架](#3.1 基础框架)
- [3.2 web根目录](#3.2 web根目录)
- [3.3 拿到url,更好的访问网页](#3.3 拿到url,更好的访问网页)
- [3.4 get和post](#3.4 get和post)
- [3.5 404 页面](#3.5 404 页面)
- [3.6 重定向](#3.6 重定向)
- [3.7 设置 Content-Type](#3.7 设置 Content-Type)
- [3.8 设置 Cookie](#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格式
c
协议://主机名:端口号/路径/文件名?查询参数#片段标识符
- 主机名 :也称为域名,是用于定位资源所在服务器的标识。它通过域名系统(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会被编码成(截取了部分)
c
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
c
GET / HTTP/1.1
Host: <目标服务器域名>
bash
[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.com
HTTP/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" />
<meta
name="description"
content="全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。"
/>
<link rel="shortcut icon" href="//www.baidu.com/favicon.ico" type="image/x-icon" />
<link
rel="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
cpp
#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include <thread>
#define PORT 9000
class 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函数
cpp
#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
文件
bash
.
├── HttpServer.cc
├── HttpServer.hpp
├── log.hpp
├── Makefile
├── myServer
├── Socket.hpp
└── wwwroot
└── index.html
index.html
就是简单的显示
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
获取,该函数进行文件操作
cpp
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()
方法用来解析请求行
cpp
#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()
cpp
// 获得请求,发送响应
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文件,此时目录结构如下
bash
.
├── 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
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>
<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()
处理
cpp
// 获得请求,发送响应
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()
方法,让浏览器无论如何都帮我们重定向到一个新的网页,目的只是为了看看效果。
cpp
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 {
// 正常读取,为了测试,这里将状态码改为302
statusLine = "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()
方法加了一点东西
cpp
#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和suffix
void parseHeadLine()
{
istringstream iss(reqHeader[0]);
iss >> method >> url >> httpVersion;
// 得到filePath
if(url == "/" || url == "/index.html" || url == "/favicon.ico") {
// 进入默认页面 (现在是./wwwroot)
filePath += "/";
filePath += HOMEPAGE;
/*ps: "/favicon.ico" 是请求的资源路径。"/" 表示服务器的根目录路径,"favicon.ico" 是文件名。
这个文件通常是网站的图标文件。当浏览器访问一个网站时,会自动尝试获取这个图标文件,
用于在浏览器的标签页、书签栏等位置显示网站的图标。这样可以让用户更容易识别不同的网站。*/
} else {
// 如果用户输入了url(假设输入为/aaab,就进入它输入的那个
filePath += url;
}
// 得到suffix
auto 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()
cpp
#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include "HttpRequest.hpp"
#include <thread>
#include <iostream>
#include <fstream>
#include <unordered_map>
#define PORT 9000
class 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后缀中的文件格式的conteType
string 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
如下,让其显示两张图片
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>
目录结构如下
shell
[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
cpp
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: ";
// 设置cookie
header += "session_id=123456789abcdef\r\n";
string blankLine = "\r\n";
string response = (statusLine + header + blankLine + text);
// ...
}
用浏览器访问根目录,查看cookie,刷新网页,仍能看到cookie

服务器当然也可以看到:
