在上一篇文章中,我们了解了 协议 的制定与使用流程,不过太过于简陋了,真正的 协议 会复杂得多,也强大得多,比如在网络中使用最为广泛的 HTTP/HTTPS 超文本传输协议
但凡是使用浏览器进行互联网冲浪,那必然离不开这个 协议,HTTP/HTTPS 不仅支持传输文本,还支持传输图片、音频、视频等 资源
1.预备知识
1.1.URL
Internet上的每一个网页都具有一个唯一的名称标识,通常称之为URL(Uniform Resource Locator, 统一资源定位器)。它是www的统一资源定位标志,简单地说URL就是web地址,俗称“网址”。
URL是对互联网上得到的资源的位置和访问方法的一种简洁表示,是互联网上标准资源的地址。URL它具有全球唯一性,正确的URL应该是可以通过浏览器打开此网页的,但如果您访问外网,会提示网页无法打开,这并不能说明这个URL是错误的。只不过在国内不能访问而已。
下面以浏览一组网页来说URL,如下图:
- 事实上每一个URL背后都会转换成唯一的一个IP地址
- 此外这种知名的协议绑定的端口号是固定下来的,http是80,https是443
这样子URL里面就包含了IP地址和端口号,这就完全可以让客户端对服务端进行通信。
URL的组成结构如下:
注:登录信息现在已经不使用了,因为不够安全
URL的结构
- 1.协议
URL 的第一部分是协议(scheme),它表示浏览器必须使用的协议来请求资源(协议是计算机网络中交换或传输数据的一组方法)。通常对于网站,协议是 HTTPS 或 HTTP(它的非安全版本)。访问网页需要这两者之一,但浏览器还知道如何处理其他方案,比如
mailto:
(打开邮件客户端),所以如果你看到其他协议也不要感到惊讶。
- 2.服务器地址(包括域和端口号)
接下来是服务器地址,它与方案之间用字符模式
://
分隔。如果存在,服务器地址会包括域(例如www.example.com
)和端口(80
),由冒号分隔:
- 域指示被请求的 Web 服务器。通常这是一个域名,但也可以使用 IP 地址(但这很少见,因为它不太方便)。
- 端口指示用于访问 Web 服务器上资源的技术“门户”。如果 Web 服务器使用 HTTP 协议的标准端口(HTTP 为 80,HTTPS 为 443)来授予对其资源的访问权限,则通常会省略端口。否则,端口是强制的。
备注:协议和服务器地址之间的分隔符是
://
。冒号将方案与 URL 的下一部分分隔开,而//
表示 URL 的下一部分是权威。不使用服务器地址的 URL 示例之一是邮件客户端(
mailto:foobar
)。它包含方案,而不使用权威的部分。因此,冒号后没有跟着两个斜杠,并且只充当方案和邮件地址之间的分隔符。
- 3.资源路径
/path/to/myfile.html
是 Web 服务器上资源的路径。在 Web 的早期阶段,像这样的路径表示 Web 服务器上的物理文件位置。如今,它主要是由没有任何物理现实的 Web 服务器处理的抽象。
- 4.参数
?key1=value1&key2=value2
是提供给 Web 服务器的额外参数。这些参数是用&
符号分隔的键/值对列表。在返回资源之前,Web 服务器可以使用这些参数来执行额外的操作。每个 Web 服务器都有自己关于参数的规则,唯一可靠的方式来知道特定 Web 服务器是否处理参数是通过询问 Web 服务器所有者。
- 5.锚点
#SomewhereInTheDocument
是资源本身的另一部分的锚点。锚点表示资源中的一种“书签”,给浏览器显示位于该“加书签”位置的内容的方向。例如,在 HTML 文档上,浏览器将滚动到定义锚点的位置;在视频或音频文档上,浏览器将尝试转到锚代表的时间。值得注意的是,# 后面的部分(也称为片段标识符)不会随请求被发送到服务器。
下面是一个示例URL:http://www.example.com:8080/path/to/resource?param1=value1¶m2=value2
解释:
- 协议:URL的第一部分是协议,这里是"http"。协议指定了浏览器与服务器之间的通信规则,常见的有HTTP和HTTPS。
- 域名(或IP地址):在示例中,域名是"www.example.com"。域名是用于标识互联网上特定站点的字符串,也可以使用IP地址来代替。
- 端口号:示例中的端口号是"8080"。默认情况下,HTTP使用80端口,HTTPS使用443端口,但可以使用不同的端口号来访问特定的服务。
- 路径:路径指定了在服务器上资源的位置,示例中是"/path/to/resource"。路径可以是文件、目录或其他资源的位置。
- 查询参数:在示例中,查询参数是"?param1=value1¶m2=value2"。查询参数用于向服务器传递额外的信息,以便执行特定的操作或获取特定的结果。
再举一个例子
IP地址在哪呢?
blog.csdn.net
叫做 域名,可以通过 域名 解析为对应的IP地址- 使用 域名 解析工具解析后,下面就是
CSDN
服务器的IP地址
那端口号呢?
- 为了给用户提供良好的体验,一个成熟的服务是不会随意改变端口号的
- 只要是使用了 HTTP 协议,默认使用的都是 80 端口号,而 HTTPS 则是 443
- 如果我们没指明端口号,浏览器就会使用 协议 的默认端口号
- 现在大多数网站使用的都是 HTTPS 协议,更加安全,默认端口为 443
至于资源路径,这是 Linux 中的文件路径,比如下面这个 URL
https://csdnnews.blog.csdn.net/article/details/136575090?spm=1000.2115.3001.5926
其资源路径为 /article/details/136575090,与 Linux 中的路径规则一致,这里的路径起源于 web 根目录(不一定是 Linux 中的根目录)
在 Linux 机器中存放资源(服务器),客户端访问时只需要知晓目标资源的存储路径,就能访问了,
除了上面这些信息外,URL 中还存在特殊的 分隔符
- :// 用于分隔 协议 和 IP地址
- : 用于分隔 IP地址 和 端口号
- / 表示路径,同时第一个 / 可以分隔 端口号 和 资源路径
- ? 则是用来分隔 资源路径 和 参数
这些特殊 分隔符 很重要,这是属于 协议 的一部分,就像我们之前定义的 两正整数运算协议 中的 一样,如果没有 分隔符,那就无法获取 URL 中的信息
1.2.URLencode(url编码)和URLdecode(url解码)
- 1.为什么要对URL进行编码?
URL在定义时,定义为只支持ASCII字符,所以URL的发送方与接收方都只能处理ASCII字符。所以当你的URL中有非ASCII字符时就需要编码转换。
在Web程序中进行URL请求时,常会遇到URL中含有特殊字符的问题,常见的特殊字符有 ?$&*@等字符,或者是中文。
遇到这种情况时,就要对URL进行编码,用一种规则替换掉这些特殊字符,这就是URLencode
例如
像 / ? : 等这样的字符,已经被url当做特殊意义理解了。因此这些字符不能随意出现。此时浏览器发现某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义,然后再发送给百度服务器。转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式,例如:
"/" 被转义成了 "%2F"、"?" 被转义成了 "%3F"、":" 被转义成了 "%3A",这个就是urlencode,这种少量的情况,我们提交或者获取的数据本身可能包含和url中特殊的字符冲突的字符,要求BS双方进行编码(encode)和解码(decode),urldecode就是urlencode的逆过程。
- 2.什么是URL编码?
URLEncode是一种将特殊字符(常见的特殊字符有 ?$&*@等字符,或者是中文)转换成百分号编码的方法,以便浏览器和服务器之间能够正确地处理它们。该方法会将某些字符替换为由 ‘%’ 和其后面的两个十六进制数字所组成的编码。这些字符包括字母、数字、下划线、连字符、句点以及某些保留字符。
URLEncode的目的是将URL或者HTTP请求中的非ASCII字符编码成可以使用的ASCII字符,以保证正确传递和处理,例如将空格编码成"%20"、中文编码成"%E4%BD%A0%E5%A5%BD"等。通常在使用HTTP GET请求提交参数时,需要对参数进行URLEncode编码,以防止出现特殊字符导致的错误。
例如,浏览器中进行百度搜索“你好”时,链接地址会被自动编码:
- (编码前)https://www.baidu.com/s?wd=你好
- (编码后)https://www.baidu.com/s?wd=%E4%BD%A0%E5%A5%BD
出现以上情况是网络请求前,浏览器对请求URL进行了URL编码(URL Encoding)。
URL编码(URL Encoding):也称作百分号编码(Percent Encoding), 是特定上下文的统一资源定位符 URL的编码机制。URL编码(URL Encoding)也适用于统一资源标志符(URI)的编码,同样用于 application/x-www-form-urlencoded MIME准备数据。
URL解码就是URL编码的逆过程!
- URL编码和解码应该是成对使用的,对一个URL进行编码后,再对其进行解码应该得到原始的URL。
- URLEncoder和URLDecoder只对URL中的特殊字符进行编码和解码,不会对整个URL进行处理。
- URL编码和解码的目的是确保URL在传输和处理过程中不会出现问题,例如特殊字符引起的歧义或错误。
大家可以去这里去试试看:UrlEncode编码和UrlDecode解码-在线URL编码解码工具
2.什么是HTTP
HTTP(超文本传输协议)是一种用于在Web浏览器和Web服务器之间传输数据的应用层协议。它是一种无状态协议,即服务器不会保留与客户端的任何连接状态信息,每个请求都被视为一个独立的事务。
假设你使用Web浏览器(例如Chrome)访问一个网页。当你在浏览器中输入网址并按下"Enter"键时,浏览器会向服务器发送一个HTTP请求。你也可以理解为HTTP协议是在客户端(浏览器)和服务器之间传输数据的基础(约定)。
3.HTTP协议格式
3.1.HTTP请求协议格式
从宏观角度来看,HTTP
请求 分为这几部分:
- 请求行:包括请求方法(
GET / POST
)、URL
、协议版本(http/1.0
http/1.1
http/2.0
)] - 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的,每组属性之间使用\r\n分隔,遇到空行表示Header部分结束
- 空行:遇到空行表示请求报头结束,空行,区分报头和有效载荷。
- 请求正文 /有效载荷(可以没有):请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
我们要特别注意在
HTTP
协议中是使用\r\n
作为 分隔符 的
如何分离 协议报头 与 有效载荷 ?
- 以空行
\r\n
进行分隔,空行之前为协议报头,空行之后为有效载荷
如何进行 序列化与反序列?
- 序列化:使用
\r\n
进行拼接 - 反序列化:根据
\r\n
进行读取
3.2.HTTP响应协议格式
HTTP响应由以下四部分组成:
- 状态行:[http版本]+[状态码]+[状态码描述]
- 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的,每组属性之间使用\r\n分隔,遇到空行表示Header部分结束
- 空行:遇到空行表示响应报头结束,区分报头和有效载荷。
- 响应正文 / 也叫有效载荷,即客户端请求的资源:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。
3.3.使用工具见见HTTP响应和请求
3.3.1.见一见HTTP响应
我们之前使用过telnet这个工具
- telnet
telnet是一种用于远程登录的网络协议,可以将本地计算机链接到远程主机。
Linux提供了telnet命令,它允许用户在本地计算机上通过telnet协议连接到远程主机,并执行各种操作。
使用telnet命令可以建立客户端与服务器之间的虚拟终端连接,这使得用户可以通过网络远程登录到其他计算机,并在远程计算机上执行命令,就像直接在本地计算机上操作一样。
要使用telnet命令,首先需要确保你的Linux系统已经安装了telnet软件包。如果未安装,可以通过以下命令在终端中安装:
sudo yum install telnet
安装完成后,你可以通过以下命令来使用telnet命令:
telnet [选项] [主机名或IP地址] [端口号]
可以使用不同的选项和参数来执行不同的操作,下面是一些常用的选项和参数:
- -l 用户名:指定用户名进行登录。
- -p 端口号:指定要连接的远程主机端口号。
- -E:在telnet会话中打开字符转义模式。
- -e 字符:指定telnet会话中的转义字符。
- -r:在执行用户登录之前不要求用户名。
- -K:在连接到telnet会话时要求密码。
事实上,我们可以使用telnet去连接百度官网,注意端口号是http特定的80
现在我们就要写请求报头
GET / HTTP/1.1
我们发现这就是一个http响应报头
上面那个是正确的,我们试试一个错误的
也是符合这个结构,我其实只想说明,即使请求失败也需要响应
3.3.1.见一见HTTP请求
这里需要下载一个新工具——fiddler:免费下载 Telerik 的 Fiddler Web 调试工具
Fiddler 是一款免费网络代理调试工具。
Fiddler是一个蛮好用的抓包工具,可以将网络传输发送与接受的数据包进行截获、重发、编辑、转存等操作。也可以用来检测网络安全,反正好处多多,举之不尽呀!
然后我们打开浏览器,我们直接输入www.baidu.com,然后按回车
回到我们的Fiddler,就能发现下面这个
我们点击第一个,然后就能看到下面这个
这样子对上了吧
3.4.HTTP服务器_V1版本——单一静态网页
3.4.1.见见简单的HTTP请求
事实上,上面那些http请求报头都是加密过,其实显示出来的效果不好。我们可以自己写一个代码来获取HTTP请求
首先我们需要使用套接字,所以我们需要将我们之前封装好的套接字拿过来
Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定义一些错误代码
enum
{
SocketErr = 2, // 套接字创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列的长度
const int backlog = 10;
class Sock //服务器专门使用
{
public:
Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字
{
}
~Sock()
{
// 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源
}
// 创建套接字
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
printf("socket error, %s: %d", strerror(errno), errno); //错误
exit(SocketErr); // 发生错误时退出程序
}
}
// 将套接字绑定到指定的端口上
void Bind(uint16_t port)
{
//让服务器绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));//清零
local.sin_family = AF_INET; // 网络
local.sin_port = htons(port); // 我设置为默认绑定任意可用IP地址
local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) //让自己绑定别人
{
printf("bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 监听端口上的连接请求
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
printf("listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受一个连接请求
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
printf("accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd; // 返回新的套接字文件描述符
}
// 连接到指定的IP和端口——客户端才会用的
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;//服务器的信息
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
// 关闭套接字
void Close()
{
close(sockfd_);
}
// 获取套接字的文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 套接字文件描述符
};
如果我们不想使用上面的工具,我们可以自己来写一个代码来获取到HTTP请求。
#pragma once // 防止头文件被重复包含
#include <iostream> // 引入标准输入输出流库
#include <string> // 引入字符串库
#include <pthread.h> // 引入POSIX线程库
#include "Socket.hpp" // 假设这是一个封装了socket操作的类
using namespace std; // 使用标准命名空间
// 定义默认端口号
static const uint16_t defaultport = 8080;
// 线程数据结构体,用于在线程间传递socket文件描述符
struct ThreadData
{
int sockfd; // socket文件描述符
};
// HTTP服务器类
class HttpServer
{
public:
// 构造函数,初始化服务器监听的端口
HttpServer(uint16_t port = defaultport)
: _port(port) // 初始化成员变量_port
{
}
// 启动服务器
bool Start()
{
_listensockfd.Socket(); // 创建socket
_listensockfd.Bind(_port); // 绑定socket到指定的端口
_listensockfd.Listen(); // 开始监听端口
for(;;) // 无限循环,等待连接
{
string clientip; // 用于存储客户端IP地址
uint16_t clientport; // 用于存储客户端端口号
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 接受连接,返回新的socket文件描述符
pthread_t tid; // POSIX线程标识符
printf("get a new connect, sockfd: %d\n", sockfd);
// 创建线程数据并传递给新线程
ThreadData *td = new ThreadData;
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td); // 创建新线程处理客户端请求
}
return true; // 注意:这里的return实际上永远不会被执行,因为for循环是无限的
}
// 静态成员函数,用于处理客户端请求
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程,让线程在结束时自动释放资源
ThreadData *td = static_cast<ThreadData *>(args); // 将void*类型的参数转换为ThreadData*
char buffer[10240]; // 接收数据的缓冲区
ssize_t n = recv(td->sockfd, buffer, sizeof(buffer)-1, 0); // 接收客户端发送的数据
if (n > 0) // 如果接收到数据
{
buffer[n] = 0; // 确保字符串以null字符结尾
cout << buffer; // 输出HTTP请求
}
close(td->sockfd); // 关闭socket连接
delete td; // 释放线程数据占用的内存
return nullptr; // 线程结束
}
// 析构函数,用于清理资源(但在这个例子中,没有特别的资源需要清理)
~HttpServer()
{
}
private:
Sock _listensockfd; // 监听socket对象
uint16_t _port; // 服务器监听的端口号
};
然后我们通过主函数给我们的服务器传入端口号,就可以正常启动我们的服务器了。
#include "HttpServer.hpp"
#include <iostream>
#include <memory>
#include <pthread.h>
using namespace std;
int main(int argc, char *argv[])
{
if(argc != 2)
{
exit(1);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<HttpServer> svr(new HttpServer(port));
svr->Start();
return 0;
}
随后我们就来运行一下:
然后我们到浏览器搜索一下我们的公网IP:端口号
我们回到我们的Xshell就会看到2个http协议格式的请求!!!!
这个HTTP请求协议格式很明显了吧 ,拿第一条出来看看
此外使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个这样的请求GET /favicon.ico HTTP/1.1,当你在Chrome或其他Web浏览器中访问一个网站时,浏览器除了加载您请求的主页面之外,还会自动尝试获取该网站的 favicon.ico 文件。favicon.ico 是一个网站的收藏夹图标、或站点图标,通常显示在浏览器的标签页、书签栏或历史记录中,作为该网站的视觉标识。
注意:
我们实际运行之前需要将8877端口开放,实际分两步
- 1.防火墙开放
打开防火墙
sudo systemctl start firewalld.service
查看防火墙状态
sudo firewall-cmd --state
开放TCP端口
sudo firewall-cmd --zone=public --add-port=80/tcp --permanent # 开放TCP 8877端口
配置生效——千万不要忘了这一步
sudo firewall-cmd --reload
- 2.安全组设置
这个我之前在UDP套接字编程的时候讲过,不提了
3.4.2.见见简单的HTTP响应
实现一个最简单的HTTP服务器,只在网页上输出 "hello world",只要我们按照HTTP协议的要求构造数据,就很容易能做到。
我们在上面那个代码的基础上进行改进
HTTPserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include"Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
class HttpServer;//声明
class ThreadData//传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
//获取用户信息
string clientip;
uint16_t clientport;
//两个都是输出型参数
//注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport);//这里获取了客户端IP和端口号
if(sockfd < 0) continue;
std::cout<<"get a new connect, sockfd:"<<sockfd<<std::endl;
//下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
//注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0)//读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 返回响应的过程,这里需要返回一个HTTP响应协议
string text = "hello world";//HTTP协议有效载荷
string response_line = "HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());//分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);//执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;//用于监听的
uint16_t _port;//用于发送消息的
};
HTTPserver.cc
#include"HTTPserver.hpp"
#include <iostream>
#include <string>
#include <memory>
int main()
{
unique_ptr<HttpServer> psvr (new HttpServer());
psvr->Start();
return 0;
}
makefile
HttpServer : HTTPserver.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf HttpServer
我们编译运行看看
我们发现是可以连接的哦!!!
这个时候我们在我们的浏览器上搜索
公网IP:端口号(这里是8877)
这样子也是做出了一个简单的HTTP请求
这里HTTP服务器只是响应了一个字符串,如果我想要响应一个网页咋办?这需要借助HTML
这里使用html写了一个超级简单的网页
string text = "<html><body><h3>hello world</h3></body></html>";//HTTP协议有效载荷——这里是一个简单的网页
这就是一个简单的网页
我们编译运行一下,再拿浏览器搜索我们的公网IP加端口号
怎么样!!!厉害吧!! 这就是一个简易的网页开发!!!网页就在我们的响应正文里面!!
3.5.HTTP服务器_V2版本
第一个版本实现了一个超级简单的网页,但是这个网页是静态的啊!!!我们需要让它变化起来该怎么办?这个是第2版需要完成的任务
我们知道我们使用浏览器浏览我们的网页时,
我们在服务端看到的HTTP请求报头上写的是
我们发现我们会发现此时HTTP请求的url都是根目录,因为我们在访问域名:端口号后面什么也没有加上
事实上这个/不是我们Linux下的根目录,是web上的根目录,而web上的根目录是需要我们设置的,所以我们需要做一些设置,设置好我们的网络根目录
const string wwwroot="./wwwroot"; // web 根目录
我们在当前路径下创建了一个wwwroot目录,这个目录就是我们网页的根目录
在这个网页根目录里面我们可以创建一些html文件——这些其实就是网页,要打开哪个网页就打开哪个html文件
我们去./wwwroot下创建一个叫index.html的文件
我们将我们的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>Document</title>
</head>
<body>
<h1>这个是我们的首页</h1>
</body>
</html>
好了,我们现在可以去修改代码
HttpServer.hpp
#include <fstream>
.......
class HttpServer
{
public:
static string ReadHtmlContent(const string &htmlpath)//读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if(!in.is_open()) return "404";
string content;
string line;
while(getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
//注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0)//读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 返回响应的过程,这里需要返回一个HTTP响应协议
string text = ReadHtmlContent("./wwwroot/index.html");
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在./wwwroot/index.html
.....
}
}
.....
};
HttpServer.hpp完整代码
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include"Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
class HttpServer;//声明
class ThreadData//传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
//获取用户信息
string clientip;
uint16_t clientport;
//两个都是输出型参数
//注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport);//这里获取了客户端IP和端口号
if(sockfd < 0) continue;
std::cout<<"get a new connect, sockfd:"<<sockfd<<std::endl;
//下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath)//读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if(!in.is_open()) return "404";
string content;
string line;
while(getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
//注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0)//读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 返回响应的过程,这里需要返回一个HTTP响应协议
string text = ReadHtmlContent("./wwwroot/index.html");
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在./wwwroot/index.html
string response_line = "HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());//分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);//执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;//用于监听的
uint16_t _port;//用于发送消息的
};
我们运行一下,并且使用浏览器去访问
这个时候我们去修改我们的./wwwroot/index.html
./wwwroot/index.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>
<h1>这个是我们的首页</h1>
</body>
</html>
刷新看看!!!!
这样子我们都不用 关闭服务器,从而实现网页的刷新!!!
3.6.HTTP服务器_V3版本
第二个版本中虽然实现了不用 关闭服务器,从而实现网页的刷新,但是我们访问的终都是同一个文件,我们能不能让用户来自己决定访问哪个网页?
这就是第3个版本要解决的问题!!
首先我们得知道
我们知道我们使用IP+端口号浏览器浏览我们的网页时,
我们在服务端看到的HTTP请求报头上写的是
我们发现我们会发现此时HTTP请求的url都是根目录,因为我们在访问域名:端口号后面什么也没有加上,如果我们带上路径呢?
我们发现结果相同,这里输出相同的结果是因为我们代码中默认只有这个处理行为,未来我们可以根据路径做出不同的行为
我们在HTTP服务器上收到了
这个目录就是用户指定的目录,那么我们就需要从HTTP协议请求里面拿到这个url来。
,所以关键我们就要对浏览器发过来的HTTP协议进行反序列化。
好了,知道要做什么了那就好好的做!
- 2.对HTTP请求协议反序列化
我们假设我们读到了完整的HTTP请求
注意我们只对请求行反序列化
const std::string sep = "\r\n"; class HttpRequest { public: // 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体 void Deserialize(std::string req) { // 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义 // 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n" // while循环用于遍历字符串,直到找不到分隔符 while (true) { // 在请求字符串中查找分隔符\r\n的位置 size_t pos = req.find(sep); // 如果没有找到分隔符\r\n,则退出循环 if (pos == string::npos) break; // 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分 string temp = req.substr(0, pos); // 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串) if (temp.empty()) break; // 将截取的字符串添加到请求头向量中 req_header.push_back(temp); // 从请求字符串中移除已经处理的部分,包括分隔符 req.erase(0, pos + sep.size()); } // 剩下的字符串(如果有的话)被认为是请求正文 text = req; } // 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本) void Parse() { // 使用stringstream和字符串流输入操作符来解析请求行的第一个元素 std::stringstream ss(req_header[0]);//将第一行交给ss // 将请求行解析为HTTP方法、URL和HTTP版本 ss >> method >> url >> http_version; //gei第一个单词会给method,第二个会给url,第3个会给http_version } // 调试打印函数,用于输出请求的所有信息 void DebugPrint() { // 遍历请求头,并打印每一行 for (auto &line : req_header) { std::cout << "--------------------------------" << std::endl; std::cout << line << "\n\n"; } // 打印解析后的HTTP方法、URL和HTTP版本 std::cout << "method: " << method << std::endl; std::cout << "url: " << url << std::endl; std::cout << "http_version: " << http_version << std::endl; // 打印请求正文 std::cout << text << std::endl; } public: std::vector<std::string> req_header; // 用于存储请求头的字符串向量 std::string text; // 用于存储请求正文的字符串 // 解析请求行之后得到的结果 std::string method; // HTTP方法,如GET、POST std::string url; // 请求的URL std::string http_version; // HTTP的版本号,如HTTP/1.1 };
这里需要补充知识——stringstream
- stringstream可以用来分割字符串,如果是空格,可以直接分割
#include <string> #include <sstream> #include <iostream> using namespace std; int main() { // 定义一个包含空格分隔单词的字符串 string str = "i am a boy"; // 使用字符串流来读取字符串中的单词 stringstream ss(str); // 定义一个字符串变量来存储从字符串流中读取的每个单词 string word; // 使用循环和输入流操作符从字符串流中读取单词,直到没有更多单词可读 cout << "从字符串中读取的单词有:" << endl; while (ss >> word) { // 输出读取的单词 cout << word << endl; } return 0; }
- 3.修改HttpServer.hpp代码
class HttpServer { public: ........ static void HandlerHttp(int sockfd) { char buffer[10240]; // 假设我们读取的就是一个完整的独立的http请求 ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (n > 0) { buffer[n] = 0; cout << buffer; // 输出HTTP请求 //这里是巨大变化 HttpRequest req; req.Deserialize(buffer); req.Parse(); req.DebugPrint(); } close(sockfd); } ......... }
HttpServer.hpp测试 版
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const std::string sep = "\r\n";
class HttpRequest
{
public:
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "http_version: " << http_version << std::endl;
// 打印请求正文
std::cout << text << std::endl;
}
public:
std::vector<std::string> req_header; // 用于存储请求头的字符串向量
std::string text; // 用于存储请求正文的字符串
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
};
我们编译运行一下,并用浏览器浏览
我们发现它确实分离开来!!现在url可是/a/b/c/d,如果我访问的是/,如果这放到Linux下,那岂不是我整个Linux系统的文件都让你拿到了!!这样子不安全,我们需要将其进行修改
接下来我们就要学习修改我们的web根目录,
- 需要来知道怎么修改我们的web根目录
const string wwwroot = "./wwwroot"; // web 根目录 std::string url="/a/b/c";//用户输入进来的 std::string path=wwwroot; path+=url;//最后都会在url前面拼接变成./wwwroot/a/b/c
很简单吧
我们现在就来修改代码
const std::string sep = "\r\n";
const string wwwroot = "./wwwroot/"; // web 根目录
const string homepath="index.html";//网页
class HttpRequest
{
public:
....
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]);//将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; //gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path=wwwroot;//./wwwroot/
if(url=="/" || url=="/index.html")
{
file_path+=homepath;// ./wwwroot/index.html
}
else
{
file_path+=url;//./wwwroot/url
}
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
...
std::cout << "file_path: " << file_path << std::endl;
....
}
public:
std::vector<std::string> req_header; // 用于存储请求头的字符串向量
std::string text; // 用于存储请求正文的字符串
std::string file_path;//用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
};
HttpServer.hpp测试版
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const string wwwroot = "./wwwroot/"; // web 根目录
const string homepath = "index.html";
const std::string sep = "\r\n";
class HttpRequest
{
public:
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path=wwwroot;//./wwwroot/
if(url=="/" || url=="/index.html")
{
file_path+=homepath;// ./wwwroot/index.html
}
else
{
file_path+=url;//./wwwroot/url
}
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
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 file_path;//用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
};
我们运行一下
很好,现在只差做出HTTP响应了
HttpServer.hpp
class HttpServer
{
.....
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
string text = ReadHtmlContent(req.file_path);
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
string response_line = "HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
....
};
HttpServer.hpp测试版
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const string wwwroot = "./wwwroot"; // web 根目录
const string homepath = "/index.html";
const std::string sep = "\r\n";
class HttpRequest
{
public:
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path=wwwroot;//./wwwroot
if(url=="/" || url=="/index.html")
{
file_path+=homepath;// ./wwwroot/index.html
}
else
{
file_path+=url;//./wwwroot/url
}
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
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 file_path;//用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
string text = ReadHtmlContent(req.file_path);
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
string response_line = "HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
};
所以现在如果我们不传路径或者传/index.html,默认访问的就是 index.html
我们发现我们进入这个网页默认是打开index.html的,其实我们还可以指定打开哪个文件。
我们现在创建一个新的html文件
index2.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>
</body>
</html>
服务器别关,我们使用浏览器浏览一下
怎么样?我们还能根据不同的路径访问到不同的网页。
这个是不是有网站那股味道了??其实还差点,我们需要学习html的更多知识才能完成这个网站的创建!!!我们可以来试试看
index2.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>
<a href="https://www.baidu.com/">百度官网</a>
</body>
</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>Document</title>
</head>
<body>
<h1>这个是我们的首页</h1>
<a href="http://121.36.254.82:8877/a/b/c/index2.html">尾页</a>
</body>
</html>
来看看
点击尾页,就跳转啦!!!
厉害吧!!!
注意:这里要注意我们的网址是http的,不能是https的,因为我写的服务器目前只支持http的
注意:我们写的这个HTTP服务器是不支持https协议的,如果在使用我们这个 HTTP服务器时频繁出现段错误,就要考虑是不是哪里使用了https,将https改成http即可
4.HTTP协议
4.1.HTTP的请求方法
- 知道这个请求方法在哪里吗?
HTTP请求方法是用来表示客户端对服务器资源的操作方式。常见的请求方法有:
目前我们写的代码都是GET方法,事实上GET和POST方法占据了85%的场景
其实就是下面这些
- GET:获取资源。
- POST:提交数据。
- PUT:更新资源。
- DELETE:删除资源。
- HEAD:获取资源的头部信息,不返回资源内容。
- CONNECT:建立TCP连接。
- Options:获取服务器支持的HTTP方法。
- Trace:追踪请求的传输路径。
- PATCH:部分更新资源。
你们日常使用某些网始(http/https),你是如何把你的数据提交给服务器的呢?日常怎么提交的? ? ?
数据都是通过表单提交的!
我们下面就来写一个表单
index.html (get方法)
<!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>
<form action="/a/b/c/index2.html" method="get">
name:<input type="text" name="name"><br>
passwd:<input type="password" name="passwd"><br>
<input type="submit" value="提交">
</form>-->
<h1>这个是我们的首页</h1>
</body>
</html>
此时我们采用的method是GET方法哟!!!看看运行结果。
我们在name后面输入zs_108,在passwd里面输入123456
随后我们进行提交,看看能不能跳转到我们的尾页。
我们发现 如果我们要提交参数给我们的服务器,我们使用get方法的时候,url上加上了我们的参数,而我们提交的参数是通过url提交的!但是此时在我们网页根目录之下不存在这样的路径,所以就返回404。
随后我们在html里面将我们的get方法改为post方法,
index.html (post方法)
<!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>
<form action="/a/b/c/index2.html" method="post">
name:<input type="text" name="name"><br>
passwd:<input type="password" name="passwd"><br>
<input type="submit" value="提交">
</form>-->
<h1>这个是我们的首页</h1>
</body>
</html>
我们来看看结果:
我们在name后面输入zs_108,在passwd里面输入123456
随后我们进行提交,看看能不能跳转到我们的尾页。
完美啊!!!
post方法也支持参数提交,但是它是请求的正文来提交参数!但是post相较于get而言是更私密一点的,且get是通过url进行参数提交的,而url长度受限且我们上面发现直接将我们输入的账号和密码直接显示了!!!
GET 和 POST 是 HTTP 协议中最常用的两种请求方法,它们之间的区别主要体现在以下几个方面:
语义:
- GET:用于从服务器获取资源,不应该对服务器端的数据进行修改。GET 请求是幂等的,即多次执行相同的 GET 请求应该返回相同的结果,不会产生副作用。
- POST:用于向服务器提交数据,通常用于创建新的资源或者对服务器端的数据进行修改。POST 请求不是幂等的,即多次执行相同的 POST 请求可能会产生不同的结果,可能会对服务器端的数据产生副作用。
参数传递:
- GET:参数通过 URL 的查询字符串(query string)传递,参数会附加在 URL 的末尾,如 http://example.com/resource?key1=value1&key2=value2。由于参数附加在 URL 中,有长度限制,并且会被保存在浏览器的历史记录和服务器的访问日志中。
- POST:参数通过请求体(request body)传递,参数不会附加在 URL 中,而是作为请求体的一部分发送。POST 请求没有长度限制,可以传递大量的数据,且参数不会暴露在 URL 中,更加安全。
安全性:
- GET:由于参数暴露在 URL 中,可能会被恶意用户通过网络抓包工具等获取,因此不适合传递敏感信息,如密码等。GET 请求更适合用于获取公开信息。
- POST:参数在请求体中发送,相对于 GET 请求更安全,适合传递敏感信息,如登录表单的用户名和密码等。
缓存:
- GET:请求结果可以被缓存,可以被浏览器缓存、代理服务器缓存等,如果请求相同的 URL,可以直接使用缓存结果,提高性能。
- POST:请求结果通常不会被缓存,每次请求都会向服务器发送数据,不会使用缓存结果。
幂等性:
- GET:由于 GET 请求是幂等的,多次执行相同的 GET 请求应该返回相同的结果,不会对服务器端的数据产生影响,因此适合用于查询操作。
- POST:POST 请求不是幂等的,多次执行相同的 POST 请求可能会产生不同的结果,可能会对服务器端的数据产生影响,因此适合用于对数据进行修改或者创建新的资源。
总结一下要点:
定义上:GET主要是用于获取资源,POST主要是创建新的资源或者对服务器端的数据进行修改
参数传递:
- GET主要通过参数传递,比如http://example.com/resource?key1=value1&key2=value2或者restful的形式``http://example.com/resource/value1/value2`。
- 而POST主要将请求参数放在请求Body中,不会在url上直接体现;
但是,无论是GET还是POST在http协议下,传递数据都是不安全的,因为代理服务器还是可以拦截请求看到请求体的body,那GET就更不用说了;所以建议在网站上线时使用https,因为https对数据进行了加密,具体的可自行上网搜索如何保证数据安全性
缓存:例如在我们向服务器请求静态资源时,通常是GET请求,浏览器为了提高访问速度,通常会将静态资源缓存在浏览器;而POST请求不会
4.2.HTTP的状态码
- 知道HTTP状态码在哪里吗?
HTTP状态码是用来表示HTTP请求的处理结果的三位数字代码。它们可以分为五大类:信息响应(1xx)、成功响应(2xx)、重定向(3xx)、客户端错误(4xx)和服务端错误(5xx)。
信息响应(1xx)
信息响应表示请求已经被接受,但处理尚未完成。常见的状态码有:
- 100 Continue:服务器已经接收到请求头,并且客户端应继续发送请求体。
- 101 Switching Protocols:服务器已经理解并同意将请求切换到新的协议。
成功响应(2xx)
成功响应表示请求已经被成功处理。常见的状态码有:
- 200 OK:请求已成功处理,返回结果。
- 201 Created:请求已被实现,而且有一个新的资源被创建。
- 204 No Content:服务器成功处理了请求,但没有返回任何内容。
重定向(3xx)
重定向表示请求的资源已经被移动到了一个新的位置,客户端需要重新发送请求。常见的状态码有:
- 301 Moved Permanently:请求的资源已经被永久移动到新的位置。
- 302 Found:请求的资源已经被临时移动到新的位置。
- 303 See Other:请求的资源可以通过GET方法访问另一个URI。
- 307 Temporary Redirect:与302类似,但指定了临时重定向。
客户端错误(4xx)
客户端错误表示或者客户端发送的请求有问题。常见的状态码有:
- 400 Bad Request:服务器无法理解客户端发送的请求。
- 401 Unauthorized:请求需要用户验证。
- 403 Forbidden:服务器拒绝处理请求,可能是因为客户端没有权限。
- 404 Not Found:请求的资源不存在。
服务端错误(5xx)
服务端错误表示服务器在处理请求时发生了错误。常见的状态码有:
- 500 Internal Server Error:服务器在处理请求时发生了未知的错误。
- 503 Service Unavailable:服务器暂时无法处理请求,通常是因为服务器过载或维护。
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway),我们可以来见一见404错误码的场景。
4.2.1.见一见404状态码
首先我们要明白这个404一定是没找到资源才会触发的
err.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>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
HttpServer.hpp修改部分
....
class HttpServer
{
public:
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "";//注意这里
std::cout << htmlpath << std::endl;
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
....
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer << std::endl; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
std::cout << req.file_path << std::endl;
bool ok=true;
string text = ReadHtmlContent(req.file_path);
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
if(text.empty())
{
ok=false;
std::string err_html=wwwroot;
err_html+="/err.html";
text=ReadHtmlContent(err_html);//
}
string response_line;
if(ok)
{
response_line="HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
}
else{
response_line="HTTP/1.0 404 Not Found\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
}
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
...
};
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const string wwwroot = "./wwwroot"; // web 根目录
const string homepath = "/index.html";
const std::string sep = "\r\n";
class HttpRequest
{
public:
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path=wwwroot;//./wwwroot
if(url=="/" || url=="/index.html")
{
file_path+=homepath;// ./wwwroot/index.html
}
else
{
file_path+=url;//./wwwroot/url
}
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
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 file_path;//用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "";
std::cout << htmlpath << std::endl;
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer << std::endl; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
std::cout << req.file_path << std::endl;
bool ok=true;
string text = ReadHtmlContent(req.file_path);
//HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
if(text.empty())
{
ok=false;
std::string err_html=wwwroot;
err_html+="/err.html";
text=ReadHtmlContent(err_html);//
}
string response_line;
if(ok)
{
response_line="HTTP/1.0 200 OK\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
}
else{
response_line="HTTP/1.0 404 Not Found\r\n";//HTTP响应协议的第一行的版本号 状态码 状态码描述
}
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
};
这样子我们就hold得住404了
我们输入一个不存在的路径
我们正常输入就是下面这样子
现在很好的
404并不代表服务器不响应,而是此时我们访问的网页不存在。
4.2.2.见一见3XX状态码
我们说
当浏览器(客户端)访问的目标网站地址发生改变时,浏览器会返回 3xx
重定向错误码,用于引导浏览器访问正确的网址,注意这个过程需要使用Location键值对
重定向:让服务器指导浏览器,让浏览器访问新的地址
我们修改一下我们的代码
class HttpServer
{
......
static void HandlerHttp(int sockfd)
{
.......
response_line="HTTP/1.0 302 Found\r\n";
string response_header = "Content-Length: ";//HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n";//结束这一行
response_header += "Location: https://www.baidu.com";//注意这里
string block_line = "\r\n";//这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
//response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0);//注意这里不用write了,
//send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
....
}
};
我们搜索一下自己的公网IP+端口,却发现直接跳转到百度官网来了!!!!
我们得知道我们的重定向是有临时重定向和永久重定向的
4.3.HTTP常见Header
- 首先我们得知道Header在哪里?
常用的请求头 Request Headers
- Accept:定义客户端可接受的响应内容类型;
- Accept-charset:定义客户端可接受的字符集;
- Accept-Encoding:定义客户端可接受的编码方式,比如打包方式--gzip 等;
- cookie:由服务器通过 set-cookie 设置的 cookie;
- Content-Length:请求体的长度;
- Content-Type:请求体的数据类型:Content-Type: application/x-www-form-urlencoded;
- Date:发送该请求的日期和时间;
- Host:所请求的服务器地址;
- Referrer:表示页面是从哪个地链接过来的,可以用来统计网页上的链接访问量;
- User-Agent:显示客户端的身份标识;
- If-Modified-Since:用于协商缓存,取自 Response haders 中的 Last-Modified 字段。服务器通过对比这两个字段判断缓存是否有效;
- If-None-Match:用于协商缓存,取自 Response Headers 中的 E-tag 字段。服务器通过对比这两个字段判断缓存是否有效;
常用的响应头 Response Headers
- Access-Control-Allow-Origin:指定哪些网站以跨域资源共享(CORS);
- Content-Encoding:响应资源的编码方式;
- Content-Language:响应资源所使用的语言;
- Content-Length:响应资源的长度;
- Content-type:响应内容的数据类型;
- Date:消息被发送时的日期和时间;
- Location:用于重定向;
- Set-Cookie:用于设置客户端的 cookie ;
- Status:用来说明当前 Http 连接的状态;
- Last-Modified:服务器返回给客户端,下次请求通过在 Resquest Headers 中的 If-Modified-Since 字段携带过来。
- E-tag:服务器返回给客户端,下次请求通过在 Resquest Headers 中的 If-None-Matched 字段携带过来。
- Cache-Control:
max-age:缓存有效期,浏览器自己通过计时判断缓存是否过期。如果未过期则命中强制缓存。
no-cache:不使用强制缓存,直接进入协商缓存。
no-store:不使用缓存,每次请求都会进行 http 请求。
下面我详细介绍一些
(1) Host:
请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的。
例如我们在浏览器中输入:https://www.baidu.com,浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.baidu.com(此处使用缺省端口号443,若指定了端口号,则变成:Host:指定端口号
(2)Referer
当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器该请求是从哪个页面链接过来的,服务器借此可以获得一些信息用于处理。比如从我主页上链接到一个朋友那里,他的服务器就能够从HTTP Referer中统计出每天有多少用户点击我主页上的链接访问他的网站。
(3)User-Agent
这个对于爬虫比较重要 因为一般都需要添加该属性,否则稍微处理过的网站,都无法爬取。
告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。
我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。
例如: User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; CIBA; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; InfoPath.2; .NET4.0E)应用程序版本“Mozilla/4.0”表示:你使用Maxthon 2.0 浏览du器使用 IE8 内核;
版本标识“zhiMSIE 8.0”
平台自身的dao识别信息“Windows NT 5.1”表示“操作系统为zhuan Windows XP”
Trident内核版本“Trident/4.0”,浏览器的一种内核,还有一种就是WebKit内核(4)Content-type
表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
常见的媒体格式类型如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
以application开头的媒体格式类型:
- application/xhtml+xml :XHTML格式
- application/xml : XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json : JSON数据格式
- application/pdf :pdf格式
- application/msword : Word文档格式
- application/octet-stream : 二进制流数据(如常见的文件下载)
- application/x-www-form-urlencoded : 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的:
- multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式。
(5)Accept-Language
Accept-Langeuage:指出浏览器可以接受的语言种类,如en或en-us指英语,zh或者zh-cn指中文,当服务器能够提供一种以上的语言版本时要用到。
(6)Cookie
Cookie:浏览器用这个属性向服务器发送Cookie。Cookie是在浏览器中寄存的小型数据体,它可以记载和服务器相关的用户信息,也可以用来实现会话功能。
我们可以去看看我们自己接收到的http请求
Host,Connection,Accept等,大家自己去搜索看看什么意思。
4.3.1.Connection——长短链接
我们来看看Connection
- 长连接
长连接(long connnection),指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
长连接:连接->传输数据->保持连接 -> 传输数据-> …->直到一方关闭连接,客户端关闭连接。长连接指建立SOCKET连接后无论使用与否都要保持连接。
- 短连接
短连接(short connnection),是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时才去建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送。
短连接:连接->传输数据->关闭连接。下次一次需要传输数据需要再次连接。
- 短连接是指客户端与服务器完成一次请求-响应周期后,就关闭连接的通信方式。每次客户端有新的请求时,都需要重新建立TCP连接。在HTTP/1.0中,支持短连接。
- 长连接是指客户端与服务器完成一次请求-响应后,连接并不立即关闭,而是保持一段时间的活跃状态,等待后续的请求和响应。在HTTP/1.1中,默认开启了Keep-Alive,支持长连接技术。
HTTP协议根本没有长短连接这一说,这里要强调一下,HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP连接就结束了,或者更准确的说,是本次HTTP请求就结束了,根本没有长连接这一说。那么自然也就没有短连接这一说了。
之所以网络上说HTTP分为长连接和短连接,其实本质上是说的TCP连接。TCP连接是一个双向的通道,它是可以保持一段时间不关闭的,因此TCP连接才有真正的长连接和短连接这一说
不管怎么说,一定要务必记住,长连接是指的TCP连接,而不是HTTP连接。
- 1.只要设置Connection为keep-alive就算是长连接了?
当然是的,但要服务器和客户端都设置。
2.我们平时用的是不是长连接?
这个也毫无疑问,当然是的。(现在用的基本上都是HTTP1.1协议,你观察一下就会发现,基本上Connection都是keep-alive。而且HTTP协议文档上也提到了,HTTP1.1默认是长连接,也就是默认Connection的值就是keep-alive)
3.我们这种普通的Web应用(比如博客园,我的个人博客这种)用长连接有啥好处?需不需要关掉长连接而使用短连接?
首先,长连接是为了复用。那既然长连接是指的TCP连接,也就是说复用的是TCP连接。那这就很好解释了,也就是说,长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。
比如你请求了博客园的一个网页,这个网页里肯定还包含了CSS、JS等等一系列资源,如果你是短连接(也就是每次都要重新建立TCP连接)的话,那你每打开一个网页,基本要建立几个甚至几十个TCP连接,这浪费了多少资源就不用LZ去说了吧。
但如果是长连接的话,那么这么多次HTTP请求(这些请求包括请求网页内容,CSS文件,JS文件,图片等等),其实使用的都是一个TCP连接,很显然是可以节省很多消耗的。
我们上面的代码虽然是支持长连接的,但是我们一次只发送了一次请求,我们就断开了,那我们也要一次发送多个请求看看浏览器输出结果,此时希望不仅仅是打开网页,还要请求显示图片。
这要怎么做呢?
- 1.首先需要将windows的照片上传
我们使用rz来上传我们图片
- 修改代码
index.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>
<!-- 根据src向我们的服务器浏览器自动发起第一次请求 index网页-->
<h5>这个是我们的首页</h5>
<!-- 根据src向我们的服务器浏览器自动发起第二次请求 -->
<img src="/image/1.jpg" alt="这是1个皮卡丘" width="100" height="100">
<!-- 根据src向我们的服务器浏览器自动发起第三次请求 -->
<img src="/image/2.jpg" alt="这是1个小女孩">
</body>
</html>
注意这里要把重定向的代码删除
我们发现是下面这样子
此时由于图片是二进制的,我们并没有告诉浏览器我们的读取图片格式,所以此时也就没有显示出来,此时我们就需要使用Content-Type: 数据类型这个报头属性,由于html的格式容易分辨,所以我们上面没有传入也没有错误,但是图片就不行啦!!!
Content-type
表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
常见的媒体格式类型如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
以application开头的媒体格式类型:
- application/xhtml+xml :XHTML格式
- application/xml : XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json : JSON数据格式
- application/pdf :pdf格式
- application/msword : Word文档格式
- application/octet-stream : 二进制流数据(如常见的文件下载)
- application/x-www-form-urlencoded : 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的:
- multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式。
但是我们要怎么根据请求做出不同的Content-Type: 数据类型这个报头属性呢?
我们可以取出每次请求的路径的后缀和Content-Type: 数据类型,利用map进行绑定起来即可。
首先我们必须要知道我们要干什么,我们就是想往我们的http响应报头里面加一个Content-Type: 数据类型而已,但是我们需要做以下这些步骤才能完成
- 1.我们怎么知道我们要响应的文件是什么类型的啊?
可以通过以取出每次请求的路径的后缀来知道文件的类型 ,
class HttpRequest { public: // 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体 void Deserialize(std::string req) { // 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义 // 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n" // while循环用于遍历字符串,直到找不到分隔符 while (true) { // 在请求字符串中查找分隔符\r\n的位置 size_t pos = req.find(sep); // 如果没有找到分隔符\r\n,则退出循环 if (pos == string::npos) break; // 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分 string temp = req.substr(0, pos); // 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串) if (temp.empty()) break; // 将截取的字符串添加到请求头向量中 req_header.push_back(temp); // 从请求字符串中移除已经处理的部分,包括分隔符 req.erase(0, pos + sep.size()); } // 剩下的字符串(如果有的话)被认为是请求正文 text = req; } 。。。 };
然后通过后缀名找到Content_Type找到对应的数据类型,这里需要写一个Content_Type对照表
class HttpServer { public: HttpServer(uint16_t port = defaultport) : _port(port) { //初始化我们的//Content_Type对照表 content_type.insert({".html","text/html"}); content_type.insert({".png","image/png"}); } ....... std::unordered_map<std::string,std::string> content_type;//Content_Type对照表 };
现在我们就可以写我们的
class HttpServer { public: HttpRequest() { //初始化我们的//Content_Type对照表 content_type.insert({".html","text/html"}); content_type.insert({".png","image/png"}); } 。。。 //根据文件后缀名来找到Content_Type找到对应的数据类型,这里需要一个Content_Type对照表 std::string SuffixToDesc(const std::string suffix) { auto iter=content_type.find(suffix); if(iter==content_type.end()) return content_type[".html"];//默认返回html else return content_type[suffix];//返回传入的 } 。。。 std::unordered_map<std::string,std::string> content_type;//Content_Type对照表 };
接下来就是修改HttpServer类了,最后就是往我们的http响应报头里面加一个Content-Type: 数据类型
response_header= "Content-Type: "; response_header+=SuffixToDesc(req.suffix);
但是运行起来确实有一个错误
这样子我们不能使用静态方法来解决,我们得将HttpServer当作资源传递给每个线程
class ThreadData // 传给线程的数据 { public: ThreadData(int fd,HttpServer* a) : _sockfd(fd),_a(a) { } public: int _sockfd; HttpServer* _a;//注意这里 };
class HttpServer { .... bool Start() { _listensockfd.Socket(); _listensockfd.Bind(_port); _listensockfd.Listen(); for (;;) { pthread_t tid; ThreadData *td = new ThreadData(sockfd,this); pthread_create(&tid, nullptr, ThreadRun, td); } return true; } static void *ThreadRun(void *args) { pthread_detach(pthread_self()); // 分离线程 ThreadData *td = static_cast<ThreadData *>(args); td->_a->HandlerHttp(td->_sockfd); // 执行http的任务 delete td; return nullptr; } ... }
这个时候我们去测试一下
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_map>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const string wwwroot = "./wwwroot"; // web 根目录
const string homepath = "/index.html";
const std::string sep = "\r\n";
class HttpRequest
{
public:
HttpRequest()
{
}
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path = wwwroot; //./wwwroot
if (url == "/" || url == "/index.html")
{
file_path += homepath; // ./wwwroot/index.html
}
else
{
file_path += url; //./wwwroot/url
}
auto pos = file_path.rfind("."); // 从文件名最后开始找点
if (pos == std::string::npos)
suffix = ".html";
else
{
suffix = file_path.substr(pos); // 从pos开始往最后截取,即截取的是文件后缀
}
// 至此suffix就是文件的后缀名了
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
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 file_path; // 用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
std::string suffix; // 文件后缀
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd,HttpServer* a)
: _sockfd(fd),_a(a)
{
}
public:
int _sockfd;
HttpServer* _a;//注意这里
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
// 初始化我们的//Content_Type对照表
content_type.insert({".html", "text/html"});
content_type.insert({".jpg", "image/jpeg"});
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd,this);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "";
std::cout << htmlpath << std::endl;
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
// 根据文件后缀名来找到Content_Type找到对应的数据类型,这里需要一个Content_Type对照表
std::string SuffixToDesc(const std::string suffix)
{
auto iter = content_type.find(suffix);
if (iter == content_type.end())
return content_type[".html"]; // 默认返回html
else
return content_type[suffix]; // 返回传入的
}
void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer << std::endl; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
std::cout << req.file_path << std::endl;
bool ok = true;
string text = ReadHtmlContent(req.file_path);
// HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/err.html";
text = ReadHtmlContent(err_html); //
}
string response_line;
if (ok)
{
response_line = "HTTP/1.0 200 OK\r\n"; // HTTP响应协议的第一行的版本号 状态码 状态码描述
}
else
{
response_line = "HTTP/1.0 404 Not Found\r\n"; // HTTP响应协议的第一行的版本号 状态码 状态码描述
}
string response_header = "Content-Length: "; // HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n"; // 结束这一行
response_header = "Content-Type: ";
response_header += SuffixToDesc(req.suffix);
response_header +="\r\n";
string block_line = "\r\n"; // 这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
// response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0); // 注意这里不用write了,
// send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
td->_a->HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
std::unordered_map<std::string, std::string> content_type; // Content_Type对照表
};
我们使用telnet来看看
这正常吗?正常,因为照片是二进制的,我们去浏览器看看
一片空白,还是不能显示,这是因为 我们的ReadHtmlContent函数有问题,它之前是用于读取文本的,但是今天它要读取二进制。这显然不能做到。我们需要修改这个函数
// 定义一个函数,用于读取HTML文件的内容并将其存储在字符串中
string ReadHtmlContent(const string &htmlpath) {
// 使用ifstream以二进制模式打开HTML文件路径指定的文件
// 以二进制模式读取可以避免某些文件在文本模式下读取时出现的编码或换行符转换问题
ifstream in(htmlpath, std::ios::binary);
// 检查文件是否成功打开
// 如果文件打开失败,则函数返回空字符串
if (!in.is_open())
return "";
// 将文件流的读取位置移动到文件末尾
// 这一步是为了获取文件的总大小
in.seekg(0, std::ios_base::end);
// 获取当前位置(即文件末尾)的偏移量,这就是文件的长度
auto len = in.tellg();
// 将文件流的读取位置移回到文件开头
// 以便从头开始读取文件内容
in.seekg(0, std::ios_base::beg);
// 创建一个字符串来存储文件内容
// 并通过resize方法预先分配足够的空间以存储整个文件内容
std::string content;
content.resize(len);
// 读取文件内容到预先分配好的字符串中
// 注意:这里使用c_str()来获取字符串的内部字符数组指针,但直接修改这个指针指向的内容是不安全的
// 因为c_str()返回的指针指向的是const char*,而且std::string管理自己的内存
// 正确的方式是让read直接写入到string的内部缓冲区(但这样做通常不推荐,因为std::string的API不直接支持)
// 更好的做法是使用std::vector<char>或者std::stringstream,然后将其内容转移到std::string中
// 但为了保持原意,这里仍然按照原代码注释
in.read((char*)content.c_str(), content.size());
// 关闭文件流
in.close();
// 返回包含文件内容的字符串
return content;
}
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_map>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8877;
const string wwwroot = "./wwwroot"; // web 根目录
const string homepath = "/index.html";
const std::string sep = "\r\n";
class HttpRequest
{
public:
HttpRequest()
{
}
// 反序列化函数,用于将字符串格式的HTTP请求分解为请求头和请求体
void Deserialize(std::string req)
{
// 假设sep是一个成员变量,用于分隔请求头和请求体,但在此代码段中未定义
// 这里应该是用于分割请求头中每一行的分隔符,例如"\r\n"
// while循环用于遍历字符串,直到找不到分隔符
while (true)
{
// 在请求字符串中查找分隔符\r\n的位置
size_t pos = req.find(sep);
// 如果没有找到分隔符\r\n,则退出循环
if (pos == string::npos)
break;
// 截取从开头到分隔符\r\n之前的字符串作为请求头的一部分
string temp = req.substr(0, pos);
// 如果截取的字符串为空,则也退出循环(这通常是不必要的,因为find不会返回0位置除非是空字符串)
if (temp.empty())
break;
// 将截取的字符串添加到请求头向量中
req_header.push_back(temp);
// 从请求字符串中移除已经处理的部分,包括分隔符
req.erase(0, pos + sep.size());
}
// 剩下的字符串(如果有的话)被认为是请求正文
text = req;
}
// 解析函数,用于解析请求行的第一部分(通常是HTTP方法、URL和HTTP版本)
void Parse()
{
// 使用stringstream和字符串流输入操作符来解析请求行的第一个元素
std::stringstream ss(req_header[0]); // 将第一行交给ss
// 将请求行解析为HTTP方法、URL和HTTP版本
ss >> method >> url >> http_version; // gei第一个单词会给method,第二个会给url,第3个会给http_version
file_path = wwwroot; //./wwwroot
if (url == "/" || url == "/index.html")
{
file_path += homepath; // ./wwwroot/index.html
}
else
{
file_path += url; //./wwwroot/url
}
auto pos = file_path.rfind("."); // 从文件名最后开始找点
if (pos == std::string::npos)
suffix = ".html";
else
{
suffix = file_path.substr(pos); // 从pos开始往最后截取,即截取的是文件后缀
}
// 至此suffix就是文件的后缀名了
}
// 调试打印函数,用于输出请求的所有信息
void DebugPrint()
{
// 遍历请求头,并打印每一行
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
// 打印解析后的HTTP方法、URL和HTTP版本
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 file_path; // 用于存储转换后的文件地址
// 解析请求行之后得到的结果
std::string method; // HTTP方法,如GET、POST
std::string url; // 请求的URL
std::string http_version; // HTTP的版本号,如HTTP/1.1
std::string suffix; // 文件后缀
};
class HttpServer; // 声明
class ThreadData // 传给线程的数据
{
public:
ThreadData(int fd,HttpServer* a)
: _sockfd(fd),_a(a)
{
}
public:
int _sockfd;
HttpServer* _a;//注意这里
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
// 初始化我们的//Content_Type对照表
content_type.insert({".html", "text/html"});
content_type.insert({".jpg", "image/jpeg"});
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
// 获取用户信息
string clientip;
uint16_t clientport;
// 两个都是输出型参数
// 注意Accept成员函数会返回一个新的套接字,专门用于发送信息的
int sockfd = _listensockfd.Accept(&clientip, &clientport); // 这里获取了客户端IP和端口号
if (sockfd < 0)
continue;
std::cout << "get a new connect, sockfd:" << sockfd << std::endl;
// 下面使用多线程来和用户端进行通信
pthread_t tid;
ThreadData *td = new ThreadData(sockfd,this);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
string ReadHtmlContent(const string &htmlpath) // 读html文件,将它的内容存到content
{
// 坑
ifstream in(htmlpath,std::ios::binary);
if (!in.is_open())
return "";
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;
}
// 根据文件后缀名来找到Content_Type找到对应的数据类型,这里需要一个Content_Type对照表
std::string SuffixToDesc(const std::string suffix)
{
auto iter = content_type.find(suffix);
if (iter == content_type.end())
return content_type[".html"]; // 默认返回html
else
return content_type[suffix]; // 返回传入的
}
void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
// 注意这里我们不用read了,而是使用recv函数,这个和read非常类似,第3个参数为0的时候,功能和read一模一样
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << buffer << std::endl; // 输出HTTP请求
// 这里是巨大变化
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
// 返回响应的过程,这里需要返回一个HTTP响应协议
std::cout << req.file_path << std::endl;
bool ok = true;
string text = ReadHtmlContent(req.file_path);
// HTTP协议有效载荷——这里是一个简单的网页,这个网页放在req.file_path
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/err.html";
text = ReadHtmlContent(err_html); //
}
string response_line;
if (ok)
{
response_line = "HTTP/1.0 200 OK\r\n"; // HTTP响应协议的第一行的版本号 状态码 状态码描述
}
else
{
response_line = "HTTP/1.0 404 Not Found\r\n"; // HTTP响应协议的第一行的版本号 状态码 状态码描述
}
string response_header = "Content-Length: "; // HTTP报头的最后一行需要记录有效载荷的大小
response_header += to_string(text.size()); // 11
response_header += "\r\n"; // 结束这一行
response_header = "Content-Type: ";
response_header += SuffixToDesc(req.suffix);
response_header +="\r\n";
string block_line = "\r\n"; // 这一行是空行,用来区分协议报头和有效载荷
string response = response_line;
response += response_header;
response += block_line;
response += text;
// response 最终就是"HTTP/1.0 200 OK\r\nContent-Length: 11\r\n\r\nhello world"
send(sockfd, response.c_str(), response.size(), 0); // 注意这里不用write了,
// send前几个参数和write基本一样,当send第4个为0的时候功能和write一模一样
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self()); // 分离线程
ThreadData *td = static_cast<ThreadData *>(args);
td->_a->HandlerHttp(td->_sockfd); // 执行http的任务
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd; // 用于监听的
uint16_t _port; // 用于发送消息的
std::unordered_map<std::string, std::string> content_type; // Content_Type对照表
};
我们还发现此时有三次HTTP请求,做到了一次多个请求,浏览器立马就为我们响应,然后浏览器发现我们还要图片,再去请求,每次请求的时候建立一次连接,这就是短连接,而我们的浏览器采用的是长连接,但是我们的代码处理不了,只能一次一次的去连接。(长连接的代码我们就不讲了)
4.3.2.Cookie
HTTP协议是无状态的!
这意味着HTTP协议的服务器不会在两次请求之间保留任何关于客户端的状态信息。
每一次HTTP请求都是独立的,服务器不自动记录之前的请求或响应的信息。
那奇怪了,我今天登录验证了一个网址,跳转到另一个网址后它怎么知道当前用户的状态,但是每一次HTTP请求都是独立的,但是我们依然能访问,它怎么做到的呢?
主要是保存在浏览器的cookie文件里面。
怎么证明?
我们直接打开B站,然后立马登录
点击cookie和站点数据
如果我们想自己写一个Cookie
我们也能观察到HTTP发送过来的请求中确实存在Cookie字段。
从此以后,每一次请求都会有Cookie请求。(因为已经保存下来了)
我们去浏览器查看一下也能看到我们的Cookie
如果我们要写多个Cookie,我们在发送的时候多写几个Set-Cookie即可。
由于Cookie保存着用户的账户和密码,这是非常重要的,所以我们就会出现Cookie被盗取和个人信息被泄漏的问题,因为Cookie保存在用户端(浏览器)的,黑客就可能盗取我们的Cookie文件,导致Cookie被盗取和个人信息被泄漏的问题,那怎么办呢?
讲讲吧
session ID(是由服务器统一管理的,它可以进行回收)
我们不再把个人信息存储到用户端的浏览器,而是存到服务器里的session文件(存放用户的密码,登录地址……) ,对于这个密码信息都由服务端来维护,客户端只需保存session ID即可,所以个人信息的安全得到了很好得保障。
但是黑客可以盗取用户端的浏览器的session ID,还能以session ID去登录这个哔哩哔哩软件(连接服务端),服务端通过session ID来查找对应用户信息(这里找登陆地址),当黑客登录的地址和用户登陆的地址不同时,服务器会检查出异常登录,会把这个session ID给删掉,没有session ID,黑客就不能登录啦!!!这个时候我们的真正的用户也访问不了了,它需要重新申请session ID才能登录了。
有点像下面这样子
但是还是不太安全,客户端信息还是被别人盗走了。HTTP不能解决这个问题。
学到这里,HTTP的知识已经完全够用了!!! 因为HTTP不够安全,所以大众基本摒弃了HTTP,转而去使用HTTPS。
所以我们要来学习一下HTTPS协议 ,这个留到下一篇文章来说吧。