一、封装Socket API
HTTP协议基于TCP/IP协议运行,其socket接口我们在【socket编程】TCP网络通信模型-CSDN博客中已经详细解释过了,这里就不做赘述。
socket.hpp
#include "logmessage.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
#include <string>
using namespace std;
class Socket
{
int _sockfd=-1;
const static int s_backlog = 10;
public:
Socket() // 构造时创建套接字文件
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd == -1)
{
LogMessage(FATAL, "socket:%d:%s", errno, strerror(errno));
exit(errno);
}
LogMessage(DEBUG, "create socket success!");
}
~Socket() // 析构时关闭套接字文件
{
if(_sockfd!=-1)
{
close(_sockfd);
}
}
void Bind(uint16_t port) //服务器需要显式的绑定端口
{
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; //绑定任意ip
int ret = bind(_sockfd, (sockaddr*)&local, sizeof(local));
if(ret == -1)
{
LogMessage(FATAL, "bind:%d:%s", errno, strerror(errno));
exit(errno);
}
LogMessage(DEBUG, "bind socket success!");
}
void Listen() //服务器需要设置套接字为监听状态
{
int ret = listen(_sockfd, s_backlog);
if(ret == -1)
{
LogMessage(FATAL, "listen:%d:%s", errno, strerror(errno));
exit(errno);
}
LogMessage(DEBUG, "listen socket success!");
}
int Accept(string *ip, uint16_t *port) //服务器需要接收并建立链接
{
sockaddr_in client;
socklen_t len = sizeof(client);
int svcsock = accept(_sockfd, (sockaddr*)&client, &len);
if(svcsock == -1)
{
LogMessage(WARNING, "accept:%d:%s", errno, strerror(errno));
return -1;
}
*port = ntohs(client.sin_port);
char buffer[64];
inet_ntop(AF_INET, &client.sin_addr, buffer, sizeof(buffer));
*ip = buffer;
return svcsock;
}
void Connect(const string& ip, uint16_t port) //客户端需要发起链接
{
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);
int ret = connect(_sockfd, (sockaddr*)&server, sizeof(server));
if(ret == -1)
{
LogMessage(FATAL, "connect:%d:%s", errno, strerror(errno));
exit(errno);
}
LogMessage(DEBUG, "connect server success!");
}
int GetSocket()
{
return _sockfd;
}
};
二、封装HTTP协议
关于HTTP协议的内容我们在上一章中做了详细的解释:【应用层协议】HTTP协议 {统一资源定位符URL;HTTP协议格式;HTTP请求的方法;HTTP响应的状态码;HTTP头部的常见字段;HTTP协议的特性:无状态性,无连接性}-CSDN博客
protocol.hpp
#include "logmessage.hpp"
#include <string>
#include <unordered_map>
#include <sstream>
#include <fstream>
#include <iostream>
using namespace std;
const string WebRoot = "./webroot"; //Web根目录
const string SEP = "\r\n"; //HTTP协议的行分割符
struct Request
{
string _method;
string _url;
string _url_args;
string _http_version;
string _path;
string _suffix;
unordered_map<string, string> _header;
string _content;
// 将HTTP报文反序列化为Request结构,如果报文不完整返回false继续读取
bool Deserialize(string &buffer) // 传入应用层缓冲区进行解析
{
int left = 0, right = 0; // 标定当前解析区间的左右端点
// 1.解析请求行
right = buffer.find(SEP);
if (right == string::npos)
{
return false;
}
stringstream ss(buffer.substr(left, right - left));
ss >> _method >> _url >> _http_version;
// 解析url参数
if(_method == "GET")
{
int pos = _url.find('?');
if(pos != string::npos)
{
_url_args = _url.substr(pos+1);
_url.erase(pos);
}
}
// 解析文件路径
_path = WebRoot;
if (_url != "/")
_path += _url;
else
_path += "/index.html";
// 解析文件后缀
int pos = _url.rfind('.');
if(pos == string::npos)
_suffix = "html";
else
_suffix = _url.substr(pos+1);
// 2.解析请求头,将各键值对存入哈希表便于查找
left = right + 2;
while (true)
{
right = buffer.find(SEP, left);
if (left == right)
break; // 3.找到空行结束
if (right == string::npos)
{
return false;
}
int sp_pos = buffer.find(": ", left);
string key = buffer.substr(left, sp_pos - left);
string value = buffer.substr(sp_pos + 2, right - (sp_pos + 2));
_header[key] = value;
left = right + 2;
}
// 4.解析请求体
left += 2;
if (_header.count("Content-Length")) //通过Content_Length关键字获取到请求体长度
{
int len = stoi(_header["Content-Length"]);
_content = buffer.substr(left, len);
if (_content.size() != len)
{
return false;
}
left+=len;
}
buffer.erase(0, left); // 将缓冲区中解析完成的数据去除
return true;
}
//将客户端上传的数据写入到html文件,包括GET方法的url参数和POST方法的请求体内容
int WriteFile()
{
ofstream ofs("./webroot/clientdata.html");
if (!ofs.is_open())
return -1;
if(!_url_args.empty())
ofs << "<p><h1>url_args: " << _url_args << "</h1></p>" << endl;
if(!_content.empty())
ofs << "<p><h1>content: " << _content << "</h1></p>" << endl;
ofs.close();
return _content.size();
}
//将反序列化后的Request结构体打印出来
void DebugPrint()
{
cout << "Request Line:" << endl;
cout << "method: " << _method << endl;
cout << "url: " << _url << endl;
cout << "url_args: " << _url_args << endl;
cout << "http_version: " << _http_version << endl;
cout << endl;
cout << "Header:" << endl;
for (auto &[k, v] : _header)
{
cout << k << ": " << v << endl;
}
cout << endl;
cout << "Body:" << endl;
cout << _content << endl;
cout << "-----------------------------------------------------------------------------" << endl;
}
};
struct Response
{
string _http_version;
int _state_code;
static unordered_map<int, string> _desc_map; //保存状态码对应的状态描述
static unordered_map<string, string> _content_types; //保存文件后缀对应的报文数据类型
unordered_map<string, string> _header;
string _content;
Response(const string &version)
: _http_version(version)
{
}
// 打开url指定的文件,将文件内容读取到_content
int ReadFile(const string &path)
{
ifstream ifs(path.c_str(), ifstream::binary);
if (!ifs.is_open())
return -1;
ifs.seekg(0, ifs.end);
int len = ifs.tellg();
ifs.seekg(0, ifs.beg);
_content.resize(len);
ifs.read((char*)_content.c_str(), len);
ifs.close();
return len;
}
// 将Response结构按照HTTP协议标准进行序列化
string Serialize()
{
stringstream ss;
// 1.序列化响应行
ss << _http_version << " " << to_string(_state_code) << " " << _desc_map[_state_code] << SEP;
// 2.序列化响应头
for (auto &[k, v] : _header)
{
ss << k << ": " << v << SEP; //key: value之间使用冒号+空格进行分割
}
// 3.空行
ss << SEP;
// 4.序列化响应体
ss << _content;
return ss.str();
}
};
unordered_map<int, string> Response::_desc_map = {{200, "OK"}, {404, "Not Found"}, {302, "Found"}};
unordered_map<string, string> Response::_content_types = {{"html", "text/html"}, \
{"mp3", "audio/mp3"}, {"jpeg", "image/jpeg"}};
三、实现HttpServer
http_server.hpp
#include "logmessage.hpp"
#include "socket.hpp"
#include "protocol.hpp"
#include <pthread.h>
class HttpServer
{
Socket _listensock;
uint16_t _port;
public:
HttpServer(uint16_t port)
: _port(port)
{
_listensock.Bind(_port);
_listensock.Listen();
}
void Run()
{
while (true)
{
string client_ip;
uint16_t client_port;
int svcsock = _listensock.Accept(&client_ip, &client_port);
if(svcsock == -1)
continue;
LogMessage(DEBUG, "accept connect success! %s:%d", client_ip.c_str(), client_port);
//将获取到的新链接交给子线程处理
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)svcsock);
}
}
static void* ThreadRoutine(void *args)
{
pthread_detach(pthread_self()); //注意分离线程,主线程不必等待子线程结束
int svcsock = (int)(long)args; //注意64操作系统void*是64位,直接强转int(32位)会编译报错。
string buffer;
char temp[1024];
while(true)
{
int s = recv(svcsock, temp, sizeof(temp), 0);
if(s > 0)
{
buffer.append(temp, s);
Request req;
if(!req.Deserialize(buffer))
{
continue; //如果读取到的不是一个完整报文,继续读取
}
req.WriteFile();
req.DebugPrint();
Response resp("HTTP/1.1");
int len = resp.ReadFile(req._path);
if(len == -1) //打开文件失败,文件不存在
{
resp._state_code = 404; //将状态码设置为404
//将路径重新设置为提前预置的404错误页面
req._path = WebRoot;
req._path += "/404page.html";
req._suffix = "html";
len = resp.ReadFile(req._path);
}
else
resp._state_code = 200;
//填写响应头内容:响应体长度和数据类型
resp._header["Content-Length"] = to_string(len==-1?0:len);
resp._header["Content-Type"] = resp._content_types[req._suffix];
// 测试Cookie机制
// resp._header["Set-Cookie"] = "username=zty";
// resp._header["Set-Cookie"] = "password=123123"; //哈希表自动去重,存在问题
//测试302临时重定向
// resp._state_code = 302;
// resp._header["Location"] = "https://www.qq.com";
//将Response结构序列化后,发送给客户端
string resp_str = resp.Serialize();
send(svcsock, resp_str.c_str(), resp_str.size(), 0);
break; //我们实现的是短链接通信,要为每条链接创建和关闭套接字
}
else
{
break;
}
}
close(svcsock); //子线程负责关闭服务套接字
return nullptr;
}
};
四、测试
查看Web根目录下的所有文件
4.1 超文本标记语言HTML
HTML(HyperText Markup Language),中文译为“超文本标记语言”,是一种用于创建网页的标准标记语言。它不是一种编程语言,而是一种标记语言(Markup Language),通过一系列标签(tags)来告诉浏览器如何显示网页内容,包括文本、图片、视频、游戏以及任何你可以在网页浏览器中看到的内容。
HTML的基本结构
HTML文档由一系列的标签(tags)组成,这些标签通常成对出现(开始标签和结束标签),但也有部分标签是自闭合的,如<img>
、<br>
等。一个基本的HTML文档结构如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>页面标题</title>
</head>
<body>
<h1>这是一个标题</h1>
<p>这是一个段落。</p>
</body>
</html>
<!DOCTYPE html>
声明了文档类型和HTML版本,这里指的是HTML5。<html>
元素是所有其他HTML元素的容器。<head>
元素包含了文档的元(meta)数据,如<meta charset="UTF-8">
定义了字符编码,<title>
定义了文档的标题。<body>
元素包含了可见的页面内容,如标题(<h1>
到<h6>
)、段落(<p>
)、链接(<a>
)、图片(<img>
)等。
HTML的特点
- 平台无关性:HTML可以在任何支持Web浏览器的设备上运行,无论操作系统或硬件平台如何。
- 易用性:HTML的编写相对简单,即使不是专业的程序员也能通过简单的学习创建基本的网页。
- 可扩展性:HTML允许通过其他技术(如CSS和JavaScript)来扩展其功能和表现能力,创建更加丰富和动态的网页。
- 超链接:HTML文档中的超链接允许用户从一个页面跳转到另一个页面,实现网页间的互连。
HTML的发展
HTML自1991年由Tim Berners-Lee发明以来,已经历了多个版本的迭代。目前广泛使用的是HTML5,它引入了许多新的元素和属性,支持多媒体内容(如视频和音频)的嵌入,并提高了网页的表现能力和交互性。
总结
HTML是构建网页的基础,通过其提供的标签和属性,我们可以创建出结构清晰、内容丰富、功能强大的网页。随着Web技术的不断发展,HTML也在不断演进,以适应新的需求和挑战。
4.2 首页
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>首页</title>
</head>
<body>
<h1>Hello World</h1>
<h2>跳转链接</h2>
<p><a href="//www.w3cschool.cn/">Visit W3Cschool</a></p>
<p><a href="/imagepage.html">Image Page</a></p>
<p><a href="/audiopage.html">Audio Page</a></p>
<h2></h2>
<h2>请输入姓名</h2>
<form action="/clientdata.html" method="post">
First name: <input type="text" name="FirstName" value="Chen"><br>
Last name: <input type="text" name="LastName" value="Jackie"><br>
<input type="submit" value="提交">
</form>
<p>点击"提交"按钮,表单数据将被写入到服务器上的“clientdata.html”。</p>
</body>
</html>
测试结果
4.3 图片页
imagepage.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Image Page</title>
</head>
<body>
<h2>1.jpeg</h2>
<img border="0" src="/image/1.jpeg" alt="Pulpit rock" width="304" height="228">
<h2>2.jpeg</h2>
<img border="0" src="/image/2.jpeg" alt="Pulpit rock" width="304" height="228">
<h2>3.jpeg</h2>
<img border="0" src="/image/3.jpeg" alt="Pulpit rock" width="304" height="228">
</body>
</html>
测试结果
4.4 音频页
audiopage.html
<!DOCTYPE html>
<html>
<head>
<meta charset = "utf-8">
<title>Audio Page</title>
</head>
<body>
<embed height="50" width="100" src="/audio/apple.mp3">
<p>If you cannot hear the sound, your computer or browser doesn't support the sound format.</p>
<p>Or, you have your speakers turned off.</p>
</body>
</html>
测试结果
注意:会发出苹果手机提示音
4.5 通过表单提交数据
protocol.hpp
//将客户端上传的数据写入到html文件,包括GET方法的url参数和POST方法的请求体内容
int WriteFile()
{
ofstream ofs("./webroot/clientdata.html");
if (!ofs.is_open())
return -1;
if(!_url_args.empty())
ofs << "<p><h1>url_args: " << _url_args << "</h1></p>" << endl;
if(!_content.empty())
ofs << "<p><h1>content: " << _content << "</h1></p>" << endl;
ofs.close();
return _content.size();
}
测试结果
在浏览器上填写表单并提交
post方法上传数据:请求体数据
服务器上请求报文的解析结果打印(DebugPrint)
GET方法上传数据:url参数
服务器上请求报文的解析结果打印(DebugPrint)
4.6 404错误页
404page.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面未找到</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<h1>404</h1>
<p>很抱歉,我们找不到您要的页面。</p>
<a href="/index.html">返回首页</a>
</body>
</html>
测试结果
4.7 测试Cookie机制
http_server.hpp
将HttpServer::ThreadRoutine中的Cookie机制测试放开
// 测试Cookie机制
resp._header["Set-Cookie"] = "username=zty";
resp._header["Set-Cookie"] = "password=123123"; //哈希表自动去重,存在问题
- Set-Cookie:这是服务器发送给客户端的。当服务器想要设置或更新客户端的cookie时,它会在HTTP响应的头部中包含一个或多个
Set-Cookie
字段。这些字段告诉客户端(如浏览器)应该存储哪些cookie信息。 - Cookie(如果cookie存在):这是客户端发送给服务器的。在随后的请求中,如果客户端需要向服务器发送之前存储的cookie,它会在HTTP请求的头部中包含一个
Cookie
字段。这个字段包含了客户端想要发送给服务器的所有cookie的名称和值,通常以分号和空格分隔。
测试结果
利用调试代理工具Fiddler查看测试结果
可以在浏览器中查看本地存储的Cookie数据
注意:
- 代码中我们设置了两个Set-Cookie字段,但实际的响应报头中只有一个password字段。这是因为我们使用哈希表维护Herder结构,而哈希表具有自动去重功能,因此存在这样的问题。
- 解决方案有:1.使用
vector<string>
维护Herder结构 2.使用unordered_map<string, vector<string>>
维护Herder结构
4.8 302临时重定向
4.8.1 测试
http_server.hpp
将HttpServer::ThreadRoutine中的302测试放开
//测试302临时重定向
resp._state_code = 302; //设置302状态码
resp._header["Location"] = "https://www.qq.com"; //设置Location属性,只是客户端接下来要访问的URL
测试结果
302临时重定向的测试结果在浏览器中不能很好的体现,我们通过fiddler抓包观察结果:
成果跳转到了www.qq.com
4.8.2 永久重定向和临时重定向
HTTP永久重定向
定义:HTTP永久重定向,通常使用HTTP状态码301(Moved Permanently)来表示,意味着请求的资源已经被永久性地移动到了新的URL上。客户端(如Web浏览器)和搜索引擎在收到301状态码后,会更新其缓存和索引,以反映这一永久性的变化。
应用场景:
- 网站迁移:当整个网站迁移到新的域名或URL结构时,可以使用301重定向来确保所有旧的链接都能正确地指向新的资源位置。
- 页面更新:当某个页面被完全替换或内容发生重大变化,并且希望搜索引擎和用户都访问新的页面时,可以使用301重定向。
- URL规范化:在网站中存在多个URL指向同一资源时,为了SEO(搜索引擎优化)和用户体验,可以使用301重定向将所有重复的URL重定向到一个主要的、规范的URL上。
HTTP临时重定向
定义:HTTP临时重定向,通常使用HTTP状态码302(Found)或307(Temporary Redirect)来表示,意味着请求的资源只是暂时位于新的URL上。客户端(如Web浏览器)会跟随这个重定向,但搜索引擎通常不会更新其索引。
应用场景:
- 网站维护:在网站进行维护或升级期间,可以将用户重定向到一个临时的维护页面或通知页面。
- 资源临时移动:当某个资源暂时被移动到新的URL上时,可以使用临时重定向来告知客户端。
- 负载均衡:在某些情况下,服务器可能会根据负载均衡的策略将用户请求临时重定向到不同的服务器上。