目录
一、HTTP协议
1、概念
2、URL
二、HTTP协议格式
1、请求协议格式
2、响应协议格式
三、HTTP的请求方法
四、HTTP的状态码
五、HTTP常见的报头
六、HTTP和HTTPS
1、HTTPS协议
2、HTTPS的加密原理
1、基本概念
2、加密原理
通常我们程序员在网络编程的时候,一般是处于应用层的。而我们的http和https协议就是典型的应用层协议。
一、HTTP协议
1、概念
超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
2、URL
统一资源定位系统(uniform resource locator;URL)是因特网的万维网服务程序上用于指定信息位置的表示方法。
比如下面的这个URL:
协议名称:就是服务器所使用协议,我们平常所见到的就是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性(具体后面讲)。
域名:也就是我们常说的服务器IP地址。但是IP一般是不适合用户看的,所以就会转化成用户能够认出的名称。比如通过拼音baidu我们大概就能知道这是百度服务器下的内容。其实除了IP,还有一个应该是端口号,可是这里没有,因为端口号是众所周知的,所以一般不会显示地写在URL中。
带层次的文件路径:这就指明了用户想要访问的内容在服务器上的具体路径。访问服务器的目的是获取服务器上的某种资源,而服务器上的资源是保存在文件中的,所以通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径,就能找到资源所在的文件,获取资源了。
仔细观察你会发现,这里的路径分隔符是/
,而不是\
,这也就证明了实际很多服务都是部署在Linux上的。
二、HTTP协议格式
从报文的角度出发,http是基于行的文本协议。
1、请求协议格式
说明:
1、请求行。从左到右,请求方法,url连接,http协议版本(以空行隔开)。
请求方法:
2、请求报头。请求报头中含有各种属性信息,比如,正文大小等。
3、空行:http协议通过空行来区分报头和有效载荷。
4、请求正文:通过请求报头得知其大小。
前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
客户端在发起HTTP请求时告诉服务器自己所使用的http版本,此时服务器就可以根据客户端使用的http版本,为客户端提供对应的服务。
HttpServer服务器:
在网络协议栈中,应用层的下一层叫做传输层,而HTTP协议底层通常使用的传输层协议是TCP协议,因此我们可以用套接字编写一个TCP服务器,然后启动浏览器访问我们的这个服务器。
下面我们编写一个简单的TCP服务器,这个服务器要做的就是把浏览器发来的HTTP请求进行打印。
HttpServer.hpp:
#pragma once
#include <iostream>
#include "Sock.hpp"
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <functional>
class HttpServer
{
public:
using func_t = std::function<void(int)>;
HttpServer(const uint16_t &port, func_t func) : port_(port), func_(func)
{
listen_ = sock_.Socket();
sock_.Bind(listen_, port_);
sock_.Listen(listen_);
}
void start()
{
signal(SIGCHLD, SIG_IGN);
for (;;)
{
uint16_t client_port = 0;
std::string client_ip;
int sockfd = sock_.Accept(listen_, &client_ip, &client_port);
if (sockfd < 0)
continue;
if (fork() == 0)
{
close(listen_);
func_(sockfd);
close(sockfd);
exit(0);
}
close(sockfd);
}
}
~HttpServer()
{
if (listen_ >= 0)
close(listen_);
}
private:
uint16_t port_;
Sock sock_;
int listen_;
func_t func_;
};
Sock.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstdbool>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Sock
{
public:
const static int gmv = 20;
Sock()
{
}
int Socket()
{
// 1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cout << "创建套接字失败!" << std::endl;
exit(1);
}
std::cout << "创建套接字成功!" << std::endl;
return sock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
// 2.进行绑定
struct sockaddr_in src_server;
bzero(&src_server, sizeof(src_server));
src_server.sin_family = AF_INET;
src_server.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &src_server.sin_addr);
socklen_t len = sizeof(src_server);
if (bind(sock, (struct sockaddr *)&src_server, len) < 0)
{
std::cout << "绑定失败!" << std::endl;
exit(2);
}
std::cout << "绑定成功!" << std::endl;
}
void Listen(int sock)
{
// 3.开始监听,等待连接
if (listen(sock, gmv) < 0)
{
std::cout << "监听失败!" << std::endl;
exit(3);
}
std::cout << "服务器监听成功!" << std::endl;
}
int Accept(int sock, std::string *ip, uint16_t *port)
{
// 4.获取链接
struct sockaddr_in client_sock;
socklen_t len = sizeof(client_sock);
int serversock = accept(sock, (struct sockaddr *)&client_sock, &len);
if (serversock < 0)
{
std::cout << "获取链接失败!" << std::endl;
return -1;
}
if (port)
*port = ntohs(client_sock.sin_port);
if (ip)
*ip = inet_ntoa(client_sock.sin_addr);
return serversock;
}
~Sock()
{
}
};
HttpServer.cc:
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "HttpServer.hpp"
void handerHttprequest(int sock)
{
char buffer[10240];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "---------------------\n"
<< std::endl;
}
}
static void usage(std::string proc)
{
std::cout << "\n"
<< proc << " port"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
uint16_t Serverport = atoi(argv[1]);
std::unique_ptr<HttpServer> Server(new HttpServer(Serverport, handerHttprequest));
Server->start();
return 0;
}
因为我们没有具体向服务器请求什么内容,所以并没有请求正文。 而请求报头当中确实全部都是以key: value形式按行陈列的各种请求属性。
上图是我使用我的手机进行访问服务器,在服务器端获得的请求。
2、响应协议格式
HTTP响应由以下四部分组成:
~ 状态行:http版本 + 状态码 + 状态码描述
~ 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
~ 空行:遇到空行表示响应报头结束。同样通过空行来区分报头和有效载荷。
~ 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。
下面我们构建一个HTTP响应给浏览器。我们将上面的handerHttprequest函数改为:
void handerHttprequest(int sock)
{
char buffer[10240];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "---------------------\n"
<< std::endl;
}
std::string Response = "HTTP/1.1 200 OK\r\n";
Response += "\r\n";
Response += "<html><h3>hello HTTP</h3></html>";
send(sock, Response.c_str(), Response.size(), 0);
}
url当中的/不能称之为我们的linux服务器上根目录,这个/表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录。 我们一般访问网页,也就是访问web根目录下的某一个资源。/a/b/c/d.html就是web根目录下的某一个资源。
实际我们在进行网络请求的时候,如果不指明请求资源的路径,此时默认你想访问的就是目标网站的首页,也就是web根目录下的。
三、HTTP的请求方法
首先,我们介绍一个命令 telnet:该命令可以远程登录到某个服务器。
使用:telnet 服务器IP 端口号。
常用的请求方法:
方法 | 说明 |
GET | 获取资源 |
POST | 传输实体主体 |
PUT | 传输文件 |
HEAD | 获得报文首部 |
DELETE | 删除文件 |
OPTIONS | 询问支持的方法 |
TRACE | 追踪路径 |
CONNECT | 要求用隧道协议连接代理 |
LINK | 建立和资源之间的联系 |
UNLINK | 断开连接关系 |
我们平时的上网行为其实一般就两种:从服务器端拿到资源,或者将客户端的数据提交到服务端,所以最常用的方法就是:GET和POST这两个方法。GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器。但实际我们上传数据时也有可以使用GET方法。
比如下面,我们使用telnet远程登陆百度主页,然后发送GET方法的请求:
我们就可以得到百度的主页内容:
GET方法和POST方法的区别。
GET方法和POST方法都可以带参:GET方法是通过url传参的。而POST方法是通过正文传参的。使用POST方法传参更加私密,因为POST方法通过正文传参,不会将你的参数回显到url当中,此时也就不会被别人轻易看到。GET会将参数传给url,而url是能够被所有人看见的。
下面我们就来演示一下两者的区别。
首先,我们需要明白一个概念:表单——作用就是收集用户数据,并把数据推送给服务器。表单中的数据会作为HTTP请求的一部分被推送。
对于下面的淘宝网的登陆界面,我们可以查看其相关前端代码:
如果使用GET方法,那么淘宝账号和密码就会作为参数传给URL,也就是说用户可以在浏览器上方的网址栏的URL中看到账号和密码。而如果使用POST方法,那么淘宝账号和密码就会作为请求的正文,传给服务器,在URL中我们也就不会看见,所以POST更加私密。但是不代表就是安全的!要做到安全只能通过加密来完成。
POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。
四、HTTP的状态码
类别 | 原因短语 | |
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
我们平时常见的状态码:比如200(OK),404(Not Found),403(Forbidden 无请求权限),302(Redirect,重定向),504(Bad Gateway)
Redirect,重定向状态码
重定向: 当我们对某些网页进行请求访问时,因为功能要求,我们跳转到去请求访问其他网页。
例如:我们在使用网页版的视频网站的时候,最先进入的是其首页。当我们想要登陆,点击登陆就会跳转到登陆页面,这是重定向。而当我们登陆成功后又会返回主页面,这也是重定向。
重定向可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。
下面我们就来演示一下重定向,我们直接修改HttpServer.cc:直接在报文中添加 "Location: https://www.qq.com/\r\n",将原来的页面重定向到qq网页。
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "HttpServer.hpp"
void handerHttprequest(int sock)
{
char buffer[10240];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "---------------------\n"
<< std::endl;
}
std::string Response = "HTTP/1.1 302 Found\r\n";
Response += "<html><h3>hello world!</h3></html>";
Response += "\r\n";
Response += "Location: https://www.qq.com/\r\n";
send(sock, Response.c_str(), Response.size(), 0);
}
static void usage(std::string proc)
{
std::cout << "\n"
<< proc << " port"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
uint16_t Serverport = atoi(argv[1]);
std::unique_ptr<HttpServer> Server(new HttpServer(Serverport, handerHttprequest));
Server->start();
return 0;
}
然后启动服务器,通过浏览器访问我们的网页。我们就发现直接跳转到了qq的网页。
下图是服务器获得的请求:
五、HTTP常见的报头
HTTP常见的Header:
1、Content-Type:数据类型(text/html等)。
2、Content-Length:正文的长度。
3、Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
4、User-Agent:声明用户的操作系统和浏览器的版本信息。
5、Referer:当前页面是哪个页面跳转过来的。就是浏览器中的回退功能。
6、Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
7、Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。 8、connection:表明客户端和服务器之间的连接是长连接还是短链接。
会话管理:
http是一种无状态协议。无状态:HTTP协议不会记录用户的上一次的请求。即其不会对用户的行为作任何的记录。
但是我们使用浏览器的时候发现并不是这样的。在实际中,一般网站都会记录下用户的状态。比如我们平时打开电脑,访问csdn的时候,由于我们之前已经登陆过,发现我们之后访问csdn的时候就不用再登陆了,csdn并没有要求我们再次输入账号和密码。
上面这种便捷的服务是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据。
当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据比对后判定你是一个合法的用户,就允许登陆。那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置。(Set-Cookie也是HTTP报头当中的一种属性信息)服务器会把用户的账号和密码等私密信息保存在浏览器客户端的cookie文件中。
当用户下次使用该浏览器访问时,cookie文件中的私密信息就会包含在HTTP请求中(也就是报头中的cookie字段),发送给服务器,进行身份验证。
可是,如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,非法用户不仅知道了你的私密信息,还可以以你的身份去访问你曾经访问过的网站。
所以,单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏。我们就要使用下面的策略来解决这个问题。
当我们第一次登录某个网站输入账号和密码后,服务器认证成功后会在服务端生成一个唯一的session id,这个session id与用户信息是不相关的。系统会将所有登录用户的session id值统一维护起来。
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的session id返回给浏览器。浏览器收到响应后会自动提取出session id的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个session id。
而服务器识别到HTTP请求当中包含了session id,就会提取出这个session id,然后再到对应的结构当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了。
当然,这也并不是一定安全的,因为非法用户可以获取我们的session id,任然能够访问我们曾经访问过的网站。所以这只是相对安全的。
connection:
keep-alive:长连接。建立连接后,客户端可以不断的向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接。长连接适用于需要频繁通信的场景。
close:短连接。短连接是在数据传送过程中,只在需要发送数据时建立一个连接,数据发送完成后,连接会被断开。这种连接方式适用于非实时通信场景,短连接的特点是每次通信都会建立新的连接,通信结束后,服务器和客户端会关闭连接,它不会长期占用通道。
六、HTTP和HTTPS
早期,我国的互联网行业刚刚兴起的时候,互联网所用的协议是HTTP协议,而HTTP无论是用GET方法还是POST方法传参,都是没有经过任何加密的。因此,HTTP协议在携带数据的时候是明文的,所以用户信息等私密数据很有可能会被不法分子获取,这样就非常不安全。而只有对数据进行加密后,我们才能在一定程度上保证用户数据的安全,所以后来就有了HTTPS协议。
1、HTTPS协议
HTTPS协议实际就是在应用层和传输层协议之间加了一层加密层(SSL&TLS),这层加密层本身也是属于应用层的。它会对用户的个人信息等私密信息进行各种程度的加密。HTTPS协议在交付数据时先把数据交给加密层,由加密层对数据加密后再交给传输层继续向下封装。
相同的,当对端从下向上交付的时候,传输层收到数据后,会先将数据交给加密层,由加密层对数据进行解密后再将数据交给应用层。
这样,即使不法分子在网络中拿到了数据后,拿到的也是加密后的数据,它无法解密,也就拿不到原文,这样就保护了用户的信息。
2、HTTPS的加密原理
1、基本概念
一般来说,我们可以将加密方法分为对称加密和非对称加密。
对称加密:也叫单密钥加密。采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密。即加密和解密所用的密钥相同。
特点:加密速度快,加密效率高。
举例:假如我们现在有一个明文 a = 1234,密钥 key = 8888。我们使用异或方法对a进行加密:a^key得到密文b = 9834,然后再对密文b进行解密 b^key = 1234,就得到了明文a。这就是一个简单的对称加密。
非对称加密:需要两个不同的密钥来进行加密和解密。一个是公开密钥(公钥),一个是私有密钥(私钥)。
特点:算法强度复杂,加密速度比对称加密慢很多。通过公钥对明文进行加密使其变成密文,通过私钥对密文进行解密使其变成明文(当然,也可以反过来使用:公钥解密形成明文,私钥加密形成密文)。
数据摘要:又称数据指纹,其基本原理就是利用单向散列函数(Hash函数)对信息进行运算,生成一串固定长度的数据摘要,不同的文本经过同种Hash形成的摘要一定是不同的,通常用来进行数据比对。注:并不是一种加密机制,摘要经过加密会得到数字签名。
2、加密原理
HTTPS所使用的加密原理是对称加密和非对称加密相结合的方法。
具体原理如下:
首先,客户端向服务端发起通信HTTPS请求,客户端和服务端进行密钥协商。服务端有自己的非对称密钥:公钥s和私钥s'。 服务端将公钥s发送给客户端,客户端拿到s后,自动形成对称密钥x。
然后,客户端使用公钥s对对称密钥x进行加密,接着将密文传送给服务端。服务端拿到密文后,使用私钥s'对密文进行解密,拿到对称密钥x。之后,客户端和服务端就通过对称密钥x进行通信。
好处:即使有不法人员在网络中拿到x加密后的密文,因为其没有私钥s',无法解密,所以也无法知道通信所使用的密钥x,也就无法解密经对称密钥x加密后的密文。这样就保证了服务端和客户端之间通信的安全。
但是,这也并不是一定安全的,如果在服务端和客户端进行密钥协商的时候,不法分子就开始攻击了,任然无法保证安全。如下图: