应用层(Application layer)是OSI模型的第七层。应用层直接和应用程序接口并提供常见的网络应用服务。应用层也向表示层发出请求。应用层是开放系统的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
而学习应用层协议,绕不开http协议和https协议,本文重点介绍http协议。
在WWW(万维网)上,每一个信息资源都有统一的且在网上的地址,该地址就叫URL(Uniform Resource Locator,统一资源定位器),它是WWW的统一资源定位标志,就是指网络地址。
protocol :// hostname[:port] / path / [:parameters][?query]#fragment
例如:
http://user:[email protected]:80/filder/index.html?uid=1#ch1
protocol(协议) | 最常用的是HTTP协议 |
---|---|
hostname (主机名) | 是指存放资源的服务器的域名系统(DNS) 主机名或 IP 地址 。有时,在主机名前也可以包含连接到服务器所需的用户名和密码(格式:username:password@hostname)。 |
port(端口号) | 整数,可选,省略时使用方案的默认端口,各种传输协议都有默认的端口号,如http的默认端口为80,https默认端口为443。 |
parameters(参数) | 这是用于指定特殊参数的可选项,有服务器端程序自行解释。例如带层次的文件路径,标识网络资源(文件、视频、图片) |
query(查询) | 可选,用于给动态网页(如使用CGI、ISAPI、PHP/JSP/ASP/ASP.NET等技术制作的网页)传递参数,可有多个参数,用“&”符号隔开,每个参数的名和值用“=”符号隔开。 |
fragment(片断标识符) | 字符串,用于指定网络资源中的片段。例如一个网页中有多个名词解释,可使用fragment直接定位到某一名词解释。 |
url中不乏有像/ ? :等这样的字符,而这些字符在url中被解释为特殊含义。例如
+ | 表示空格 |
---|---|
/ | 分隔目录和子目录 |
? | 分隔实际和URL和参数 |
# | 表示书签 |
& | URL中指定的参数间的分隔符 |
= | URL中指定的参数的值 |
那么在参数中若存在这些特殊字符就需要对其转义
转义的规则:
- Url编码默认使用的字符集是US-ASCII。
- urlencode:对字符串中除了 -_. 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位 十六进制数,空格则编码为加号(+),形式为%XY。
- urldecode:对参数值进行编码。即将%XY的形式转换回对应的非字母数字字符。
US-ASCII码表
例如:
因此,网络上也有很多工具可以将你输入的字符转换为url转换工具
请求格式
请求方法 url HTTP版本\r\n
的形式存在。key:value
键值对的方式存在,每条属性之间以\r\n
分隔。请求报头遇到空行部分结束。Context-length
来标识请求正文的长度。响应格式
HTTP版本号 状态码 状态码描述\r\n
的形式存在key:value
键值对的方式存在,每条属性之间以\r\n
分隔。响应报头遇到空行部分结束。Context-length
来标识响应正文的长度。一个完整的HTTP请求
在应用层中,客户端自上向下交付request給传输层,即通过tcp链接发送給服务器。因为下三层的的通信细节由操作系统完成,因此我们不需要太过关心。在这里需要知道的是客户端将request交付給传输层,传输层通过TCP链接发送給服务器。服务器从传输层中读取request到应用层。对数据做完处理后生成一个response,然后也是以自上向下交付的形式交给传输层。传输层通过TCP链接发送給客户端,客户端将response从传输层读取到应用层。这样就完成了一次HTTP请求。
基于以上的请求格式和响应格式,如何保证能够读到完整的报文?
字符串\r\n
的结构存在,因此只需要按行读取,直至读到空行,就能将请求行和请求报头读完。Context-length
来标识请求正文的长度。因此按照该属性向空行后读相应大小的正文即可把请求正文读完。基于以上的请求格式和响应格式,如何实现序列化和反序列化。
再看序列化定义:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
\r\n
为间隔标识的字符串,就能够在写入和读取中进行传输。而正文除了有一条属性Context-length
标识外,正文通常是文件、图片、视频、音频等二进制的方式存在,因此只需要标定正文长度能够读取完整即可。该程序仅仅实现客户端向服务器发送请求,然后再服务器端将该请求进行打印,并没有实现响应功能。
httpserver.cc
#include
#include
#include
#include"protocol.hpp"
#include"httpserver.hpp"
using namespace std;
using namespace Server;
void Usage(string proc)
{
cout<<"Usage:\n\t"<<proc<<" port\r\n";
}
bool Get(const HttpRequest& req,HttpResponse& rep)
{
cout<<"-----------------http start-----------------------"<<endl;
cout<<req._inbuffer<<endl;
cout<<"-----------------http end-------------------------"<<endl;
return true;
}
int main(int args,char* argv[])
{
if(args!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port=atoi(argv[1]);
unique_ptr<httpserver> hs(new httpserver(Get,port));
hs->inithttpserver();
hs->start();
return 0;
}
httpserer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"protocol.hpp"
#define NUM 1024
static const uint16_t gport = 8080;
static const int gbacklog=5;
using namespace std;
namespace Server
{
enum
{
USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
};
class httpserver;
using func_t= function<bool(const HttpRequest&,HttpResponse&)>;//重定义func_t
class httpserver
{
public:
httpserver(func_t func,const uint16_t& port=gport):_port(port),_listensock(-1),_func(func){}
void inithttpserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
exit(SOCK_ERR);
}
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
exit(BIND_ERR);
}
//3.将套接字设置为监听模式
if(listen(_listensock,gbacklog)<0)
{
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
char buffer[4096];
HttpRequest req;
HttpResponse rep;
//1.读取到完整报文
ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);//大概率能读到完整报文
if(n>0)
{
buffer[n]=0;
req._inbuffer=buffer;
_func(req,rep);
}
}
void start()
{
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
continue;
}
cout<<"accept sock: "<<sock<<endl;
//多进程版---
pid_t id=fork();//创建子进程
if(id==0)//子进程进入
{
close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
if(fork()>0) exit(0);
// //孙子进程
HandlerHttp(sock);//调用操作函数
close(sock);
exit(0);
}
//父进程
close(sock);//父进程不使用文件描述符就关闭
waitpid(id,nullptr,0);
}
}
~httpserver(){}
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
func_t _func;
};
}
protocol.hpp
#pragma once
#include
#include
#include
#include
#include
#include
using namespace std;
const string SEP="\r\n";
const string default_path="./wwwroot";
const string home_page="index.html";
const string html_404="wwwroot/404.html";
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
public:
string _inbuffer;
string _method;
string _url;//
string _httpversion;//版本
string _path;//路径
string _suffix;
int _size;//文件内容的大小
};
class HttpResponse
{
public:
std::string _outbuffer;
};
将云服务器的公网ip和端口号在浏览器上以公网ip:端口号
的形式进行连接,云服务器就能收到浏览器发来的请求
/
。url当中的/
不能称之为我们云服务器上根目录,这个/
表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录。客户端发送请求給服务器,在服务器打印请求的基础上,服务器发送相应的相应給客户端。
httpserver.cc
#include
#include
#include
#include "protocol.hpp"
#include "httpserver.hpp"
using namespace std;
using namespace Server;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " port\r\n";
}
bool Get(const HttpRequest& req,HttpResponse& rep)
{
cout<<"-----------------http start-----------------------"<<endl;
cout<<req._inbuffer<<endl;
cout<<"method: "<<req._method<<endl;
cout<<"url: "<<req._url<<endl;
cout<<"httpversion: "<<req._httpversion<<endl;
cout<<"-----------------http end-------------------------"<<endl;
std::string respline = "HTTP/1.0 200 OK\r\n";//响应的状态行
std::string respheader = "Context-Type:txt/html\r\n";//响应报头中的响应正文属性
std::string respblank = "\r\n";//响应的空行
std::string body = "for test hello world
第二把都不起 翻了就被翻了呗 第三把都能拉满,再输连败奖励也能叠满。第二局赢了更是血赚。?
\r\n";//响应正文
//将响应的内容添加到HttpResponse类的对象的成员中
rep._outbuffer+=respline;
rep._outbuffer+=respheader;
rep._outbuffer+=respblank;
rep._outbuffer+=body;
cout<<"---------------http respone start---------------------"<<endl;
cout<<rep._outbuffer<<endl;//打印响应
cout<<"---------------http respone end---------------------"<<endl;
return true;
}
int main(int args, char *argv[])
{
if (args != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<httpserver> hs(new httpserver(Get, port));
hs->inithttpserver();
hs->start();
return 0;
}
httpserver.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "protocol.hpp"
#define NUM 1024
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using namespace std;
namespace Server
{
enum
{
USAGE_ERR = 1,
SOCK_ERR,
BIND_ERR,
LISTEN_ERR
};
class httpserver;
using func_t = function<bool(const HttpRequest &, HttpResponse &)>; // 重定义func_t
class httpserver
{
public:
httpserver(func_t func, const uint16_t &port = gport) : _port(port), _listensock(-1), _func(func) {}
void inithttpserver()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(SOCK_ERR);
}
// 2.bind ip和port
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定失败
{
exit(BIND_ERR);
}
// 3.将套接字设置为监听模式
if (listen(_listensock, gbacklog) < 0)
{
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
char buffer[4096];
HttpRequest req;
HttpResponse rep;
// 1.读取到完整报文
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 大概率能读到完整报文
if (n > 0)
{
buffer[n] = 0;
req._inbuffer = buffer;
// 2.req反序列化
req.parse();//拿客户端发送来的报文去构建req对象的成员变量
// 3.调用外部参数
_func(req,rep);//
// 4.rep序列化
// 5.server send data to client
send(sock,rep._outbuffer.c_str(),rep._outbuffer.size(),0);
}
}
void start()
{
while (true)
{
struct sockaddr_in cli;
socklen_t len = sizeof(cli);
bzero(&cli, len);
int sock = accept(_listensock, (struct sockaddr *)&cli, &len);
if (sock < 0)
{
continue;
}
cout << "accept sock: " << sock << endl;
// 多进程版---
pid_t id = fork(); // 创建子进程
if (id == 0) // 子进程进入
{
close(_listensock); // 子进程不需要用于监听因此关闭该文件描述符
if (fork() > 0)
exit(0);
// //孙子进程
HandlerHttp(sock); // 调用操作函数
close(sock);
exit(0);
}
// 父进程
close(sock); // 父进程不使用文件描述符就关闭
waitpid(id, nullptr, 0);
}
}
~httpserver() {}
private:
int _listensock; // 用于监听服务器的sock文件描述符
uint16_t _port; // 端口号
func_t _func;
};
}
protocol.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include"Until.hpp"
using namespace std;
const string SEP="\r\n";
const string default_path="./wwwroot";
const string home_page="index.html";
const string html_404="wwwroot/404.html";
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
void parse()
{
//1. 从inbuffer内拿到第一行数据
string line=Until::getOneline(_inbuffer,SEP);
if(line.empty()) return;
//2.从line中提取三个字段
stringstream ss(line);//
ss>>_method>>_url>>_httpversion;
}
public:
string _inbuffer;
string _method;
string _url;//
string _httpversion;//版本
string _path;//路径
string _suffix;
int _size;//文件内容的大小
};
class HttpResponse
{
public:
std::string _outbuffer;
};
\r\n
取到请求的请求行,然后以空格作为分隔符填充 _method(方法) 、 _url(url)、 _httpversion(版本)Until.hpp
#pragma once
#include
#include
#include
class Until
{
public:
static std::string getOneline(std::string& inbuffer,const std::string& sep)
{
auto pos=inbuffer.find(sep);
if(pos==std::string::npos)
{
return "";
}
std::string outbuffer=inbuffer.substr(0,pos);
return outbuffer;
}
static bool readFile(const std::string& in, char* out,int size)
{
std:: ifstream tmp(in,std::ios::binary);
if(!tmp.is_open())
return false;
tmp.read(out,size);
tmp.close();
return true;
}
};
scc、hpp、p 、u
httpserver.cc
#include
#include
#include
#include "protocol.hpp"
#include "httpserver.hpp"
using namespace std;
using namespace Server;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " port\r\n";
}
string suffixtodos(const string &src)
{
string ret = "Contect-Type: ";
if (src == ".html")
{
ret += "text/html";
}
else if (src == ".jpg")
{
ret += "application/x-jpg";
}
else if (src == ".png")
{
ret += "application/x-png";
}
ret += "\r\n";
return ret;
}
bool Get(const HttpRequest& req,HttpResponse& rep)
{
cout<<"-----------------http start-----------------------"<<endl;
cout<<req._inbuffer<<endl;
cout<<"method: "<<req._method<<endl;
cout<<"url: "<<req._url<<endl;
cout<<"httpversion: "<<req._httpversion<<endl;
cout<<"path: "<<req._path<<endl;
cout<<"suffix: "<<req._suffix<<endl;
cout<<"size: "<<req._size<<"字节"<<endl;
cout<<"-----------------http end-------------------------"<<endl;
std::string respline = "HTTP/1.0 200 OK\r\n";
std::string respheader = suffixtodos(req._suffix);
if(req._size>0)
{
respheader +="Context-Length";
respheader +=to_string(req._size);//如果这里給了Context-Length但是没有給实际的size浏览器默认行为是将路径的文件下载下来
//且在响应报头处没有給正文的长度,网页是无法加载的
respheader+="\r\n";
}
std::string respblank = "\r\n";
std::string body;
body.resize(req._size+1);
if(!Until::readFile(req._path,(char*)body.c_str(),req._size))
{
Until::readFile(html_404,(char*)body.c_str(),req._size);
//根据客户端发送来的path字段,对对于的路径读取文件,若所读取的路径不存在文件,那么就将html_404对应的文件填充到body字段中
}
rep._outbuffer+=respline;
rep._outbuffer+=respheader;
rep._outbuffer+=respblank;
rep._outbuffer+=body;
cout<<"---------------http respone start---------------------"<<endl;
cout<<rep._outbuffer<<endl;
cout<<"---------------http respone end---------------------"<<endl;
return true;
}
int main(int args, char *argv[])
{
if (args != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<httpserver> hs(new httpserver(Get, port));
hs->inithttpserver();
hs->start();
return 0;
}
响应行
,响应报头
,响应空行
,响应正文
形式。在响应报头中,包含了响应正文的类型和长度。注意的是,响应正文的长度定义在protocol.hpp
文件的HttpRequest类中,若在响应报头中填充了响应正文的长度,就需要获取实际正文的大小,否则浏览器会做出不一样的行为。例如浏览器会执行默认动作为下载文件。protocol.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include"Until.hpp"
using namespace std;
const string SEP="\r\n";
const string default_path="./wwwroot";
const string home_page="index.html";
const string html_404="wwwroot/404.html";
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
void parse()
{
//1. 从inbuffer内拿到第一行数据
string line=Until::getOneline(_inbuffer,SEP);
if(line.empty()) return;
//2.从line中提取三个字段
stringstream ss(line);//
ss>>_method>>_url>>_httpversion;
//3.添加默认路径
_path+=default_path;
_path+=_url;//如果客户端没有要求打开任何路径,那么该url为空
if(_path[_path.size()-1]=='/') _path+=home_page;//如果没选定路径默认进首页
//4.找到资源的后缀
auto pos =_path.find(".");
if(pos==std::string::npos)
{
_suffix=".html";//没找到默认文件后缀为.html
}else
{
_suffix=_path.substr(pos);
}
//5.获取文件内容的大小
struct stat st;
int n=stat(_path.c_str(),&st);
if(n==0)
{
_size=st.st_size;//定义了size就必须获取,否则浏览器默认行为是下载
}else
{
_size=-1;
}
}
public:
string _inbuffer;
string _method;
string _url;//
string _httpversion;//版本
string _path;//路径
string _suffix;
int _size=0;//文件内容的大小
};
class HttpResponse
{
public:
std::string _outbuffer;
};
wwwroot
的目录作为web根目录,由于url自动給web根目录后加\
,因此定义该目录时不需要加\
。home_page
),homepage是一个.html文件。
- stat函数
stat
函数是用于获取文件或目录的元数据信息的系统调用函数。通过提供文件或目录的路径,stat
函数将获取到的信息填充到struct stat
结构体中。函数原型
#include
#include #include int stat(const char *path, struct stat *buf);
-
path
:要获取信息的文件或目录的路径字符串。buf
:一个指向struct stat
结构体的指针,用于存储获取到的元数据信息。- 通常需要先定义一个stat结构体,然后通过stat函数传参要获取信息的文件路径的字符串和指向该结构体的指针,stat函数会将获取到的信息填充到stat指针指向的结构体中。
Until.hpp
#pragma once
#include
#include
#include
class Until
{
public:
static std::string getOneline(std::string& inbuffer,const std::string& sep)
{
auto pos=inbuffer.find(sep);
if(pos==std::string::npos)
{
return "";
}
std::string outbuffer=inbuffer.substr(0,pos);
return outbuffer;
}
static bool readFile(const std::string& in, char* out,int size)
{
std:: ifstream tmp(in,std::ios::binary);
if(!tmp.is_open())
return false;
tmp.read(out,size);
tmp.close();
return true;
}
};
httpserver.cc
中传入文件所在的路径,以二进制的方式将指定路径的指定文件读取到out参数中。index.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>
<img src="https://non1.oss-cn-guangzhou.aliyuncs.com/write1/202309152048396.png" alt="头像">
<a href="https://www.baidu.com/index.htm">百度一下a>
<a href="https://www.bilibili.com/">哔哩哔哩a>
<a href="https://www.csdn.net/">csdna>
<a href="/test/b.html">无畏契约启动a>
<a href="/test/a.html">原神启动a>
<form action="/a/b/c.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>
请求
User-Agent
是客户端的相关信息。Accept
是客户端可以接收的响应文件类型。Referer
是客户端在发送请求时,所处在的当前网页对应的web端的文件路径。路径前是ip和端口号响应
HTML 表单用于搜集不同类型的用户输入。
<form>
.
form elements
.
form>
key:value
的方式存在。例如
类型 | 描述 |
---|---|
text | 定义常规文本输入。 |
password | 定义密码输入。 |
submit | 定义提交按钮(提交表单)。 |
在网页中打开开发者工具查看,有一个表单用于输入账号和密码。账号的key是text,其对应的值为xname,该值由用户输入。密码的key为password,其对应的值为ypwd,该值由用户输入。登录按钮的key为submit,其对应的值为登陆,用户点击后提交表单。
在表单是上面可以看到形如的代码。其action为该表单提交到对应路径的的文件中,这里是
/a/b/c.py
method为提交的方法,这里用的是GET方法。在后期可以在表单提交对应的文件中取到表单进行操作。
我们在提交数据时,本质上前端会以form表单的形式提交,浏览器会将表单的内容转换为POST或GET方法作为请求发送給服务器。
HTTP常见的提交方法有:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINE | 断开连接关系 | 1.0 |
其中最常用的是GET方法和POST方法
GET方法
服务器ip:端口/提交表单获取的资源?表单提交的内容
。一是提交服务器的ip端口与提交表单对应的路径是以\
相连,说明网上的资源多数在Linux上部署。二是提交表单对应的路径与表单提交的内容是以?
相连。三是表单的内容之间是以&
相连。提交表单所获取的资源?表单提交的内容
方式存在。POST方法
现只把index.html中的提交方法改成POST
浏览器连接上后提交表单,可以看到:
提交表单获取的资源
的方式存在。因此可以知道POST方法并不会呈现表单内容給用户看,相比于GET方法多了一些隐蔽性。
GET方法和POST方法的性质
- GET方法通过url传递参数,POST方法通过请求正文传递参数。
- POST方法通过请求正文提交参数,一般情况下用户看不到,因此POST方法私密性比GET方法更好,但私密性不等同安全性。
- 无论GET方法还是POST方法都是不安全的,HTTP协议是以明文提交,注定协议本身是不安全的,相比之下安全性更高的协议是HTTPS。
- GET方法通过url传递参数,该参数注定不能太大。而POST方法通过请求正文提交,正文可以很大。因此若要上传图片、视频等通常要使用POST方法。
- GET方法的url:
资源路径?提交参数
。服务器会以?作为分隔符,拿着?右边的参数传递給?左边的资源路径对应的文件,进行相关操作。而POST方法的提交参数在请求正文中,本身就是于资源路径分离的。
HTTP常见的状态码有:
类别 | 原因短语 | |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作加以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的状态码有200(OK),404(Not Found),404(Forbidden),302(Redirect重定向),504(Bad Gateway)。
重定向的基本原理
重定向有临时重定向和永久重定向。其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的就是重定向后的网站。而如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。
重定向演示
httpserver.cc
bool Get(const HttpRequest& req,HttpResponse& rep)
{
cout<<"-----------------http start-----------------------"<<endl;
cout<<req._inbuffer<<endl;
cout<<"method: "<<req._method<<endl;
cout<<"url: "<<req._url<<endl;
cout<<"httpversion: "<<req._httpversion<<endl;
cout<<"path: "<<req._path<<endl;
cout<<"suffix: "<<req._suffix<<endl;
cout<<"size: "<<req._size<<"字节"<<endl;
cout<<"-----------------http end-------------------------"<<endl;
std::string respline = "HTTP/1.0 307 Temporary Redirect\r\n";//临时重定向307
std::string respheader = suffixtodos(req._suffix);
if(req._size>0)
{
respheader +="Context-Length";
respheader +=to_string(req._size);//如果这里給了Context-Length但是没有給实际的size浏览器默认行为是将路径的文件下载下来
//且在响应报头处没有給正文的长度,网页是无法加载的
respheader+="\r\n";
}
respheader += "Location: https://www.bilibili.com/\r\n";//重定向到的网址
std::string respblank = "\r\n";
std::string body;
body.resize(req._size+1);
if(!Until::readFile(req._path,(char*)body.c_str(),req._size))
{
Until::readFile(html_404,(char*)body.c_str(),req._size);
//根据客户端发送来的path字段,对对于的路径读取文件,若所读取的路径不存在文件,那么就将html_404对应的文件填充到body字段中
}
rep._outbuffer+=respline;
rep._outbuffer+=respheader;
rep._outbuffer+=respblank;
rep._outbuffer+=body;
cout<<"---------------http respone start---------------------"<<endl;
cout<<rep._outbuffer<<endl;
cout<<"---------------http respone end---------------------"<<endl;
return true;
}
std::string respline = "HTTP/1.0 307 Temporary Redirect\r\n";//临时重定向307
respheader += "Location: https://www.bilibili.com/\r\n";//重定向到的网址
在请求中默认连接方式是keep-alive
即默认是长连接。
长连接属性
- http网页有多种元素组成,意味着客户端需要向服务端发送多次请求以获取到足够多的响应,然后浏览器对网页进行组合和渲染才能得到一个完整的网页。而HTTP是基于TCP,那么多次发送HTTP请求就面临着TCP面对连接需要频繁创建连接问题,即需要多次进行三次握手来创建信道,四次挥手断开连接。如果没有长连接,每次请求就需要完成一次三次握手四次挥手。
- 如果有长连接,在一个TCP连接中就可以持续发送多份数据而不会断开连接,即请求可以复用这个信道。
- 但长连接也有缺陷,存在队头阻塞问题。如果仅仅使用一个连接,它需要发送请求,等待响应。之后才能发起下一个请求。在请求应答过程中,若出现状况,剩下的所有工作就会阻塞在这次请求响应中,即所谓的“队头阻塞”问题。它会阻碍网络传输和web页面渲染,直至失去响应。
HTTP协议本身是无状态的。例如浏览器向服务器发送了三次请求,当前请求无法得知上一次请求了什么,也无法得知下一次会请求到什么,即HTTP协议不做状态记录。若按照HTTP协议的无状态属性,我们在网页上登录账号后,退出当前网页或跳转到其他网页再次点击进入该网页时,浏览器需要向服务器发送请求,那么我们需要再次登录账号。事实是我们在网页登录账号,当退出当前网页后,再次点击该网页也不需要登录了,其原因在于浏览器帮助做了会话保持。
浏览器一是会对过去用户查看的视频、图片等做强制缓存处理,下次用户再查看时就从缓存中获取资源,不需要向服务器发送请求。二是浏览器会以某种方式记录用户的账号密码,使得用户再次进入网页时不需要进行登录操作。即HTTP本身是无状态的,但由于用户需要,需要HTTP周边会话保持。
而当我们需要访问网址的会员资源时,浏览器会向服务器发送请求,请求中包含用户的信息,服务器会对该信息进行身份认证,若满足会员状态,就将给予当前用户权限去访问会员资源。且用户登录上后,往后用户访问的是同一个网站,浏览器会自动推送历史保留信息。
浏览器将用户信息即用户账号和密码保存起来的技术称为cookie。
浏览器向服务器发送登录请求,服务器响应save文件,浏览器将将用户信息保存到本地形成Cookie文件。往后浏览器向服务器发送请求都需要携带用户信息,每次服务器都需要进行登录操作,才能响应资源。
- 用户信息保存在服务器上而不是本地,用户信息的保存安全问题交给了服务器,即互联网产品厂商,让他们去保护用户信息,用户信息的安全性大大加强了。
- 每次请求资源携带的是session id而不是用户信息,避免被非法分子拦截获取用户信息。
- 非法分子拿到session id,也需要向服务器发送请求才能获取资源,服务器在鉴别session id时会判断当前用户的ip地址等等,若当前账号处于非法状态,服务器立即对用户账号进行封号保护。
当然我们也可以自己设置cookie。在httpserver.cc的Get函数中,在响应报头中携带cookie属性。
httpserver.cc
//......
bool Get(const HttpRequest& req,HttpResponse& rep)
{
//......
respheader+="Set-Cookie: name=1234567abcdefg; Max-Age=120\r\n";
//......
}