目录
前言
1.HTTP协议
2.认识URL
3.urlencode和urldecode
4.HTTP的请求和响应格式
5.编码验证
6.HTTP的方法
7.HTTP的状态码
8.HTTP常见Header
总结
之前基于传输层协议UDP和TCP我们在应用层实现了数据传输,并且使用UDP协议实现了一个翻译服务器,使用TCP协议实现了一个网络版本计算器,因为是使用TCP协议,所以在实现网络版本计算器的时候定制了协议,实现了序列化和反序列化的工作,而今天要为大家介绍的HTTP协议是基于传输层TCP协议,HTPP协议是已经有人在应用层实现的协议,主要用途是在web端,可以明确的一点是HTTP协议是基于传输层TCP协议实现的,那必然要解决协议定制和实现序列化和反序列化的工作,那么下面我们就一起来具体看看HTTP协议是如何实现的。
HTTP协议是在应用层人们早已制定好了的成熟协议,主要用途是使用HTTP协议客户端可以向服务端请求一些资源,包含文本,音频,图片等资源,客户端发起请求,服务端根据客户端的请求向服务端返回资源,所以HTTP协议被称为是超文本传输协议。
平时我们俗称的 "网址" 其实就是说的 URL,使用HTTP协议,客户端向服务端发起请求是以URL的形式进行请求。
如图所示:
一般URL中会包含服务器地址,服务器端口号,以及请求资源的路径,这里的路径本质上是Linux中存放资源的位置。
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例:urlencode
"+" 被转义成了 "%2B"
urldecode就是urlencode的逆过程;
HTTP请求和响应宏观结构如图所示:
HTTP请求包含四部分:请求行,请求报头,空行,请求正文
HTTP响应包含四部分:状态行,响应报头,空行,响应正文
既然HTTP协议是基于TCP协议实现的,那么必然要解决定制协议和序列化和反序列化的过程,首先定制协议是为了在应用层完整的读取到一个报文,按照HTTP的请求宏观结构:
a.以\r\n结尾,可以完整的读取到一行
b.以空行作为结尾,可以循环读取,知道空行,可以将所有的请求行和请求报头读完
c.如何保证把正文读完,报头有一个属性Content-Length:XXX正文的长度
d.解析出来内容长度,再根据内容长度,读取正文即可
请求和响应实现序列化和反序列化
HTTP自己实现的,将请求行和请求报头按照"\r\n",将一个个的字符串全部拼接起来就可以了
说明:使用HTTP协议只需要实现一个服务端就可以了,客户端可以使用浏览器充当,客户端是默认支持HTTP协议的。
服务器实现:
因为是验证HTTP请求报头格式,所以我们的实现思路是,服务端和客户端建立连接成功后,此时只是为了测试,所以就不再进行定制协议,序列化和反序列化的过程,而是直接默认大概率可以读取到一个完整的报文,直接将读取上来的报文放到读取缓冲区中,然后采用回调的方式处理请求,为了测试,直接将请求格式打印出来,然后观察验证是否和前面所说的http请求宏观格式对应
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Protocol.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = std::function;
class HttpServer
{
public:
HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 创建socket文件套接字对象
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(SOCKET_ERR);
}
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// 3. 设置socket 为监听状态
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
HttpRequest req;
HttpResponse resp;
size_t n = recv(sock, buffer, sizeof(buffer)-1, 0); // 大概率我们直接就能读取到完整的http请求
if(n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
void start()
{
for (;;)
{
// 4. server 获取新链接
// sock, 和client进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if(fork()>0) exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
waitpid(id, nullptr, 0);
}
}
~HttpServer() {}
private:
int _listensock;
uint16_t _port;
func_t _func;
};
} // namespace server
运行截图:
如上图所示,可以观察到HTTP的请求格式与我们上面所说的是相符的,首行是请求行,接下来是请求报头,是冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束,然后是空行,然后是请求正文,因为是测试,所以没有请求正文的信息,接下来我们加上请求正文在进行观察。
说明:客户端向服务端发起请求的时候是需要获取资源的,所以需要客户端在访问的时候指明路径,当服务端收到客户端的请求后,对请求资源路径做提取,服务端上默认是存在一个根目录wwwroot,先给服务器访问资源的路径拼接上wwwroot,然后再追加url,然后根据这个路径访问Linux上的资源,如果是合法请求,并且该资源存在就客户端返回这个资源,如果该资源不存在,wwwroot路径下一定存在一个资源就是404.html,将该资源返回给客户端,告诉客户端你访问的资源不存在,如果是客户端直接访问根目录(/),服务器收到请求之后,wwwroot路径拼接好之后,后面再会追加一个wwwroot目录下一定存在的一个文件index.html,将该资源返回给服务端,下面我们来一起验证以下:
代码编写:
protocol.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot"; //默认根目录
const std::string home_page = "index.html"; //默认根目录首页
const std::string html_404 = "404.html";
class HttpRequest
{
public:
HttpRequest() {}
~HttpRequest() {}
public:
//对字符串进行解析
void parse()
{
// 1. 从inbuffer中拿到第一行,分隔符\r\n
std::string line = Util::getOneLine(inbuffer, sep);
if(line.empty()) return;
// 2. 从请求行中提取三个字段
//stringstream:默认以空格将字符串的内容写到里面
std::stringstream ss(line);
ss >> method >> url >> httpversion;
// 3. 添加web默认路径
path = default_root; // ./wwwroot,
path += url; //./wwwroot/a/b/c.html, ./wwwroot/
//以/结尾,添加index.html
if(path[path.size()-1] == '/') path += home_page;
auto pos = path.rfind(".");
if (pos == std::string::npos)
suffix = ".html";
else
suffix = path.substr(pos);
// 5. 得到资源的大小
struct stat st;
int n = stat(path.c_str(), &st);
if(n == 0) size = st.st_size;
else size = -1;
}
public:
std::string inbuffer;
std::string method;
std::string url;
std::string httpversion;
std::string path;
std::string suffix;
size_t size;
std::string parm;
};
class HttpResponse
{
public:
HttpResponse() {}
~HttpResponse() {}
public:
std::string outbuffer;
};
httpServer.cc:
#include "httpServer.hpp"
#include
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
std::string suffixToDesc(const std::string suffix)
{
std::string ct = "Content-Type: ";
if (suffix == ".html")
ct += "text/html";
else if (suffix == ".jpg")
ct += "application/x-jpg;image/jpeg";
ct += "\r\n";
return ct;
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
//客户端向服务端的请求格式:
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer << std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
std::cout << "suffix: " << req.suffix << std::endl;
std::cout << "size: " << req.size << "字节" << std::endl;
cout << "----------------------http end---------------------------" << endl;
//服务端给客户端返回的请求格式
std::string respline = "HTTP/1.1 200 OK\r\n";
std::string respheader = suffixToDesc(req.suffix);
if (req.size > 0)
{
respheader += "Content-Length: ";
respheader += std::to_string(req.size);
respheader += "\r\n";
}
std::string respblank = "\r\n";
std::string body;
//在wwwroot路径下查找是否存在请求的资源,存在则将该资源返回
if (!Util::readFile(req.path, &body))
{
//不存在就返回404.html
Util::readFile(html_404, &body); // 一定能成功
}
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
return true;
}
// ./httpServer 8080
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr httpsvr(new HttpServer(Get, port));
httpsvr->initServer();
httpsvr->start();
return 0;
}
说明:客户端向服务端发起请求的时候,请求行第一个字段就是请求方法,请求方法不同代表着不同的含义,如下图所示:
一般常用的就是GET和POST方法
区别:
GET通过url传递参数,具体:http//ip:port/XXX/YY?name1=value1&name2=value2
POST提交参数通过http请求的正文提交参数
POST方法通过正文提交参数,所以一般用户看不到,私密性更好,GET方法不私秘
无论是GET和POST方法,都不安全,要谈安全,需要使用https协议
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
如图所示:
Connection: keep-alive:标识长连接
通过浏览器向服务端请求资源,服务端返回资源,在请求的时候客户端可能同时请求多个资源,即进行多次http请求,但是http是基于tcp协议实现,而tcp是面向连接的,所以就可能会存在频繁创建连接的问题,为了解决这个问题,服务端和客户端都支持长连接,当获取一大份资源的时候,通过一条连接完成。
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
会话保持严格意义上不是http天然具备的,是后面使用发现需要的,因为http协议是无状态的。
如何理解http协议是无状态的,举个简单的例子,当我们访问bilibili服务端的时候,客户端可能需要登录,当登录完成之后,退出客户端之后,再次登录客户端,可以发现用户的登录信息任然是存在的,对于http协议来说,每次在访问的时候都应该重新发起请求,即重新登录,但是对于用户来说查看新的网页是常规操作,如果发生网页跳转,那么新的页面也就无法识别是哪一个用户了,为了让用户一经登录,可以在整个网站按照自己的身份进行随意访问,就需要解决http无状态的问题了
解决方式:
如图所示:
存在的问题:
可能会有黑客在你的服务上部署一些病毒,然后将你的用户名和密码获取到,此时黑客就可以拿着你的用户名和密码冒充你从事一些违法活动。
改进:
当浏览器向服务端进行登录之后,服务端会形成一个session文件,该文件中保存用的用户名和密码等认证信息,浏览器在cookie文件中保存sessionid,当再次请求的时候,请求报头中会携带sessionid,然后服务端进行认证,采用这种方案就基本上可以解决用户名和密码泄漏的问题,但是有小伙伴可能会问,黑客既然用户名和密码获取不了,但是可以获取sessionid,依旧可以冒充身份从事非法活动,对于这个问题的解决方式采取了相应的策略,其中包含会对sessionid的ip地址识别,当发现sessionid的ip地址和之前的不一样了,会将当前sessionid失效,通过这种相关策略就可以解决sessionid被别人获取冒充身份了。
以上就是关于HTTP我们要介绍的所有内容了,包含什么是HTTP,如何使用HTTP请求资源,理解了HTTP的相关字段,以及获取网页资源是如何实现的等相关话题,感谢大家的阅读,我们下次再见。