Http协议和解析实战
Posted 霜落梅寒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Http协议和解析实战相关的知识,希望对你有一定的参考价值。
一、浏览器的B-S架构和C-S架构
1、C-S架构:客户机-服务器,简单点就是需要下载的软件,相关资源(图片、视频等会比较流畅),但是也缺少通用性(各种手机兼容),系统维护性,升级需要重新设计和开发,增加了维护和管理的难度。
2、B-S架构:浏览器和服务器架构模式,WEB浏览器是客户端最主要的应用软件,将系统功能实现的核心部分集中到服务器上,简化了系统的开发。维护和使用,但是会依赖网络环境,UI动画等需要网络加载快,使用才不会卡顿。(企业一般优先开发WEB端)
3、什么是URL(统一资源定位符,获取服务器资源的一种)
(1)标准格式:协议://服务器IP:端口/路径1/路径N?key1=value1&key2=value2
例如:
- https://baidu.com/#/index (域名去DNS服务器解析成ip)
- https://baidu.com/s?wd=5
- http 默认端口 80
- https 默认端口443 (加了s更安全)
① 协议:不同的协议有不同的解析方式
②服务器ip:网络中存在无数的主机,要访问哪一台,通过公网ip区分 (我们常用是区域网,不使用公网ip)
③端口:一台主机上运行着很多的进程,为了区分不同进程,一个端口对应一个进程,http默认的端口是80 (可以理解为这栋楼的层号)
④路径:资源N多种,为了更进一步区分资源所在路径(后端接口,一般称为“接口路径”,“接口”)【可以理解为这层楼的几号房】
二、http超文本传输协议
1、协议
- 协议是一种约定,规定好一种信息格式,如果发送方按照这种请求格式发送信息,那么接收端就要按照这样的格式解析数据,这就是协议。
- json协议
"name":"jack", "age":23
- xml协议
<body> <name>jack</name> <age>234</age> </body>
-
http超文本协议 (类似将写的内容打包传输)
2、什么是http协议
-
-
即超⽂本传送协议(Hypertext Transfer Protocol ),是Web联⽹的基础,也是⼿机PC联⽹常⽤的协议之⼀,HTTP协议是建⽴在TCP协议之上的⼀种应⽤
-
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,从建⽴连接到关闭连接的过程称为“⼀次连接”
-
HTTP请求-HTTP响应
-
响应码:
- 1xx:信息
- 2xx:成功 200 OK,请求正常
- 3xx:重定向 (一般会再给一个地址)
- 4xx:客户端错误 404 Not Found 服务器⽆法找到被请求的⻚⾯ (一般是客户端的问题,前端)
- 5xx:服务器错误 503 Service Unavailable,服务器挂了或者不可用 (一般是服务端的问题,后端)
-
3、发展历史
-
- http0.9-》http1.0-》http1.1-》http2.0
- 不多优化协议,增加更多功能
4、和https的关系
-
- Hyper Text Transfer Protocol over SecureSocket Layer
- 主要由两部分组成:HTTP + SSL / TLS (加固的,更加安全)
- 比 HTTP 协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性,增加破解成本
- 缺点:相同网络环境下,HTTPS 协议会使页面的加载时间延长近 50%,增加额外的计算资源消耗,增加 10%到 20%的耗电等;不过利大于弊,所以Https是趋势,相关资源损耗也在持续下降
- 如果做软件压测:直接压测内网ip,通过压测公网域名,不管是http还是https,都会带来额外的损耗导致结果不准确 (局域网可以无限扩增,公网有带宽的影响)
三、超文本传输协议Http消息体拆分
1、Http请求消息结构
-
请求行
- 请求方法
- URL地址
- 协议名
-
请求头
- 报文头包含若干个属性 格式为“属性名:属性值”,
- 服务端据此获取客户端的基本信息
-
请求体
- 请求的参数,可以是json对象,也可以是前端表单生成的key=value&key=value的字符
2、Http响应消息结构
-
-
响应行
- 报文协议及版本、状态码
-
响应头
- 报文头包含若干个属性 格式为“属性名:属性值”
-
响应正文
- 响应报文体,我们需要的内容,多种形式比如html、json、图片、视频文件等
-
四、HTTP的九种请求方法和响应码
1、浏览器请求方法
-
-
http1.0定义了三种:
- GET: 向服务器获取资源,比如常见的查询请求
- POST: 向服务器提交数据而发送的请求 (登录、提交表单) 【细分成put、patch、delete】
- Head: 和get类似,返回的响应中没有具体的内容,用于获取报头
-
http1.1定义了六种
- PUT:一般是用于更新请求,比如更新个人信息、商品信息全量更新
- PATCH:PUT 方法的补充,更新指定资源的部分数据
- DELETE:用于删除指定的资源
- OPTIONS: 获取服务器支持的HTTP请求方法,服务器性能、跨域检查等
- CONNECT: 方法的作用就是把服务器作为跳板,让服务器代替用户去访问其它网页,之后把数据原原本本的返回给用户,网页开发基本不用这个方法,如果是http代理就会使用这个,让服务器代理用户去访问其他网页,类似中介
- TRACE:回显服务器收到的请求,主要用于测试或诊断
-
2、Http响应码
-
- 浏览器向服务器请求时,服务端响应的消息头里面有状态码,表示请求结果的状态
-
-
分类
-
1XX: 收到请求,需要请求者继续执行操作,比较少用
-
2XX: 请求成功,常用的 200
-
3XX: 重定向,浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取;
-
好处:网站改版、域名迁移等,多个域名指向同个主站导流
-
必须记住
- 301:永久性跳转,比如域名过期,换个域名
- 302:临时性跳转(例如:百度调用的会使用会重定位到百度安全验证里面)
-
-
4XX: 客户端出错,请求包含语法错误或者无法完成请求 (前端问题,客户端)
-
必须记住
- 400: 请求出错,比如语法协议
- 403: 没权限访问
- 404: 找不到这个路径对应的接口或者文件
- 405: 不允许此方法进行提交,Method not allowed,比如接口一定要POST方式,而你是用了GET
-
-
5XX: 服务端出错,服务器在处理请求的过程中发生了错误(后端问题,服务端,一般要看服务端的日志)
-
必须记住
- 500: 服务器内部报错了,完成不了这次请求
- 503: 服务器宕机
-
-
-
五、 Http请求头
-
http请求分为三部分:请求行,请求头, 请求体
-
请求头
- 报文头包含若干个属性 格式为“属性名:属性值”,
- 服务端据此获取客户端的基本信息
-
常见的请求头
-
Accept: 览器支持的 MIME 媒体类型, 比如 text/html,application/json,image/webp,/ 等(告诉服务端所能接收的类型 (大类)/(小类) )
-
Accept-Encoding: 浏览器发给服务器,声明浏览器支持的编码类型,gzip, deflate
-
Accept-Language: 客户端接受的语言格式,比如 zh-CN
-
Connection: keep-alive , 开启HTTP持久连接 (长链接)
-
Host:服务器的域名
-
Origin:告诉服务器请求从哪里发起的,仅包括协议和域名 CORS跨域请求中可以看到response有对应的header,Access-Control-Allow-Origin
-
Referer:告诉服务器请求的原始资源的URI,其用于所有类型的请求,并且包括:协议+域名+查询参数; 很多抢购服务会用这个做限制,必须通过某个入来进来才有效(重定向:来源)
-
User-Agent: 服务器通过这个请求头判断用户的软件的应用类型、操作系统、软件开发商以及版本号、浏览器内核信息等; 风控系统、反作弊系统、反爬虫系统等基本会采集这类信息做参考
-
Cookie: 表示服务端给客户端传的http请求状态,也是多个key=value形式组合,比如登录后的令牌等(后续请求会把Cookie带过去,服务端就会看http这个请求头的Cookie是否是正确的,是的话才给访问对应的资源)
-
Content-Type: HTTP请求提交的内容类型,post提交时才需要设置,比如文件上传,表单提交、json等(告诉服务端提交的类型)
- form表单提交:application/x-www-form-urlencoded
- json方式提交:application/json
-
六、 Http响应头
-
响应头
- 报文头包含若干个属性 格式为“属性名:属性值”
-
常见的响应头
- Allow: 服务器支持哪些请求方法
- Content-Length: 响应体的字节长度
- Content-Type: 响应体的MIME类型(看请求和响应的类型是否一致)
- Content-Encoding: 设置数据使用的编码类型
- Date: 设置消息发送的日期和时间
- Expires: 设置响应体的过期时间,一个GMT时间,表示该缓存的有效时间 (png等,一般作用就是节省带宽)
- cache-control: Expires的作用一致,都是指明当前资源的有效期, 控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据,优先级高于Expires,控制粒度更细,如max-age=240,即4分钟
- Location:表示客户应当到哪里去获取资源,一般同时设置状态代码为3xx(重定向的位置)
- Server: 服务器名称
- Transfer-Encoding:chunked 表示输出的内容长度不能确定,静态网页一般没,基本出现在动态网页里面
- Access-Control-Allow-Origin: 定哪些站点可以参与跨站资源共享(前后端的协议 域名 端口一致才能去解析。如果任意一个不同就是跨域,浏览器就不去解析,这个就可以告诉允许哪些站点能跨域名共享的)
七、 Http常见请求/响应头content-type内容类型
-
Content-type: 用来指定不同格式的请求响应信息,俗称 MIME媒体类型
-
常见的取值
- text/html :HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
- application/json:JSON数据格式
- application/pdf :pdf格式(预览)
- application/octet-stream :二进制流数据,一般是文件下载
- application/x-www-form-urlencoded:form表单默认的提交数据的格式,会编码成key=value格式
- multipart/form-data: 表单中需要上传文件的文件格式类型(文件上传得用这个,后端才能识别)
实战项目自主web服务器
文章目录
项目简介
什么是web服务器: 通过HTTP协议构建一个web服务器,该服务器能够处理浏览器发过来的http请求,并根据http请求返回相应的http相应给浏览器。相当于搭建个人网站,你可以在个人网站上存放各种资源,别人可以通过浏览器访问你的网站,并获取资源。
背景: 该项目主要用http协议,在网络中,http协议被广泛使用如移动端,pc端浏览器。http协议是打开互联网应用窗口的重要协议,它在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
目标: 该项目的目标是,对http协议的理论有更深刻的理解,从零开始完成web服务器开发,连接下三层协议,从技术到应用,让网络难点无处遁形。
简述: 采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,做完该项目,你可以从技术上完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节。
技术特点: 该项目是一个后端开发项目,开发环境为centos 7 + vim/gcc/gdb/VS Code,开发语言为C/C++,应用网络编程(TCP/IP协议, socket流式套接字,http协议),多线程技术,cgi技术,线程池等技术。
认识http协议
http分层概览
与http相关的重要协议有TCP,IP,DNS
这里介绍一下DNS
DNS(域名系统)是将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。
协议之间是如何协同运作的呢?
http背景知识补充
目前主流服务器使用的是http/1.1版本,该项目是按照http/1.0版本来完成讲解,同时,我们还会对比1.1和1.0的区别。
http协议的特点如下
- 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
- 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
- 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)。
- 无状态(HTTP协议自身不具有保存之前发送过的请求或响应的功能)
注意: http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性。
URI & URL & URN
我们想访问web上资源需要用资源标志符(URI)进行定位,和URI相关的还有URL,URN。
- URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。
- URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
- URN,uniform resource name,统一资源命名,是通过名字来标识资源。
URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI,URL是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是URL 。
HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:
http://host[":"port][abs_path]
- http表示要通过HTTP协议来定位网络资源
- host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
- port指定一个端口号,为空则使用缺省端口80。(通常会根据协议采用默认端口号)
- abs_path指定请求资源的URI
- 如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。(如果没有abs_path,会默认访问该服务端首页)。
例:
一个较为完整的http请求:
http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1
构建tcp服务器
我们这里的tcp服务器是对网络套接字创建,绑定,监听等进行封装,方便之后的使用。
这里我们把 tcp server 类设计为单例模式,让程序访问到的 tcp server 是同一个。代码如下
#pragma once
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#define PORT 8081
#define BACKLOG 5
class TcpServer
private:
int port;// 端口号
int listen_sock;// 套接字
static TcpServer *svr;
private:
TcpServer(int _port = PORT):port(_port),listen_sock(-1)
TcpServer(const TcpServer &s)
public:
static TcpServer *getinstance(int port)
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 线程互斥锁,防止多线程时产生竞争
if(nullptr == svr)
pthread_mutex_lock(&lock);
if(nullptr == svr)
svr = new TcpServer(port);// 创建tcpserver
svr -> InitServer();// 初始化
pthread_mutex_unlock(&lock);
return svr;
void InitServer()// 初始化
Socket();
Bind();
Listen();
void Socket()
listen_sock = socket(AF_INET,SOCK_STREAM,0);// 创建套接字
if(listen_sock < 0)
exit(1);
int opt = 1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));// 设置套接字选项,允许地址复用
void 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;// 云服务器不能直接绑定公网IP
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)// 绑定失败
exit(2);
void Listen()
if(listen(listen_sock,BACKLOG)<0)
exit(3);
~TcpServer()
;
TcpServer* TcpServer::svr = nullptr;
关于setsockopt函数可以参考这篇博客setsockopt()函数功能介绍
HTTP请求和响应
请求和响应过程
浏览器请求服务器资源需要给服务器发送http请求,服务器收到请求需要返回http响应给浏览器,我们需要详细了解请求和响应的格式,为之后编码做准备。
HTTP请求报文
HTTP请求报文分为 请求行,请求报头,空行,请求正文
- 请求行: 请求方法,请求URL,HTTP协议和版本
- 请求报头: 请求属性,每一行表示一个属性,遇到空行表示请求报头结束。
- 空行:用于分割请求报头和请求正文
- 请求正文:如果存在正文,请求报头中就需要Content-Length属性来标识正文的大小
HTTP响应报文
HTTP响应报文分为 响应行,响应报头,空行,响应正文
- 响应行:http协议和版,状态码,状态描述符
- 响应报头:响应属性,每一行表示一个属性,遇到空行表示响应报头结束。
- 空行:分割响应报头和响应正文
- 响应正文:如果存在正文,响应报头中就需要Content-Length属性来标识正文的大小
工具类
我们在编写协议时难免会有一些程序经常用到,如:读取报头中的一行。我们可以设计一个工具类,将这些程序存放其中。
ReadLine()函数
ReadLine()函数的作用是读取sock套接字中的一行数据。无论是请求报头还是响应报头,它们的属性都是按行进行划分的。但是不同的浏览器发送过来的http请求,它们中的行分割符是不同的,大致分为如下三类
- xxxxx \\r\\n
- xxxxx \\n
- xxxxx \\r
我们要设计一个算法,兼容各种行分割符。
读取数据,如果读取到 \\n 表示改行已结束,如果读取到 \\r ,需要判断下一个字符是否为 \\n 。不是,那么该行已结束,否则需要将 \\r\\n 转变为 \\n。
这里我们需要用到 recv 函数
recv函数
功能: 接收已连接的数据报或流式套接口的数据。
函数原型:
int recv( int sockfd, void *buf, size_t len,int flags);
参数说明:
- sockfd :发送数据的套接字
- buf :存放recv函数接收到的数据
- len :buf的长度
- flags :recv发送数据的选项,一般设置为0
返回值: 成功返回0,错误返回-1
编码
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
//工具类
class Util
public:
static int ReadLine(int sock,std::string &out)
char ch = 'x';
while(ch != '\\n')
ssize_t s = recv(sock,&ch,1,0);// 从sock中读取一个字符到ch中
if(s > 0)
if(ch == '\\r')
recv(sock,&ch,1,MSG_PEEK);// MSG_PEEK 窥探下一个字符(不从sock中取走)
if(ch == '\\n')
// 把\\r\\n -> \\n
// 窥探成功读取该字符
recv(sock,&ch,1,0);
else
ch = '\\n';// \\r -> \\n
// 到这有两种情况 1.普通字符 2.\\n
out.push_back(ch);
else if(s == 0)
return 0;
else
return -1;
return out.size();
;
CutString()函数
CutString()函数的功能是将字符串按特定字符切分成左右两部分,代码如下
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
//工具类
class Util
public:
static int ReadLine(int sock,std::string &out)
...
static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
size_t pos = target.find(sep);// 查找分割符
if(pos != std::string::npos)
sub1_out = target.substr(0,pos);
sub2_out = target.substr(pos+sep.size());
return true;
return false;
// 传入
// target = Content-Length : 18
// sep = " : "
// 传出
// sub1_out = Content-Length
// sub2_out = sep
;
日志信息
为了方便代码解析和调试,我们需要设计函数来记录日志信息,日志信息的内容如下
乍一看传入的参数是不是有点多,实际上我们只需传入 日志级别 和 日志信息 这两个参数,剩下的参数我们可以让操作系统去查找,为此可以设置宏,通过宏替换来获取 日志文件 和 代码行数 。
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(level,message) Log(#level,message,__FILE__,__LINE__)
void Log(std::string level,std::string message,std::string file_name,int line)
std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file_name << "]" << "[" << line << "]" << std::endl;
请求和响应类的设计
http服务器收到一个请求时,服务器需要做如下工作:读取请求,分析请求,构建响应,发送响应。为了更好的管理请求报头和响应报头,我们需要设计一个类来存放请求报头和响应报头。
http请求类
class HttpRequest
public:
std::string request_line;// 请求行
std::vector<std::string> request_header;// 请求报头
std::string blank;// 请求空行
std::string request_body;// 请求正文
// 请求行的解析
std::string method;
std::string uri;
std::string version;
// 请求报头解析
std::unordered_map<std::string, std::string> header_kv;// 属性名:属性信息
int content_length;// 请求正文长度
// URI解析
std::string suffix;// 请求文件名的后缀
std::string path;// URI路径
std::string query_string;// URI参数
bool cgi;// cgi处理标志符
int size;// 打开的文件大小
public:
HttpRequest():content_length(0),cgi(false)
~HttpRequest()
;
http响应类
#define LINE_END "\\r\\n"
class HttpResponse
public:
std::string status_line;// 响应行
std::vector<std::string> response_header;// 响应行
std::string blank;// 响应空行
std::string response_body;// 响应正文
int status_code;// 响应状态码
int fd;// 暂时保存文件描述符
public:
HttpResponse():blank(LINE_END),status_code(OK),fd(-1)
~HttpResponse()
;
请求处理
请求报文
处理请求报头时,先将报头的数据读取出来,再对其进行处理
void RecvHttpRequest()
// 读取请求行和报头
if((!RecvHttpRequestLine()) && (!RecvHttpRequestHeader()))
ParseHttpRequestLine();// 解析请求行
ParseHttpRequestHeader();// 解析请求报头
RecvHttpRequestBody();// 读取请求正文
读取请求行
利用工具类中的ReadLine()函数将请求行读入到请求类中
bool RecvHttpRequestLine()
auto& line = http_request.request_line;
if(Util::ReadLine(sock,line) > 0)
line.resize(line.size()-1);// 去除行尾 \\n
LOG(INFO,http_request.request_line);// 记录日志
else
stop = true;// 标记读取失败
return stop;
读取请求报头
按行为单位,将读取到的报头属性插入到请求报头中。
bool RecvHttpRequestHeader()
std::string line;
while(line!="\\n")
line.clear();// 读取前清空line
if(Util::ReadLine(sock,line) <= 0)// 按行读取
stop = true;
break;
if(line == "\\n")// 读取到空行,请求报头已读完
http_request.blank = line;
break;
line.resize(line.size()-1);// 去除最后的'\\n'
http_request.request_header.push_back(line);// 将读取到的报头属性尾插入请求报头中
LOG(INFO,line);
return stop;
解析请求行
请求行由 请求方法,请求URI,HTTP协议版本 构成,它们中间由空格隔开,如下图
解析请求行就是将字符串按空格分割。
将字符串按空格分割我们可以调用库函数中的stringstream
stringstream使用示例:
#include<iostream>
#include<string>
#include<sstream>
int main()
std::string msg = "GET /a/b/c.html http/1.0";
std::string method;
std::string uri;
std::string version;
std::stringstream ss(msg);
ss >> method >> uri >> version;
std::cout << method << std::endl;
std::cout << uri << std::endl;
std::cout << version << std::endl;
运行结果
解析请求行代码
void ParseHttpRequestLine()
auto &line = http_request.request_line;// 获取请求行
std::stringstream ss(line);
ss >> http_request.method >> http_request.uri >> http_request.version;
auto &method = http_request.method;// 获取请求行中的请求方法
std::transform(method.begin(),method.end(),method.begin(),::toupper);《Wireshark数据包分析实战》(三)地址解析协议(ARP)
Raft协议实战之Redis Sentinel的选举Leader源码解析