目录
一、认识HTTP协议
1.上网的本质
不知道你在上网的时候是否想过,你看到的这些文字、图片还有视频等信息都是怎么出现在你的电脑屏幕上的?
像我们之前写的udp和tcp协议的客户端(client)和服务器(server)中,客户端会把对资源的请求发送给服务器,服务器会根据请求将客户端需要的数据发回。这种客户端与服务端相互发送数据的机制一般称为CS模式,我们目前市面上的各种app都使用这样的机制运行。
2.应用层的运行逻辑
还记得我们之前实现的网络计算器吗?
它的执行流程如下:
我们之前说过,应用层包括应用层、表示层和会话层。
会话层负责建立连接,在我们之前的代码中对应了socket、bind等函数建立连接和发送信息的过程。
应用层负责转化不同形式类型的数据,在我们的代码中对应了加去报头和序列化反序列化过程。
会话层负责针对特定应用的协议,在我们的代码中对应了Request和Response结构体的处理。
这三层都需要我们自己实现,虽然我们经常把这三层看为一层,但三层的功能泾渭分明,每一层都自己的工作。
3.HTTP的概念
你可能还会说,这不对呀。服务器上的数据有视频、图片还有其他数据。那不同的数据又都是怎么发送到我们的电脑上的呢?这就要讲到HTTP了。
HTTP协议中文名为超文本传输协议,既是最经典的应用层协议,也是应用最广泛的协议。它可以将服务器上的任意类型的数据拉取到本地浏览器,浏览器对其进行解释可以得到网页文本 、图片 、视频、音频等资源。
二、url
1.认识网址
我们上网都需要网址,通过网址我们就能跳转到某一个网页,比如说百度搜索的地址:百度一下,你就知道
在HTTP协议中,我们常说的网址被称为url。一个url字段的组成大致是这样的:
http/https:标识当前使用的协议,原来使用较多的是http协议,近十年来大部分网站都使用了更安全的https协议。
//:表示 URL想访问服务器的 什么资源。
www.example.jp:表示服务器的IP地址,虽然我们只能看到字符串,但它可以转换为IP地址。
80:表示服务器进程的端口号,由于其特殊性质,端口号是一个不能随便更改的值,比如说https协议常用的端口号为443,http协议常用的端口号为80。协议名称和端口号之间是一对一,强相关的。
/:80后面的第一个斜杠表示web根目录。
/dir/index.htm:这个字符串表示文件路径,当请求发到服务端上时,就会在这个目录下查找文件传输。
?和其他/:这样的字符被url当做特殊意义理解,一般当作分隔符使用。
uid=1#ch1:?后面的分布可以看作一个的键值对,=左边的uid可看作是key,=右边的1可看作value。
真实的url与示例上的会不太一样,有省略的或者新增的部分。
三、HTTP协议的宏观理解
1.HTTP请求
HTTP的请求结构以行(hang)为单位,可分为四个部分:请求行、请求报头、空行、有效载荷。
(1)请求行
HTTP协议请求结构的第一行被称为请求行,以空格为分隔符包含请求方法、请求地址和协议版本。
比如:GET / HTTP/1.1,其中GET是请求方法,还有一个方法叫做POST。/表示请求地址,也就是我需要哪个目录下的文件,这里就表示网络的根目录。HTTP/1.1是协议版本,HTTP常用的有三个版本http/1.0、http/1.1和http/2.0,我们以后使用的都是1.1版本。
(2)请求报头
请求报头是由多个Key:Value结构构成的多行结构,请求报头中包含许多请求属性,每一条属性为一行,使用\r\n结尾。
(3)空行
只包含\r\n,有分隔请求报头和有效载荷的效果。
(4)有效载荷
这里储存的一般是用户可能提交的参数,这部分内容不是http协议必需的。
2.HTTP响应
HTTP的相应结构也以行(hang)为单位,同样可分为四个部分:状态行、响应报头、空行和有效载荷。
(1)状态行
HTTP协议响应结构的第一行被称为请求行,以空格为分隔符包含协议版本状态码和状态码描述。
协议版本就不说了,而对状态码而言,你可能不知道http协议,但你一定在上网时遇到过打不开的网站,页面会告诉你404 not found。
这里的404就是状态码,而not found就是404状态码的描述。
(2)响应报头
响应报头与请求报头基本一致,只是二者存储的属性会略微不同。
(3)空行
只包含\r\n,有分隔请求报头和有效载荷的效果。
(4)有效载荷
有效载荷主要是需要传回的资源,可能是html/css的文件资源,也可能是请求对应的图片等等。
3.实际的HTTP请求
(1)测试代码
为了验证真正的HTTP请求是否和我们描述的一样,我们使用下面TCP通信服务端改造后的代码作为服务端,接收不同平台浏览器发来的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>
using namespace std;
enum
{
Socket_false = 1, // 创建套接字失败
Bind_false, // 绑定失败
Listen_false, // 监听失败
Accept_false, // 等待客户端连接失败
Connect_false, // 连接服务器失败
};
const int backlog = 5;
class Sock
{
public:
// 构造函数
Sock(int listensockfd = -1)
: _listensockfd(listensockfd)
{
}
// 创建套接字
void Socket()
{
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:代表IPv4,SOCK_STREAM:代表Tcp协议
if (_listensockfd < 0)
{
cout << "socket false" << endl;
exit(Socket_false);
}
cout << "socket success" << endl;
}
// 绑定,传端口号,是因为sockaddr_in需要,用来保存客户端的IP地址和端口号
void Bind(uint16_t port)
{
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; // 代表0.0.0.0
socklen_t len = sizeof(local);
int n = bind(_listensockfd, (struct sockaddr *)&local, len);
if (n < 0)
{
cout << "bind false" << endl;
exit(Bind_false);
}
cout << "bind success" << endl;
}
// 监听
void Listen()
{
int n = listen(_listensockfd, backlog);
if (n < 0)
{
cout << "listen false" << endl;
exit(Listen_false);
}
cout << "listen success" << endl;
}
// 如果没有客户端连接服务端,则accept会阻塞等待新连接,等待客户端的连接
// 可以通过Accept函数,拿到客户端的ip地址,端口号,用于网络通信的描述符sock
int Accept(string &clientip, uint16_t &clientport)
{
sockaddr_in addr;
socklen_t len = sizeof(addr);
// 调用accpt函数,会将客户端的数据保存在addr中(ip,port)
int sock = accept(_listensockfd, (struct sockaddr *)&addr, &len);
if (sock < 0)
{
cout << "accept false" << endl;
exit(Accept_false);
}
cout << "accept success" << endl;
// 将客户端的ip和port,放入
clientip = inet_ntoa(addr.sin_addr); // 将in_addr_t类型的ip转为char*类型的ip
clientport = ntohs(addr.sin_port); // 将其转为主机字节序
return sock;
}
// 连接服务器,这个ip和port是我们在启动客户端的时候输入的
bool Connect(string &ip, uint16_t &port)
{
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));//初始化
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = inet_addr(ip.c_str());
peer.sin_port = htons(port);
socklen_t len = sizeof(peer);
int n = connect(_listensockfd, (struct sockaddr *)&peer, len);
if (n < 0)
{
cout << "connect false" << endl;
//exit(Connect_false);
return false;
}
cout << "connect success" << endl;
return true;
}
//关闭网络文件适配符
void Close()
{
close(_listensockfd);
}
//获取网络文件适配符
int Getlistensockfd()
{
return _listensockfd;
}
// 析构函数
~Sock()
{
}
private:
int _listensockfd; // 网络文件适配符
};
httpserver.hpp
#include "socket.hpp"
#include<functional>
const int PORT = 8888;
//请求
class HttpRequest
{
public:
HttpRequest()
{}
public:
string inbuffer;
};
//回应
class HttpResponse
{
public:
HttpResponse()
{}
public:
string outbuffer;
};
//http服务器
class HttpServer
{
typedef function<void(HttpRequest&, HttpResponse&)> func_t;
public:
HttpServer(func_t func,uint16_t port = PORT)
:_func(func) ,_port(port)
{
}
//初始化
void Init()
{
_listensockfd.Socket();//创建套接字
_listensockfd.Bind(_port);//绑定
_listensockfd.Listen();//监听
}
//运行
void Start()
{
while (true)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(clientip, clientport);//链接等待
if (socket < 0)
{
cout << "Accept false" << endl;
continue;
}
pid_t pid = fork();//创建子进程
if (pid == 0)
{
_listensockfd.Close();//关闭监听网络适配符
if (fork() > 0)//创建子进程,将父进程关闭,使其成为孤儿进程
{
exit(0);
}
hander_enter(sockfd);//处理客户端的信息
close(sockfd);//关闭网络适配符
exit(0);
}
}
}
//处理
void hander_enter(int sockfd)
{
HttpRequest req;//创建请求
HttpResponse resp;//创建回应
char buffer[4096];//存储客户端发来的信息
ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);
if(n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;//将信息给请求
_func(req, resp);//将请求给回应
send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);//将回应写回
}
}
private:
uint16_t _port;
Sock _listensockfd;
func_t _func;
};
httpservermain.cc
#include"httpserver.hpp"
#include<memory>
//将请求给回应,并且将请求打印
void Delreq(HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
resp.outbuffer = req.inbuffer;
cout << req.inbuffer;
cout << "------------------http end------------------" << endl;
}
int main(int args,char* argv[])
{
uint16_t port=stoi(argv[1]);
unique_ptr<HttpServer> p(new HttpServer(Delreq,port));
p->Init();
p->Start();
return 0;
}
(2)接收HTTP请求
我们首先打开电脑上的浏览器,输入url:http://+云服务器公网IP+:+端口号(例如:http:12.34.56.78:8080)
我们可以观察到进入网站后,Xshell屏幕上出现了按行打印的http请求(第一个)。
(3)真实HTTP请求的结构
第一行为请求行,下面是请求报头,空行将有效载荷和请求报头隔开,只是这个请求没有有效载荷。
请求报头中包含许多请求属性,每个都采用name:val的键值对形式,并且都是一个字符串。每一条属性占一行,使用\r\n结尾。
Host:43.143.106.44:8080,表示客户端请求服务端的套接字(IP地址 + 端口号)。
Connection:keep-alive,表示长连接。
Upgrade-Insecure-Requests: 表示浏览器(客户端)支持自动将HTTP请求升级到HTTPS请求,在学习https协议后再详谈。
User-Agent:客户端的相关信息,内容包括客户端的操作系统和使用的浏览器等信息,使用华为手机、其他安卓手机、iPhone和电脑发送http请求都可以看到不同的信息。
Accept: 相关信息,表示该请求要请求的资源类型。
Accept-Encoding:gzip, deflate,表示客户端支持两种encode格式。
服务器可以根据客户端的支持情况采用不同的压缩算法进行内容压缩。常见的压缩算法有gzip和deflate。
Accept-Language:zh-CN,zh;q=0.9,表示客户端支持的语言格式。
请求报头中的所有属性都是采用name: val的键值对形式,并且是一个字符串。
后面就是一个空行,只有一个\r\n。
4.实际的HTTP响应
我们并不能打印浏览器接收到的响应,所以我们需要自己构造http请求和响应,理解HTTP的响应。
我们在handler函数中除了接收http请求,还要构建一个http的响应发回客户端
#include"HttpServer.hpp"
#include<memory>
#include<unistd.h>
#include<fcntl.h>
using namespace std;
void Delreq(const HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
cout << req.inbuffer;
cout << "------------------http end------------------" << endl;
string resp_line = "HTTP/1.1 200 OK\r\n";//构造状态行
string resp_hander = "Content-Type:text/html\r\n";//构造响应报头
string resp_black = "\r\n";//构造空行
//响应的正文也不是必要的,这里就不写了
//响应序列化,即把它们按顺序拼接好
resp.outbuffer += resp_line;
resp.outbuffer += resp_hander;
resp.outbuffer += resp_black;
}
int main(int argc, char* argv[])
{
uint16_t port = atoi(argv[1]);
unique_ptr<Httpserver> p(new Httpserver(Delreq, port));
p->initserver();
p->start();
return 0;
}
这次我们就不需要通过浏览器发送请求了,可以使用telnet+IP地址127.0.0.1+端口号的方式进行本地环回发送http请求。
但是使用telnet指令需要输入yum install telnet指令安装telnet工具。
首先,运行上述代码编译的程序,我使用8081作为端口号。
然后,输入telnet 127.0.0.1 8081向服务器发送请求。
接着,按Ctrl+ ]键会显示telnet>,再按Enter键跳到下一行。
最后,手动输入请求行GET / HTTP/1.1,按Enter键。
此时我们就向服务端发送了请求,通过处理后我们也能收到服务器的响应。
下面红色框的部分就是我们构建的响应。可以看到状态行(HTTP/1.1 200 OK)、响应报头(Content-Type:text/html)和空行(\r\n),和我们学习的宏观响应一致。
在这里我们也确实能看到,HTTP是基于请求和响应的应用层协议。使用TCP套接字,客户端向服务端发送request请求,服务端接收到请求后经过处理返回response响应,实现了服务端和客户端的通信。
四、HTTP通信的简单模拟
1.构建完整的HTTP协议通信过程
前面通过代码已经让大家认识了HTTP的请求和响应的结构,也看到了真实的请求和响应,接下来我们使用上面的代码构建一个基于HTTP协议的接收请求和发送响应的服务端程序。
(1)增加getfirstline函数
Util.hpp
class Util
{
public:
//截取该请求的第一行,并且将该请求的这一行删除,inbuffer是请求,set是"\r\n"
//将这里设置为静态函数,是为了防止其他函数传参给了this指针
static string getfirstline(string& inbuffer)
{
auto pos=inbuffer.find(set);//在请求中找"\r\n"
if(pos==string::npos)
{
return "";//如果找不到,则返回空
}
string str=inbuffer.substr(0,pos);//找到,截取第一行
inbuffer.erase(0,pos);//将第一行,从请求中删除
return str;//返回第一行
}
};
(2)改造HttpRequest
然后,我们在请求类中增加几个成员变量,包括请求的访问方法method,请求的http版本httpversion,以及请求路径url。
通过成员函数parse对请求进行反序列化,首先使用getfirstline读取请求中的请求行,然后将请求行中的三个字段分离出来。stringstream变量可以将字符串以空格进行分割,流提取可以将数据放入变量。
// 请求
class HttpRequest
{
public:
HttpRequest()
{
}
//将请求的第一行的方法,路径,版本分别放入该字符串中
void parse()
{
//得到请求的第一行
string line=Util::getfirstline(inbuffer);
if(line.size()==0)
return;//没有获取到一行信息,出错
//使用这一行信息构造一个stringstream变量
std::stringstream ss(line);
//从该变量中以空格为分隔符分别将信息放入变量中
ss >> method >> url >> httpversion;
}
public:
string inbuffer; //完整请求
string method; //请求方法
string url; //请求路径
string httpversion; //请求版本
};
// 回应
class HttpResponse
{
public:
HttpResponse()
{
}
public:
string outbuffer;
};
(3)改造HttpRequest
服务器的处理入口HttpServer::handler_enter中使用parse对请求进行反序列化。
// 处理
void hander_enter(int sockfd)
{
HttpRequest req; // 创建请求
HttpResponse resp; // 创建回应
char buffer[4096]; // 存储客户端发来的信息
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer; // 将信息给请求 // 反序列化
req.parse(); //将请求反序列化,将请求中的数据存入HttpRequest类中
_func(req, resp); // 将请求给回应
send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0); // 将回应写回
}
}
(4)改造Delreq
Delreq函数用于处理请求并构建响应,增加将请求行中的三个字段打印出来的代码。
这次构造响应正文的时候,我们将一段html代码以字符串的形式拼接到了响应上。关于html的使用我们不做教学,可以去网上搜一搜html构建网页,直接下载源码使用。
//将请求给回应,并且将请求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
cout << req.inbuffer;
cout<<"反序列化后,成员的值"<<endl;
cout<<"method: "<<req.method<<endl;
cout<<"url: "<<req.url<<endl;
cout<<"httpversion: "<<req.httpversion<<endl;
cout << "------------------http end------------------" << endl;
//客户端自己会打印resp
string resp_line = "HTTP/1.1 200 OK\r\n";//构造状态行
string resp_hander = "Content-Type:text/html\r\n";//构造响应报头
string resp_black = "\r\n";//构造空行
string resp_body = "<html><head></head><body><h1>Hello HTTP</h1></body></html>";//响应正文
//响应序列化,即把它们按顺序拼接好
resp.outbuffer += resp_line;
resp.outbuffer += resp_hander;
resp.outbuffer += resp_black;
resp.outbuffer += resp_body;
}
(5)测试
使用telnet工具向服务端发起请求,此时就会得到服务端的响应,如上图所示,包括响应正文(html的代码)。
在服务端就可以看到telnet在发送请求是输入的请求行中的三个字段。
如果用windows上的浏览器来访问服务器,这段html代码就可以得到一个如图所示的网页。
第二个请求是浏览器发来的
我们可以发现正文中的Hello HTTP字段被显示到了网页中。响应正文中的html代码代表了一个网页,这个网页被服务端响应给客户端。
由于Linux中使用telnet得到响应正文并没有被解释,html代码并没有被处理。而Windows的浏览器是我们能使用到的软件中开发难度最大的,所以它本身就已经非常智能,浏览器得到响应正文会解释它的含义,解释后呈现给我们的结果就是一个网页。
2.网页构建与协议通信的解耦
(1)添加html文件
首先,我们在保存服务器代码的目录中创建一个wwroot目录作为http访问的网络根目录,然后在内部创建两个html文件和一个test目录,index.html用于构建网站的首页,404.html用于构建非法访问返回的404页面,test目录下也储存两个构建网站的代码。
还是一样的,html作为前端知识我们这里不做介绍,大家直接使用这些代码即可。
当客户端发起的请求中的url为\时,此时客户端访问的就是web根目录,也就是./wwwroot目录。这时我们将index.html作为响应返回。就会将该文件中的内容作为响应正文返回给客户端。
inde.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>网页首页</h1>
</body>
</html>
当客户端请求中url错误或者无效时,会将该404.html文件中的内容作为响应正文返回给客户端,告知客户端访问资源不存在并显示404错误码。
404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>资源不存在</title>
</head>
<body>
<h1>你所访问的资源并不存在,404!</h1>
</body>
</html>
当客户端发起的请求中url为/test/a.html或/test/b.html的时候,服务器就会将这两个文件的内容作为响应正文返回给客户端,客户端得到a或b网页。
a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>我是网页a</h1>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>我是网页b</h1>
</body>
</html>
(2)readfile函数
首先,既然我们要分离网页和服务器,那我们就需要使用文件操作将储存在文件中的html代码读取到服务器进程中。
在Util类中增加一个readfile函数:
//打开网页文件,将代码提取到缓冲区中
//resourse:文件的路径,buffer:缓冲区,size:文件的大小
//将这里设置为静态函数,是为了防止其他函数传参给了this指针
static bool readfile(string& resourse,char* buffer,int size)
{
ifstream in(resourse,ios::binary);//打开文件
if(!in.is_open())
{
return false;//文件打开失败,则返回false
}
in.read(buffer,size);//将网页代码读到buffer中
in.close()//关闭文件
}
(3)改造httprequest类
我们在HttpRequest类中增加两个成员变量string path和int size,分别标识请求路径和网络资源的大小。
既然增加了这两个变量,那么对反序列化成员函数prase也要增加对这两个变量的处理。
首先将目录设置为./wwwroot,此时再拼接传递过来的url,如果url为/则拼接出来的是./wwwroot/,如果是其他的内容比如a/b/c.html,最后得到的是./wwwroot/a/b/c.html。也就是说,只有对根目录的请求是不精确到某个文件的,所以如果path的最后一个字符是/就表明它是在对根目录做请求,所以我们只要是对根目录做请求我们就再在index.html,也就变相满足了对根目录的申请。
接着我们处理size变量,对于获取文件的大小,Linux中存在系统调用stat,可用与于获取文件大小。
stat函数()
int stat(const char* path, struct stat* buf);
头文件:sys/socket.h、sys/stat.h、unistd.h
功能:获取文件的大小。
参数:const char* path是表示目标文件的路径的字符串。 struct stat* buf是一个struct stat类型的结构体变量,它的成员变量off_t st_size就指示了文件的大小(以字节为单位)。
返回值:调用成功返回0,调用失败返回-1。
struct stat的定义:
struct stat {
mode_t st_mode; //文件对应的模式,文件,目录等
ino_t st_ino; //inode节点号
dev_t st_dev; //设备号码
dev_t st_rdev; //特殊设备号码
nlink_t st_nlink; //文件的连接数
uid_t st_uid; //文件所有者
gid_t st_gid; //文件所有者对应的组
off_t st_size; //普通文件,对应的文件字节数
time_t st_atime; //文件最后被访问的时间
time_t st_mtime; //文件内容最后被修改的时间
time_t st_ctime; //文件状态改变时间
blksize_t st_blksize; //文件内容对应的块大小
blkcnt_t st_blocks; //伟建内容对应的块数量
};
如果我们获取文件的大小失败,则意味着这个文件很可能不存在,我们将大小设置为404.html的大小就可以了,以后返回的正文也会是404.html的内容。
// 将请求的第一行的方法,路径,版本分别放入该字符串中
// 将客户端的访问路径放入path,将要访问的网络文件的字节数放入size
void parse()
{
// 得到请求的第一行
string line = Util::getfirstline(inbuffer, set);
if (line.size() == 0)
return; // 没有获取到一行信息,出错
// 使用这一行信息构造一个stringstream变量
std::stringstream ss(line);
// 从该变量中以空格为分隔符分别将信息放入变量中
ss >> method >> url >> httpversion;
// 添加路径
path += root_directory; // 先设置网络根目录./wwwroot
path += url; // url:表示根目录之后的路径,再将url追加在根目录之后,例如:./wwwroot/text/a.html
// 如果path最后1一个是'/',则表示客户端访问的是该网站的首页
if (path[path.size() - 1] == '/')
{
path += home_page; // 将首页追加到path中: ./wwwwroot/index.html
}
// 网络文件的大小
struct stat st;
int n = stat(path.c_str(), &st);
if (n == 0) // 如果返回值为0,则资源获取成功,将该网络文件的字节数赋给size
{
size = st.st_size;
}
else // 如果资源获取失败,则设置404.html,并将其字节数赋给size
{
n = stat(html_404.c_str(), &st);
size = st.st_size;
}
}
(4)改造Delreq函数
对于Delreq函数,除了需要增加两个新变量的打印,还要增加从文件中读取正文的代码。
resp_body用于储存响应正文,先给它开辟正文总字节数加一的空间,然后通过readfile读取文件,如果读取失败,就读取404文件作为正文。
//将请求给回应,并且将请求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
cout << req.inbuffer<<endl; //完整请求
cout<<"反序列化后,成员的值"<<endl;
cout<<"method: "<<req.method<<endl; //请求方法
cout<<"url: "<<req.url<<endl; //请求url
cout<<"httpversion: "<<req.httpversion<<endl; //请求版本
cout<<"请求网络文件路径: "<<req.path<<endl; //请求网络文件路径
cout<<"请求网络文件大小: "<<req.size<<endl; //请求网络文件大小
cout << "------------------http end------------------" << endl;
//客户端自己会打印resp
string resp_line = "HTTP/1.1 200 OK\r\n";//构造状态行
//设置响应的类型,将响应发给浏览器,并告诉浏览器文件类型,text/html:表示文件为html的文档
string resp_hander = "Content-Type:text/html\r\n";//构造响应报头
string resp_black = "\r\n";//构造空行
string resp_body;//响应正文
resp_body.resize(req.size+1);//开辟响应正文的大小,比网络文件的大小加一,可以将网络文件全部存储到响应正文中
//将该路径下网络文件的代码,全部存储在resp_body中,也就是放入响应正文中
if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size))
{
//如果读取失败,则将html_404文件的代码,放入响应正文中
Util::readfile(html_404,(char*)resp_body.c_str(),req.size);
}
//响应序列化,即把它们按顺序拼接好
resp.outbuffer += resp_line;
resp.outbuffer += resp_hander;
resp.outbuffer += resp_black;
resp.outbuffer += resp_body;
}
(5)测试
我们输入url:http:公网ip:端口号,对web根目录进行请求,得到首页。
我们输入url:http:公网ip:端口号/test/a.html,对a.html文件进行请求,返回网页a。
我们输入url:http:公网ip:端口号/test/a/b/c.html,对一个不存在的文件进行请求,返回网页404。(其实我们应该将返回的状态码也改成404,但是我们只是简单模拟,就不在意这些细节了)
3.在网页增加跳转
我们经常在网页中经常使用跳转,我们只要点击带有链接的文字就能跳转到另一个网页。其实很简单,只需要增加一些html的语句就能实现这样的操作。
比如说,我们构造了两个语句蓝字表示链接的html文件,黑字表示链接文字的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>网页首页</h1>
<a href="/text/a.html">a网页</a>
<a href="/text/b.html">b网页</a>
</body>
</html>
我们启动测试:
我们点击a网页,就能跳转到a网页。
4.http传递其他类型文件
http作为超文本传输协议,当然可以支持图片、视频、音频等文件的传输,所以我们修改代码使得我们的服务器也支持图片的传递。
比如说,我们在wwwroot中创建一个image文件夹,文件夹中存储一个图片文件1.png。(这个图片不要太大,可以将图片拖拽进vscode保存在云服务器上)
此时我们想把这个图片也传递到首页中
(1)修改html文件
首先要在index.html增加图片信息的代码,代码中的alt后面的文字表示图片加载失败时显示在屏幕上的文字。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>网页首页</h1>
<a href="/text/a.html">a网页</a>
<a href="/text/b.html">b网页</a>
<img src="image/1.png" alt="Linux">
</body>
</html>
(2)HttpRequest类
由于发送http的request本质是在对某一个文件进行申请,而url指示了该文件的位置与名称,所以我们在httprequest类中再增加一个成员变量suffix储存标识文件类型的后缀。
然后,由于我们在prase函数中已经有了获取文件路径path的代码,所以我们只需要从path中获取资源后缀即可。(第四大块,代码后有注释说明)
// 将请求的第一行的方法,路径,版本分别放入该字符串中
// 将客户端的访问路径放入path,将要访问的网络文件的字节数放入size
void parse()
{
// 得到请求的第一行
string line = Util::getfirstline(inbuffer, set);
if (line.size() == 0)
return; // 没有获取到一行信息,出错
// 使用这一行信息构造一个stringstream变量
std::stringstream ss(line);
// 从该变量中以空格为分隔符分别将信息放入变量中
ss >> method >> url >> httpversion;
// 添加路径
path += root_directory; // 先设置网络根目录./wwwroot
path += url; // url:表示根目录之后的路径,再将url追加在根目录之后,例如:./wwwroot/text/a.html
// 如果path最后1一个是'/',则表示客户端访问的是该网站的首页
if (path[path.size() - 1] == '/')
{
path += home_page; // 将首页追加到path中: ./wwwwroot/index.html
}
//获取path对应的资源后缀
//例:./wwwroot/index.html
//./wwwroot/text/a.html
//./wwwroot/image/1.png
auto pos=path.find(".");//从后往前找
if(pos==string::npos)
{
suffix=".html";//找不到,默认它的后缀为html
}
else
{
suffix=path.substr(pos);//找到了,从.开始截取字符,存入suffix中
}
// 网络文件的大小
struct stat st;
int n = stat(path.c_str(), &st);
if (n == 0) // 如果返回值为0,则资源获取成功,将该网络文件的字节数赋给size
{
size = st.st_size;
}
else // 如果资源获取失败,则设置404.html,并将其字节数赋给size
{
n = stat(html_404.c_str(), &st);
size = st.st_size;
}
}
(3)修改HttpServer.cc
首先,我们在Delreq中已经拿到了需求文件的后缀。
我们原先的代码中,http响应相关属性的Content-Type是写死的,我们在这里要增加一个suffixToDesc函数用于拼接不同文件的Content-Type属性字符串。
其他类型文件的Content-Type对应类型标识可以看下面的表格。
在Delreq函数中增加构造Content-Type和Content-Length两个属性字符串的代码。
//根据网络文件的后缀,来确定"Content_Type"的类型
string suffixToDesc(const string suffix)
{
string ct="Content_Type: ";
if(suffix==".png")
{
ct+="application/x-png";
}
else if(suffix==".html")
{
ct+="text/html";
}
ct+="\r\n";
return ct;
}
//将请求给回应,并且将请求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
cout << req.inbuffer<<endl; //完整请求
cout<<"反序列化后,成员的值"<<endl;
cout<<"method: "<<req.method<<endl; //请求方法
cout<<"url: "<<req.url<<endl; //请求url
cout<<"httpversion: "<<req.httpversion<<endl; //请求版本
cout<<"请求网络文件路径: "<<req.path<<endl; //请求网络文件路径
cout<<"请求网络文件大小: "<<req.size<<endl; //请求网络文件大小
cout << "------------------http end------------------" << endl;
//客户端自己会打印resp
string resp_line = "HTTP/1.1 200 OK\r\n";//构造状态行
//设置响应的类型,将响应发给浏览器,并告诉浏览器文件类型,text/html:表示文件为html的文档
string resp_hander=suffixToDesc(req.suffix);//根据文件类型构造Content-Type
//构造Content-Length
string resp_len="Content-Length: ";
resp_len+=to_string(req.size);
resp_len+="\r\n";
string resp_black = "\r\n";//构造空行
string resp_body;//响应正文
resp_body.resize(req.size+1);//开辟响应正文的大小,比网络文件的大小加一,可以将网络文件全部存储到响应正文中
//将该路径下网络文件的代码,全部存储在resp_body中,也就是放入响应正文中
if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size))
{
//如果读取失败,则将html_404文件的代码,放入响应正文中
Util::readfile(html_404,(char*)resp_body.c_str(),req.size);
}
//响应序列化,即把它们按顺序拼接好
resp.outbuffer += resp_line;
resp.outbuffer += resp_hander;
resp.outbuffer += resp_len;
resp.outbuffer += resp_black;
resp.outbuffer += resp_body;
}
(4)运行
我们再次打开服务器访问网页,可以看到图片显示出来了。
5.总代码
util.hpp:将网络文件读取到缓冲区中,和拿到请求的第一行
socket.hpp:里面包含了套接字的创建,绑定,监听,连接等等
ios.hpp:HttpRequest类和HttpResponse类
httpserver.hpp:服务区的头文件
httpservermain.cc:服务器的main函数
util.hpp
#include<string>
#include<fstream>
using namespace std;
class Util
{
public:
//截取该请求的第一行,并且将该请求的这一行删除,inbuffer是请求,set是"\r\n"
//将这里设置为静态函数,是为了防止其他函数传参给了this指针
static string getfirstline(string& inbuffer,const string& set)
{
auto pos=inbuffer.find(set);//在请求中找"\r\n"
if(pos==string::npos)
{
return "";//如果找不到,则返回空
}
string str=inbuffer.substr(0,pos);//找到,截取第一行
inbuffer.erase(0,pos);//将第一行,从请求中删除
return str;//返回第一行
}
//打开网页文件,将代码提取到缓冲区中
//resourse:文件的路径,buffer:缓冲区,size:文件的大小
//将这里设置为静态函数,是为了防止其他函数传参给了this指针
static bool readfile(const string& resourse,char* buffer,int size)
{
ifstream in(resourse,ios::binary);//打开文件
if(!in.is_open())
{
return false;//文件打开失败,则返回false
}
in.read(buffer,size);//将网页代码读到buffer中
in.close();//关闭文件
return true;
}
};
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>
using namespace std;
enum
{
Socket_false = 1, // 创建套接字失败
Bind_false, // 绑定失败
Listen_false, // 监听失败
Accept_false, // 等待客户端连接失败
Connect_false, // 连接服务器失败
};
const int backlog = 5;
class Sock
{
public:
// 构造函数
Sock(int listensockfd = -1)
: _listensockfd(listensockfd)
{
}
// 创建套接字
void Socket()
{
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:代表IPv4,SOCK_STREAM:代表Tcp协议
if (_listensockfd < 0)
{
cout << "socket false" << endl;
exit(Socket_false);
}
cout << "socket success" << endl;
}
// 绑定,传端口号,是因为sockaddr_in需要,用来保存客户端的IP地址和端口号
void Bind(uint16_t port)
{
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; // 代表0.0.0.0
socklen_t len = sizeof(local);
int n = bind(_listensockfd, (struct sockaddr *)&local, len);
if (n < 0)
{
cout << "bind false" << endl;
exit(Bind_false);
}
cout << "bind success" << endl;
}
// 监听
void Listen()
{
int n = listen(_listensockfd, backlog);
if (n < 0)
{
cout << "listen false" << endl;
exit(Listen_false);
}
cout << "listen success" << endl;
}
// 如果没有客户端连接服务端,则accept会阻塞等待新连接,等待客户端的连接
// 可以通过Accept函数,拿到客户端的ip地址,端口号,用于网络通信的描述符sock
int Accept(string &clientip, uint16_t &clientport)
{
sockaddr_in addr;
socklen_t len = sizeof(addr);
// 调用accpt函数,会将客户端的数据保存在addr中(ip,port)
int sock = accept(_listensockfd, (struct sockaddr *)&addr, &len);
if (sock < 0)
{
cout << "accept false" << endl;
exit(Accept_false);
}
cout << "accept success" << endl;
// 将客户端的ip和port,放入
clientip = inet_ntoa(addr.sin_addr); // 将in_addr_t类型的ip转为char*类型的ip
clientport = ntohs(addr.sin_port); // 将其转为主机字节序
return sock;
}
// 连接服务器,这个ip和port是我们在启动客户端的时候输入的
bool Connect(string &ip, uint16_t &port)
{
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));//初始化
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = inet_addr(ip.c_str());
peer.sin_port = htons(port);
socklen_t len = sizeof(peer);
int n = connect(_listensockfd, (struct sockaddr *)&peer, len);
if (n < 0)
{
cout << "connect false" << endl;
//exit(Connect_false);
return false;
}
cout << "connect success" << endl;
return true;
}
//关闭网络文件适配符
void Close()
{
close(_listensockfd);
}
//获取网络文件适配符
int Getlistensockfd()
{
return _listensockfd;
}
// 析构函数
~Sock()
{
}
private:
int _listensockfd; // 网络文件适配符
};
ios.hpp
#pragma once
#include <string>
#include "util.hpp"
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
using namespace std;
const string set = "\r\n";
const string home_page = "index.html"; // 首页
const string root_directory = "./wwwroot"; // 根目录
const string html_404 = "./wwwroot/404.html";
// 请求
class HttpRequest
{
public:
HttpRequest()
{}
// 将请求的第一行的方法,路径,版本分别放入该字符串中
// 将客户端的访问路径放入path,将要访问的网络文件的字节数放入size
void parse()
{
// 得到请求的第一行
string line = Util::getfirstline(inbuffer, set);
if (line.size() == 0)
return; // 没有获取到一行信息,出错
// 使用这一行信息构造一个stringstream变量
std::stringstream ss(line);
// 从该变量中以空格为分隔符分别将信息放入变量中
ss >> method >> url >> httpversion;
// 添加路径
path += root_directory; // 先设置网络根目录./wwwroot
path += url; // url:表示根目录之后的路径,再将url追加在根目录之后,例如:./wwwroot/text/a.html
// 如果path最后1一个是'/',则表示客户端访问的是该网站的首页
if (path[path.size() - 1] == '/')
{
path += home_page; // 将首页追加到path中: ./wwwwroot/index.html
}
//获取path对应的资源后缀
//例:./wwwroot/index.html
//./wwwroot/text/a.html
//./wwwroot/image/1.png
auto pos=path.find(".");//从后往前找
if(pos==string::npos)
{
suffix=".html";//找不到,默认它的后缀为html
}
else
{
suffix=path.substr(pos);//找到了,从.开始截取字符,存入suffix中
}
// 网络文件的大小
struct stat st;
int n = stat(path.c_str(), &st);
if (n == 0) // 如果返回值为0,则资源获取成功,将该网络文件的字节数赋给size
{
size = st.st_size;
}
else // 如果资源获取失败,则设置404.html,并将其字节数赋给size
{
n = stat(html_404.c_str(), &st);
size = st.st_size;
}
}
public:
string inbuffer; // 完整请求
string method; // 请求方法
string url; // 请求url
string httpversion; // 请求版本
string path; // 请求路径
string suffix; //文件后缀
int size; // 网络文件的大小
};
// 回应
class HttpResponse
{
public:
HttpResponse()
{}
public:
string outbuffer;
};
httpserver.hpp
#pragma once
#include "socket.hpp"
#include <functional>
#include <sstream>
#include "ios.hpp"
const uint16_t PORT=8989;
// http服务器
class HttpServer
{
typedef function<void(const HttpRequest&, HttpResponse&)> func_t;
public:
HttpServer(func_t func, uint16_t port = PORT)
: _func(func), _port(port)
{}
// 初始化
void Init()
{
_listensockfd.Socket(); // 创建套接字
_listensockfd.Bind(_port); // 绑定
_listensockfd.Listen(); // 监听
}
// 运行
void Start()
{
while (true)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(clientip, clientport); // 链接等待
if (socket < 0)
{
cout << "Accept false" << endl;
continue;
}
pid_t pid = fork(); // 创建子进程
if (pid == 0)
{
_listensockfd.Close(); // 关闭监听网络适配符
if (fork() > 0) // 创建子进程,将父进程关闭,使其成为孤儿进程
{
exit(0);
}
hander_enter(sockfd); // 处理客户端的信息
close(sockfd); // 关闭网络适配符
exit(0);
}
}
}
// 处理
void hander_enter(int sockfd)
{
HttpRequest req; // 创建请求
HttpResponse resp; // 创建回应
char buffer[4096]; // 存储客户端发来的信息
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer; // 将信息给请求 // 反序列化
req.parse(); //将请求反序列化,将请求中的数据存入HttpRequest类中
_func(req, resp); // 将请求给回应
send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0); // 将回应写回
}
}
private:
uint16_t _port; //端口号
Sock _listensockfd; //套接字
func_t _func;
};
httpservermain.cc
#include"httpserver.hpp"
#include<memory>
#include"ios.hpp"
//根据网络文件的后缀,来确定"Content_Type"的类型
string suffixToDesc(const string suffix)
{
string ct="Content_Type: ";
if(suffix==".png")
{
ct+="application/x-png";
}
else if(suffix==".html")
{
ct+="text/html";
}
ct+="\r\n";
return ct;
}
//将请求给回应,并且将请求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{
cout << "------------------http start------------------" << endl;
cout << req.inbuffer<<endl; //完整请求
cout<<"反序列化后,成员的值"<<endl;
cout<<"method: "<<req.method<<endl; //请求方法
cout<<"url: "<<req.url<<endl; //请求url
cout<<"httpversion: "<<req.httpversion<<endl; //请求版本
cout<<"请求网络文件路径: "<<req.path<<endl; //请求网络文件路径
cout<<"请求网络文件大小: "<<req.size<<endl; //请求网络文件大小
cout << "------------------http end------------------" << endl;
//客户端自己会打印resp
string resp_line = "HTTP/1.1 200 OK\r\n";//构造状态行
//设置响应的类型,将响应发给浏览器,并告诉浏览器文件类型,text/html:表示文件为html的文档
string resp_hander=suffixToDesc(req.suffix);//根据文件类型构造Content-Type
//构造Content-Length
string resp_len="Content-Length: ";
resp_len+=to_string(req.size);
resp_len+="\r\n";
string resp_black = "\r\n";//构造空行
string resp_body;//响应正文
resp_body.resize(req.size+1);//开辟响应正文的大小,比网络文件的大小加一,可以将网络文件全部存储到响应正文中
//将该路径下网络文件的代码,全部存储在resp_body中,也就是放入响应正文中
if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size))
{
//如果读取失败,则将html_404文件的代码,放入响应正文中
Util::readfile(html_404,(char*)resp_body.c_str(),req.size);
}
//响应序列化,即把它们按顺序拼接好
resp.outbuffer += resp_line;
resp.outbuffer += resp_hander;
resp.outbuffer += resp_len;
resp.outbuffer += resp_black;
resp.outbuffer += resp_body;
}
int main(int args,char* argv[])
{
uint16_t port=stoi(argv[1]);
unique_ptr<HttpServer> p(new HttpServer(Delreq,port));
p->Init();
p->Start();
return 0;
}
五、GET与POST方法
http的请求方法有很多,其中最常用的就是GET和POST这两种方法。
1. 认识表单
不知道你们在浏览器中是否在网页中是否使用过浏览器的开发者工具,在网页中点击F12即可打开。
比如说,我打开搜狗搜索并点击F12,右侧点击元素就可以查看构造该网页的html代码。
我们点击弹窗左上角的箭头图标(或按快捷键 Ctrl+Shift+C)进入选择元素模式,从页面中选择需要查看的元素,可以在开发者工具元素(Elements)一栏中定位到该元素源代码的具体位置。
我们将鼠标挪动到搜索框,它对应的html代码对应了下图黑色框的内容,而这块内容就叫做表单。
其实,我们之所以能够搜索信息,是因为搜索框本质是一个form表单。我们搜索的关键字会被填入这个表单中,然后浏览器会通过HTTP协议发送包含该表单的请求到服务端,服务端再处理发回响应,从而实现搜索。
而当我们进行数据提交的时候,推送数据的方法一般使用这两种:GET和POST。
2.观察GET和POST方法
我们在index.html中也增加表单代码,其中action="/test.py"表示使用网络根目录下的test.py处理表单,method="GET"就表明申请的发送使用GET方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我构建的网页</title>
</head>
<body>
<h1>网页首页</h1>
<a href="/test/a.html">a网页</a>
<a href="/test/b.html">b网页</a>
<img src="/image/1.jpg" alt="滨江道步行街">
<form action="/test.py" method="GET">
姓名:<br>
<input type="text" name="xname">
<br>
密码:<br>
<input type="password" name="ypwd">
<br><br>
<input type="submit" value="登陆">
</form>
</body>
</html>
运行,姓名输入zhangsan,密码输入123456
因为我们并没有创建对应的python文件,所以给我们返回了404。
我们再将GET修改为method="POST",此时申请的发送就改为了POST方法。
重复上述动作也可以实现同样的效果。
3.二者的区别
GET方法中,form表单中我们输入的姓名和密码以xname=zhangsan&ypwd=123456的形式拼接在了url中。
而使用POST方法提交form表单时,表单的内容并没有拼接到url中。
通过上面对比我们发现,GET和POST请求方法表单信息的存储位置是不同的:GET方法通过url提交参数,POST方法通过HTTP请求的正文提交参数。
如果客户端使用GET方法,在提交form表单的时候,内容就会拼接到url中,在浏览器的网址栏中可以看到。而如果使用POST方法,在提交form表单的时候,内容是通过请求正文提交的,在浏览器的网址栏中看不到。
既然POST方式通过正文传递信息,而正文信息人们一般看不到,那么是否可以说POST方法是安全的呢?
答案是否定的,POST和GET两种方法中信息都是暴露在外的,这些封装后的数据还可以通过抓包的方式被他人获取。在我们后面讲到的https协议才能实现真正的安全信息传输。
六、长链接
在http请求属性中有一行属性表示长链接:Connection: keep-alive
我们构建首页的index.html文件中有文字、网页跳转链接、图片、表单这些代码内容。浏览器申请根目录时,收到的网页中也出现了这么多的内容,而且需要传输的这些文件存储位置不同。而我打开网页时,只在浏览器上输入了一次网址。
也就是说一个网页中有多种类型的资源,而一次请求只能获取一种类型的资源。
那么一个网页的形成就大概率需要浏览器发起多次http请求,而服务器根据请求返回的多个响应共同组成了这个网页。
我们观察服务器也确实发现该网站的建立的确发送了多个http请求。
在底层HTTP协议使用的是TCP套接字,所以客户端每发送一个请求,服务器都会经历一次创建套接字的流程。
如果客户端真的每发送一次数据都创建一个套接字,一方面系统调用的开销回增加程序的运行时间,还有这样的逻辑使得访问一个网页需要创建多个套接字,会导致套接字资源紧张,所以就出现了长连接,对应属性就是上面的Connection: keep-alive。
长连接:一个客户端对应一个套接字,客户端的一个请求响应完后,套接字不关闭,只有客户端退出了,或者指定关闭时,套接字才关闭。
也就是说,一个客户端无论有多少个请求,都通过一个套接字和服务器进行网络通信。
当然http中也存在短链接(Connection: close),短链接仅支持客户端每发送一次信息就重新建立TCP链接,一般用于大量用户使用的资源。这个属性我们并不能直观地看到。
七、会话保持
1.认识Cookie技术
我们在使用浏览器访问CSDN等网页时往往需要登陆账号,比如说我在CSDN首页登陆账号。
那么我只需要输用户名和密码登录这一次,之后我就可以在浏览该网站的其他网页时登录也不会退出,或者关闭浏览器,在重新进入,都不需要输入账号和密码
但是HTTP是一种无状态协议,每次请求并不会记录它曾经请求了什么。所以,在第一次登录CSDN后,在站内进行网页跳转(从一篇文章跳转到另一篇文章)时,你打开了一个新的网页,理论上需要再次输入账号密码登录,浏览器发送表单验证身份信息,但现实是我们不需要第二次登录就可以浏览站内的各个网页。
由于http本身不支持上述保持登录的功能,所以Cookie技术就应运而生了。
要想实现登陆状态的保持效果,浏览器就需要在我们第一次登录CSDN时,将我们的账号密码等登录信息保存下来。我们每次打开CSDN站内的网站时,浏览器会自动将已保存的用户登录信息添加到了请求报头中,通过HTTP协议发送给服务器。服务器会根据拿到的信息进行登陆状态的鉴别并返回对应的响应。
所以说,进入新网页后的登录还是需要的,只是支持Cookie技术的浏览器帮我们做了这件事,我们一直没注意到而已。
如图所示,点击网址前面的锁,就可以查看当前浏览器正在使用的Cookie。
我们将图中和CSDN有关的Cookie数据删除。
刷新页面后,你就会发现你的登录状态失效了。
2.内存级与文件级Cookie技术
我们在登录CSDN后,关掉浏览器后再次打开CSDN你还是能保持登录状态。
这又是怎么实现的呢?
Cookie又分为内存级和文件级
内存级Cookie:将登录信息保存在浏览器的缓冲区中,当浏览器被关闭时,进程结束,保存的信息也失效了,重新打开浏览器后还需要重新登录。
文件级Cookie:将信息保存在文件中,文件是放在磁盘上的,无论浏览器怎么打开关闭,文件中的信息都不会删除,在之后发送HTTP请求时,浏览器从该文件中读取信息并加到请求报头中。
根据日常使用浏览器的情况,大部分网站在你登陆后,关闭浏览器再次打开时登陆状态依旧保持,所以大部分情况下的Cookie都是文件级别的,而且这些文件是可以从我们的计算机中找到的。
3.Session文件
既然Cookie文件储存了许多我们的隐私信息,那么一旦这些Cookie文件被不法份子盗取,他们就可以冒用我们的身份进行一些非法操作,并且进行一些非法操作。(比如说QQ盗号)
所以为了保证信息安全,现在的很多公司都会将用户的账号密码以及浏览痕迹等信息保存在服务器中。每个用户对应在服务器上创建一个Session文件储存信息。由于服务器上的Session文件有很多,所以每个文件名都会设置为一个独一无二的Session id。
服务器将这个Session id放入响应返回给用户,此时用户浏览器的Cookie中保存的就是这个id值而不再是储存信息的文件。
这种服务端存储用户信息的技术就叫做Session技术。
为什么会话保持使用的Session技术能够提高用户信息的安全性呢?
这是因为,互联网公司的服务器都是由专业的人员维护的,服务器中存在木马病毒的可能性相比我们的计算机而言更小,所以用户信息在服务端会更加安全。
如果客户端储存的Cookie中的Session id被盗用,当不法分子使用该id向服务端发起请求时,因为不法分子的IP地址与你常用IP地址大都不一样,所以服务端就会将所有登录该账号的设备强制下线,此时只有手里真正有账号密码的人才能够再次登录。
当然,保证Session安全的策略非常多,有兴趣的小伙伴可以自行了解。
写入Cookie信息:
我们知道,浏览器的Cookie信息是服务端响应返回的,所以在我们构建响应的时候也可以构建Cookie信息让浏览器去保存。
在DealReq函数中构建响应时,设置Cookie信息,内容是name=123456abc,有效时间是三分钟,然后加到响应报头中返回给客户端,如上图所示。
使用浏览器访问根目录的时候,如上图所示,会得index.html文件表示的网页,查看该网页的Cookie信息,可以看到name是123456abc,有效时间是3分钟,和我们在服务端构建响应时写的内容一模一样。
浏览器将我们在响应中设置的Cookie内容当作了Session id。
真正生成Session id是有一套复杂的算法的,它能够保证每一个Session文件的id都是独一无二的。
Cookie和Session两种技术共同实现了HTTP的会话保持。
八、HTTP状态码
1.HTTP状态码的分类
在相应的报头中,除了正常响应的200,资源不存在的404,还有很多的错误码,基本上可分为五种类型,分别以1~5开头:
我们可以这样理解这几类状态码:
1开头的状态码称为信息性状态码,表示客户端完成了一个提交动作,但服务端处理的过程耗时较长,为了表明自己的状态,服务器就会返回1开头的状态码,告诉客户端我已经受理了这个请求,正在处理。
2开头的状态码称为成功状态码,表示服务器已经根据你发来的请求将响应发回,客户端可以正常处理响应。
3开头的状态码称为重定向状态码,重定向有两种永久重定向和临时重定向。
4开头的状态码称为客户端错误状态码,比如404和403。不过,有这样一个问题,404错误码表示发生错误的是客户端还是服务器呢?
其实404错误码表示客户端出错了,正是因为客户端给服务端发送了错误的请求,所以服务端会告知客户端你的发送的请求有问题,我无法处理。
这是我们浏览网页时最常见的一些状态码:200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。
2.重定向状态码(3XX)
相信大家都有过这样的经历,打开一个网址后,网站在白屏加载时突然就跳转到了一些无关的广告网页,其实这就是重定向的应用。
将服务端发送的网络请求资源转为其他无关的网络资源即为重定向,浏览器发送请求给服务端,服务端返回一个新的url,并且状态码是3XX,浏览器会自动用这个新的url向新地址的服务端发起请求。而我们看到的表现就是打开一个网页时突然又跳转到了另一个完全不相干的网站,此时服务器相当于提供了引路的服务。
所以说,重定向是由客户端完成的,当客户端浏览器收到的响应中状态码是3XX后,它就会自动从响应中寻找返回的新的url并再次发送请求。
重定向又有两种:
- 永久重定向:状态码为301。
- 临时重定向:状态码为302和307。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。
如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时直接访问的就是重定向后的网站。
而如果某个网站是临时重定向,那么每次访问该网站时都需要浏览器来帮我们完成重定向跳转到目标网站。
永久重定向无法演示出来,效果和临时重定向一样。