文章目录
- 1、http协议
- 1.1 认识URL
- 1.2 http协议格式
- 1.3 http的方法(GET和POST)
- 1.4 状态码
- 1.5 cookie
- 1.6 短连接和长连接
- 2、https协议
- 2.1 常见的加密方式
- 2.2 探究https协议的加密
- 2.3 CA证书
1、http协议
在之前学习序列化和反序列化的时候,认识到主机之间传输结构数据的时候,最好是通过某种约定将结构数据序列化成一串字符串,接收方再通过反序列化将字符串转换成结构数据。以上说的这种约定,其实可以看成是用户层通信的一种协议,是由程序猿自己定的。
实际上,已经有很多大佬写了很多很好的应用层上的协议供我们参考学习。http协议(超文本传输协议)就是其中一个。
1.1 认识URL
早期的网址是用http协议的,不过经过长时间的技术发展后,目前网址一般都是用https协议的。
不过得先认识http,这样才能更好了解https。
下面就是一种网址格式。
实际上,目前的URL普遍省略了以下部分:
登录信息(一般都用一个可以互动的窗口让用户填写)
端口号(端口号被缺省了,因为每个协议所有的端口号被固定了,比如http:80,https:443)
那么URL究竟是个什么呢?
URL(uniform Resource Locator)又被称为统一资源定位符,顾名资源定位符,在互联网上用来标识和定位资源的一种文本字符串。URL可以帮助我们快速定位到网络上的不同资源。(比如网页、图片、视频等)
URL如何做到?
在URL中,http协议指定了如何访问资源,服务器地址(也称域名,域名是可以转换成IP地址的)标识了互联网上主机的唯一性,因此再通过特定端口,就可以访问到特定的服务端进行连接。可以看到URL上还有带层次的文件路径,没错,服务端一般是在Linux系统上搭建的,这就是需要访问的linux服务器的资源文件路径。
值得注意的是,这个文件路径不是简单的在服务器上的绝对路径的,而是大多数情况下,服务端进程(Web服务器)会将请求的URL映射到服务端的文件系统路径。例如,如果基础路径是服务端上的目录/wwwroot,请求的URL是“index.html”,那么请求的资源文件路径就是/wwwroot/index.html。
urlencode和urldecode
可以看到像/ ? : 这样的字符在URL中有特殊的含义,为了不让这些字符随意出现,如果参数中要有一些特殊字符就需要通过urlencode进行转义。
一些网站对于中文,也会进行转义,比如在bing中搜索C++,就变成了C%2B%2B。
unldecode就是逆过程。
转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式(比如‘+’的ASCII码是43,对应16进制就是2B)
1.2 http协议格式
http在被设计的时候也有着自己的格式。
任何协议的请求或接受都是报头+有效载荷。
其中请求部分
- \r\n就是一个约定分隔符,代表每一行的结束。
- 请求报头中有着一些属性(具体的下面说),有效载荷中代表一些用户和服务端的交互数据。(比如登录信息、Up主上传的音频、视频)
- 读取完整报头时按行读取直到读取空行,空行的作用就是能代表报头读取完毕,接下来就是有效载荷部分。
- 如何保证读完正文呢? 在报头中有一个记录正文长度的属性Content-Length。
其中响应方
- http常用版本是http1.0和http1.1,可以看到客户端和服务端都有自己的版本,这也是为了服务端能根据版本信息去适配低版本客户端而有的。
- 状态码和状态码描述,可以代表响应的情况。(比如访问网页的404错误码和错误信息)
- 响应正文就是客户端需要拿到的资源。
下面通过一个在传输层以TCP协议传输的代码来体会一下。
#include <iostream>
#include <string>
#include <string.h>
#include <fstream>
#include <sys/socket.h>
#include <assert.h>
#include <stdarg.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <pthread.h>
using namespace std;
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
std::string getPath(std::string http_request)
{
std::size_t pos = http_request.find(CRLF);
if(pos == std::string::npos) return "";
std::string request_line = http_request.substr(0, pos);
//GET /a/b/c http/1.1
std::size_t first = request_line.find(SPACE);
if(pos == std::string::npos) return "";
std::size_t second = request_line.rfind(SPACE);
if(pos == std::string::npos) return "";
std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
//当url为/时,不能代表获得全部资源,而是类似主页的index
if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
return path;
}
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
if(!in.is_open()) return "404";
std::string content;
std::string line;
while(std::getline(in, line)) content += line;
in.close();
return content;
}
//http的请求 {method} 路径 版本
void handlerHttpRequest(int sock)
{
//读取发送方信息
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
//开始响应
//获得资源在服务端下的地址
std::string path = getPath(buffer);
std::string recource = ROOT_PATH;
recource += path;
std::cout << recource << std::endl;
//读取html资源(前端代码)
std::string html = readFile(recource);
//http响应方
std::string response;
//报头
response = "HTTP/1.0 200 OK\r\n";
response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
response += "Set-Cookie: this is my cookie content;\r\n";
//空行
response += "\r\n";
//回应正文
response += html;
//发送给客户端
send(sock, response.c_str(), response.size(), 0);
}
class tcpServer
{
public:
tcpServer() :_listenSock(-1), _quit(false)
{}
tcpServer(uint16_t port, const std::string& ip = "")
:_listenSock(-1)
,_serverPort(port)
,_serverIp(ip)
,_quit(false)
{}
~tcpServer()
{
if(_listenSock >= 0)
close(_listenSock);
}
void init()
{
//1、创建套接字
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if(_listenSock < 0)
{
exit(1);
}
//2、bind服务器信息
//2.1 填充服务器信息
struct sockaddr_in server;
//类似memset 清0
bzero(&server, sizeof(server));
//填写协议簇
server.sin_family = PF_INET;
//填写端口 因为是向网络中传输,所以要进行网络字节序的转换
server.sin_port = htons(_serverPort);
//填写IP 如果ip为空,设置为INADDR_ANY, 系统会帮忙填写
server.sin_addr.s_addr = _serverIp.empty() ? INADDR_ANY : inet_addr(_serverIp.c_str());
//2.2 bind 绑定
if(bind(_listenSock, (const struct sockaddr*)&server, sizeof(server)) < 0)
{
exit(2);
}
//3、监听 获取连接 因为tcp是需要建立连接的,这一步相当于等待客户端请求连接
if(listen(_listenSock, 5) < 0)
{
exit(3);
}
}
void start()
{
while(!_quit)
{
//4.建立连接
//客户端信息
struct sockaddr_in peer;
socklen_t peerLen = sizeof(peer);
//accept返回一个文件描述符
//_listenSock用来寻找连接
//serviceSock用来建立连接
//accept 将一个输出新参数传入,运行完后接收
int serviceSock = accept(_listenSock, (struct sockaddr*)&peer, &peerLen);
if(_quit)
{
break;
}
if(serviceSock < 0)
{
exit(4);
}
//至此服务器和客户端的连接建立完成
//5.获取客户端信息
//获取客户端ip
std::string peerIp = inet_ntoa(peer.sin_addr);
uint16_t peerPort = ntohs(peer.sin_port);
signal(SIGCHLD, SIG_IGN);
if(fork() == 0)
{
close(_listenSock);
handlerHttpRequest(serviceSock);
exit(0);
}
close(serviceSock);
}
}
void quitServer()
{
_quit = true;
}
private:
//监听套接字
int _listenSock;
//端口
uint16_t _serverPort;
//ip
std::string _serverIp;
//安全退出
bool _quit;
};
static void Usage(const std::string& proc)
{
std::cout << "Usage:\t\n" << proc << " port : ip" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serviceIp;
uint16_t servicePort;
if(argc == 2)
{
servicePort = atoi(argv[1]);
}
if(argc == 3)
{
serviceIp = argv[2];
}
tcpServer ts(servicePort, serviceIp);
ts.init();
ts.start();
return 0;
}
前端测试代码
基础路径是服务端前提下:/wwwroot/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试</title>
</head>
<body>
<h3>hello my server!</h3>
<p>我终于测试完了我的代码</p>
<form action="/a/b/c.html" method="get">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
运行代码,通过浏览器输入IP和端口让浏览器以客户端的方式向服务端发送以下信息。
服务端收到数据以后,向客户端响应,就读取了对应的前端html代码。
Linux下的telnet命令
telnet命令,能够通过远程的,以协议方式登录某种服务。
当出现^],就说明连接成功,就可以通过ctrl+] 然后回车,输入请求行信息,就可以获取响应端报头和有效载荷内容。
1.3 http的方法(GET和POST)
因为HTTP其它的方法不太常用。
这里只具体说GET和POST。
GET方法
可以注意到,在之前的前端代码中。method就是get。
method="get"
还是上面那个代码,在响应网站中输入账号和密码后,会发现一个这样的现象。
首先,404错误的出现肯定不是重点,因为我的代码没有提供确认按钮请求之后的响应。重点是这次URL中,账号和密码竟然出现在了参数部分。
是的,http中GET方法提交参数,会以明文的方式将我们对应的参数,拼接在URL中。
POST方法
其它都一样,只将method改为post。
method="post"
可以看到提交的参数放到了正文中。
GET和POST的比较
- GET通过URL传参,POST通过正文传参。
- GET方法传参不私密直接暴露出来,POST正文传参相对比较私密。(但是都不安全,通过抓包非常容易能得到)
- GET方法传参一般传小数据,一般一些比较大的内容都通过POST方式传(文件、电影)
1.4 状态码
这里只关注这几个常见的和常用的
200代表客户端响应成功。
当因为客户端错误时,会有404(Not Found)无法访问,403(Forbidden)禁止访问。
当服务器错误,会有504(bad gateway),错误的网关。
较为重要的是3XX重定向状态码。其中有301永久重定向和302临时重定向。
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;(这个可以用在,比如用户在下载网页的时候,可以通过对不同的OS类型,率先提供匹配的下载版本。)
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
重定向状态码
对应服务端两者每什么区别,区别是在客户端上的。
301:永久重定向,比如一个拥有很多客户的旧网站需要关闭更换新的网站,那么就可以通过这个永久重定向跳转到新的网站。这就需要客户记住新的网站,不过现在有些浏览器会记录经常访问的网站。
302:临时重定向,比如一些服务要升级,就可以临时重定向到一个新的网站,这个拷贝之前旧的服务,当服务升级完成后,就可以关闭这个临时重定向。这对客户不影响。
直接通过代码演示,将上述代码的处理函数改为以下。
void handlerHttpRequest(int sock)
{
//读取发送方信息
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
std::string response = "http/1.0 302 Movedtemporatily\r\n";
//填写location属性
response += "Location: https://blog.csdn.net/Ahaooooooo\r\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
通过浏览器输入ip:端口,结果也是成功返回到我的主页
1.5 cookie
其实http协议有一个特点是无状态。也就是http没有记录,要什么给什么。
不过我们经常会看到一个现象,就是我们登录网站时,在填写一次登录信息后,不管是关浏览器还是关机。下一次再登录网站时,就默认是登录状态了。这明显不符合http的特点。其实就是cookie策略在帮助记录的。
还是通过最开始的代码运行。
通过网站访问可以看到,cookie就是浏览器维护的文件,真正存在磁盘或者是一种内存级文件(这也符合那种关闭浏览器就需要重新登录的)
cookie的安全性问题
上述讨论简单传文件的是在http1.0中较为常见的。如果一些恶意软件将cookie软件找到,那么里面看到的就是裸露的数据,这就会造成安全性问题。(cookie这种机制也不止运用在浏览器,也会在一些软件中。)
因此现在主流的都是一种cookie+session的方案。
这让信息只放到了服务端,由产品公司进行保护,而公司相对于客户的安全能力肯定是更高的。
1.6 短连接和长连接
http在不同版本有不同的连接方式,在对应属性中有Connection字段,代表着不同连接。
http 1.0: closed 意为连接后响应一次就关闭对应文件描述符。
http 1.1: keep-alive 意为保持长连接,不用总是连接完再断开。
用户所看到的完整网页内容,背后可能是无数次http请求。http主流底层采用的是TCP协议!如果采用短连接,那么在底层tcp就会不断的进行3次握手和四次挥手(可以理解为连接和断开),这就会出现低效率问题。
长连接,对于多个请求访问,在一次连接时就可以都收到,并且为了防止出现顺序问题,也会通过一种Pipeline技术,使得依次响应。
其实http是无连接的,它只是借助了tcp建立连接的功能。因此http被称为一个超文本传输协议,是一个无状态无连接的应用层协议。
2、https协议
http因为其传输数据都是以明文方式进行传输的,因此在传输的过程中,是极有可能被第三方通过一些手段获取或更改数据,而造成数据安全问题的。
https通过在http的基础上添加了一层软件层:SSL/TLS加密协议,使得数据在传输的时候具有一定安全性。
http默认端口是80,https默认端口是443。
因此它们两个是两个不同的服务。
那么是不是https可以直接取代http了?
并不是,https因为需要加密解密的存在,肯定会导致效率更低的问题。因此在一些比较安全同时看重效率的场景下,http还是很有用的。
2.1 常见的加密方式
首先一个明文通过一个密钥的转换,就称为了一个密文。
对称加密: 采用单钥密码系统加密的方式,可同时用作信息加密和解密。(比如按位与 a^key = b, b^key = a),它的特点是计算量小,加密效率高。
非对称加密: 需要两个密钥来加密和解密。两个密钥分别是公钥和私钥。
用法除了正着用也可以反着用。它的特点是算法越复杂,安全性越高。因此相对于对称加密,加密速度非常慢。
数据摘要(也称数据指纹): 原理是将数据通过一种单向散列函数(Hash函数)生成一串固定的长度的数据摘要。数据摘要不是一种加密算法,而是用来判断数据有没有被篡改。
数字签名: 数据摘要通过加密就得到了数字签名。(这个下面讲)
2.2 探究https协议的加密
因为数据安全问题,数据应该在加密之后以密文的方式在网络中进行传输,那么具体的方案,应该是什么样的呢?
方案一:只使用对称加密
方案二:只使用非对称加密
K代表公钥,K’代表私钥
方案三:只使用非对称加密
并且,前面说过非对称加密是非常影响效率的,因此如果考虑效率,这个方案也是绝对不行的。
方案四:非对称加密+对称加密
第四个方案,其实效率问题已经解决了,但是还是有密钥在传输的过程中被第三方篡改的问题。为了解决这个问题,就需要下面一个方案。
2.3 CA证书
服务器在使用https之前,需要向CA机构申领一份CA数字证书,数字证书里面含有证书申请者信息、公钥信息等。服务端将证书传给浏览器,浏览器从其中拿去公钥。证书像是一个服务端公钥的身份证,证明其权威性。
申请证书时,需要在特定的平台生成,并且会给一对密钥,也就是公钥和私钥,其中公钥会随着CA证书交给客户端,服务端自己保存私钥。
当服务端申请CA证书的时候,CA机构会对该服务端进行审核,并且为该网站形成数字签名,具体过程如下:
- CA机构有非对称的公钥A和私钥A’
- CA机构将证书的明文数据通过Hash函数,形成数据摘要。
- 数据摘要通过私钥加密就得到一个签名S,附加到数据上就成了数字证书,就可以颁发给服务端了。
方案五:CA证书+非对称加密+对称加密
首先在传输CA证书的过程中,中间人是可以获得CA证书的,也可以对其修改,但这是没有用的。
因为在Client收到证书时,需要通过Hash函数将证书明文转换成散列值和通过证书公钥解密的数字签名进行比对,如果不同就说明出了问题,就能察觉的到。
中间人也无法伪造证书,因为其中涉及了服务端的信息。
总结:超文本传输http协议解决了客户端访问服务端资源的问题,https则完善了其安全问题。因此在应用层上,认识这两个协议非常有必要。
本章完~