HTTP与HTTPS
HTTP协议
我们在输入一个URL时,显示出各种各样的Web页面,这些页面从何而来,又是经过了什么操作呈现在我们面前的?
Web 界面当然不会凭空出来,根据 Web 浏览器地址栏中指定的 URL,Web 使用一种名为 HTTP 的协议作为规范,完成从客户端到服务端的一些流程。可以说,Web 是建立在 HTTP 协议上进行通信的。
什么是HTTP
HTTP:超文本传输协议(HyperText Transport Protocol)是当今互联网应用最广泛的一种网络协议,所有的万维网(WWW)文件都必须遵守这个协议,和TCP/IP协议族的其他协议一样,HTTP用于客户端和服务端之间的饿通信。
多版本的HTTP(0.9,1.0,1.1,2.0)
0.9版本和1.0版本由于已经很少使用,就不多描述了,主要总结后两种
HTTP/1.1:1997 年公布的 HTTP 1.1 是目前主流的 HTTP 协议版本。当年的 HTTP 协议的出现主要是为了解决文本传输的难题,现在的 HTTP 早已超出了 Web 这个框架的局限,被运用到了各种场景里。
新特性
- 新引入Connection字段,可以设置长连接(keep-alive)保持连接状态,不用像之前一样一个请求建立一次连接
- 管道化——基于长连接的基础,可以不等待响应到达就继续发送请求,但是响应的顺序还是按照请求的先后顺序排序
- 缓存处理
- 断点传输
HTTP/2.0:2.0版本的HTTP协议是对1.x版本的协议进行了升级优化,并不是完全的改头换面了。
新特性
- 二进制分帧
- 多路复用—在共享TCP连接的基础上同时发送请求和响应(就是说我们在发送请求的同时也可以一并把响应也发送过去)
- 头部压缩—http1.x的头带有大量信息,而且每次都要重复发送。http/2使用encoder来减少需要传输的header大小。
- 服务器推送—服务器可以额外的向客户端推送资源,而无需客户端明确的请求
区分URL和URI
我们在DNS协议中提到过URL其实就是服务器资源的准确路径,又叫做网址,比如 http://baidu.com。
而URI是Uniform Resource Identifier的缩写,中文就是统一资源标识符,而URL是Uniform Resource Location的缩写,中文是统一资源定位符
- Uniform-用统一的规格处理不同形式的资源
- Resource-资源的定义是可标识的任意东西,不一定是单一的,也可以是一个集合
- Identifier-资源独一无二的标识,就好比人的身份证,这里叫做标识符。
总结来说:URI用来标识某部分网络的资源,而URL用来定位资源的地点,URL是URI的子集
我们知道在应用层的数据格式是HTTP请求报文和响应报文,那么这些数据里到底包含了什么信息和数据呢?
HTTP 协议规定:在两台计算机之间使用 HTTP 协议进行通信时,在一条通信线路上必定有一端是客户端,另一端则是服务端。客户端要先发起HTTP请求,服务器收到后再返回响应报文,所以说,一定是由客户端开始建立通信的。
HTTP请求报文
HTTP 请求报文由 3 大部分组成:
1)请求行(必须在 HTTP 请求报文的第一行)
2)请求头(从第二行开始,到第一个空行结束。请求头和请求体之间存在一个空行)
3)请求体(通常以键值对 {key:value}方式传递数据)(可以没有)
HTTP请求行中的请求方法
请求行中的方法作用在于可以指定服务器中的资源按期望做出某种行为,就是客户端使用请求方法给服务器下操作命令
常用的方法有(1.0之后):GET(0.9唯一支持),POST,PUT,HEAD,DELETE,OPTIONS,TRACE等等,当然最常用的还是前面四个方法。
1)GET获取资源
GET方法用来获取已经被URI标识的资源,服务器收到请求后返回对应的解析内容
2)POST传输实体主体
POST主要用来传输数据,服务器收到请求后会返回这次请求的处理结果
3)PUT传输文件
PUT方法用来传输文件,任何人都可以使用,所以安全性不高,一般不使用
4)HEAD获取报文首部
和GET方法类似,但是返回的不是真正的报文首部,响应报文里包含的是报文首部里的具体信息,比如URI的有效性和资源的更新日期等等。
5)DELETE删除文件
和PUT功能相反,也不带有验证机制,任何人都可以依照URI删除对应文件
6)OPTIONS查询支持的请求方法
用于查询当前URI标识资源支持的请求方法,响应报文的响应头中会包含一个Allow字段,value就是对应的方法——GET,POST等等
7)TRACE回显服务器收到的方法指令,用于测试
HTTP请求头
请求头用于补充请求的附加信息,比如说客户端信息,对响应内容相关的优先级等内容。以下列出常见请求头:
1)Referer:告知服务器这个请求是从哪个URI跳转过来的(就好像链路层的路由一样),如果是直接从客户端发起的请求就不会包含这个字段。
**2)Accept:**告诉服务端,这个请求支持的响应数据类型,就比如说我发起一个请求报文支持图片返回,服务端如果响应的是视频类型就不行。
(响应报文头里也包含一个字段Content-Type,表示响应的数据类型,如果Content-Type和Accept里的值不对应,就会报错)
**3)Host:**告知服务器资源所处的互联网主机地址和端口号,这样服务器就知道去哪里找这个资源了。这个字段在HTTP1.1版本中规定是必须包含的
**4)Cookie:**客户端的Cookie就是通过这个字段传给服务器的,cookie就相当于服务器给客户端的一个凭证,服务器依靠这个字段持有客户端状态
Cookie: JSESSIONID=15982C27F7507C7FDAF0F97161F634B5
5)Connection:表示此次连接类型,keep-alive表示长连接,close是连接关闭
6)Content-Length:请求体的长度
7)Range:对资源的范围请求,比如说有一个1G大小的资源,我只需要128M,就可以在Range字段中指定。
(后面还有一些面试不常问,有兴趣可以自己了解)
HTTP响应报文
HTTP响应报文和请求报文一样,也是分成三个部分:
1.响应行
2.响应头
3.响应体
我们可以看到响应行和请求行的字段是有差异的
请求行=请求方法(GET)+资源标识符(URI)+协议版本(HTTP1.1)
响应行=协议版本(HTTP1.1)+状态码(200)+原因短语(OK)
响应行的状态码和原因短语是响应报文的核心,也是本章节的重点
HTTP请求行的状态码
HTTP状态码表示服务器在接收客户端请求后的返回结果,服务端处理结果是否正常,通知错误出现等情况,与日常开发息息相关。
状态码由3位数字组成,开头的第一个数字定义了响应的类别
1xx:接收的请求正在处理
2xx:接收的请求正常处理完毕
3xx:资源重定向,仍需附加操作
4xx:服务器无法处理请求
5xx:服务器处理请求出错
(1xx类型的状态码一般很少出现,大概知道什么意思即可)
2xx:请求正常处理完毕
-
200 OK:客户端请求处理成功!
-
204 No Content:服务器成功处理请求,无内容返回。通常是客户端向服务器发送信息的场景
-
206 Partial Content:服务器完成了部分GET请求(请求报文中包含Range字段),返回一部分资源(响应报文中包含Content-Range)
3xx:资源重定向状态码(需要附加操作) -
301 Move Permanently:永久重定向,请求资源已经永久挪除,不在这个服务器上了
-
302 Found:临时重定向,请求的资源暂时挪到了其他地方
-
303 See Other:也是临时重定向,和302的唯一区别在于响应报文中会明确告知客户端应该使用GET方法请求资源
-
304 Not Modified(特殊):当客户端请求报文中包含的附加条件无法满足时(请求头中含有if语句)会返回这个状态码,和重定向一分钱关系都莫得
-
307 Temporary Redirect:临时重定向,和302一样,不像303会把POST变成GET
4xx:客户端请求出错
- 400 Bad Request:客户端请求有语法错误,服务端无法理解
- 401 Unauthorized:请求未经授权,这个代码必须和WWW-Authorized配合使用
- 403 Forbidden:服务端拒绝处理请求
- 404 Not Found:请求资源没有找到,这时候应该查看URI是否写错了
- 415 Unsupported media type:不支持的媒体类型
5xx:服务端出错,未能处理合法的请求
- 500 Internal Server Error:服务端发生不可预知的错误(十分恐怖)
- 503 Server Unavailable:服务器现在可能超载或者正在维护,暂时无法处理请求,可以过段时间就恢复正常。
HTTP响应头
响应头包含一些附加信息,服务端信息,以及对客户端的附加请求等
**1)Allow:**和请求头中的OPTIONS配合,返回服务器支持的请求方法
**2)Content-Type:**和请求头中的Accept配合,返回响应包含的数据类型
3)Last Modified:资源最后一次改动时间
4)Location:告知客户端应该去哪里寻找资源,会返回302状态码
5)Set-Cookie:设置和页面关联的Cookie,下次客户端发起请求后就会带上这个Cookie当做凭证。
HTTP连接类型
短连接
在1.0版本及以前,使用的都是短连接,即每发起一个请求,就建立一次TCP连接,返回响应后又断开连接。这样的弊端就是效率十分低下,每个请求都伴随着一次TCP三次握手和四次挥手
为了解决这个问题,长连接出现了——Keep-Alive
长连接
从1.1版本开始连接方式就变成了长连接,使用长连接的HTTP协议,会在请求头加上Connection:keep-alive字段
使用长连接的客户端和服务器,在完成一次请求响应后并不会马上断开连接,而是会保持连接状态,当然这个连接不会永久保持,我们可以在不同的服务器软件(如Apache)中自己设置,实现长连接需要客户端和服务器都支持。
(HTTP协议的长连接,实质就是TCP协议的长连接)
但是长连接也并不是完美的,即是说每发起一个请求,必须等待响应才能继续发送下一个请求,那假设某个响应时间过长,这样时间不就白白浪费了?
为了解决这个问题,流水线机制被引入了。
流水线/管道(Pipeline)
流水线其实也是基于长连接的基础上实现的,即在同一条长连接线路上发送请求,无需等待响应就可以继续发送下一个请求,这样就可以做到并行发送多个请求,大大提高了效率。
无状态的HTTP协议
HTTP是无状态的协议,即是说它不对之前完成的请求和响应进行管理,它无法根据之前的状态对这个请求进行处理。
这样就会造成一个弊端,如果我们每次在一个网页上发起请求,都要重新登录一次?这也太麻烦了。而且服务器要记住所以的客户端,对服务器性能也会有影响。
所以为了保留HTTP无状态协议速度快的优点,解决页面跳转的弊端,引入了Cookie技术
1)第一次没有Cookie的请求
2)下次请求加上Cookie
HTTP断点重传
断点重传的意思其实就是下载传输文件的时候可以中断,下次再开始下载时就从中断的地方开始下,不用重新开始,这个大家应该经常遇到。
它的原理其实也非常简单,就是请求头中的Range字段和响应头的Content-Range字段的配合使用。客户端一块一块的请求服务端的资源文件,服务端也一块一块的发送回去。
但是HTTP其实有一些很致命的问题,就是它的安全性问题:
- 通信使用明文传输,内容可能被窃听
- 不提供验证机制,无法辨别请求是客户端还是攻击者发起的
- 无法保证数据的完整性,所以有可能被篡改
为了避免上面的弊端,引入了HTTPS协议
更安全的HTTPS协议
HTTPS协议并不是应用层上的一个新的协议,只不过是在HTTP协议的通信接口部分使用SSL(Secure Socket Layer)协议和TLS(Transport Layer Security)协议进行安全性保证而已。
简单的说,HTTP协议是直接和TCP协议进行通信的,而HTTPS协议则是HTTP协议先和SSL协议通信,SSL再和TCP协议通信。
有了SSL协议之后,HTTP协议就具有了加密,验证机制和完整性保护的功能了,这正好解决了上述出现的问题。
(Tips:SSL协议是一个独立的协议,所以所有应用层的协议都可以和SSL配合使用)
下面我们来详解SSL的每个功能是如何工作的。
加密
HTTP在传输过程中是明文传输的,无法保证不被窃取,既然我不能防止被窃听,我就给传输的数据加密呗。(就好像以前战争时期,特工们使用摩斯密码进行交流,窃听的人不掌握解密的方法是没有用的)
加密的方式有三种,分别是对称密钥加密,非对称密钥加密,混合密钥加密
对称秘钥加密
简单来说,就是服务器有一把公钥A1,服务器把公钥发送给客户端,然后客户端再发起请求时就使用A1进行加密,服务器收到数据进行解密即可。
但是这存在一个问题,如果在发送A1的过程中被攻击者窃取了,和明文传输也没差别
所以要如何解决这个问题呢?我们往下看
非对称密钥加密
服务器有一把公钥A1和私钥B1,公钥是谁都可以拥有的,私钥是只能自己持有,这两个密钥是成对出现的。
首先服务器将公钥发送给客户端,客户端拿到之后使用A1对数据进行加密,服务器收到后再使用B1解密即可,此时哪怕A1被攻击者窃取也没事,他没有B1也无法解密数据。
混合加密方式
但是上述的非对称密钥加密虽然安全性保证了,但是处理速度下降不少,那么有没有什么方法可以同时拥有速度快和安全性的保证呢?那么混合加密方式就不遑多让了
混合加密其实就是用公钥对共享密钥进行加密,举个例子
- 服务器有非对称密钥—公钥A1,私钥B1
- 服务器把A1发送给客户端
- 客户端随机生成一个共享密钥X,使用A1加密之后返回给服务器
- 服务器解密后取得X,后面双方就可以使用X进行共享加密的方式传输数据了。
但是混合密钥的方法还是有一个弊端的,我们来详细解析 - 服务器有公钥A1,私钥B1
- 客户端发起请求后,服务器在响应报文中加入公钥A1
- 此时这个响应报文被拦截,攻击者拥有公钥A2,私钥B2
- 攻击者在这个响应报文中把A1窃取,并且换成A2
- 客户端收到后,生成一个共享密钥X,然后傻乎乎的使用A2进行加密
- 攻击者再把这个报文拦截下来,使用B2进行解密后获得X,然后再用截取的A1对X加密发送给服务器
- 服务器解密后得到密钥X,但是此时客户端、攻击者、服务器都拥有X,安全性也就无需再说了。
所以上面的加密过程体现了一个问题,就是客户端怎么知道发送过来的密钥就是服务器的密钥呢?这时候就需要身份验证!
数字证书+数字签名——身份验证的保障
我们日常生活中,假如需要证明我的身份,是不是拿出我的身份证就可以了,身份证是由具有公信力的政府颁发的;那么在互联网中,也有类似的这么一个机构–数字证书认证机构 Certificate Authority, CA,CA颁发的数字证书就是类似身份证一样的东西。那么还有一个问题,怎么证明这个证书不是假冒伪劣产品,就是依靠的数字签名
我们来详细讲述CA颁发证书的过程
- CA有公钥C1和私钥C2
- 服务器向CA申请数字证书,并发送自己的公钥A1
- CA对A1进行hash,得到摘录信息MIC
- 对MIC使用自己的私钥进行加密,得到数字签名
- 将数字签名和服务器的A1一起放进数字证书返回给服务器
然后就是客户端验证数字证书的过程了
- 客户端收到服务器发送的数字证书,得到公钥A1和数字签名S1
- 客户端使用事先植入的CA公钥C1对S1进行解密,得到S2
- 用数字证书包含的算法对A1进行hash,得到A2
- 看A2是否等于S2,如果相等,代表服务器正常,不是攻击者,对A1可以放心使用
为什么HTTPS这么安全没有全面使用——贵
以上,TCP/IP五层模型的每一层都已经详细总结完成,下面再补充最后一点,网络I/O。
高性能网络I/O
参考自知乎-勤劳的小手
公众号-我是程序员小贱
1.阻塞I/O
我们在调用某个函数的时候,马上会返回相应的结果,然后下面的业务逻辑就根据这个返回值进行后续操作,如果结果还没有返回就一直等待。
阻塞I/O其实就是这个原理:当进程在等待某个数据时,如果这个数据没有准备好,这个进程就会一直阻塞直到数据准备完成。此时CPU就会分配给其他的进程,在用户角度看,这个进程就好像卡住了。
传统阻塞I/O模型
特点:
- 通过阻塞式I/O获取输入数据
- 每个请求都采用单独的线程进行数据读取,业务操作以及数据返回等操作
非阻塞I/O
非阻塞I/O和阻塞I/O相比,其实就在于非阻塞I/O在数据没有准备好时,不会阻塞线程,而是马上返回。
用专业术语来说:非阻塞I/O嗲用Recvfrom函数读取数据时,如果此时内存中数据没有准备好,就会返回一个EWOULDBLOCK错误,不会让线程一直等待,然后线程就会不断的轮询,查看数据是否准备完毕。
具体流程:
- 线程向内核调用Recvfrom函数读取数据
- 数据未准备好,返回EWOULDBLOCK错误
- 线程轮询,继续向内核调用Recvfrom函数
- 数据准备好,进行下一步(否则仍然返回错误)
- 将数据从内核复制到用户空间
- 完成操作,返回结果
读操作和写操作
- 1)读操作
我们知道每个套接字Socket都有一个接收缓冲区,如果缓冲区为空,阻塞IO会直接阻塞直到数据可读,非阻塞IO则会返回一个EWOULDBLOCK状态,触发轮询 - 2)写操作
上述的读操作是基于接收缓冲区,那么写操作肯定也有一个发送缓冲区,当发送缓冲区空闲时,阻塞IO会将所有数据都写入发送缓冲区才返回,否则就阻塞,直到数据全部写入,而非阻塞IO则是尽可能多的写入,当缓冲区写满之后,返回一个值告诉线程还剩多少数据需要下次空闲时写入。
那么(非)阻塞I/O模型存在什么问题呢?
- 高并发场景下,多个用户发起请求,需要创建大量的线程处理这些请求,消耗大量系统资源。
- 当数据没有准备好时,大量的线程什么都不干,被阻塞在Read操作(非阻塞就一直轮询,相当于CPU空转),线程资源浪费。
我们知道线程是有限的,我们耗费这么多资源在询问数据状态上,是否有些太浪费了?这时候,I/O多路复用模型出现了。
2.I/O多路复用模型
I/O多路复用的实质—创建一个线程来监控多个网络请求fd(Linux将网络请求用fd来描述),这样就可以用一个或几个线程完成数据状态询问的操作,当有数据准备好了,再去唤醒对应的线程进行后续操作。这样就可以节省大量的线程资源了。
如上图所示,IO多路复用模型提供了一个函数去监控多个网络请求fd的数据状态,当数据准备好后这个函数就会返回可读状态,对应的线程被唤醒进行后续操作,这个函数就是我们常说的select,poll和epoll函数。
术语:进程将一个或多个网络请求fd传递给select函数,当数据未准备好时,仅阻塞select操作,由select帮助我们监视fd状态,当有fd准备就绪时,select返回可读状态,线程调用Recvfrom函数读取数据
select函数
当使用select函数时,先通知内核挂起当前进程,然后当一个或多个IO事件发生时,控制权将返回给进程,由进程处理IO。
IO事件包括:
1.网络请求fd可读
2.套接字Socket准备好可以写
3.如果一个IO事件等待超过10s,发生超时
select使用方法:
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
- maxdp参数:等待处理的fd基数,就相当于最大的fd数量加1,比如说我们现在的网络请求fd有{0,1,2},那么maxdp=3+1=4
- readset:读集合,有数据可读时通知内核
- writeset:写集合,有数据可写时通知内核
- exceptset:异常集合,异常发生时告诉内核
但是select函数有一个缺点,就是**它支持的文件描述符/网络请求(fd)是有限的,默认为1024个,**所以引入了poll函数
poll函数
select默认支持1024个fd,数量有限,用poll解决这个问题
我们看一下poll函数的使用方法
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
我们看一下struct pollfd是什么结构
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 描述符待检测的事件 */
short revents; /* returned events */
};
- pollfd包括了文件描述符fd和fd对应的事件,其中事件由二进制格式表示,比如说POLLIN是读事件,POLLOUT是写事件。
#define POLLIN 0x0001 /* any readable data available */
#define POLLPRI 0x0002 /* OOB/Urgent readable data */
#define POLLOUT 0x0004 /* file descriptor is writeable */
- 那么除了fd和events,还有一个revents事件,这个参数表示对事件的备份,相当于poll函数会将每个fd的事件检测结果保留在revents,不用每次都从头到尾检测一遍。(相当于保存每个fd及其事件的状态)
那么poll函数的返回值有哪些呢?
- 可读,内核会通知进程可进行读操作
- 可写,内核会通知进程可进行写操作
- 小于0:表示事件发生之前一直等待
- -1:发生错误
- 0:在规定的时间内没有任何事件发生(超时)
那么poll函数是如何改进select函数支持文件描述符有限的呢?
就是通过控制pollfd的大小来改变支持fd的数量。
epoll函数
我们来看一下面对不同数量的fd,select,poll和epoll函数的性能差异
可以发现select和poll的性能差不多,而epoll在数据量则远超前面两者,它是如何做到的?
epoll通过监控注册多个描述字进行IO事件的分发。不同poll的是,epoll不仅提供默认的level-trigger机制还提供了边缘触发机制。
信号驱动IO模型
复用IO模型解决了一个线程监控一个fd的问题,但是select这些函数采用轮询的方式来监控多个fd,当轮询到状态可读/可写再进行下一步操作;我们会觉得这种方式有点太暴力了,因为大部分情况下的轮询都是无效的。于是有人提出—能不能不要我去询问数据是否准备好,而是你准备好数据之后来告诉我呢?——信号驱动IO模型应运而生。
**信号驱动IO不是用轮询的方式去监控fd状态,而是调用signification时建立一个SIGIO信号联系,当内核中的数据准备完毕后,再通过SIGIO信号通知线程数据可读,当线程接收到信号后,调用recvfrom函数读取数据,进行后续操作。**因为信号驱动IO模型下的应用线程在系统调用signification后即可返回,不会阻塞,所以一个询问线程就可以监控多个fd状态。
术语:首先开启套接字中的信号驱动IO功能,并通过系统调用signification执行一个信号处理函数,此时请求立即返回;当数据准备完毕后,生成对应进程的SIGIO信号,通过信号回调通知应用线程调用recvfrom函数
IO多路复用的select函数,实质是不断的通过轮询来监控fd的状态,而大多数时候的轮询其实是没有意义的,而信号IO驱动模型通过建立信号关联的模式,实现了发出请求后只需要等待数据准备完成的通知即可,避免了大量无意义的轮询操作。
异步IO
我们可以发现,不管是多路复用还是信号驱动,都至少需要两段的操作,首先通过select轮询或者建立信号联系监控状态,然后线程再调用recvfrom函数读取数据。
所以这就引起了人们的思考,我们为什么不能告诉内核我们需要这种数据,内核准备好之后直接发给我。一步到位。
所以有大佬设计了一个方案:应用进程向内核发送一个read请求,告诉内核它需要什么数据之后立即返回;内核收到read请求后会与进程建立一个信号联系,当数据准备就绪后,内核会把数据主动复制到用户空间,当这些操作完成后,内核生成一个通知告诉应用进程。这种方式就叫做异步IO模型
术语:应用告知内核启动某个操作,并让内核再完成这个操作后通知应用。和信号驱动IO的区别在于,信号驱动在收到通知后还要调用recvfrom函数去读取数据,而异步IO是连读数据都由内核完成,一步到位。
异步IO解决了IO多路复用和信号驱动IO的弊端,无需在询问数据状态之后再去读取,而是在数据读取完成后通知应用。应用只需要向内核发起一次请求即可。
总结
- 阻塞IO:我去买衣服,店员告诉我衣服还没到货,我就一直在店里等,等到到货了再去做别的事情
- **非阻塞IO:**还是去买衣服,衣服没到货,我先去做别的事情,比如说打篮球,吃饭等等,做完这些事情后再来询问衣服到货没有,到了就买下来,没到货就继续做别的事情
- IO多路复用:去买衣服,还是没到货,这次学聪明了,请了个小弟在那帮我问,每隔一分钟就去问一次,如果没到货就等一分钟再问,如果问到了马上通知我过来买
- 信号驱动IO:上次请的小弟说我黑心,没必要隔一分钟问一次,基本都是没有意义的问,所以我买通了店员,让它一到货就通知我过来买衣服
- 异步IO:我发财了,直接花三倍价钱,让店长在衣服到的时候给我送到家里,毕竟有钱随便操作。