Fork me on GitHub

随笔分类 - web

详解网络连接

[TOC] 一直以来,对于网络连接中的细节都不是很清楚,最近特意梳理了一下,大部分内容来自书籍网络是怎样连接的(户根勒) 首先看下连接的整体流程 ![](http://markdown.archerwong.cn/2018-12-18-08-21-57_clipboard.png) # 1.输入URL 在浏览器中输入URL网址就可以得到我们想要的网页,这有两个要素,浏览器和URL网址。浏览器是一个具备多种客户端功能的综合性客户端软件,它需要一些东西来判断应该使用其中哪种功能来访问相应的数据, 而各种不同的URL 就是用来干这个的, 比如访问 Web 服务器时用“http:”, 而访问 FTP服务器时用“ ftp:”。有了url浏览器就会通过我们url定义的规则去访问我们需要的资源。 下图是几种URL的含义 ![](http://markdown.archerwong.cn/2018-12-18-08-22-18_clipboard.png) # 2.DNS域名解析 我们知道域名背后指向的是服务器地址,显然这种关系不是凭空产生的,需要我们自己来定义,所以就有了域名解析。通过域名解析可以把域名指向对应的服务器地址,构成映射关系,这样别人就可以通过域名找到你的主机了。如果你有购买过域名,服务商肯定会提供域名解析管理页面,类似下面,这样你就可以根据自己的情况,定义想要的映射关系。 ![](http://markdown.archerwong.cn/2018-12-18-08-22-40_clipboard.png) ## 2.1 为什么要用域名和ip地址的组合呢? 域名是给人来用的,为了便于记忆,人们要记住一串由数字组成的ip地址是十分困难的。ip地址是给机械使用的,在信息传递过程中,存在无数的路由器, 它们之间相互配合, 根据 IP地址来判断应该把数据传送到什么地方。IP 地址的长度为 32 比特, 也就是 4 字节,相对地, 域名最短也要几十个字节, 最长甚至可以达到 255 字节。 换句话说, 使用 IP 地址只需要处理 4 字节的数字,而域名则需要处理几十个到 255个字节的字符,这增加了路由器的负担,传送数据也会花费更长的时间。所以需要有一个机制能够通过名称来查询 IP 地址, 或者通过 IP地址来查询名称,这样就能够在人和机器双方都不做出牺牲的前提下完美地解决问题。这个机制就是 DNS。 ## 2.2 DNS域名解析过程 DNS 中的域名都是用句点来分隔的, 比如 www.lab.glasscom.com, 这里的句点代表了不同层次之间的界限。 在域名中, 越靠右的位置表示其层级越高, 比如 www.lab.glasscom.com 这个域名如果按照公司里的组织结构来说, 大概就是“ com 事业集团 glasscom 部 lab 科的 www”这样。 其中, 相当于一个层级的部分称为域。 因此, com 域的下一层是glasscom 域, 再下一层是 lab 域, 再下面才是 www 这个名字。真是因为这种层次的关系,上级保存下级的信息,所以我们只需找到最上层的信息就可以找到所有的信息了。com,cn,jp这样的算得上是顶层DNS服务器,但实际上上面还有一层就是根域,根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。 下图表示的就是DNS的查询过程,先找到最近的DNS服务器(本地DNS服务器,也就是客户端TCP/IP设置中填写的DNS服务器地址),然后再去访问根域DNS服务器,然后一层一层往下找。 ![](http://markdown.archerwong.cn/2018-12-18-08-23-02_clipboard.png) 在实际的情况更复杂,为了加快查找,有些层会有缓存,这样就不一定要从根域名开始往下找,大体流程如下。 1> 首先,检查浏览器是否有缓存 浏览器本身是会缓存域名的,但是这是有限制的,不仅浏览器缓存大小有限制,而且缓存的时间也有限制。这个缓存时间太长和太短都不好,如果缓存时间太长,一旦域名被解析到的IP有变化,会导致被客户端缓存的域名无法解析到变化后的IP地址,以致该域名不能正常解析。 2> 其次,检查系统缓存 + hosts 操作系统自身也会有dns缓存 我们可以通过设置hosts文件内容来进行域名绑定 3> 如果以上都没有命中,那么就会检查本地DNS服务器(LDNS) LDNS一般是电信运营商提供的,也可以使用像Google提供的DNS服务器。 在我们的网络配置中都会有“DNS服务器地址”这一项,这个地址就用于解决前面所说的如果两个过程无法解析时要怎么办,操作系统会把这个域名发送给这里设置的LDNS,也就是本地区的域名服务器。这个DNS通常都提供给你本地互联网接入的一个DNS解析服务。大约80%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。 4> 依然没有命中,则直接查找最顶端的根域DNS服务器 根域DNS服务器会返回顶层DNS服务器地址给本地DNS服务器。 5> 接下来,访问 4 步中返回的顶层DNS服务器 本地DNS服务器拿到DNS顶层服务器地址后会去发送请求,顶层DNS服务器查询后返回管理方DNS服务器地址给本地DNS服务器。 6> 最后,访问 5 步中返回的管理方DNS服务器 本地DNS服务器拿到管理方DNS服务器地址后会去发送请求,管理方DNS服务器查询存储的域名和IP的映射关系表,并返回给本地DNS服务器,本地DNS服务器返回该域名对应的IP和TTL值给浏览器。 ## 2.3 DNS查询IP的底层原理 下面是原理的伪代码,可以按照这个图简单分析,实际DNS查询和TCP连接建立比较类似,这里可以先简单了解,后面会详细介绍 ![](http://markdown.archerwong.cn/2018-12-18-08-23-25_clipboard.png) 从上图我们可以了解到的信息有 1> 从上到下分别是应用程序,Socket库,操作系统协议栈,网卡。。。 2> 浏览器要获取IP地址,先会去调用Socket库(就是一堆通用程序组件的集合,其他的应用程序都需要使用其中的组件)中的域名解析器,并预留将返回的结果存在应用程序的内存中。 3> 浏览器和Socket库并不具备使用网络收发数据的功能,需要使用协议栈执行发送消息的操作, 然后通过网卡将消息发送给 DNS服务器。并且我们注意到这里了使用的是UDP协议(后面章节的建立连接会讲到TCP握手,这里并不是使用TCP协议)。 4> 查询到后dns服务器会一层一层的返回数据信息。 # 3 建立TCP连接,(4 发送请求,7 断开连接) 这里会将步骤3 4 7 连起来一起讲解,这样更利于整体把握。 知道了 IP 地址之后, 就可以委托操作系统内部的协议栈向这个目标 IP 地址, 也就是我们要访问的 Web 服务器发送建立连接的请求了。要更好的理解这部分内容就要对TCP/IP软件的分层结构有个认识。 ![](http://markdown.archerwong.cn/2018-12-18-08-23-51_clipboard.png) 分为不同的层次,分别承担不同的功能。上面部分会向下面部分委派工作,下面部分接受委派并进行实际执行,这一上下关系只是一个总体的规则, 其中也有一部分上下关系不明确, 或者上下关系相反的情况, 所以也不必过于纠结。 可以按照 Socket库 -> 协议栈 -> 网卡驱动程序 -> 网卡 -> 网络 这个大体的思路进行分析。 1> 最上面部分是网络应用程序,比如浏览器,电子邮件客户端,web服务器等。 2> 然后是Socket库,前面DNS解析是提过,包括域名解析器,用来向DNS服务器发出查询,还有其他一些其他功能,后面章节会提及。 3> 再下面是操作系统内部,其中包括协议栈,协议栈的上半部分有两块, 分别是负责用 TCP 协议收发数据的部分和负责用 UDP 协议收发数据的部分, 它们会接受应用程序的委托执行收发数据的操作。下面一半是用 IP 协议控制网络包收发操作的部分。 在互联网上传送数据时, 数据会被切分成一个一个的网络包 , 而将网络包发送给通信对象的操作就是由 IP 来负责的。 此外, IP 中还包括ICMP协议和 ARP 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息, ARP 用于根据 IP 地址查询相应的以太网 MAC 地址 。 4> IP 下面的网卡驱动程序负责控制网卡硬件, 而最下面的网卡则负责完成实际的收发操作, 也就是对网线中的信号执行发送和接收的操作。 ## 3.1 收发数据的全貌 ![](http://markdown.archerwong.cn/2018-12-18-08-24-11_clipboard.png) 我们可以把数据通道想象成一条管道, 将数据从一端送入管道, 数据就会到达管道的另一端然后被取出。 数据可以从任何一端被送入管道, 数据的流动是双向的。 不过, 这并不是说现实中真的有这么一条管道, 只是为了帮助大家理解数据收发操作的全貌,要注意到这些操作并不是浏览器等程序完成的,而是由协议栈来代劳的,向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用 Socket 库中的程序组件。。 关于这条管道,大致总结以下 4 个阶段。 ( 1)创建套接字(创建套接字阶段) ( 2)将管道连接到服务器端的套接字上(连接阶段) ( 3)收发数据(通信阶段) ( 4)断开管道并删除套接字(断开阶段) 我们事先提取几个关键词,简单理解下, 1> 应用程序:浏览器等。 2> Socket库:应用程序会通过调用Socket库向协议栈发出委托。 3> 协议栈:协议栈负责数据的收发工作。 4> 套接字:于管道两端的数据出入口,协议栈是根据套接字中记录的控制信息来工作的,在协议栈内部有一块用于存放控制信息的内存空间, 这里记录了用于控制通信操作的控制信息, 例如通信对象的 IP 地址、 端口号、 通信操作的进行状态等。协议栈则需要根据这些信息判断下一步的行动。 5> 描述符:套接字的唯一标识,号码牌 注意:为了便于整体把握,我们将这四个阶段一起讲解。下图是收发数据的底层原理伪代码 ![](http://markdown.archerwong.cn/2018-12-18-08-24-31_clipboard.png) ## 3.2 创建套接字阶段 浏览器调用socket库提出创建套接字申请,然后协议栈根据申请执行创建套接字的操作,这个过程中,协议栈首先会分配一个用于存放套接字的内存空间,相当于给控制信息准备一个容器,我们需要往其中写入控制信息。创建之初,数据收发还没开始,先向该内存空间写入初始状态控制信息,这样就完成了创建套接字的操作。 在我们创建套接字的同时,每个套接字就有了唯一编号,称之为描述符,接下来需要将这个套接字的描述符告知应用程序。之后,应用程在向协议栈收发数据委托的时候就需要提供这个描述符,这个描述符可以确定相应的套接字,套接字中又有相关信息,这样一来,只需提供这个描述符,应用程序就不用每次都告诉协议栈应该和谁进行通信了。 ## 3.3 连接阶段 套接字创建之后,应用程序会调用connect,随后协议栈会将本地的套接字和服务器的套接字进行连接。在连接操作过程中,还需要提前分配一块用来临时存放要收发的数据的内存空间,这块空间被称为缓冲区,该缓冲区在执行数据收发操作的时候要用到。 **分别看下客户端和服务端的情况** 客户端:套接字创建之初,里面只有初始信息,不知道通讯的对象是谁,这时即便想要发送数据,协议栈也不知道应该将数据发给谁。但是其实应用程序是知道通讯对象的,通过DNS域名解析得到目标服务器的IP地址,同时不同的服务占用服务器不同的端口,一般来说这都是预先定好的,这些能帮助我们找到通讯对象。应用程序知道,协议栈不知道通讯对象,是因为在调用 socket 创建套接字时, 这些必要信息并没有传递给协议栈,所以我们需要将这些信息告诉协议栈,这也是连接操作的目的之一。 服务端:同样也会创建套接字,初始阶段也不知道要和谁通讯,甚至连服务端应用程序都不知道应该和谁通讯,所以一般来说服务器会一直监听等待客户端的通讯,客户端在通讯的过程中会带着通讯的必要信息。 ### 3.3.1 调用connect 调用 connect 时,需要指定描述符、服务器 IP 地址和端口号这 3 个参数。 第 1 个参数, 即描述符,connect 会将应用程序指定的描述符告知协议栈, 然后协议栈根据这个描述符来判断到底使用哪一个套接字去和服务器端的套接字进行连接, 并执行连接的操作。 第 2 个参数, 即服务器 IP 地址, 就是通过 DNS 服务器查询得到的我们要访问的服务器的 IP 地址。 第 3 个参数, 即端口号,同时指定 IP 地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。 **这里思考两个问题** 问题1:到目前我们了解到能够识别套接字有两套机制,一种是描述符,另一种是IP+端口号,那么他们有什么区别呢? 1> 描述符是用来在一台计算机内部识别套接字的机制, 比如浏览器和协议栈之间 2> 那么IP+端口号就是用来让通信的另一方能够识别出套接字的机制,比如客户端和服务端之间,实际上web端和服务端又会有不同,后面第5章节介绍WEB服务器原理时会讲到。 问题2:服务端和客户端使用端口号的区别? 1> 服务器上所使用的端口号是根据应用的种类事先规定好的, 仅此而已。 比如 Web 是 80 号端口, 电子邮件是 25 号端口。 2> 客户端在创建套接字时, 协议栈会为这个套接字随便分配一个端口号 。 接下来, 当协议栈执行连接操作时, 会将这个随便分配的端口号通知给服务器。 ### 3.3.2 通讯双方交换控制信息 连接操作实际上是通讯双方交换控制信息,通讯操作使用的控制信息大体可以分为两类, 一类就是我们上面提到的保存在套接字中的信息,用来控制协议栈操作。 还有一类就是客户端和服务器相互联络时交换的控制信息,称为TCP头部信息,这些信息不仅连接时需要, 包括数据收发和断开连接操作在内, 整个通信过程中都需要,这些内容在 TCP 协议的规格中进行了定义。注意区分IP头部,以太网头部,TCP头部是不同的头部。 ![](http://markdown.archerwong.cn/2018-12-18-08-24-55_clipboard.png) ### 3.3.3 连接操作的实际过程 下面来看一下具体的操作过程。 这个过程是从应用程序调用 Socket 库的 connect 开始的。 connect( < 描述符 >, < 服务器 IP 地址和端口号 >, …) 上面的调用提供了服务器的 IP 地址和端口号, 这些信息会传递给协议栈中的 TCP 模块。 然后, TCP 模块会与该 IP 地址对应的对象, 也就是与服务器的 TCP 模块交换控制信息, 这一交互过程包括下面几个步骤。 首先, 客户端先创建一个包含表示开始数据收发操作的控制信息的头部。 头部包含很多字段, 这里要关注的重点是发送方和接收方的端口号。 到这里, 客户端(发送方)的套接字就准确找到了服务器(接收方)的套接字, 也就是搞清楚了我应该连接哪个套接字。 然后, 我们将头部中的控制位的 SYN 比特设置为 1,大家可以认为它表示连接 。 此外还需要设置适当的序号和窗口大小。 当 TCP 头部创建好之后, 接下来 TCP 模块会将信息传递给 IP 模块并委托它进行发送 。 IP 模块执行网络包发送操作后,网络包就会通过网络到达服务器, 然后服务器上的 IP 模块会将接收到的数据传递给 TCP 模块,服务器的 TCP 模块根据 TCP 头部中的信息找到端口号对应的套接字, 也就是说, 从处于等待连接状态的套接字中找到与 TCP 头部中记录的端口号相同的套接字就可以了。 当找到对应的套接字之后, 套接字中会写入相应的信息, 并将状态改为正在连接 。 上述操作完成后, 服务器的 TCP 模块会返回响应, 这个过程和客户端一样, 需要在 TCP 头部中设置发送方和接收方端口号以及 SYN 比特。 此外,在返回响应时还需要将 ACK 控制位设为1, 这表示已经接收到相应的网络包。 网络中经常会发生错误, 网络包也会发生丢失, 因此双方在通信时必须相互确认网络包是否已经送达 , 而设置ACK 比特就是用来进行这一确认的。 接下来, 服务器 TCP 模块会将 TCP头部传递给 IP 模块, 并委托 IP 模块向客户端返回响应。 然后, 网络包就会返回到客户端, 通过 IP 模块到达 TCP 模块, 并通过 TCP 头部的信息确认连接服务器的操作是否成功。 如果 SYN 为 1 则表示连接成功, 这时会向套接字中写入服务器的 IP 地址、 端口号等信息, 同时还会将状态改为连接完毕。 到这里, 客户端的操作就已经完成, 但其实还剩下最后一个步骤。 刚才服务器返回响应时将 ACK 比特设置为 1, 相应地,客户端也需要将 ACK 比特设置为 1 并发回服务器, 告诉服务器刚才的响应包已经收到。 当这个服务器收到这个返回包之后, 连接操作才算全部完成。现在, 套接字就已经进入随时可以收发数据的状态了, 大家可以认为这时有一根管子把两个套接字连接了起来。 以上实际就是大名鼎鼎的TCP三次握手,下面是图解 ![](http://markdown.archerwong.cn/2018-12-18-08-25-22_clipboard.png) ## 3.4 通讯阶段 当套接字连接起来之后, 剩下的事情就简单了。 只要将数据送入套接字, 数据就会被发送到对方的套接字中。 当然, 应用程序无法直接控制套接字, 因此还是要通过 Socket 库委托协议栈来完成这个操作。 这个操作需要使用 write 这个程序组件,需要指定描述符和发送数据, 然后协议栈就会将数据发送到服务器。 由于套接字中已经保存了已连接的通信对象的相关信息, 所以只要通过描述符指定套接字, 就可以识别出通信对象, 并向其发送数据。 协议栈并不关心应用程序传来的数据是什么内容。 应用程序在调用 write 时会指定发送数据的长度, 在协议栈看来, 要发送的数据就是一定长度的二进制字节序列而已。 ### 3.4.1 协议栈何时发送网络包 协议栈并不是一收到数据就马上发送出去, 而是会将数据存放在内部的发送缓冲区中, 并等待应用程序的下一段数据。 应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据,协议栈并不能控制这一行为。 如果一收到数据就马上发送出去, 就可能会发送大量的小包, 导致网络效率下降, 因此需要在数据积累到一定量时再发送出去。 至于要积累多少数据才能发送, 不同种类和版本的操作系统会有所不同, 不能一概而论, 但都是根据下面几个要素来判断的。 第一个判断要素是每个网络包能容纳的数据长度, 协议栈会根据一个叫作 MTUA 的参数来进行判断。 MTU 表示一个网络包的最大长度, 在以太网中一般是 1500 字节。 MTU 是包含头部的总长度, 因此需要从MTU 减去头部的长度, 然后得到的长度就是一个网络包中所能容纳的最大数据长度, 这一长度叫作 MSSC。 当从应用程序收到的数据长度超过或者接近 MSS 时再发送出去, 就可以避免发送大量小包的问题了 另一个判断要素是时间。 当应用程序发送数据的频率不高的时候, 如果每次都等到长度接近 MSS 时再发送, 可能会因为等待时间太长而造成发送延迟, 这种情况下, 即便缓冲区中的数据长度没有达到 MSS, 也应该果断发送出去。 为此, 协议栈的内部有一个计时器, 当经过一定时间之后,就会把网络包发送出去。 判断要素就是这两个, 但它们其实是互相矛盾的。 如果长度优先, 那么网络的效率会提高, 但可能会因为等待填满缓冲区而产生延迟; 相反地,如果时间优先, 那么延迟时间会变少, 但又会降低网络的效率。 因此, 在进行发送操作时需要综合考虑这两个要素以达到平衡。 协议栈也给应用程序保留了控制发送时机的余地。 应用程序在发送数据时可以指定一些选项, 比如如果指定“ 不等待填满缓冲区直接发送”, 则协议栈就会按照要求直接发送数据。 像浏览器这种会话型的应用程序在向服务器发送数据时, 等待填满缓冲区导致延迟会产生很大影响, 因此一般会使用直接发送的选项。 ### 3.4.2 对较大数据进行拆分 HTTP 请求消息一般不会很长, 一个网络包就能装得下, 但如果其中要提交表单数据, 长度就可能超过一个网络包所能容纳的数据量,这种情况下, 发送缓冲区中的数据就会超过 MSS 的长度, 这时我们当然不需要继续等待后面的数据了。 发送缓冲区中的数据会被以 MSS 长度为单位进行拆分, 拆分出来的每块数据会被放进单独的网络包中。 ![](http://markdown.archerwong.cn/2018-12-18-08-25-43_clipboard.png) ### 3.4.3使用ACK号确认网络包已收到 首先, TCP 模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节, 接下来在发送这一块数据时, 将算好的字节数写在 TCP 头部中,“ 序号” 字段就是派在这个用场上的。 然后, 发送数据的长度也需要告知接收方, 不过这个并不是放在TCP 头部里面的, 因为用整个网络包的长度减去头部的长度就可以得到数据的长度, 所以接收方可以用这种方法来进行计算。 ![](http://markdown.archerwong.cn/2018-12-18-08-26-09_clipboard.png) 在实际的通信中,序号并不是从 1 开始的, 因为如果序号都从 1 开始, 通信过程就会非常容易预测,所以初始值是随机的,这样对方就搞不清楚序号到底是从多少开始计算的, 因此需要在开始收发数据之前将初始值告知通信对象。 实际上, 连接操作中在将 SYN 设为 1 的同时, 还需要同时设置序号字段的值, 而这里的值就代表序号的初始值 首先, 客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值, 并将这个值发送给服务器(①)。 接下来, 服务器会通过这个初始值计算出 ACK 号并返回给客户端(②)。 初始值有可能在通信过程中丢失, 因此当服务器收到初始值后需要返回 ACK 号作为确认。 同时, 服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值, 并将这个值发送给客户端(图 2.9 ②)。 接下来像刚才一样, 客户端也需要根据服务器发来的初始值计算出 ACK 号并返回给服务器( ③)。 到这里, 序号和 ACK 号都已经准备完成了,实际上这也是上面提到的TCP三次握手的内容。 接下来就可以进入数据收发阶段了。 数据收发操作本身是可以双向同时进行的, 但 Web 中是先由客户端向服务器发送请求, 序号也会跟随数据一起发送( ④)。 然后, 服务器收到数据后再返回 ACK 号(⑤)。 从服务器向客户端发送数据的过程则正好相反(⑥⑦)。 ![](http://markdown.archerwong.cn/2018-12-18-08-26-41_clipboard.png) 通过“序号”和“ACK 号”可以确认接收方是否收到了网络包。TCP 采用这样的方式确认对方是否收到了数据, 在得到对方确认之前, 发送过的包都会保存在发送缓冲区中。 如果对方没有返回某些包对应的 ACK 号, 那么就重新发送这些包。 ### 3.4.4 根据网络包平均往返时间调整 ACK 号等待时间 TCP 采用了动态调整等待时间的方法, 这个等待时间是根据 ACK 号返回所需的时间来判断的。 具体来说, TCP 会在发送数据的过程中持续测量 ACK 号的返回时间, 如果 ACK 号返回变慢, 则相应延长等待时间; 相对地, 如果 ACK 号马上就能返回, 则相应缩短等待时间。 ### 3.4.5 使用窗口有效管理 ACK 号 每发送一个包就等待一个 ACK 号的方式是最简单也最容易理解的, 但在等待 ACK 号的这段时间中, 如果什么都不做那实在太浪费了。 为了减少这样的浪费, TCP 采用滑动窗口方式来管理数据发送和 ACK 号的操作。 所谓滑动窗口, 就是在发送一个包之后, 不等待 ACK 号返回, 而是直接发送后续的一系列包。 这样一来, 等待 ACK 号的这段时间就被有效利用起来了。 ![](http://markdown.archerwong.cn/2018-12-18-08-27-04_clipboard.png) 这种方式效率高,但是也会有个问题,如果不等返回 ACK 号就连续发送包, 就有可能会出现发送包的频率超过接收方处理能力的情况。可以通过下面的方法来避免这种情况的发生。 首先, 接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制, 这就是滑动窗口方式的基本思路。图中, 接收方将数据暂存到接收缓冲区中并执行接收操作。 当接收操作完成后, 接收缓冲区中的空间会被释放出来, 也就可以接收更多的数据了,这时接收方会通过 TCP 头部中的窗口字段将自己能接收的数据量告知发送方。 这样一来, 发送方就不会发送过多的数据, 导致超出接收方的处理能力了。 ![](http://markdown.archerwong.cn/2018-12-18-08-27-28_clipboard.png) ### 3.4.6 ACK 与窗口的合并 接收方在发送 ACK 号和窗口更新时, 并不会马上把包发送出去, 而是会等待一段时间, 在这个过程中很有可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里面发送了。当需要连续发送多个 ACK 号时, 也可以减少包的数量, 这是因为 ACK 号表示的是已收到的数据量, 也就是说, 它是告诉发送方目前已接收的数据的最后位置在哪里, 因此当需要连续发送 ACK 号时, 只要发送最后一个 ACK 号就可以了, 中间的可以全部省略。当需要连续发送多个窗口更新时也可以减少包的数量, 因为连续发生窗口更新说明应用程序连续请求了数据, 接收缓冲区的剩余空间连续增加。 这种情况和 ACK 号一样, 可以省略中间过程, 只要发送最终的结果就可以了。 ### 3.4.7 接收HTTP响应消息 当消息返回后, 需要执行的是接收消息的操作。 接收消息的操作是通过 Socket 库中的 read 程序组件委托协议栈来完成的。 首先, 协议栈尝试从接收缓冲区中取出数据并传递给应用程序, 但这个时候请求消息刚刚发送出去, 响应消息可能还没返回。 响应消息的返回还需要等待一段时间, 因此这时接收缓冲区中并没有数据, 那么接收数据的操作也就无法继续。 这时, 协议栈会将应用程序的委托, 也就是从接收缓冲区中取出数据并传递给应用程序的工作暂时挂起, 等服务器返回的响应消息到达之后再继续执行接收操作。 协议栈接收数据的具体操作过程简单总结一下 。 首先,协议栈会检查收到的数据块和 TCP 头部的内容, 判断是否有数据丢失, 如果没有问题则返回 ACK 号。 然后,协议栈将数据块暂存到接收缓冲区中, 并将数据块按顺序连接起来还原出原始的数据, 最后将数据交给应用程序。 具体来说, 协议栈会将接收到的数据复制到应用程序指定的内存地址中, 然后将控制流程交回应用程序。将数据交给应用程序之后, 协议栈还需要找到合适的时机向发送方发送窗口更新。 ## 3.5 断开阶段 收发数据结束的时间点应该是应用程序判断所有数据都已经发送完毕的时候。协议栈在设计上允许任何一方先发起断开过程,客户端和服务端都可以断开连接。无论哪种情况, 完成数据发送的一方会发起断开过程, 这里我们以服务器一方发起断开过程为例来进行讲解。 ![](http://markdown.archerwong.cn/2018-12-18-08-27-53_clipboard.png) 第一次挥手(FIN=1,seq=u) 假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。 发送完毕后,客户端进入 FIN_WAIT_1 状态。 第二次挥手(ACK=1,ACKnum=u+1) 服务器端确认客户端的 FIN包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。 发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。 第三次挥手(FIN=1,seq=w) 服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN置为1。 发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。 第四次挥手(ACK=1,ACKnum=w+1) 客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK包。 服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。 客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED状态。 网络上比较主流的文章都说关闭TCP会话是四次挥手,但是实际上为了提高效率通常合并第二、三次的挥手,即三次挥手。 和服务器的通信结束之后, 用来通信的套接字也就不会再使用了, 这时我们就可以删除这个套接字了。 不过, 套接字并不会立即被删除, 而是会等待一段时间之后再被删除,等待这段时间是为了防止误操作, 引发误操作的原因有很多。 比如,最后客户端返回的 ACK 号丢失了, 结果会如何呢? 这时, 服务器没有接收到 ACK 号, 可能会重发一次 FIN。 如果这时客户端的套接字已经删除了, 会发生什么事呢? 套接字被删除, 那么套接字中保存的控制信息也就跟着消失了, 套接字对应的端口号就会被释放出来。 这时, 如果别的应用程序要创建套接字, 新套接字碰巧又被分配了同一个端口号,而服务器重发的 FIN 正好到达, 会怎么样呢? 本来这个 FIN 是要发给刚刚删除的那个套接字的, 但新套接字具有相同的端口号, 于是这个 FIN 就会错误地跑到新套接字里面, 新套接字就开始执行断开操作了。 之所以不马上删除套接字, 就是为了防止这样的误操作。 ## 3.7 小结 最后整体回顾下这一章节讲的TCP收据收发全过程 ![](http://markdown.archerwong.cn/2018-12-18-08-28-26_clipboard.png) 数据收发操作的第一步是创建套接字。 一般来说, 服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。 客户端则一般是在用户触发特定动作, 需要访问服务器的时候创建套接字。 在这个阶段,还没有开始传输网络包。 创建套接字之后, 客户端会向服务器发起连接操作。 首先, 客户端会生成一个 SYN 为 1 的 TCP 包并发送给服务器(图 2.13 ①)。 这个 TCP 包的头部还包含了客户端向服务器发送数据时使用的初始序号, 以及服务器向客户端发送数据时需要用到的窗口大小 A。 当这个包到达服务器之后, 服务器会返回一个 SYN 为 1 的 TCP 包(图 2.13 ②)。 和图 2.13 ①一样, 这个包的头部中也包含了序号和窗口大小, 此外还包含表示确认已收到包①的ACK 号 B。 当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的 ACK 号的 TCP 包(图 2.13 ③)。 到这里, 连接操作就完成了, 双方进入数据收发阶段。 数据收发阶段的操作根据应用程序的不同而有一些差异, 以 Web 为例, 首先客户端会向服务器发送请求消息。 TCP 会将请求消息切分成一定大小的块, 并在每一块前面加上 TCP 头部, 然后发送给服务器(图 2.13 ④)。TCP 头部中包含序号, 它表示当前发送的是第几个字节的数据。 当服务器收到数据时, 会向客户端返回 ACK 号(图 2.13 ⑤)。 在最初的阶段, 服务器只是不断接收数据, 随着数据收发的进行, 数据不断传递给应用程序,接收缓冲区就会被逐步释放。 这时, 服务器需要将新的窗口大小告知客户端。 当服务器收到客户端的请求消息后, 会向客户端返回响应消息, 这个过程和刚才的过程正好相反(图 2.13 ⑥⑦)。 服务器的响应消息发送完毕之后, 数据收发操作就结束了, 这时就会开始执行断开操作。 以 Web 为例,服务器会先发起断开过程 A。 在这个过程中, 服务器先发送一个 FIN 为 1 的 TCP 包(图 2.13 ⑧), 然后客户端返回一个表示确认收到的 ACK 号(图 2.13 ⑨)。 接下来, 双方还会交换一组方向相反的 FIN 为 1 的 TCP 包(图 2.13 ⑩)和包含 ACK 号的 TCP 包(图 2.13k)。最后, 在等待一段时间后, 套接字会被删除。 # 5 WEB服务器 ## 5.1 服务器程序的结构 服务器需要同时和多个客户端通信, 但一个程序来处理多个客户端的请求是很难的, 因为服务器必须把握每一个客户端的操作状态。 因此一般的做法是, 每有一个客户端连接进来, 就启动一个新的服务器程序, 确保服务器程序和客户端是一对一的状态。 具体来说, 服务器程序的结构如图 6.1 所示。 首先, 我们将程序分成两个模块, 即等待连接模块(图 6.1( a))和负责与客户端通信的模块(图6.1 ( b))A。 当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块( a)。 这个模块会创建套接字, 然后进入等待连接的暂停状态。 接下来, 当客户端连发起连接时, 这个模块会恢复运行并接受连接,然后启动客户端通信模块( b), 并移交完成连接的套接字。 接下来, 客户端通信模块( b)就会使用已连接的套接字与客户端进行通信, 通信结束后,这个模块就退出了。 ![](http://markdown.archerwong.cn/2018-12-18-08-29-24_clipboard.png) 每次有新的客户端发起连接, 都会启动一个新的客户端通信模块( b),因此( b)与客户端是一对一的关系。 这样,( b)在工作时就不必考虑其他客户端的连接情况, 只要关心自己对应的客户端就可以了。 通过这样的方式, 可以降低程序编写的难度。 服务器操作系统具有多任务 A、 多线程 B 功能, 可以同时运行多个程序 C, 服务器程序的设计正是利用了这一功能。 当然, 这种方法在每次客户端发起连接时都需要启动新的程序, 这个过程比较耗时, 响应时间也会相应增加。 因此, 还有一种方法是事先启动几个客户端通信模块, 当客户端发起连接时, 从空闲的模块中挑选一个出来将套接字移交给它来处理。后面会研究nginx服务器的工作方式。 ## 5.2 服务端的具体工作过程 伪代码如下 ![](http://markdown.archerwong.cn/2018-12-18-08-29-46_clipboard.png) 首先, 协议栈调用 socket 创建套接字(图 6.2( 1)), 这一步和客户端是相同的。 接下来调用 bind 将端口号写入套接字中(图 6.2( 2-1))。 在客户端发起连接的操作中, 需要指定服务器端的端口号。设置好端口号之后, 协议栈会调用 listen 向套接字写入等待连接状态这一控制信息(图 6.2( 2-1))。 这样一来, 套接字就会开始等待来自客户端的连接网络包。 然后, 协议栈会调用 accept 来接受连接(图 6.2( 2-2))。 由于等待连接的模块在服务器程序启动时就已经在运行了, 所以在刚启动时, 应该还没有客户端的连接包到达。 可是, 包都没来就调用 accept 接受连接, 可能大家会感到有点奇怪, 不过没关系, 因为如果包没有到达, 就会转为等待包到达的状态, 并在包到达的时候继续执行接受连接操作。 因此, 在执行accept 的时候, 一般来说服务器端都是处于等待包到达的状态, 这时应用程序会暂停运行。 在这个状态下, 一旦客户端的包到达, 就会返回响应包并开始接受连接操作。 接下来, 协议栈会给等待连接的套接字复制一个副本, 然后将连接对象等控制信息写入新的套接字中(图 6.3)。 刚才我们介绍了调用 accept 时的工作过程, 到这里, 我们就创建了一个新的套接字,并和客户端套接字连接在一起了。 当 accept 结束之后, 等待连接的过程也就结束了, 这时等待连接模块会启动客户端通信模块, 然后将连接好的新套接字转交给客户端通信模块,由这个模块来负责执行与客户端之间的通信操作。 之后的数据收发操作和刚才说的一样, 与客户端的工作过程是相同的。 问题1:在复制出一个新的套接字之后, 原来那个处于等待连接状态的套接字会怎么样呢? 其实它还会以等待连接的状态继续存在, 当再次调用 accept, 客户端连接包到达时, 它又可以再次执行接受连接操作。 问题2:服务端新创建的套接字副本和原来的等待连接的套接字具有相同的端口号,这样不会有问题吗? 端口号是用来识别套接字的, 但是,实际上服务端新创建的套接字副本和原来的等待连接的套接字具有相同的端口号。然而这并不会有问题,要确定某个套接字时, 不仅使用服务器端套接字对应的IP地址和端口号, 还同时使用客户端的端口号再加上 IP 地址, 总共使用下面 4 种信息来进行判断。服务器上可能存在多个端口号相同的套接字, 但客户端的套接字都是对应不同端口号的, 因此我们可以通过客户端的端口号来确定服务器上的某个套接字。 ## 5.3 nginx服务器是如何接收消息的 Nginx 采用的是多进程(单线程) & 多路IO复用模型。使用了 I/O 多路复用技术的 Nginx,就成了”并发事件驱动“的服务器。这里我们探讨下和上面联系比较紧密的多进程工作模式。 我们看下nginx的多进程工作模式 ![](http://markdown.archerwong.cn/2018-12-18-08-30-15_clipboard.png) 1、Nginx 在启动后,会有一个 master 进程和多个相互独立的 worker 进程。 2、接收来自外界的信号,向各worker进程发送信号,每个进程都有可能来处理这个连接。 3、 master 进程能监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动启动新的 worker 进程。 使用多进程模式,不仅能提高并发率,而且进程之间相互独立,一个 worker 进程挂了不会影响到其他 worker 进程。 ### 5.3.1 惊群现象 主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。 那么,由于所有子进程都继承了父进程的 sockfd,那么当连接进来时,所有子进程都将收到通知并“争着”与它建立连接,这就叫“惊群现象”。大量的进程被激活又挂起,只有一个进程可以accept() 到这个连接,这当然会消耗系统资源。 ### 5.3.2 Nginx对惊群现象的处理 Nginx 提供了一个 accept_mutex 这个东西,这是一个加在accept上的一把共享锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept()。有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),这样就不会有惊群问题了。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。 # 6 语言解析:PHP-FPM nginx服务器起得作用实际上是内容分发,我们会有很多请求,比如我们请求一个静态图片,只需要找到相应目录下的文件即可,但是如果有个php请求,那么这就超出了nginx的能力范围,需要有人专门处理php请求。 如果你有这方面经验,那么在nginx的虚拟主机配置中有这么一段 ``` 21 location ~ \.php$ { 22 fastcgi_pass 127.0.0.1:9000; 23 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 24 include fastcgi_params; 25 } ``` 这段配置的意思就是,如果是php请求,那么nginx就将请求发送给本机的9000端口处理,关于fastcgi_params的两句是定义nginx变量和fastcgi变量的关系,在/etc/nginx/目录下会有个 fastcgi_params文件,可以打开看下,比较简单。 这里的主角其实就是监听9000端口的php-fpm,这个端口是可以通过配置文件自己定义的,一般默认使用9000,service php-fpm start 运行后,查看9000端口情况 ``` $ netstat -anlp | grep 9000 tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 750/php-fpm: master ``` 讲到这里,我们要了解下这段历史,区分几个概念 1> CGI CGI全称是“公共网关接口”(Common Gateway Interface),HTTP服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上。 CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等。 服务器接收到请求后,如果是index.html这样的静态文件,可以直接去相应的目录找到这个文件,然后返回给客户端,但是当发送的请求是index.php这样请求,显然这个是需要解析的,此时就需要服务器将这个请求传递给cgi程序解析,解析完成后返回结果。但是要传递什么内容呢,这个就是cgi来规定的。 2> Fastcgi Fastcgi是用来提高CGI程序性能的,是CGI的升级版,一种语言无关的协议 服务器每次将请求传递给cig程序解析的时候都会解析配置文件,比如php.ini,想想就知道这回影响性能,fastcgi会先启动一个master解析配置文件,初始化环境,然后再启动多个worker,当请求过来的时候master会传递给woker,然后立即去接受下一个请求。当worker不够用的时候会增加,当空闲的worker多的时候会停掉一些,这样的机制可以提高性能,节省资源。 3> php-cgi PHP-CGI是php自带的Fast-CGI管理器. php.ini修改之后,必须kill掉php-cgi再启动php.ini 才生效。不可以平滑的重启 内存不能动态分配 启动php,指定启动的worker ,长期驻留在内存里 ,用户访问php文件, php-cgi 处理请求,返回结果 4> Php-fmp 非官方fastCgi进程管理器,后来php5.4开始,被官方收录了 可以平滑重启php 动态调度进程 启动php,动态指定启动的worker ,长期驻留在内存里 ,根据来访压力动态增减worker的进程数量,用户访问php文件, php-fpm 处理请求,返回结果 php-cgi和php-fpm的关系呢 php54是之前是一种关系,php54之后另一种关系。php54之前,php-fpm(第三方编译)是管理器,php-cgi是解释器。php54之后,php-fpm(官方自带),master 与 pool 模式。php-fpm 和 php-cgi 没有关系了。php-fpm又是解释器,又是管理器网上大部分说法:php-fpm 是管理php-cgi 的,是针对php54之前的

JWT

[TOC] # 1. 简介 JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。 实际上就只规范如何产生一个加密的字符串 token,它就长这个样子 ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6Ly8xMC4zLjE5Ljg2OjkwODAvYXBpL3NpZ25pbiIsImlhdCI6MTUyMTYxODc3MSwiZXhwIjoxNTIxNjMzMTcxLCJuYmYiOjE1MjE2MTg3NzEsImp0aSI6ImhjMHNQMFdXcnVHbmJ4SFMifQ.Yc9X6q0A1QRGoEXfzS63cbeICoZPB_7vWkRqBypdiiU ``` # 2. token的组成 上面的token由.分隔成三段,第一部分称为头部(header),第二部分们称为载荷(payload),第三部分是签证(signature) ## 2.1 header jwt的头部承载两部分信息: - 声明类型,这里是jwt - 声明加密的算法 通常直接使用 HMAC SHA256 ``` { 'typ': 'JWT', 'alg': 'HS256' } ``` 将头部信息进行base64加密(可以对称解密),构成第一部分,暂且记做$header_str ## 2.2 payload 载荷负责存放有效信息,实际上包括标准声明和非标准声明 1. 标准声明(建议并不强制使用) - iss: jwt签发者 - sub: jwt所面向的用户 - aud: 接收jwt的一方 - exp: jwt的过期时间,这个过期时间必须要大于签发时间 - nbf: 定义在什么时间之前,该jwt都是不可用的. - iat: jwt的签发时间 - jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 2. 非标准部分可以写入用户的id等需要的信息 ``` { "iss": "test", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "user_id": 1, } ``` 将载荷信息进行base64机密,构成第二部分,暂且记做$payload_str ## 2.3 signature jwt的第三部分是一个签证信息,这个签证信息由三部分组成: - header (base64后的) - payload (base64后的) - secret 1. 将加密后的第一部分和第二部分使用.连接形成字符串 ``` $header_payload_str = $header_str . '.' . $payload_str; ``` 2. 使用header中声明的加密方式加密,形成第三部分,记做$signature ``` $signature = HMACSHA256($header_payload_str, 'secret'); ``` 最后将上面加密后的三部分使用.连接,构成最终的token ``` $token = $header_str . '.' . $payload_str . '.' . $signature; ``` # 3. 使用 用户进行登录操作,如果登录信息合法,服务端生成token字符串,响应中将token信息传递给客户端,客户端并将token信息存储在客户端,以后每次请求的时候带着token信息,服务端收到每次请求后都验证token是否存在并且合法有效,来确认用户是否登录 一般是在请求头中加入,比如我在vue中axios拦截中每次请求带着token信息 ``` axios.interceptors.request.use( config => { let token = sessionStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, err => { return Promise.reject(err); } ) ``` # 4. 单点登录 ## 4.1 同一顶级域名下 举个例子 - www.taobao.com 服务器www - a.tabao.com 服务器a - b.tabao.com 服务器b 不同的二级域名对应不同的服务器,如果使用服务器存储session的方式,就需要我们在多台服务器上同步session信息,但是使用jwt就没有这个问题,jwt_token已经记录了用户信息,并且已经存储在客户端,我们只需要将token信息存储在顶级域名下即可。 比如 ``` Set-Cookie: jwt=lll.zzz.xxx; HttpOnly; max-age=980000; domain=.taobao.com ``` 注意domain必须设置为一个点加顶级域名,即.taobao.com。这样,taobao.com和*.taobao.com就都可以接受到这个Cookie,并获取JWT了 ## 4.2 跨域单点登录 举个例子 - www.taobao.com 服务器www - a.com 服务器a - b.com 服务器b 提供两个解决方法 1. 一个域名登录成功,在客户端针对其他每个站点在浏览器端设置cookie信息 2. 借助单独的sso服务器,只需存储该sso服务器域名下的cookie信息,在这种模型下,针对任何站点的请求都将会先重定向到SSOsite去验证一个身份验证cookie是否存在。如果存在,则验证过的页面将会发送给浏览器。否则用户将会被重定向到登录页面。 # 5. 关于token泄露 token如果泄露,别人就可以通过token来进行非法操作,因为服务端只判定token是否合法,并不会验证使用者。 泄露一般存在两个地方 1.传输层 token在传输层被拦截,传统的cookie存储的sessionId也有一样的问题,所以建议使用https协议,安全性更有保证。 2.客户端 如果cookie可以被窃取,token也没办法解决这个问题。拦截到cookie后,把cookie放在另一台电脑上也是可以使用的,这个跟token是一样的。 # 6. 总结 1. jwt是一个标准,可以跨语言使用。 2. jwt构成简单,这用字节小,便于传输,不会因为每次请求头都带着token过于影响性能。 3. 不需要在服务端保存会话信息,减轻服务器压力同时易于扩展。 4. payload部分是对称解密,不应该存储敏感信息。 5. 保护好私钥,否则别人可以伪造token

http报文

[TOC] # 1. 请求报文 HTTP 请求由三部分组成:请求行、 请求头和请求正文 ![](http://markdown.archerwong.cn/2018-12-18-07-56-56_clipboard.png) ## 1.1请求行 请求的第一行是“方法 URL 协议/版本”,并以 回车换行作为结尾。请求行以空格分隔。格式如下: ``` POST /index.php HTTP/1.1 ``` 以上代码中“GET”代表请求方法,“/index.php”表示URI,“HTTP/1.1代表协议和协议的版本 ## 1.2 请求头 ### 一些常用的请求头信息 - Host 客户端指定自己想访问的WEB服务器的域名/IP 地址和端口号。 - Connection 客户端(浏览器)想要优先使用的连接类型,keepalive(当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的 网页,会继续使用这一条已经建立的连接) - Accept 可接受的响应内容类型(Content-Types)。 Accept: text/plain - Accept-Charset 可接受的字符集 Accept-Charset: utf-8 - Accept-Encoding 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法。 Accept-Encoding: gzip, deflate - Accept-Language: 可接受的响应内容语言列表。 Accept-Language: en-US - User-Agent: 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本. - Cookie: 很重要的header,客户端的Cookie就是通过这个报文头属性传给服务端的! - If-Modified-Since 把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比。如果时间一致,那么返回304,客户端就直接使用本地缓存文件。如果时间不一致,就会返回200和新的文件内容。客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示在浏览器中。 例如:If-Modified-Since: Thu, 09 Feb 2012 09:07:57 GMT。 - If-None-Match If-None-Match和ETag一起工作,工作原理是在HTTP Response中添加ETag信息。 当用户再次请求该资源时,将在HTTP Request 中加入If-None-Match信息(ETag的值)。如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。否则将返回200状态和新的资源和Etag. 使用这样的机制将提高网站的性能 例如: If-None-Match: “03f2b33c0bfcc1:0” - Pragma 防止页面被缓存, 在HTTP/1.1版本中,它和Cache-Control:no-cache作用一模一样 Pargma只有一个用法, 例如: Pragma: no-cache 注意: 在HTTP/1.0版本中,只实现了Pragema:no-cache, 没有实现Cache-Control - Cache-Control 这个是非常重要的规则。 这个用来指定Response-Request遵循的缓存机制。如以下设置,相当于让服务端将对应请求返回的响应内容不要在客户端缓存:Cache-Control: no-cache - Content-Length 以8进制表示的请求体的长度 Content-Length: 348 - Content-Type 请求体的MIME类型 (用于POST和PUT请求中) Content-Type: application/x-www-form-urlencoded - Referer 表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer其实是Referrer这个单词,但RFC制作标准时给拼错了,后来也就将错就错使用Referer了。 Referer: http://itbilu.com/nodejs - Upgrade 要求服务器升级到一个高版本协议。 Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 - Via 告诉服务器,这个请求是由哪些代理发出的。 Via: 1.0 fred, 1.1 itbilu.com.com (Apache/1.1) # 2. 响应报文 HTTP 响应也是由三个部分组成,分别是:状态行、消息报头和响应正文 ![](http://markdown.archerwong.cn/2018-12-18-07-58-23_clipboard.png) ## 2.1 状态行 状态行由协议版本、数字形式的状态代码,及相应的状态描述组成,各元素之间以空格分隔,结尾时回车换行符,格式如下: HTTP-Version Status-Code Reason-Phrase CRLF HTTP-Version 表示服务器 HTTP 协议的版本,Status-Code 表示服务器发回的响应代码,Reason-Phrase 表示状态代码的文本描述,CRLF 表示回车换行。例如: HTTP/1.1 200 OK (CRLF) ### 1> 响应状态码 ``` 1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急...。 2xx 处理成功,一般表示:请求收悉、我明白你要的、请求已受理、已经处理完成等信息。 3xx 重定向到其它地方。它让客户端再发起一个请求以完成整个处理。 4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。 5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。 ``` ### 2> 常见的响应状态码 ``` 200 OK 处理成功! 301 永久重定向 Location响应首部的值仍为当前URL,因此为隐藏重定向; 302 临时重定向 显式重定向, Location响应首部的值为新的URL。 303 See Other redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。 304 Not Modified 告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存 400 Bad Request 客户端请求有语法错误,不能被服务器所理解。 401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。 403 Forbidden 服务器收到请求,但是拒绝提供服务。 404 Not Found 你最不希望看到的,即找不到页面。 500 Internal Server Error 看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常。 503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常。 ``` ## 2.2 响应头 常见的响应头 - Access-Control-Allow-Origin 指定哪些网站可以跨域源资源共享 Access-Control-Allow-Origin: * - Allow 对于特定资源的有效动作; Allow: GET, HEAD 固定 - Cache-Control 通知从服务器到客户端内的所有缓存机制,表示它们是否可以缓存这个对象及缓存有效时间。其单位为秒 Cache-Control: max-age=3600 固定 - Connection 针对该连接所预期的选项 Connection: close 固定 - Content-Encoding 响应资源所使用的编码类型。 Content-Encoding: gzip - Content-Language 响就内容所使用的语言 Content-Language: zh-cn - Content-Length 响应消息体的长度,用8进制字节表示 Content-Length: 348 - Content-Location 所返回的数据的一个候选位置 Content-Location: /index.htm - Content-Type 当前内容的MIME类型 Content-Type: text/html; charset=utf-8 - Date 此条消息被发送时的日期和时间(以RFC 7231中定义的"HTTP日期"格式来表示) Date: Tue, 15 Nov 1994 08:12:31 GMT - ETag 对于某个资源的某个特定版本的一个标识符,通常是一个 消息散列 ETag: "737060cd8c284d8af7ad3082f209582d" - Expires 指定一个日期/时间,超过该时间则认为此回应已经过期 Expires: Thu, 01 Dec 1994 16:00:00 GMT - Last-Modified 所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示) Last-Modified: Dec, 26 Dec 2015 17:30:00 GMT - Location 用于在进行重定向,或在创建了某个新资源时使用。 Location: http://www.itbilu.com/nodejs - Pragma 与具体的实现相关,这些响应头可能在请求/回应链中的不同时候产生不同的效果 Pragma: no-cache - Server 服务器的名称 Server: nginx/1.6.3 - Set-Cookie 设置HTTP cookie Set-Cookie: UserID=itbilu; Max-Age=3600; Version=1 - Status 通用网关接口的响应头字段,用来说明当前HTTP连接的响应状态。 Status: 200 OK - Transfer-Encoding 用表示实体传输给用户的编码形式。包括:chunked、compress、 deflate、gzip、identity。 Transfer-Encoding: chunked - Upgrade 要求客户端升级到另一个高版本协议。 Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 - Vary 告知下游的代理服务器,应当如何对以后的请求协议头进行匹配,以决定是否可使用已缓存的响应内容而不是重新从原服务器请求新的内容。 Vary: * - Via 告知代理服务器的客户端,当前响应是通过什么途径发送的。 Via: 1.0 fred, 1.1 itbilu.com (nginx/1.6.3) # 3. 浏览器缓存 ## 3.1 缓存的优点: 1> 服务器响应更快:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让服务器看上去响应更快。 2> 减少网络带宽消耗:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。 ## 3.2 缓存工作原理 ### 3.2.1 首先了解下缓存的相关字段 通用首部: 请求报文和响应报文双方都会使用的首部 字段名 | 说明 ---|--- Cache-Control | 控制缓存的行为 Pragma | http1.0产物,值为no-cache时禁用缓存 请求首部 字段名 | 说明 ---|--- if-Match | 比较ETag(资源计算得出一个唯一标志符,比如md5标志)是否一致 if-None-Match | 比较ETag(资源计算得出一个唯一标志符,比如md5标志)是否【不一致】 if-Modified-Since | 比较资源最后更新时间(Last-Modified的值)是否一致 if-Modified-Since | 比较资源最后更新时间(Last-Modified的值)是否【不一致】 响应首部 字段名 | 说明 ---|--- ETag | 资源的匹配信息,例如`Etag: "5d8c72a5edda8d6a:3239"` 实体首部:包含在请求报文和响应报文中的实体部分所使用的首部。 字段名 | 说明 ---|--- Expires | http1.0产物,实体主体过期时间 Last-Modified | 资源最后修改时间 ### 3.2.2 http1.0时代缓存方式 在 http1.0 时代,给客户端设定缓存方式可通过两个字段——Pragma和Expires来规范。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,你还是可以看到很多网站依旧会带上这两个字段。 1、Pragma 实现禁用缓存,当该字段值为no-cache的时候(事实上现在RFC中也仅标明该可选值),会知会客户端不要对该资源读缓存,即每次都得向服务器发一次请求才行。 Pragma的优先级是高于Cache-Control的。 ``` Pragma -> Cache-Control ``` 2、Expires 实现启用缓存和定义缓存时间, Expires的值对应一个GMT(格林尼治时间),比如Mon, 22 Jul 2002 11:12:01 GMT来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。 如果Pragma头部和Expires头部同时存在,则起作用的会是Pragma ### 3.2.3 http1.1时代缓存方式 针对上述的“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了 Cache-Control 来定义缓存过期时间。 注意:若报文中同时出现了 Expires 和 Cache-Control,则以 Cache-Control 为准。 也就是说优先级从高到低分别是 ``` Pragma -> Cache-Control -> Expires ``` Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。在RFC中规范了 Cache-Control 的格式为: ``` "Cache-Control" ":" cache-directive ``` 作为请求首部时,cache-directive 的可选值有: ![](http://markdown.archerwong.cn/2018-12-18-07-59-06_clipboard.png) 作为响应首部时,cache-directive 的可选值有: ![](http://markdown.archerwong.cn/2018-12-18-07-59-59_clipboard.png) 这里要注意下: - no-cahce并不是表示无缓存,而是指使用缓存一定要先验证新鲜度 - response header的`no-cache`和`max-age=0`和request header的`max-age=0`的作用是一样的:都要求在使用缓存之前验证新鲜度 Cache-Control 允许自由组合可选值,例如: ``` Cache-Control: max-age=3600, must-revalidate ``` 它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。 当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。 ### 3.2.3 http1.1时代的补充 如果客户端向服务器发了请求,那么是否意味着一定要读取回该资源的整个实体内容呢?如果客户端现在存有的缓存文件,其实跟自己所有的文件是一致的,其实是可以复用的。为了让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事情。 首先,了解下etag 和 last-modified这两个关键点,他们有如下的对应关系。 ``` etag —— if-none-match etag —— If-Match last-modified —— if-modified-since last-modified —— If-Unmodified-Since ``` 1、Last-Modified 服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。 ``` Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT ``` 客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。 至于传递标记起来的最终修改时间的请求报文首部字段一共有两个: ``` If-Modified-Since: Last-Modified-value ``` 该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。 当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。 ``` If-Unmodified-Since: Last-Modified-value ``` 该值告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。 Last-Modified 存在一定问题,如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。这个问题可以ETag来解决 2、ETag 为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。 服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端。例如: ``` Etag: "5d8c72a5edda8d6a:3239" ``` 客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。 如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。 那么客户端是如何把标记在资源上的 ETag 传回给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值: ``` If-None-Match: ETag-value ``` 告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。 当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。 ``` If-Match: ETag-value ``` 告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。 ## 3.3 火狐浏览器缓存分析实例 ### 3.3.1 第一次访问 ![](http://markdown.archerwong.cn/2018-12-18-08-00-49_clipboard.png) 请求头中没有关于缓存的字段,响应200,并且响应头中则返回了ETag字段。 ``` ETag W/"651c64-iihm0BriQfynDiDtEbdScsPqrwA" ``` ### 3.3.2 地址栏直接回车 ![](http://markdown.archerwong.cn/2018-12-18-08-01-20_clipboard.png) 请求头中带上了关于缓存的字段(If-None-Match),并且带上了第一次Etag返回的值 ``` If-None-Match W/"651c64-iihm0BriQfynDiDtEbdScsPqrwA" ``` 响应 304,并且继续返回Etag字段 ### 3.3.3 按f5 ![](http://markdown.archerwong.cn/2018-12-18-08-02-00_clipboard.png) 请求头中带上了关于缓存的字段(If-None-Match,Cache-Control),并且带上了第一次Etag返回的值 ``` Cache-Control max-age=0 If-None-Match W/"651c64-iihm0BriQfynDiDtEbdScsPqrwA" ``` 响应 304,并且继续返回Etag字段 和直接在地址栏回车的区别是加了`Cache-Control max-age=0`头部,意思是告诉浏览器要进行新鲜度的检查,我们发现检查后发现时最新资源,所有返回了304 。 ### 3.3.4 ctrl + f5 ![](http://markdown.archerwong.cn/2018-12-18-08-02-32_clipboard.png) 请求头中带上了关于缓存的字段(Cache-Control,Pragma) ``` Cache-Control no-cache Pragma no-cache ``` 响应 200,并且返回Etag字段 这个操作的意思是我不光要最新资源,并且中间的各级缓存(不只是浏览器缓存,可能还有中间的prxoy缓存等)都不要返给我。 # 3.3.5 总结: 浏览器的刷新访问行为是有自己的一套准则的,以上是我试验的火狐浏览器,大家可以测试下自己的浏览器,其实我们可以认识到不同的操作,会有不同的请求策略,一些静态资源浏览器可能会为其增加新的请求头部信息,以此来达到缓存的策略。 # 参考资料: https://www.jianshu.com/p/b1ea16450fff https://cnbin.github.io/blog/2016/02/20/http-qing-qiu-,-xiang-ying-,-huan-cun/ https://github.com/rccoder/blog/issues/12 http://imweb.io/topic/5795dcb6fb312541492eda8c

websocket协议

[TOC] # 1. websocket协议 WebSocket协议提供一个供TCP连接进行双向通讯的机制,基于TCP传输协议,并复用HTTP的握手通道。 # 2. 长连接,轮询,长轮询,websocket的前世今生 我们可能经常会听到长连接,轮询,长轮询,websocket这几个概念,并且可能对他们的区别有些模糊 ## 2.1 websocket与http的关系 他和http的协议就如同下面的关系,有共用的部分,但不是一个东西。Websocket其实是一个新协议,跟HTTP协议不同,为了兼容现有浏览器的握手规范,借用了HTTP的协议来完成一部分握手,所以他们在握手阶段是基本一样的。 ![](http://markdown.archerwong.cn/2018-12-18-08-16-09_1.jpg) ## 2.2 长连接 在我们的认识下,http协议中,Request = Response,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。 每一个请求开始后都会经历握手的过程,握手结束后开始传输数据,显然这样的效率不高,所有聪明的开发者们想到了,可不可以在一次握手结束后,发送多个请求,并接收多个请求,keep-alive应运而生。HTTP有1.1和1.0之说,http1.1一个比较重要的方面就是增加了keep-alive,HTTP1.1的连接默认使用持续连接。 ## 2.3 轮询 轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。 场景再现: 客户端:.有没有新信息(Request) 服务端:没有.(Response) 客户端:..有没有新信息(Request) 服务端:没有..(Response) 客户端:...有没有新信息(Request) 服务端:有消息,告诉你,今天要下雨...(Response) 客户端:...有没有新消息(Request) 服务端:没有..(Response) 。。。。。。 ---- loop 很明显这样的方式,轮询就会造成对网络和通信双方的资源的浪费,且非实时 ## 2.4 长轮询 长轮询其实原理跟轮询差不多,都是采用轮询的方式,不过采取的是阻塞模型,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。 场景再现 客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) 服务端:额。。 等待到有消息的时候。。来 给你(Response) 客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) 。。。。 ----loop 相比于轮询,这种方式网络带宽占用少了,但是本质上都体现了服务器的被动型,只有请求过来,才能回答,不能主动与客户端沟通。 ## 2.5 websocket 相对于http协议,websocket协议在握手阶段结束,建立起连接后,双方通讯会一直保持,并且客户端和服务端任何一方都可以主动向对方发送新消息,并且另一端通过监听onXXX事件可以收到这个信息。 # 3. websocket的详细介绍 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。WebSocket 允许服务器端与客户端进行全双工(full-duplex)的通信。 其他特点包括: (1)建立在 TCP 协议之上,服务器端的实现比较容易。 (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 (3)数据格式比较轻量,性能开销小,通信高效。 (4)可以发送文本,也可以发送二进制数据。 (5)没有同源限制,客户端可以与任意服务器通信,完全可以取代 Ajax。 (6)协议标识符是ws(如果加密,则为wss,对应 HTTPS 协议),服务器网址就是 URL。 WS协议有两部分组成:握手和数据传输。 ## 3.1 websocket握手阶段 这就是我们所了解的TCP三次握手,重温下这个经典的瞬间。。。 ![](http://markdown.archerwong.cn/2018-12-18-08-16-14_clipboard.png) 下面是我截取的一个websocket请求 ![](http://markdown.archerwong.cn/2018-12-18-08-16-17_2.png) 通过上图可以看到和http的请求响应很像, 请求头中,这里需要注意几个域,有一个 HTTP 头是Upgrade。HTTP1.1 协议规定,Upgrade表示将通信协议从HTTP/1.1转向该字段指定的协议。Connection字段表示浏览器通知服务器,如果可以的话,就升级到 WebSocket 协议。Origin字段用于提供请求发出的域名,供服务器验证是否许可的范围内(服务器也可以不验证)。Sec-WebSocket-Key则是用于握手协议的密钥,是 Base64 编码的16字节随机字符串。 响应头中,服务器同样用Connection字段通知浏览器,需要改变协议。Sec-WebSocket-Accept字段是服务器在浏览器提供的Sec-WebSocket-Key字符串后面,添加“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”字符串,然后再取 SHA-1 的哈希值。浏览器将对这个值进行验证,以证明确实是目标服务器回应了 WebSocket 请求。 ## 3.2 数据传输 看一下客户端的简单实例 ``` var ws = new WebSocket('wss://echo.websocket.org'); ws.onopen = function(evt) { console.log('Connection open ...'); ws.send('Hello WebSockets!'); }; ws.onmessage = function(evt) { console.log('Received Message: ' + evt.data); ws.close(); }; ws.onclose = function(evt) { console.log('Connection closed.'); }; ``` 代码比较简单,也容易理解,就是几个回调函数。同样服务端也要实现监听事件,这样双方就可以通讯了。需要注意的是,websocket不仅可以发送text类型数据,还可以发送二进制数据,如果有需要,on message 的时候可以进行下判断。 # 参考资料 https://www.zhihu.com/question/20215561 http://javascript.ruanyifeng.com/htmlapi/websocket.html#toc0

会话管理方式

会话管理方式 http是无状态的,这就意味着客户端即使第一次请求已经验证了身份,但是第二次请求的时候服务器仅通过http连接并不能知道是那个用户。但是我们是需要状态管理的,比如用户登录了才能做一些其它操作,这就需要我们进行会话管理。 这里主要讲两种管理方式 - 基于session的管理方式 - 基于ticket管理方式 # 1. 基于session的方法 1. 客户端送第一次请求后,服务端在服务器创建session信息,并将sessionid传送给浏览器 第一次请求 ![](https://images2018.cnblogs.com/blog/970623/201803/970623-20180327171254618-601983660.png) 第一请求并没有带cookie信息,服务器返回set-cookie字段,并带有sessionid(laralve_session) 2. 客户端再次请求会通过cookie带着这个sessionid 再次请求 ![](https://images2018.cnblogs.com/blog/970623/201803/970623-20180327171430370-1832766187.png) 因为上次的响应有set-cookie字段,将sessionid储存到cookie中,现在请求已经带着cookie字段,里面包含sessionid 3.这样我们就可以通过这个sessionid进行用户状态管理。 实际上上面的session并不具备会话管理功能,但是我们可以将我们的用户状态放到session对象里面,并存储在服务端。用户登录成功后,我们将登录凭证存储在session对象中,这样服务端在收到sessionid的时候就可以判断对应的session对象是否含有登录凭证,如果有则证明登录成功。 问题: 1. session信息需要存储在服务端,信息过多会占用较多内存 2. 服务器集群部署,多台服务器之间session信息不共享 3. cookie要存储在域名下,如果同属于一级域名下比较好解决,但是如果不同域就有些麻烦,不能跨域,这就需要针对性解决 在实际部署中我们可能会使用redis存储,这样可以减轻服务器压力,也能解决服务器session共享问题 # 2. 基于ticket的方式 用户登录信息合法,服务端产生ticket信息,响应中将ticket信息传递给客户端,客户端并将ticket信息存储在客户端,以后每次请求的时候带着ticket信息,服务端收到每次请求后都验证ticket是否存在并且合法有效,来确认用户是否登录 1. 用户使用用户名密码登录,服务端验证服务名密码是否正确,如果正确则创建登录凭证,这个登录凭证可能包含着用户id,过期时间等。 2. 将登录凭证+签名+其它加密处理,然后将加密处理的字符串存储到客户端。 3. 客户端发送请求后,服务端每次检测是否有此ticket信息,如果有则解密处理,查看签名是否正确,是否过期等等,如果都合法则说明用户登录有效。 问题: 1. 储存的用户信息过多,导致ticket过大,如果使用cookie存储的话,cookie大小有限制,加密字符串过长的话会引发其他问题 2. 每次请求都要带着ticket信息,ticket信息又比较大,降低访问性能 3. 过期自动刷新问题 实际上在客户端ticket字符串不一定非要存储在cookie中,我们还可以存储在localstorage,sessionstroage等的地方。只要我们每次请求的时候取得这个ticket信息并且让服务端知道就可以表明身份。 关于这种方式,可以符合现成的标准JWT,这样就比较规范了,有兴趣的可以了解。 # 3. 总结 1. 比较下上面两种方式,基于session的方式将用户凭证保存在服务端,基于ticket的方式保存在客户端。 2. 只要不泄露以上的两种会话机制还是比较安全的,各有各的应用场景,根据自身情况来处理 3. 以上都没有避免sessionid,ticket(加密字符串)的泄露问题,一旦泄露别人就可以冒充我们,做一些不可告人的事情。关于传输过程中的窃取可以使用https来避免,在客户端的凭证窃取需要我们自己注意了,比如laravel就使用csrf_token来避免CSRF攻击。 参考资料 http://www.cnblogs.com/lyzg/p/6067766.html

tcpdump使用

# 1. tcpdump选项 它的命令格式为: ``` tcpdump [ -DenNqvX ] [ -c count ] [ -F file ] [ -i interface ] [ -r file ][ -s snaplen ] [ -w file ] [ expression ] ``` 抓包选项: ``` -c:指定要抓取的包数量。 -i interface:指定tcpdump需要监听的接口。可以使用'any'关键字表示所有网络接口。 -n:对地址以数字方式显式,否则显式为主机名,也就是说-n选项不做主机名解析。 -nn:除了-n的作用外,还把端口显示为数值,否则显示端口服务名。 -N:不打印出host的域名部分。例如tcpdump将会打印'nic'而不是'nic.ddn.mil'。 -P:指定要抓取的包是流入还是流出的包。可以给定的值为"in"、"out"和"inout",默认为"inout"。 -s len:设置tcpdump的数据包抓取长度为len,如果不设置默认将会是65535字节。对于要抓取的数据包较大时,长度设置不够可能会产生包截断,若出现包截断, :输出行中会出现"[|proto]"的标志(proto实际会显示为协议名)。但是抓取len越长,包的处理时间越长,并且会减少tcpdump可缓存的数据包的数量, :从而会导致数据包的丢失,所以在能抓取我们想要的包的前提下,抓取长度越小越好。 ``` 输出选项: ``` -e:输出的每行中都将包括数据链路层头部信息,例如源MAC和目标MAC。 -q:快速打印输出。即打印很少的协议相关信息,从而输出行都比较简短。 -X:输出包的头部数据,会以16进制和ASCII两种方式同时输出。 -XX:输出包的头部数据,会以16进制和ASCII两种方式同时输出,更详细。 -v:当分析和打印的时候,产生详细的输出。 -vv:产生比-v更详细的输出。 -vvv:产生比-vv更详细的输出。 ``` 其他功能性选项: ``` -D:列出可用于抓包的接口。将会列出接口的数值编号和接口名,它们都可以用于"-i"后。 -F:从文件中读取抓包的表达式。若使用该选项,则命令行中给定的其他表达式都将失效。 -w:将抓包数据输出到文件中而不是标准输出。可以同时配合"-G time"选项使得输出文件每time秒就自动切换到另一个文件。可通过"-r"选项载入这些文件以进行分析和打印。 -r:从给定的数据包文件中读取数据。使用"-"表示从标准输入中读取。 ``` # 2. 基本的使用方法 1、抓取所有经过指定网络接口上的数据包 ``` tcpdump -i eth0 ``` 如果不指定网络接口,则默认抓取第一个网络接口,一般是eth0 ``` tcpdump ``` 2、过滤主机 ``` tcpdump -i eth1 host 192.168.1.1 tcpdump -i eth1 src host 192.168.1.1 //接收192.168.1.1发送的数据包 tcpdump -i eth1 dst host 192.168.1.1 //发往192.168.1.1的数据包 ``` src 指定源地址,192.168.1.1 dst 指定目的地址,192.168.1.1 3、过滤端口 ``` tcpdump -i eth1 port 25 tcpdump -i eth1 src port 25 tcpdump -i eth1 dst port 25 ``` 4、网络过滤 ``` tcpdump -i eth1 net 192.168 tcpdump -i eth1 src net 192.168 tcpdump -i eth1 dst net 192.168 ``` 5、协议过滤 ``` tcpdump -i eth1 arp tcpdump -i eth1 ip tcpdump -i eth1 tcp tcpdump -i eth1 udp tcpdump -i eth1 icmp ``` 6、常用表达式 ``` 非 : ! or "not" (去掉双引号) 且 : && or "and" 或 : || or "or" ``` 抓取主机10.3.19.185和主机10.3.19.186或10.3.19.187的通信 ``` tcpdump 'host 10.3.19.185 and (10.3.19.186 or 10.3.19.187)' ``` 抓取主机10.3.19.185除了和主机10.3.19.186之外所有主机通信的**数据包** ``` tcpdump host 10.3.19.185 and ! 10.3.19.186 ``` 抓取主机10.3.19.185除了和主机10.3.19.186之外所有主机通信的**IP包** ``` tcpdump ip host 10.3.19.185 and ! 10.3.19.186 ``` 抓取主机10.3.19.185所有在TCP 80端口的数据包 ``` tcpdump tcp port 80 and host 10.3.19.185 ``` 抓取送到主机10.3.19.185的80端口的数据包: ``` tcpdump dst port 80 and host 10.3.19.185 ``` 抓取所有经过eth1,目的地址是192.168.1.254或192.168.1.200端口是80的TCP数 ``` tcpdump -i eth1 '((tcp) and (port 80) and ((dst host 192.168.1.254) or (dst host 192.168.1.200)))' ``` 抓取所有经过eth1,目的网络是192.168,但目的主机不是192.168.1.200的TCP数据 ``` tcpdump -i eth1 '((tcp) and ((dst net 192.168) and (not dst host 192.168.1.200)))' ``` 7、保存到文件a.cap ``` tcpdump -i eth1 port 25 -w a.cap ``` # 3.wireshark使用 ## 3.1 界面介绍 主界面 ![](http://markdown.archerwong.cn/2018-12-18-08-14-23_11.png) wireshark与对应的OSI七层模型 ![](http://markdown.archerwong.cn/2018-12-18-08-14-26_12.png) tcp抓包具体内容 ![](http://markdown.archerwong.cn/2018-12-18-08-14-29_13.png) ## 3.2 wireshark过滤器的使用 几个最常用的关键字,“eq” 和 “==”等同,可以使用 “and” 表示并且,“or”表示或者。“!" 和 "not” 都表示取反。 ip地址过滤 ``` # 对源地址为192.168.0.1的包的过滤 ip.src == 192.168.0.1 # 对目的地址为192.168.0.1的包的过滤 ip.dst == 192.168.0.1 # 对源或者目的地址为192.168.0.1的包的过滤 ip.addr == 192.168.0.1 ip.src == 192.168.0.1 or ip.dst == 192.168.0.1 # 要排除以上的数据包,我们只需要将其用括号囊括,然后使用 "!" 即可。 !(表达式) ``` 协议的过滤 ``` # 仅仅需要捕获某种协议的数据包,表达式很简单仅仅需要把协议的名字输入即可。 http # 需要捕获多种协议的数据包,也只需对协议进行逻辑组合即可。 http or telnet # 排除某种协议的数据包 not arp !tcp ``` 端口的过滤(视协议而定) ``` # 捕获某一端口的数据包 tcp.port == 80 # 捕获多端口的数据包,可以使用and来连接,下面是捕获高端口的表达式 udp.port >= 2048 ``` 针对长度和内容的过滤 ``` # 针对长度的过虑(这里的长度指定的是数据段的长度) udp.length < 30 http.content_length <=20 # 针对数据包内容的过滤 http.request.uri matches "vipscu" //(匹配http请求中含有vipscu字段的请求信息) ``` ## 3.3 分析过程 ``` ⚡ [email protected]_35_1_centos  ~  curl archerwong.cn ``` ``` # root @ VM_33_19_centos in ~ [10:17:47] C:1 $ tcpdump -n host 118.25.194.34 and port 80 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 10:52:19.047261 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [S], seq 188644742, win 29200, options [mss 1424,sackOK,TS val 608533047 ecr 0,nop,wscale 7], length 0 10:52:19.047290 IP 10.105.33.19.http > 118.25.194.34.56490: Flags [S.], seq 162038118, ack 188644743, win 28960, options [mss 1460,sackOK,TS val 1190782418 ecr 608533047,nop,wscale 7], length 0 10:52:19.051949 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [.], ack 1, win 229, options [nop,nop,TS val 608533051 ecr 1190782418], length 0 10:52:19.051979 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [P.], seq 1:78, ack 1, win 229, options [nop,nop,TS val 608533051 ecr 1190782418], length 77: HTTP: GET / HTTP/1.1 10:52:19.051986 IP 10.105.33.19.http > 118.25.194.34.56490: Flags [.], ack 78, win 227, options [nop,nop,TS val 1190782423 ecr 608533051], length 0 10:52:19.107820 IP 10.105.33.19.http > 118.25.194.34.56490: Flags [P.], seq 1:1398, ack 78, win 227, options [nop,nop,TS val 1190782479 ecr 608533051], length 1397: HTTP: HTTP/1.1 200 OK 10:52:19.112434 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [.], ack 1398, win 250, options [nop,nop,TS val 608533112 ecr 1190782479], length 0 10:52:19.112605 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [F.], seq 78, ack 1398, win 250, options [nop,nop,TS val 608533112 ecr 1190782479], length 0 10:52:19.112650 IP 10.105.33.19.http > 118.25.194.34.56490: Flags [F.], seq 1398, ack 79, win 227, options [nop,nop,TS val 1190782484 ecr 608533112], length 0 10:52:19.117308 IP 118.25.194.34.56490 > 10.105.33.19.http: Flags [.], ack 1399, win 250, options [nop,nop,TS val 608533117 ecr 1190782484], length 0 ``` 上面的抓取的一次实例,包括了三次握手,数据收发,四次挥手,具体可以结合wireshark分析 下面文章写得很详细,直接推荐下,不再copy过来了 https://blog.csdn.net/hry2015/article/details/79006671

编码总结

[TOC] # 1. 基础概念 ## 1.1 字节 字节(Byte)是计算机中存储数据的单元,一个字节等于一个8位的比特,计算机中的所有数据,不论是磁盘文件上的还是网络上传输的数据(文字、图片、视频、音频文件)都是由字节组成的。 ## 1.2 字符 你正在阅读的这篇文章就是由很多个字符(Character)构成的,字符一个信息单位,它是各种文字和符号的统称,比如一个英文字母是一个字符,一个汉字是一个字符,一个标点符号也是一个字符。 ## 1.3 字符集 字符集(Character Set)就是某个范围内字符的集合,不同的字符集规定了字符的个数,比如 ASCII 字符集总共有128个字符,包含了英文字母、阿拉伯数字、标点符号和控制符。而 GB2312 字符集定义了7445个字符,包含了绝大部分汉字字符。常见的字符集有:ASCII及其扩展字符集,GB2312字符集,GBK字符集,UNICODE字符集等 ## 1.4 字符码 字符码(Code Point)指的是字符集中每个字符的数字编号,例如 ASCII 字符集用 0-127 这连续的128个数字分别表示128个字符,"A" 的编号就是65。 ## 1.5 字符编码 字符编码(Character Encoding)是将字符集中的字符码映射为字节流的一种具体实现方案,常见的字符编码有 ASCII 编码、UTF-8 编码、GBK 编码等。某种意义上来说,字符集与字符编码有种对应关系,例如 ASCII 字符集对应 有 ASCII 编码。ASCII 字符编码规定使用单字节中低位的7个比特去编码所有的字符。例如"A" 的编号是65,用单字节表示就是0×41,因此写入存储设备的时候就是b'01000001'。 ## 1.6 编码、解码 编码的过程是将字符转换成字节流,解码的过程是将字节流解析为字符。 理解了这些基本的术语概念后,我们就可以开始讨论计算机的字符编码的演进过程了。 # 2. ASCII、Unicode和UTF-8的关系 ## 2.1 ASCII(字符集) 我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。 上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。 最开始ASCII只定义了128个字符编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。 ## 2.2 Unicode(字符集) 英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。要处理中文显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,全世界有上百种语言,各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。 因此,Unicode应运而生。Unicode把所有语言都统一到一套编码里,他涵盖了全球所有的文字和二进制的对应关系,每个符号的编码都不一样,这样就不会再有乱码问题了。 **Unicode规定如何编码,解决了字符和二进制的对应关系,但是使用unicode表示一个字符,太浪费空间**。例如:利用unicode表示“Python”需要12个字节才能表示,比原来ASCII表示增加了1倍。 由于计算机的内存比较大,并且字符串在内容中表示时也不会特别大,所以内容可以使用unicode来处理,但是存储和网络传输时一般数据都会非常多,那么增加1倍将是无法容忍的!!!为了解决存储和网络传输的问题,出现了UTF,我们最常用的就是UTF-8编码。 **Unicode编码有不同的实现方式**,比如在传输和保存的过程中,“汉”字的Unicode编码是6C49,我可以用4个ascii数字来传输、保存这个编码;也可以用utf-8编码的3个连续的字节E6 B1 89来表示它。只要通信双方都要预定好,正确解析即可。 ## 2.3 UTF-8 UTF 是为unicode编码 设计 的一种 在存储 和传输时节省空间的编码方案。其中UTF-8(Unicode Transformation Format)广泛应用于互联网,它是一种变长的字符编码,可以根据具体情况用1-4个字节来表示一个字符。比如英文字符这些原本就可以用ASCII码表示的字符用UTF-8表示时就只需要一个字节的空间,和ASCII是一样的。 1、如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间: 字符 | ASCII | Unicode | UTF-8 ---|---|---|--- A | 01000001| 00000000 01000001| 01000001 中| x| 01001110 00101101| 11100100 10111000 10101101 从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。 2、对于多字节(n个字节)的字符,第一个字节的前n为都设为1,第n+1位设为0,后面字节的前两位都设为10。剩下的二进制位全部用该字符的unicode码填充。 ![](http://markdown.archerwong.cn/2019-01-02-02-19-18_clipboard.png) 以汉字“好”为例,“好”对应的Unicode是597D,对应的区间是0000 0800--0000 FFFF,因此它用UTF-8表示时需要用3个字节来存储,597D用二进制表示是: 0101100101111101,填充到1110xxxx 10xxxxxx 10xxxxxx得到11100101 10100101 10111101,转换成16进制:e5a5bd,因此“好”的Unicode"597D"对应的UTF-8编码是"E5A5BD" ``` 中文 好 unicode 0101 100101 111101 编码规则 1110xxxx 10xxxxxx 10xxxxxx -------------------------- utf-8 11100101 10100101 10111101 -------------------------- 16进制utf-8 e 5 a 5 b d ``` # 3. 计算机系统中的编码 无论以什么编码在内存里显示字符,存到硬盘上都是2进制。 ``` ascii编码(美国): l 0b1101100 o 0b1101111 v 0b1110110 e 0b1100101 GBK编码(中国): 老 0b11000000 0b11001111 男 0b11000100 0b11010000 孩 0b10111010 0b10100010 Shift_JIS编码(日本): 私 0b10001110 0b10000100 は 0b10000010 0b11001101 ks_c_5601-1987编码(韩国): 나 0b10110011 0b10101010 는 0b10110100 0b11000010 TIS-620编码(泰国): ฉัน 0b10101001 0b11010001 0b10111001 ... ``` 要注意的是,**存到硬盘上时是以何种编码存的,再从硬盘上读出来时,就必须以何种编码读**,要不然就乱了。 虽然有了unicode and utf-8 , 但是由于历史问题,各个国家依然在大量使用自己的编码,比如中国的windows,默认编码依然是gbk,而不是utf-8。 基于此,如果中国的软件出口到美国,在美国人的电脑上就会显示乱码,因为他们没有gbk编码。若想让中国的软件可以正常的在 美国人的电脑上显示,只有以下2条路可走: - 1、让美国人的电脑上都装上gbk编码 - 2、把你的软件编码以utf-8编码 但是这也是有问题 - 1、全世界有几百上千种编码,显然不可能专门为中国安装gbk,第一条方案行不通 - 2、所有软件都以utf-8编码,由于很多软件已经开发出来了,重新编码会花费巨大的精力。 但这并不是没有解决方案,试想一个中国人和一个韩国人沟通,两个人并不会对方的语言,但是两个人都会英语,那么这就简单了,直接用英语沟通就可以了。其实上面已经提到过了unicode字符集,这个字符集所有国家的电脑都能认识,假设中国使用gbk,美国人使用utf-8,只要当我们的代码从硬盘读到内存中的时候,使用正确的解码方式(比如GBK)读取数据,然后将数据转换成unicode存入内存,处理完之后,再使用正确的编码方式(比如utf-8),显示在浏览器上,这样中文就可以正常展示了。 不论是Python3x、Java还是其他编程语言,Unicode编码都成为语言的默认编码格式,而数据最后保存到介质中的时候,不同的介质可有用不同的方式,有些人喜欢用UTF-8,有些人喜欢用GBK,这都无所谓,只要平台统一的编码规范,具体怎么实现并不关心。 ![](http://markdown.archerwong.cn/2018-12-18-08-17-50_clipboard.png) # 4. python2.x中的编码 **注意:以下内容只限于python2.x** ## 4.1 python的解析 Python的诞生时间比Unicode要早很多,**Python的默认编码是ASCII** ``` >>> import sys >>> sys.getdefaultencoding() 'ascii' ``` 所以在Python源代码文件中如果不显示地指定编码的话,将出现语法错误 ``` #test.py print "学习编码" ``` 运行后会报错 ``` $ python test.py File "test.py", line 1 SyntaxError: Non-ASCII character '\xe5' in file test.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details ``` 为了在源代码中支持非ASCII字符,必须在源文件的第一行或者第二行显示地指定编码格式: ``` #!/usr/bin/python # -*- coding: utf-8 -*- ``` 有了上面的配置,当Python解释器读取源代码文件时,会按UTF-8编码读取,**我们保存文件的时候也要按照utf-8的格式保存**,utf-8是保存和读取的中间桥梁,只有双方遵守这个规则,才能正确的读取和写入。现在的IDE一般会有选项设置保存文件的编码类型,改变声明后同时换成声明的编码保存,但文本编辑器控们需要小心。 ## 4.2 python2.x中字符串 首先我们要理解字符和字节的区别,字符是用来显示的,而字节是存储和传输时使用,网络传输的是字节流,文件存储的也是字节流,而编辑器要显示文件内容,就需要转化为字符来显示,字符和字节之间的关系可以定义如下 ``` encode(字符, 编码方案) -> 字节 decode(字节, 编码方案) -> 字符 ``` 可见encode和decode是一对逆向操作,它们都需要指定编码方案,如果编码方案不一致,则会操作失败。 在Python里有两种类型的字符串类型:`字节字符串(str)`和`unicode的字符串`,**一个字节字符串就是unicode经过编码后的字节组成的序列**,有可能是 ascii, gbk, utf-8 等等中的任意一种,str中到底是用的哪一种编码,取决于它所在的场景,跟 locale ,文件编码等有关系。 ![](http://markdown.archerwong.cn/2018-12-18-08-18-20_clipboard.png) str和unicode都是basestring的子类。unicode可以通过对字节串str使用正确的字符编码进行解码后获得,str也可以通过对unicode的编码得到。 代码 ``` # coding: UTF-8 s = '好' u = u'好' print repr(s) print repr(u) print s print u print repr(s.decode('UTF-8')) print s == u print s.decode('UTF-8') == u ``` 结果 ``` '\xe5\xa5\xbd' u'\u597d' 好 好 u'\u597d' False True ``` 文件存储为utf8格式,输出规范的十六进制和ASCII码。 ``` $ hexdump -C test.py 00000000 23 20 63 6f 64 69 6e 67 3a 20 55 54 46 2d 38 0a |# coding: UTF-8.| 00000010 0a 73 20 3d 20 27 e5 a5 bd 27 0a 75 20 3d 20 75 |.s = '...'.u = u| 00000020 27 e5 a5 bd 27 0a 0a 70 72 69 6e 74 20 72 65 70 |'...'..print rep| 00000030 72 28 73 29 0a 70 72 69 6e 74 20 72 65 70 72 28 |r(s).print repr(| 00000040 75 29 0a 70 72 69 6e 74 20 73 0a 70 72 69 6e 74 |u).print s.print| 00000050 20 75 0a 70 72 69 6e 74 20 72 65 70 72 28 73 2e | u.print repr(s.| 00000060 64 65 63 6f 64 65 28 27 55 54 46 2d 38 27 29 29 |decode('UTF-8'))| 00000070 0a 0a 70 72 69 6e 74 20 73 20 3d 3d 20 75 0a 70 |..print s == u.p| 00000080 72 69 6e 74 20 73 2e 64 65 63 6f 64 65 28 27 55 |rint s.decode('U| 00000090 54 46 2d 38 27 29 20 3d 3d 20 75 0a |TF-8') == u.| 0000009c ``` 分析: - 1、首先文件的存储是字节流,观察上面的第二行和第三行,对照ascii表 第一段: ``` 73 20 3d 20 27 e5 a5 bd 27 ``` 分别对照解释 ``` 73->s, 20->空格,3d->等号,20->空格,27->单引号,`e5 a5 bd`->中文`好`,27->单引号 所以上面是 s = '好' ``` 第二段: ``` 75 20 3d 20 75 27 e5 a5 bd 27 ``` 分别对照解释 ``` 73->s, 20->空格,3d->等号,20->空格,75->u, 27->单引号,`e5 a5 bd`->中文`好`,27->单引号 所以上面是 s = '好' ``` 文件保存的时候是utf编码的字节流,我们读取的时候按照utf-8编码读取是可以正确解析的。 - 2、python2的默认编码是ASCII,想写中文,就必须声明文件头的coding为gbk or utf-8, 声明之后,python2解释器以文件头声明的编码去解释你的代码,但是加载到内存后,并不会主动帮你将str字节串转为unicode,也就是说,你的文件编码是utf-8,str字节串加载到内存里仍然为utf-8。Python2并不会自动的把文件编码转为unicode存在内存里,需要你自己人肉转。但是python3自动把文件编码转为unicode必定是调用了什么方法,这个方法就是,decode(解码) 和encode(编码) ``` UTF-8 --> decode 解码 --> Unicode Unicode --> encode 编码 --> GBK / UTF-8 .. ``` ![](http://markdown.archerwong.cn/2018-12-18-08-18-45_clipboard.png) - 3、上面的s,实际上是一个16进制(\xe5\xa5\xbd)表示的二进制字节(11100101 10100101 10111101),这样做的目的是可读性更强,这就是字节类型。u的16进制(\u597d)对应的二进制是(0101 100101 111101),两者都读入到内存中,显然是不同的。如果按照`s.decode('UTF-8')`对s进行解码得到了unicode,两个对应的二进制相同了,所以返回True。 ## 4.3 如何识别编码 chardet是一个非常优秀的编码识别模块,通过pip 安装: ``` pip install chardet ``` 使用: 复制代码 ``` >>> from chardet import detect >>> a = "中文" >>> detect(a) {'confidence': 0.682639754276994, 'encoding': 'KOI8-R'} ``` ## 4.4 读写文件 内置的open()方法打开文件时,read()读取的是str,读取后需要使用正确的编码格式进行decode()。write()写入时,如果参数是unicode,则需要使用你希望写入的编码进行encode(),如果是其他编码格式的str,则需要先用该str的编码进行decode(),转成unicode后再使用写入的编码进行encode()。如果直接将unicode作为参数传入write()方法,Python将先使用源代码文件声明的字符编码进行编码然后写入。 ``` # coding: UTF-8 f = open('test.txt') s = f.read() f.close() print type(s) # <type 'str'> # 已知是GBK编码,解码成unicode u = s.decode('GBK') f = open('test.txt', 'w') # 编码成UTF-8编码的str s = u.encode('UTF-8') f.write(s) f.close() ``` 另外,模块codecs提供了一个open()方法,可以指定一个编码打开文件,使用这个方法打开的文件读取返回的将是unicode。写入时,如果参数是unicode,则使用open()时指定的编码进行编码后写入;如果是str,则先根据源代码文件声明的字符编码,解码成unicode后再进行前述操作。相对内置的open()来说,这个方法比较不容易在编码上出现问题。 ``` # coding: GBK import codecs f = codecs.open('test.txt', encoding='UTF-8') u = f.read() f.close() print type(u) # <type 'unicode'> f = codecs.open('test.txt', 'a', encoding='UTF-8') # 写入unicode f.write(u) # 写入str,自动进行解码编码操作 # GBK编码的str s = '汉' print repr(s) # '\xba\xba' # 这里会先将GBK编码的str解码为unicode再编码为UTF-8写入 f.write(s) f.close() ``` # 5. 应用 在计算机内存中,统一使用Unicode编码(万国编码),当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。 下面介绍一些例子 ## 5.1 记事本 用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件: ![](http://markdown.archerwong.cn/2018-12-18-08-19-10_clipboard.png) ## 5.2 浏览网页 浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器: ![](http://markdown.archerwong.cn/2018-12-18-08-19-47_clipboard.png) ## 5.3 vim编辑器 Vim 有四个跟字符编码方式有关的选项,encoding、fileencoding、fileencodings、termencoding ,它们的意义如下: - encoding: Vim 内部使用的字符编码方式,包括 Vim 的 buffer (缓冲区)、菜单文本、消息文本等。你可以用另外一种编码来编辑和保存文件,如你的vim的encoding为utf-8,所编辑的文件采用cp936编码,vim会自动将读入的文件转成utf-8(vim的能读懂的方式),而当你写入文件时,又会自动转回成cp936(文件的保存编码). - fileencoding: Vim 中当前编辑的文件的字符编码方式,Vim 保存文件时也会将文件保存为这种字符编码方式 (不管是否新文件都如此)。 - fileencodings: Vim自动探测fileencoding的顺序列表, 启动时会按照它所列出的字符编码方式逐一探测即将打开的文件的字符编码方式,并且将 fileencoding 设置为最终探测到的字符编码方式。因此最好将Unicode 编码方式放到这个列表的最前面,将拉丁语系编码方式 latin1 放到最后面。 - termencoding: Vim 所工作的终端 (或者 Windows 的 Console 窗口) 的字符编码方式。如果vim所在的term与vim编码相同,则无需设置。 我们来看看 Vim的多字符编码方式支持是如何工作的。 - 1、Vim 启动,根据 .vimrc 中设置的 encoding 的值来设置 buffer、菜单文本、消息文的字符编码方式。 - 2、读取需要编辑的文件,根据 fileencodings 中列出的字符编码方式逐一探测该文件编码方式。并设置 fileencoding 为探测到的,看起来是正确的 (注1) 字符编码方式。 - 3、对比 fileencoding 和 encoding 的值,若不同则调用 iconv 将文件内容转换为encoding 所描述的字符编码方式,并且把转换后的内容放到为此文件开辟的 buffer 里,此时我们就可以开始编辑这个文件了。 - 4、编辑完成后保存文件时,再次对比 fileencoding 和 encoding 的值。若不同,再次调用 iconv 将即将保存的 buffer 中的文本转换为 fileencoding 所描述的字符编码方式,并保存到指定的文件中。建议 encoding 的值设置为utf-8。 .vimrc文件参考配置如下: ``` :set encoding=utf-8 :set fileencodings=utf-8,gbk,big5,cp936,gb18030,gb2312,utf-16 :set fileencoding=utf-8 :set termencoding=utf-8 ``` 以上配置,先尝试用utf-8进行解码,如果用utf-8解码到了一半出错(所谓出错的意思是某个地方无法用utf-8正确地解码),那么就从头来用gbk重新尝试解码,如果gbk又出错(注意gbk并不是像utf-8似的规则编码,所以所谓的出错只是说某个编码没有对应的有意义的字,比如0),就尝试用big5,仍然出错就尝试用cp936。这一趟下来,如果中间的某次解码从头到尾都没有出错,那么 vim就认为这个文件是这个编码的,不会再进行后面的尝试了。 # 参考资料 https://www.cnblogs.com/freewater/archive/2011/08/26/2154602.html https://blog.csdn.net/jq_ak47/article/details/51769841 https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819196283586a37629844456ca7e5a7faa9b94ee8000 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html http://www.cnblogs.com/fnng/p/5008884.html https://liguangming.com/how-to-use-utf-8-with-python https://segmentfault.com/a/1190000004625718 https://www.cnblogs.com/huxi/archive/2010/12/05/1897271.html https://www.zhihu.com/question/31833164