Linux:带你理解应用层的HTTP协议

http协议

    • 应用层
    • 自定制协议
        • 网络版计算器
    • HTTP协议
        • 网址
        • url编码、解码
      • HTTP协议格式
      • 编写一个简单的http服务器
    • HTTPS协议
        • 对称加密算法
        • 非对称加密算法
      • 将非对称加密和对称加密结合起来加密
    • 知识点习题


应用层

负责应用程序之间的数据沟通(我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层)

自定制协议

结构化数据的传输(通过结构体在内存中对数据对象进行组织,将二进制数据传输出去)

使用结构体进行数据对象的二进制结构化组织,进行数据传输/可持久化数据存储

  • 序列化:将数据对象按照指定协议进行组织成可持久化存储/数据传输的二进制数据串
  • 反序列化:将持久化存储/数据传输的二进制数据串按照指定协议解析出各个数据对象

网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

约定方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 “序列化” 和 “反序列化”
// proto.h 定义通信的结构体 
typedef struct Request {
	int a;
  	int b;
} Request;

结构化数据传输好处:

  • 用户感受不到数据对象按照指定协议组织以及解析的过程
  • 并且通过结构成员变量指向内存各处的方式,可以更加快捷的获取到数据对象,反序列化非常快

二进制序列化优点:反序列化解析速度快

常见的数据序列化方式还有很多:json序列化;protobuf序列化

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的. 这种约定, 就是应用层协议

HTTP协议

HTTP协议:超文本传输协议

优点:http协议给程序员留有一定的自定制空间

网址

统一资源定位符(定位网络中唯一的一份资源)- URL

统一资源定位符如何定位网络中唯一的资源? - URL的格式以及所包含的要素

网址的元素:

协议方案名称://用户名:密码@服务器IP地址:端口/资源路径?查询字符串#片段标识符
Linux:带你理解应用层的HTTP协议_第1张图片

  • 协议方案名称:http / https / ftp
  • 服务器IP地址:域名 - 服务器地址解析的一种方式 - 以更容易记忆的字符串来表示服务器地址 - 最终还是要被解析得到服务器的IP地址
  • 服务器端口:http默认使用80端口/https默认使用443端口
  • /资源路径:请求的资源在服务器上的路径 - 这个路径是一个相对根目录 - 为了告诉服务器自己想要什么
  • 查询字符串:一个个key = val 形式的键值对,键值对之间以&间隔; - 提交给服务器的数据

url编码、解码

用户提交给服务器的查询字符串中的val需要进行url编码 — 因为url中有很多的特殊字符具有特殊含义,若用户提交的数据中也包含有相同的特殊字符就会造成歧义,因此需要对val进行url编码操作

  • url编码(urlencode)

将特殊字符的每一个字节,都转换成为16进制数字的字符,例如: + -> 2b ;万一要是用户本身提交的数据就有2b,也会造成歧义;因此对每一个字节进行转换之后,需要在前边加上%表示紧跟其后的两个字符经过了url编码 + -> %2b

  • url解码(urldecode)

得到查询字符串后,在val中遇到%,则认为紧跟其后的两个字符需要解码 - 将两个字符转换为数字 %2b -> 2 11;

第一个数字左移4位或者第一个数字乘以16+后边的数字 :2*16 + 11

HTTP协议格式

http协议的实现(http协议的数据结构)

fiddler工具:浏览器的抓包工具,抓取浏览器与服务器之间的通信数据

Linux:带你理解应用层的HTTP协议_第2张图片

  • 首行:
  1. 请求首行:

包含三大信息,以空格进行间隔,并且以\r\n作为结尾; 请求方法 URL 协议版本

请求方法:

HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。
HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

  • GET:请求获取一个资源,并要求服务器返回实体数据
  • HEAD:请求获取一个资源,但是并不要求服务器返回实体数据,只要相应头部信息
  • POST:向服务器提供表单数据

GET/POST:get也能够向服务器提交数据,但是提交的数据是在url的查询字符串中(get是没有正文的)/而post提交的数据是在正文中

get提交数据是不太安全的,并且url的长度是有限制的,早期1K,现在很多都是4K/8K

方法 描述
PUT 从客户端向服务器传送的数据取代指定的文档的内容。
DELETE 请求服务器删除指定的页面。
CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS 允许客户端查看服务器的性能。
TRACE 回显服务器收到的请求,主要用于测试或诊断。
PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新 。

URL:

主要信息就是请求的资源路径以及提交的查询字符串

协议版本:

HTTP/1.1 0.9/1.0/1.1/2.0

  • 0.9:默认只支持GET请求方法,并且是短连接,http在传输层使用tcp协议,短连接指的是发送一个请求,得到相应后关闭连接
  • 1.0:支持了GET/HEAD/POST请求方法,并且支持长连接
  • 1.1:支持了更多的请求方法,并且新增了更多的特性,比如默认支持长连接,并且实现管线化传输
  • 2.0:比如以前都是客户端主动向服务器发送请求,但是在2.0中支持服务端向客户端主动推送消息…
  1. 响应首行:

包含3大信息,以空格进行间隔,以\r\n作为结尾; 协议版本 响应状态码 状态码描述\r\n

  • 协议版本:0.9/1.0/1.1/2.0

  • 响应状态码:向客户端反应本次请求的处理结果状态 — 包含5大种类:1xx / 2xx / 3xx / 4xx / 5xx

    • 1xx:一些描述信息
    • 2xx:本次请求正确处理完毕 200 - 请求成功
    • 3xx:重定向,本次请求的资源可能移动到其他位置,请客户端重新请求新的位置 301 - 永久重定向/ 302 - 临时重定向/ 303 - 查看其他
    • 4xx:客户端错误;400 - 客户端请求的语法错误/404 - 客户端无法根据客户端请求找到资源
    • 5xx:服务端错误;500 - 服务器内部错误,无法完成请求 / 502 - 代理给浏览器发送请求,连接失败 / 504 - 代理给浏览器发送请求,浏览器半天不响应

  • 状态码描述:对于本次状态码的描述信息(描述也可以自定义)

  • 头部:

一个个key: val形式的键值对,键值对之间以\r\n作为间隔

  • Connection:描述当前连接是否是长连接(close/keep-alive)
  • Content-Length:描述当前正文有多长(通过这个描述信息可以告诉对端本次请求应该接收多长的数据)
  • Content-Type:描述了正文的类型 - 告诉对方应该如何处理正文 test/html
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • Accept**:告诉对端自己能够接收什么样的数据
  • Referer:告诉服务器,本次请求是从哪个网页点击请求过来的
  • Transfer-Encoding:chunked 正文的分块传输 - 将正文分块传输,每块在发送前先告诉对方这块数据有多长; 0\r\n\r\n表示分块结束
    常用于服务端本身不确定自己要响应的数据有多长的时候
  • Location: http://123.207.58.25/ 搭配3xx状态码使用;通过描述的地址信息告诉客户端资源重新去请求指定的这个地址
  • Cookie/Set-Cookie:http协议是一个无状态协议,
    • 网上购物:http是一个短链接;买一个硬盘,需要登录一次;买个键盘,又要登录一次…
    • 服务端为每一个登陆的客户端在服务端主机上创建一个session(会话);会话中描述了客户端的各种信息;将session保存在数据库中。然后通过Set-Cookie将sessionid以及重要的信息返回给客户端;客户端会将其中的信息保存在cookie文件中;
    • 下一次请求服务端的时候,会自动从cookie文件中读取出信息,通过Cookie传递给服务端,服务端收到Cookie之后取出信息-session_id,通过这个id在数据库中找到绘画信息,就知道当前这个客户端是谁了

Cookie与Session有什么区别:

  • Session 是服务端为每一个客户端单独创建的会话,保存在服务端,其中有客户端的认证信息…
  • Cookie 是服务端通过Set-Cookie响应给客户端的信息,保存在客户端,下次请求服务器的时候会携带有Cookie信息

  • 空行:

\r\n - 间隔头部与正文
头部中最后一个头部信息也是\r\n作为结尾的;空行的重要性在于判断是否接收了完整的http头部信息

first_line\r\nkey: val\r\nkey: val\r\n……key: val\r\n\r\ncontent

通常接受http数据的流程:

  1. 接收完整的http头部 - 接收数据直到遇到\r\n\r\n的时候,认为头部到此结束
  2. 解析头部,根据头部中的Content-Length决定,正文应该接收多长;接收相应长度的正文

  • 正文: 客户端提交给服务端的数据 / 服务端响应给客户端的数据

编写一个简单的http服务器

http协议是应用层协议,在传输层使用的是tcp协议

  1. 搭建tcp服务器
  2. 等待连接到来,接收http数据(应用层数据)
    1. 接收http头部数据
    2. 解析头部(请求方法+url(资源路径+查询字符串) + 协议版本 + 各个头部键值对)
    3. 根据头部中的Content-Length接收正文
  3. 针对客户端的请求进行业务功能的处理,处理完毕之后组织http响应数据,发送给客户端
    1. 业务处理(跟具体的业务相关,不讨论)
    2. 根据http相应格式组织相应数据(首行(协议版本+状态码+描述)+ 头部 + 正文)
    3. 响应给客户端

搭建一个http服务器,收到请求之后打印出来,然后组织一个响应hello world给客户端就行

// 使用封装的TcpSocket类实例化对象实现tcp服务端程序

#include 
#include 
#include 
#include "tcpsocket.hpp"

int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em:./tcp_srv 192.168.122.132 9000\n");
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);	// stoi将字符串转换为数字
	
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(ip, port));
	CHECK_RET(lst_sock.Listen());
	while(1){
		TcpSocket cli_sock;
		bool ret = lst_sock.Accept(&cli_sock);
		if(ret == false){
			continue;
		}

		std::string http_req;
		cli_sock.Recv(&http_req);
		printf("req:[%s]\n", http_req.c_str());

		// 响应-首行(版本/状态码/描述)-描述(Content-Length)-空行-正文
		std::string body = "

Hello, Miss Xia Yanxin! I Love you~~

"
; std::string blank = "\r\n"; std::stringstream header; header << "Content - Length: " << body.size() << "\r\n"; header << "Content-Type: text/html\r\n"; std::string first_line = "HTTP/1.1 200 OK\r\n"; cli_sock.Send(first_line); cli_sock.Send(header.str()); cli_sock.Send(blank); cli_sock.Send(body); cli_sock.Close(); } lst_sock.Close(); return 0; }
// 封装实现一个tcpsocket类,向外提供简单接口:
// 使外部通过实例化一个tcpsocket对象就能完成tcp通信程序的建立

#include 
#include 
#include 
#include 
#include 
#include 

#define BACKLOG 10
#define CHECK_RET(q) if((q)== false){return -1;}

class TcpSocket{
public:
	TcpSocket():_sockfd(-1){
	}
	int GetFd(){
		return _sockfd;
	}
	void SetFd(int fd){
		_sockfd = fd;
	}
	// 创建套接字
	bool Socket(){
		// socket(地址域,套接字类型,协议类型)
		_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(_sockfd < 0){
			perror("socket error");
			return false;
		}
		return true;
	}
	
	void Addr(struct sockaddr_in *addr, const std::string &ip, uint16_t port){
		addr->sin_family = AF_INET;
		addr->sin_port = htons(port);
		inet_pton(AF_INET, ip.c_str(), &(addr->sin_addr.s_addr));
	}

	// 绑定地址信息
	bool Bind(const std:: string &ip, const uint16_t port){
		// 定义IPv4地址结构
		 struct sockaddr_in addr;
		 Addr(&addr, ip, port);
		 socklen_t len = sizeof(struct sockaddr_in);
		 int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
		 if(ret < 0){
			 perror("bind error");
			 return false;
		 }
		 return true;
	}

	// 服务端开始监听
	bool Listen(int backlog = BACKLOG){
		// listen(描述符,同一时间的并发链接数)
		int ret = listen(_sockfd, backlog);
		if(ret < 0){
			perror("listen error");
			return false;
		}
		return true;
	}
	
	// 客户端发起连接请求
	bool Connect(const std::string &ip, const uint16_t port){
		// 1.定义IPv4地址结构,赋予服务端地址信息
		struct sockaddr_in addr;
		Addr(&addr, ip, port);
		// 2.向服务端发起请求
		// 3.connect(客户端描述符,服务端地址信息,地址长度)
		socklen_t len = sizeof(struct sockaddr_in);
		int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("connect error");
			return false;
		}
		return true;
	}
	
	// 服务端获取新建连接
	bool Accept(TcpSocket *sock, std::string *ip = NULL, uint16_t *port = NULL){
		// accept(监听套接字,对端地址信息,地址信息长度)返回新的描述符
		struct sockaddr_in addr;
		socklen_t len = sizeof(struct sockaddr_in);
		// 获取新的套接字,以及这个套接字对应的对端地址信息 
		int clisockfd = accept(_sockfd, (struct sockaddr*)&addr, &len);
		if(clisockfd < 0){
			perror("accept error");
			return false;
		}
		// 用户传入了一个Tcpsocket对象的指针
		// 为这个对象的描述符进行赋值 --- 赋值为新建套接字的描述符
		// 后续与客户端的通信通过这个对象就可以完成
		sock->_sockfd = clisockfd;
		if(ip != NULL){
			*ip = inet_ntoa(addr.sin_addr);	// 网络字节序ip->字符串IP
		}
		if(port != NULL){
			*port = ntohs(addr.sin_port);
		}
		return true;
	}
	
	// 发送数据
	bool Send(const std::string &data){
		// send(描述符,数据,数据长度,选项参数)
		int ret = send(_sockfd, data.c_str(), data.size(), 0);
		if(ret < 0){
			perror("send error");
			return false;
		}
		return true;
	}
	
	// 接收数据
	bool Recv(std::string *buf){
		// recv(描述符,缓冲区,数据长度,选项参数)
		char tmp[4096] = {0};
		int ret = recv(_sockfd, tmp, 4096, 0);
		if(ret < 0){
			perror("recv error");
			return false;
		}
		else if(ret == 0){
			printf("connection break\n");
			return false;
		}
		buf->assign(tmp, ret);	// 从tmp中拷贝ret大小的数据到buf中
		return true;
	}

	// 关闭套接字
	bool Close(){
		close(_sockfd);
		_sockfd = -1;
		return true;
	}
private:
	int _sockfd;
};

Linux:带你理解应用层的HTTP协议_第3张图片

Linux:带你理解应用层的HTTP协议_第4张图片
注意:

云服务器:服务器绑定ifconfig看到的内网地址;但是在浏览器上访问的时候,要访问外网地址;尤其要注意的是要在云服务器的控制台去设置安全做,开启防火墙端口

虚拟机:记住要先关闭防火墙,否则主机无法访问进来

su root		切换管理员
systemctl stop firlwalld	关闭防火墙(虚拟机重启后,又回打开,若要一直关闭可以采用下边的命令禁用防火墙)

systemctl disable firlwalld 禁用防火墙

HTTPS协议

其实就是加密后的HTTP协议(https设置的端口号为 443 / http: 80

https到底是如何进行加密传输的?

通过 ssl加密( 非对称加密算法 / 对称加密算法) + 签名证书 保证数据的隐私安全传输

数据直接在网络中传输,很容易被劫持修改,有很大的安全隐患 — 所以要对传输过程进行加密

对称加密算法

如何加密就如何解密(加密算法和解密算法是一样的)

对称加密算法的优缺点

  • 优点:加密解密效率比较高
  • 缺点:容易被破解,使用时间稍长就会被中间劫持,根据数据规律破解出加密算法。

解决方案:最好能够每次通信都动态协商一个新的对称加密算法 — 有可能被劫持

非对称加密算法

加密和解密的方法不同 — 服务端生成一个公钥和私钥,将公钥传递给客户端,客户端使用公钥加密,服务端使用私钥揭秘。

公钥和私钥

通过加密算法-RAS算法,得到的一对密钥(就是两串数据),公钥用于对数据加密,私钥用于对加密后的数据进行解密;
因为加密方式和解密方式不同,因此很难被破解。就算中间公钥被人劫持,客户端使用公钥加密后的数据只能用私钥进行解密。

非对称加密算法的优缺点

  • 优点:安全性高,不容易被破解
  • 缺点:解密效率比较低

将非对称加密和对称加密结合起来加密

将客户端与服务端进行动态协商对称加密算法过程使用非对称加密;然后使用协商后的对称加密算法对数据通信过程进行加密;这样就即保证了安全,也保证了效率。

  1. 在通信前,服务器将公钥传递给对方
  2. 客户端使用公钥对自己支持的对称加密算法以及一个随机数进行加密,传递给服务器
  3. 服务器收到这个加密后的数据,使用私钥进行解密,得到客户端支持的对称加密算法和一个随机数,也给客户端响应一个随机数
  4. 双方通过这两个随机数和支持的加密算法计算得到一个对称加密算法
  5. 后续通信使用这个协商的对称加密算法完成

但是若中间黑客,劫持了公钥数据,然后将自己的公钥发送给客户端
因此公钥的传输也是存在安全隐患:对方的身份问题

如何确定发送公钥的这个服务端就是我心目中的那个服务端?

签名证书(CA):进行身份验证,并且传输公钥信息

注意:
公司生成一对密钥之后,拿着密钥去权威机构掏钱颁发生成一个签名证书,
证书中包含:公钥信息,权威机构信息,当前公司机构信息,有效时间…

ssl加密过程:

  • 在通信的时候,连接建立成功之后,服务端就会先将证书发送给客户端,客户端根据证书中的机构信息,进行身份验证
  • 若身份验证不通过,则可以直接断开连接(当然也可以设置是否信任这个机构)
  • 若身份验证通过,然后使用证书中的公钥加密对称加密算法的协商过程,最终使用协商成功的对称加密算法对通信进行加密。

知识点习题

  1. 请描述http请求get和post的区别,下面描述正确的有:

A. GET用于信息获取,而且应该是安全的和幂等的,POST表示可能修改变服务器上的资源的请求
B. POST比GET安全,因为采用了SSL加密
C. GET方式提交的数据最多只能是1024字节,理论上POST没有限制,可传较大量的数据
D. POST提交,把提交的数据放置在是HTTP包的包体中,GET提交的数据会在地址栏中显示出来

正确答案:A,C,D

答案解析

GET与POST方法有以下区别:

  1. 在客户端, Get 方式在通过 URL 提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交。

  2. GET方式提交的数据最多只能有1024字节,而POST则没有此限制。

  3. 安全性问题。正如在( 1 )中提到,使用 Get 的时候,参数会显示在地址栏上,而 Post 不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用 get没什么影响 ;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post 为好。

  4. 安全的和幂等的。所谓 安全的 意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说, GET 请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。 POST 请求就不那么轻松了。 POST 表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST 请求实现,因为在注解提交之后站点已经不同了(比方说文章下面出现一条注解)。

https采用了SSL加密


如果本篇博文有帮助到您,请留赞激励一下博主呐~~

你可能感兴趣的:(Linux,网络)