Interview-Network

本文对计算机网络的基础知识做了一些梳理和总结(主要针对面试中常见的一些问题)

1. 网络分层模型

OSI七层模型(Open Systems Interconnection)和五层模型:

七层模型中各层的功能简要概述

  • 物理层:底层数据传输(比特流形式),如网线;网卡标准。
  • 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。管理相邻节点之间的数据通信
  • 网络层:定义IP编址,定义路由功能(路由和寻址);如不同设备的数据转发。
  • 传输层:端到端传输数据的基本功能;如 TCP、UDP。
  • 会话层:控制(建立、维护、重连)应用程序之间会话能力;如不同软件数据分发给不同软件。
  • 表示层:数据处理(编解码、加密解密、压缩解压缩)。
  • 应用层:各种应用软件,包括 Web 应用。为计算机用户提供服务

说明

  • 在四层,既传输层数据被称作(Segments);
  • 三层网络层数据被称做(Packages);
  • 二层数据链路层时数据被称为(Frames);
  • 一层物理层时数据被称为比特流(Bits)。

总结

  • 网络七层模型是一个标准,而非实现。
  • 网络四层模型是一个实现的应用模型。
  • 网络四层模型由七层模型简化合并而来。

应用层常见协议:

网络层常见协议:

数据链路层常见协议:

注意:MAC地址(物理地址)具有唯一性,每个硬件出厂时候的MAC地址是固定的;IP地址不具备唯一性,因此很多应用软件是围绕MAC地址开发的。(MAC地址是烧录在网卡或者接口上的物理地址,具有二层意义和全球唯一性,一般不能被改变。IP地址是网络中的主机或者三层接口在网络中的逻辑地址,在同一个网络内具有唯一性。)

2. DNS

DNS是什么?

官方解释:DNS(Domain Name System,域名系统),本质上是因特网上作为域名和IP地址相互映射的一个分布式数据库(数据库中记录了域名和IP的对应关系),能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。同时也是一种用于客户端和服务端通讯的应用层的计算机网络协议。

通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。

通俗的讲,我们更习惯于记住一个网站的名字,比如www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2。

DNS的工作原理?

将主机域名转换为ip地址,属于应用层协议,使用UDP传输。(DNS应用层协议,以前有个考官问过)

总结: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

一、主机向本地域名服务器的查询一般都是采用递归查询

二、本地域名服务器向根域名服务器的查询的迭代查询

1)当用户输入域名时,浏览器先检查自己的缓存中是否 这个域名映射的ip地址,有解析结束。

2)若没命中,则检查操作系统缓存(如Windows的hosts)中有没有解析过的结果,有解析结束。

3)若无命中,则请求本地域名服务器解析( LDNS)。

4)若LDNS没有命中就直接跳到根域名服务器请求解析。根域名服务器返回给LDNS一个 主域名服务器地址。

5) 此时LDNS再发送请求给上一步返回的gTLD( 通用顶级域), 接受请求的gTLD查找并返回这个域名对应的Name Server的地址

6) Name Server根据映射关系表找到目标ip,返回给LDNS

7) LDNS缓存这个域名和对应的ip, 把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结

为什么域名解析用UDP协议?

因为UDP很快,UDP的DNS协议只要一个请求、一个应答就好了

而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手,但是UDP协议传输内容不能超过512字节。

不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。

为什么区域传送用TCP协议?

(DNS区域传送(DNS zone transfer)指的是一台备用服务器使用来自主服务器的数据刷新自己的域(zone)数据库,目的是为了做冗余备份,防止主服务器出现故障时 dns 解析不可用。)

因为TCP协议可靠性好。

要从主DNS上复制内容,不能用不可靠的UDP。 因为TCP协议传输的内容大,如果使用UDP,最大只能传512字节。如果同步的数据大于512字节就没有办

DNS的解析过程:

  • 请求一旦发起,若是chrome浏览器,先在浏览器找之前有没有缓存过的域名所对应的ip地址,有的话,直接跳过dns解析了,若是没有,就会找硬盘的hosts文件,看看有没有,有的话,直接找到hosts文件里面的ip
  • 如果本地的hosts文件没有能得到对应的ip地址,浏览器会发出一个dns请求到本地dns服务器本地dns服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动等。
  • 查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。
  • 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
  • 最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

DNS负载均衡策略

当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会崩掉。处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。

随便修改DNS的后果

DNS的作用是将网址(域名)解析成对应的IP地址,电脑必须知道IP地址才能访问网络上的资源。

比较大型和重要的网站都采用多线接入,也就是说同一个域名会被解析成不同运营商的IP地址。运营商线路自动获取到的DNS地址是你采用的运营商的本地DNS服务器,这个服务器离你很近,访问速度很快,同时你使用这个DNS服务器时会解析给你最近的IP地址来访问。

因此随便改DNS的副作用之一是DNS服务器延时比较大,访问速度不如自动获取的,另一个是可能解析给你的IP不是访问最快的,比如是联通的用户却解析给你一个电信的IP地址,跨运营商访问速度会比较慢。

也有一些通用的DNS服务采用了智能解析的功能,也就是说可以根据你的源IP解析给你一个最快的访问地址,比如阿里的DNS:223.5.5.5,百度的DNS:180.76.76.76,腾讯的:119.29.29.29。这些公共DNS在全国多个地方做了镜像,访问速度比较快。但是使用这些DNS提供方可能会收集用户信息,并且可能有DNS劫持的情况。

置于Google DNS:8.8.8.8和openDNS:208.67.222.222,这两个服务器都在国外,解析速度比较慢,而且解析出来的地址常常是离海外用户比较近的地址,国内访问效果并不好。

什么是DNS劫持

DNS服务器会告诉你A网站的IP是A,B网站的IP是B,那假如你输入的A网站域名,但是他给你B的IP,你是不是就访问到别的网站去了?

既然DNS服务器可以这么玩,我是不是可以将用户引导到我的网站上?比如你要看个电影,然后你输入了爱奇艺的官网,然后我作为DNS服务器,我把优酷的IP返回给你,最后就是你虽然输入的爱奇艺官网,但是却得到了优酷的IP并访问了优酷的IP,进入了优酷的网站上。

当然这里我只是举个例子,我举这个例子就是想告诉大家,DNS服务器想给你返回什么IP就给你返回什么,所以他可以在幕后操作一些东西。最简单的就是广告。比如A网站没有广告,你直接访问就是没有的,但是你的DNS服务器把A网站下载下来了,给这个网站加了个广告,然后重新上传到了一个IP上,并把这个IP告诉了你,那就是你虽然输入的A网站的域名,但是你访问的是一个包含了广告的复制版A网站,虽然两者功能一样,但是却完全不是一个服务器上的。

这些就被我们成为DNS劫持,DNS劫持对于网络访问的影响和体验是非常严重的,除了DNS服务器,你的路由器同样可以这么干,所以在买路由器的时候也有人会关注这个路由器是否会进行DNS劫持。

3. HTTP和HTTPS

HTTP 协议

HTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。

  • 超文本,也就是网络上的包括文本在内的各式各样的消息
  • 主要是来规范浏览器和服务器端的行为的。
  • HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。
  • 优点:扩展性强、速度快、跨平台支持性好。

HTTP 协议通信过程

HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80. 通信过程主要如下:

  1. 服务器在 80 端口等待客户的请求。创建socket()、bind()、listen()
  2. 浏览器发起到服务器的 TCP 连接(创建客户端套接字 Socket)。socket()创建socket描述符(用一个整数来描述),然后connect(),连接的过程会完成三次握手,完成握手后connect函数返回
  3. 服务器接收来自浏览器的 TCP 连接。accept()阻塞直到接收到客户端的connect(),如果顺利完成了连接,accept函数会取出一个建立好的客户端连接(连接的上限由listen函数的一个入参规定)然后立即返回用于读写的文件描述符fd
  4. 浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。read()读入,write()写出
  5. 客户端和服务端关闭 TCP 连接。close()

HTTPS 协议

HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。

  • HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443.
  • HTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。
  • 优点:保密性好、信任度高。
HTTPS 的核心—SSL/TLS协议

HTTPS 之所以能达到较高的安全性要求,就是结合了 SSL/TLS 和 TCP 协议,对通信数据进行加密,解决了 HTTP 数据透明的问题。接下来重点介绍一下 SSL/TLS 的工作原理。

SSL 和 TLS 的区别?

SSL 和 TLS 没有太大的区别。

SSL 指安全套接字协议(Secure Sockets Layer),首次发布与 1996 年。SSL 的首次发布其实已经是他的 3.0 版本,SSL 1.0 从未面世,SSL 2.0 则具有较大的缺陷(DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption)。很快,在 1999 年,SSL 3.0 进一步升级,新版本被命名为 TLS 1.0。因此,TLS 是基于 SSL 之上的,但由于习惯叫法,通常把 HTTPS 中的核心加密协议混成为 SSL/TLS。

SSL/TLS 的工作原理

  • 非对称加密

SSL/TLS 的核心要素是非对称加密。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。可以设想一个场景,

在某个自助邮局,每个通信信道都是一个邮箱,每一个邮箱所有者都在旁边立了一个牌子,上面挂着一把钥匙:这是我的公钥,发送者请将信件放入我的邮箱,并用公钥锁好。

但是公钥只能加锁,并不能解锁。解锁只能由邮箱的所有者——因为只有他保存着私钥。

这样,通信信息就不会被其他人截获了,这依赖于私钥的保密性。

非对称加密的公钥和私钥需要采用一种复杂的数学机制生成(密码学认为,为了较高的安全性,尽量不要自己创造加密方案)。公私钥对的生成算法依赖于单向陷门函数。

单向函数:已知单向函数 f,给定任意一个输入 x,易计算输出 y=f(x);而给定一个输出 y,假设存在 f(x)=y,很难根据 f 来计算出 x。

单向陷门函数:一个较弱的单向函数。已知单向陷门函数 f,陷门 h,给定任意一个输入 x,易计算出输出 y=f(x;h);而给定一个输出 y,假设存在 f(x;h)=y,很难根据 f 来计算出 x,但可以根据 f 和 h 来推导出 x。

在这里,函数 f 的计算方法相当于公钥,陷门 h 相当于私钥。公钥 f 是公开的,任何人对已有输入,都可以用 f 加密,而要想根据加密信息还原出原信息,必须要有私钥才行。

  • 对称加密

使用 SSL/TLS 进行通信的双方需要使用非对称加密方案来通信,但是非对称加密设计了较为复杂的数学算法,在实际通信过程中,计算的代价较高,效率太低,因此,SSL/TLS 实际对消息的加密使用的是对称加密。

对称加密:通信双方共享唯一密钥 k,加解密算法已知,加密方利用密钥 k 加密,解密方利用密钥 k 解密,保密性依赖于密钥 k 的保密性。

对称加密的密钥生成代价比公私钥对的生成代价低得多,那么有的人会问了,为什么 SSL/TLS 还需要使用非对称加密呢?因为对称加密的保密性完全依赖于密钥的保密性。在双方通信之前,需要商量一个用于对称加密的密钥。我们知道网络通信的信道是不安全的,传输报文对任何人是可见的,密钥的交换肯定不能直接在网络信道中传输。因此,使用非对称加密,对对称加密的密钥进行加密,保护该密钥不在网络信道中被窃听。这样,通信双方只需要一次非对称加密,交换对称加密的密钥,在之后的信息通信中,使用绝对安全的密钥,对信息进行对称加密,即可保证传输消息的保密性。

公钥传输的信赖性

SSL/TLS 介绍到这里,了解信息安全的朋友又会想到一个安全隐患,设想一个下面的场景:

客户端 C 和服务器 S 想要使用 SSL/TLS 通信,由上述 SSL/TLS 通信原理,C 需要先知道 S 的公钥,而 S 公钥的唯一获取途径,就是把 S 公钥在网络信道中传输。要注意网络信道通信中有几个前提:

  1. 任何人都可以捕获通信包
  2. 通信包的保密性由发送者设计
  3. 保密算法设计方案默认为公开,而(解密)密钥默认是安全的

因此,假设 S 公钥不做加密,在信道中传输,那么很有可能存在一个攻击者 A,发送给 C 一个诈包,假装是 S 公钥,其实是诱饵服务器 AS 的公钥。当 C 收获了 AS 的公钥(却以为是 S 的公钥),C 后续就会使用 AS 公钥对数据进行加密,并在公开信道传输,那么 A 将捕获这些加密包,用 AS 的私钥解密,就截获了 C 本要给 S 发送的内容,而 C 和 S 二人全然不知。

同样的,S 公钥即使做加密,也难以避免这种信任性问题,C 被 AS 拐跑了!

为了公钥传输的信赖性问题,第三方机构应运而生——证书颁发机构(CA,Certificate Authority)。CA 默认是受信任的第三方。CA 会给各个服务器颁发证书,证书存储在服务器上,并附有 CA 的电子签名(见下节)。

当客户端(浏览器)向服务器发送 HTTPS 请求时,一定要先获取目标服务器的证书,并根据证书上的信息,检验证书的合法性。一旦客户端检测到证书非法,就会发生错误。客户端获取了服务器的证书后,由于证书的信任性是由第三方信赖机构认证的,而证书上又包含着服务器的公钥信息,客户端就可以放心的信任证书上的公钥就是目标服务器的公钥。

数字签名

好,到这一小节,已经是 SSL/TLS 的尾声了。上一小节提到了数字签名,数字签名要解决的问题,是防止证书被伪造。第三方信赖机构 CA 之所以能被信赖,就是 靠数字签名技术

数字签名,是 CA 在给服务器颁发证书时,使用散列+加密的组合技术,在证书上盖个章,以此来提供验伪的功能。具体行为如下:

CA 知道服务器的公钥,对该公钥采用散列技术生成一个摘要。CA 使用 CA 私钥对该摘要进行加密,并附在证书下方,发送给服务器。

现在服务器将该证书发送给客户端,客户端需要验证该证书的身份。客户端找到第三方机构 CA,获知 CA 的公钥,并用 CA 公钥对证书的签名进行解密,获得了 CA 生成的摘要。

客户端对证书数据(也就是服务器的公钥)做相同的散列处理,得到摘要,并将该摘要与之前从签名中解码出的摘要做对比,如果相同,则身份验证成功;否则验证失败。

总结来说,带有证书的公钥传输机制如下:

  1. 设有服务器 S,客户端 C,和第三方信赖机构 CA。
  2. S 信任 CA,CA 是知道 S 公钥的,CA 向 S 颁发证书。并附上 CA 私钥对消息摘要的加密签名。
  3. S 获得 CA 颁发的证书,将该证书传递给 C。
  4. C 获得 S 的证书,信任 CA 并知晓 CA 公钥,使用 CA 公钥对 S 证书上的签名解密,同时对消息进行散列处理,得到摘要。比较摘要,验证 S 证书的真实性。
  5. 如果 C 验证 S 证书是真实的,则信任 S 的公钥(在 S 证书中)。
HTTPS握手过程
  1. 客户端发起HTTPS请求
  2. 服务端配置(要求有一套数字证书,其实就是一对公钥和私钥)
  3. 服务端传送证书
  4. 客户端解析证书。判断是否存在问题,公钥是否有效。如果没问题,就自己生成一个私钥(用于加密要传输的信息),然后用证书对该私钥加密
  5. 客户端传送加密信息。目的是让服务端得到这个私钥。之后就通过这个私钥来加密解密客户端的信息
  6. 服务端用私钥解密得到这个客户端的私钥,然后把内容通过该值进行对称加密。
  7. 服务端传输加密后的信息
  8. 客户端用私钥解密信息。

请求和响应报文字段

Request:

  • 请求行:Request Line(请求方法+URL+HTTP版本)
  • 请求头:Request Headers(包含若干个属性,根据此获取客户端的信息)
  • 请求体:Request Body

Response:

  • 状态行:Status Line(报文协议及版本 + 状态码)
  • 响应头:Response Headers (由多个属性组成)
  • 响应体:Response Body (服务端返回的处理信息)
HTTP请求方法

客户端发送的 请求报文 第一行为请求行,包含了方法字段。

根据 HTTP 标准,HTTP 请求可以使用多种请求方法。

HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

GET和POST的区别:

  1. get是获取数据,post是修改数据

  2. get把请求的数据放在url上, 以?分割URL和传输数据,参数之间以&相连,所以get不太安全。而post把数据放在HTTP的请求体内(request body)

  3. get提交的数据最大是2k( 限制实际上取决于浏览器的URL长度限制), post理论上没有限制。

  4. GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); POST产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

  5. GET请求会被浏览器主动缓存,而POST不会,除非手动设置。

  6. 本质区别:GET是幂等的,网络不好时会重试,而POST不是幂等的

    这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。

正因为它们有这样的区别,所以不应该且不能用get请求做数据的增删改这些有副作用的操作。因为get请求是幂等的,在网络不好的隧道中会尝试重试。如果用get请求增数据,会有重复操作的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。

常用HTTP请求头

Accept

  • Accept: text/html 浏览器可以接受服务器回发的类型为 text/html。
  • Accept: */* 代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个)。

Accept-Encoding

  • Accept-Encoding: gzip, deflate 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码)。

Accept-Language

  • Accept-Language:zh-CN,zh;q=0.9 浏览器申明自己接收的语言。

Connection

  • Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
  • Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。

Host(发送请求时,该报头域是必需的)

  • Host:www.baidu.com 请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的。

Referer

  • Referer:https://www.baidu.com/?tn=62095104_8_oem_dg 当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。

User-Agent

  • User-Agent:Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。

Cache-Control

  • Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
  • Cache-Control:public响应会被缓存,并且在多用户间共享。正常情况, 如果要求HTTP认证,响应会自动设置为 private.
  • Cache-Control:must-revalidate 响应在特定条件下会被重用,以满足接下来的请求,但是它必须到服务器端去验证它是不是仍然是最新的。
  • Cache-Control:no-cache 响应不会被缓存,而是实时向服务器端请求资源。
  • Cache-Control:max-age=10 设置缓存最大的有效时间,但是这个参数定义的是时间大小(比如:60)而不是确定的时间点。单位是[秒 seconds]。
  • Cache-Control:no-store 在任何条件下,响应都不会被缓存,并且不会被写入到客户端的磁盘里,这也是基于安全考虑的某些敏感的响应才会使用这个。

Cookie

  • Cookie是用来存储一些用户信息以便让服务器辨别用户身份的(大多数需要登录的网站上面会比较常见),比如cookie会存储一些用户的用户名和密码,当用户登录后就会在客户端产生一个cookie来存储相关信息,这样浏览器通过读取cookie的信息去服务器上验证并通过后会判定你是合法用户,从而允许查看相应网页。
  • 当然cookie里面的数据不仅仅是上述范围,还有很多信息可以存储是cookie里面,比如sessionid等。
常用HTTP响应头

Cache-Control(对应请求中的Cache-Control)

  • Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
  • Cache-Control:public 浏览器和缓存服务器都可以缓存页面信息。
  • Cache-Control:must-revalidate 对于客户机的每次请求,代理服务器必须想服务器验证缓存是否过时。
  • Cache-Control:no-cache 浏览器和缓存服务器都不应该缓存页面信息。
  • Cache-Control:max-age=10 是通知浏览器10秒之内不要烦我,自己从缓冲区中刷新。
  • Cache-Control:no-store 请求和响应的信息都不应该被存储在对方的磁盘系统中。

Content-Type

  • Content-Type:text/html;charset=UTF-8 告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。

Content-Encoding

  • Content-Encoding:gzip 告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。

Date

  • Date: Tue, 03 Apr 2018 03:52:28 GMT 这个是服务端发送资源时的服务器时间,GMT是格林尼治所在地的标准时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。

Server

  • Server:Tengine/1.4.6 这个是服务器和相对应的版本,只是告诉客户端服务器信息
Transfer-Encoding
  • Transfer-Encoding:chunked 这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。
HTTP状态码

常见的HTTP状态码有哪些?

状态码 类别 含义
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出

1xx 信息

  • 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

2xx 成功

  • 200 OK
  • 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
  • 206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。

3xx 重定向

重定向状态码要么告知客户端使用替代位置来访问她们所感兴趣的资源,要么就提供一个替代的响应而不是资源的内容。如果资源已被移动,可发送一个重定向状态码和一个可选的Location首部来告知客户端已被移走,以及现在可以在哪里找到它。这样子,浏览器就可以在不打扰用户的情况下,透明地转到新的位置。

  • 301 Moved Permanently :永久性重定向,返回信息会包括新的URL,会自动定向到新的URL
  • 302 Found :临时性重定向
  • 303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
  • 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
  • 307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。

4xx 客户端错误

  • 400 Bad Request :请求报文中存在语法错误。
  • 401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
  • 403 Forbidden :请求被拒绝。
  • 404 Not Found:代表客户端在浏览网页时,服务器无法正常提供消息,或是服务器无法回应且不知原因。通常是因为用户所访问的对应网页已被删除或从未存在

5xx 服务器错误

  • 500 Internal Server Error :服务器正在执行请求时发生错误。
  • 503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

HTTP1.0和HTTP1.1区别

  1. 连接方式 :
    • HTTP 1.0 为短连接(每次发送消息都要重新建立连接)
    • HTTP 1.1 默认支持长连接(响应头中默认保持Connection:keep-alive)。
  2. 状态响应码 :
    • HTTP/1.1中新加入了大量的状态码,光是错误响应状态码就新增了24种。比如说,100 (Continue)——在请求大资源前的预热请求,206 (Partial Content)——范围请求的标识码,409 (Conflict)——请求与当前资源的规定冲突,410 (Gone)——资源已被永久转移,而且没有任何已知的转发地址。
  3. 缓存处理 :
    • 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准
    • HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
  4. 带宽优化及网络连接的使用 :
    • HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能
    • HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  5. Host头处理 :
    • HTTP/1.1在请求头中加入了Host字段。用于客户端指定自己想访问的http服务器的域名/IP 地址和端口号

浏览器中输入Url地址后显示主页的过程

  • 根据域名,进行DNS域名解析(先查缓存,再根据配置的DNS地址查本地)
    • 浏览器缓存==》操作系统缓存==》路由器缓存==》本地(ISP, Internet Service Provide,如中国电信等)域名服务器缓存==》根域名服务器
  • 拿到解析的IP地址,建立TCP连接,完成三次握手
  • 向IP地址发送HTTP请求
    • 方式为GET
    • 这个GET请求包含了主机(host)、用户代理(User-Agent),用户代理就是自己的浏览器,connection中的keep-alive表示浏览器告诉对方服务器传输完现在请求的内容后不要断开连接,不断开的话下次继续连接速度就很快了
  • 服务器处理请求,并返回响应结果
    • 服务器收到请其后会解析这个请求(读请求头)
    • 然后生成一个响应头和具体响应内容。接着服务器会返回一个响应头和一个响应:
      • 响应头告诉了浏览器一些必要信息,例如重要的Status Code,2开头如200表示一切正常,3开头表示重定向,4开头是客户端错误,如404表示请求的资源不存在,5开头表示服务器端错误
      • 响应就是具体的请求的页面内容
  • 关闭TCP连接(根据请求头中的connection字段是否设置为keep alive来判断是否断开TCP连接)
  • 浏览器解析HTML
  • 浏览器布局渲染

4. TCP

三次握手

本质的目的:为了建立可靠的通讯信道,保证服务器端和客户端的发送和接收能力都是正常的

过程:

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。

  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

为什么需要三次握手而不是两次

弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

试想如果是用两次握手,则会出现下面这种情况:

如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

三次握手的缺点——实际上就是TCP协议的缺点:

慢,效率低,占用系统资源高,易被攻击。而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源

另外,TCP的三次握手机制,导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。

  • DOS攻击:

    DoS是“Denial of Service”的简称,中文意思即“拒绝服务”,造成DoS的攻击行为被称为“DoS攻击”,其目的是使计算机/服务器或网络无法提供正常的服务。最常见的DoS攻击有“计算机网络的带宽攻击”和“连通性攻击”。

    DoS攻击是利用TCP协议“三次握手”的缺陷进行的。当黑客要进行DoS攻击时,他会操纵很多僵尸主机向被攻击的服务器发送SYN数据包,当服务器回复ACK确认包后,僵尸主机则不再回应,这样服务器就会保持这种“半连接”的状态进行等待。每一个这样的“半连接”状态,都会耗费服务器的资源,如果有数量极大的“半连接”,服务器就会停止正常工作了。

  • DDOS攻击:

    DDoS( 英文全称: Distributed Denial of Service,缩写:DDoS ),翻译成中文,意思是“分布式拒绝服务”。DDoS攻击,是一种耗尽攻击目标的系统资源,导致攻击目标无法响应正常服务请求的网络攻击方式。这种攻击手法通过借助于“客户/服务器”技术,将多个计算机联合起来作为攻击平台,对一个或者多个目标发动攻击。

四次挥手

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的确认号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

CLOSE_WAIT状态:

出现时间:

TCP 连接断开时需要进行“四次挥手”,TCP 连接的两端都可以发起关闭连接的请求,若其中一端发起了关闭连接,但另外一端没有关闭连接,那么该连接就会处于 CLOSE_WAIT 状态

出现原因

通常来说,CLOSE_WAIT 在服务器停留的时间很短,且只会发生在被动关闭连接的一端。除非 Kill 掉进程,否则它是不会消失的,意味着一直占用资源。

如果发现有大量的 CLOSE_WAIT,那就是被动关闭的一方没有及时发送 FIN(根本原因是没有关闭连接),一般来说有以下几种可能:

  1. 代码问题:请求的时候没有显式关闭 Socket 连接,或者死循环导致关闭连接的代码没有执行到,即 FIN 包没有发出,导致 CLOSE_WAIT 不断累积
  2. 响应过慢 / 超时设置过小:双方连接不稳定,一方 Timeout,另外一方还在处理逻辑,导致 Close 被延后

Time-wait状态

主动关闭方在收到被动关闭方的FIN包后并返回ACK后,会进入TIME_WAIT状态,TIME_WAIT状态又称2MSL状态,每个TCP连接都必须有一个最大报文段生存时间MSL,在网络传输中超过这个时间的报文段将被丢弃。当TCP连接发起一个主动关闭,并发出最后一个ACK时,必须在TIME_WAIT状态停留两倍MSL时间,在2MSL等待期间,定义这个连接的插口(客户端IP地址和端口号,服务器IP地址和端口号的四元组)将不能再被使用。2MSL状态存在有两个理由:

  • 1.允许老的重复报文分组在网络中消逝。
  • 2.保证TCP全双工连接的正确关闭。

第一个理由是假如我们在192.168.1.1:500039.106.170.184:6000建立一个TCP连接,一段时间后我们关闭这个连接,再基于相同插口建立一个新的TCP连接,这个新的连接称为前一个连接的化身。老的报文很有可能由于某些原因迟到了,那么新的TCP连接很有可能会将这个迟到的报文认为是新的连接的报文,而导致数据错乱。为了防止这种情况的发生TCP连接必须让TIME_WAIT状态持续2MSL,在此期间将不能基于这个插口建立新的化身,让它有足够的时间使迟到的报文段被丢弃。

第二个理由是因为如果主动关闭方最终的ACK丢失,那么服务器将会重新发送那个FIN,以允许主动关闭方重新发送那个ACK。要是主动关闭方不维护2MSL状态,那么主动关闭将会不得不响应一个RST报文段,而服务器将会把它解释为一个错误,导致TCP连接没有办法完成全双工的关闭,而进入半关闭状态。

img

为什么需要四次挥手?

(1)第一次挥手

因此当主动方发送断开连接的请求(即FIN报文)给被动方时,仅仅代表主动方不会再发送数据报文了,但主动方仍可以接收数据报文。

(2)第二次挥手

被动方此时有可能还有相应的数据报文需要发送,因此需要先发送ACK报文,告知主动方“我知道你想断开连接的请求了”。这样主动方便不会因为没有收到应答而继续发送断开连接的请求(即FIN报文)。

(3)第三次挥手

被动方在处理完数据报文后,便发送给主动方FIN报文;这样可以保证数据通信正常可靠地完成。发送完FIN报文后,被动方进入LAST_ACK阶段(超时等待)。

(4)第四挥手

如果主动方及时发送ACK报文进行连接中断的确认,这时被动方就直接释放连接,进入可用状态。

TCP和UDP

TCP(Transmission Control Protocol,传输控制协议)

  • 优点: 可靠,稳定。

    • TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认和重传(接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。)、流量控制、拥塞控制机制来保证可靠传输,在数据传完后,还会断开连接用来节约系统资源。
  • 缺点: 慢,效率低,占用系统资源高,易被攻击。

    • TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。
    • 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。

UDP(User Datagram Protocol ,用户数据报协议)

  • 优点: 快,比TCP稍安全

    • UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。
    • 没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。
    • 但UDP也是无法避免攻击的,比如:UDP Flood攻击(攻击者在短时间内向目标设备发送大量的UDP报文,导致链路拥塞甚至网络瘫痪)……
  • 缺点: 不可靠,不稳定

    • 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。

区别总结:

  • TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  • TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  • TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
  • UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
  • 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  • TCP首部开销20字节;UDP的首部开销小,只有8个字节
  • TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

什么时候应该使用TCP: 当对网络通讯质量有要求的时候:

  • 整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。
  • 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输 …………

什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP ……

TCP流量控制

流量控制的目的:

  • 发送者发送数据过快,接收者来不及接收,就会有分组丢失。因而为了控制发送者的发送速度,产生了流量控制。根本目的是防止分组丢失。( 分组在路由器的输入端口输出端口都可能会在队列中排队等候处理。若分组处理的速率赶不上分组进入队列的速率,则队列的存储空间必定回减少到0,这就使得后面在进入队列的分组由于没有存储空间只能被丢弃。以前提到的分组丢失就是发生在路由器中的输入或输出队列产生溢出的时候。当然,设备或线路出故障也可能使得分组丢失。)
  • 它是构成TCP可靠性的一方面

如何实现:

  • 滑动窗口协议结合连续ARQ协议(Automatic Repeat-reQuest)实现。
  • 连续ARQ协议规定发送方采用流水线传输。流水线传输就是发送方可以连续发送多个分组,不必每发完一个分组就停下来等待对方确认
  • 滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。
  • 主要方式就是接收方返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送

流量控制引发的死锁:

  • 当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。
  • 为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。

滑动窗口协议:

  • 为什么要引入窗口

    • 我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。(停止等待ARQ协议)

    • 这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低

    • 因此TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

    • 有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

  • 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

    • 假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:

    图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。

窗口的大小由哪一方决定?

  • TCP 头里有一个字段叫 Window,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

TCP拥塞控制

和流量控制的区别:

  • 拥塞控制是作用于网络的,为了防止过多的数据注入到网络中,避免出现网络负载过大的情况,常用的方法为:
    • 慢开始、拥塞避免
    • 快重传、快恢复
  • 流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的

拥塞控制算法:(假设接收方缓存足够大,发送方的大小由网络拥塞程度决定)

  • 慢开始算法:

    • 发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的小大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接收方的接收能力,发送窗口可能小于拥塞窗口。
    • 慢开始算法的思路是不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。(每经过一个传输轮次(transmission round),也即经过一个往返时间RTT,拥塞窗口cnwd就加倍)
    • 为了防止cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh(slow start threshold)状态变量。ssthrash的用法如下:
      • 当cwnd < ssthresh时,使用慢开始算法
      • 当cwnd > ssthresh时,改用拥塞避免算法
      • 当cwnd = ssthresh时,慢开始与拥塞避免算法任意
    • 注意,这里的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd = 1,然后逐渐增大,这比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”
  • 拥塞避免算法:

    • 拥塞避免算法让拥塞窗口缓慢增长,即每经过一个RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口会按线性规律缓慢增长。

    • 无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(通过有没有按时收到确认(即使是其他原因)来判断),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

    • 这个拥塞控制流程如下图:

    • 注意:“拥塞避免”并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞

  • 快重传算法

    • 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。如下图:

  • 快恢复算法:

    • 与快重传配合使用

    • 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,但接下去并不执行慢开始算法

    • 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞(网络情况没有需要到慢开始那么严重),所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。

    • 如下如: TCP Reno版本是目前使用最广泛的版本

    • 注意:在采用快恢复算法时,慢开始算法只是再TCP连接建立时和网络出现超时时才使用

Cookie和Session

Cookie是什么?

  • 由于HTTP协议是无状态的(即服务器记不住你,比如可能每刷新一次网页都要重新输入账号密码登录),主要是为了让HTTP协议尽可能简单,使得它能够处理大量事务,HTTP/1.1引入Cookie来保存状态信息。(再次向服务器发起请求时服务器能够通过cookie认出你)
  • Cookie是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带Cookie数据,因此会带来额外的性能开销。
  • 通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie主要应用:

  • 会话状态管理(如保存用户登录状态、购物车状态、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Session

Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束

  • 工作原理:

    客户端登录完成之后,服务器会创建对应的session,session创建完之后,会把session的id发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着session id,服务器拿到session id中,在内存中找到与之对应的session这样就可以正常工作了。

使用Session维护用户登录状态的过程如下:

  1. 用户进行登录时,用户提交包含用户名和密码的表单,放入HTTP请求报文中
  2. 服务器验证该用户名和密码,如果正确则把用户信息存储到Redis中,它在Redis中的Key称为Session ID
  3. 服务器返回的响应报文的Set-Cookie首部字段包含了这个Session ID,客户端收到响应报文后将该Cookie值存入浏览器中
  4. 客户端之后对同一个服务器请求时会包含该Cookie值,服务器收到之后提取出Session ID,从Redis中取出用户信息,继续之前的业务操作

注意这里有Session ID的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的Session ID值。此外还需要经常重新生成Session ID。在对安全性要求高的场景(如转账等操作)下,除了使用Session管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码

cookie和session的区别

  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。

抽象概括一下:

  • 一个cookie可以认为是一个【变量】,形如name=value,存储在浏览器,是客户端保存用户信息的一种机制,也是实现Session的一种方式(要利用Cookie存储Session Id);

  • 一个Session是在服务端保存的一个数据结构,用来跟踪用户的状态,多数情况是键值对的形式,存储在服务器上(可存储在文件、数据库、内存、内存型数据库(如Redis)中)。

如果禁用了cookie,仍然想要使用session的话,可以通过session url重写的方式,直接把jsessionid=xxxxxx附加在URL路径的后面。也可以每个页面加一个hidden隐藏域,value协商session

cookie禁用后session id可能存储在localStorage 或者SessionStorage

网络I/O模型

IO(input output)主要指:文件IO,网络IO

网络通信如何实现:通过socket(套接字)。在Unix一切皆文件的思想下,进程间通信就被冠名为文件描述符(file desciptor),Socket是一种“打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

网络IO性能需要优化的原因:CPU处理数据的速度(纳秒级)远远大于IO准备数据的速度(微秒级)

服务端处理网络请求的大致流程:

核心过程:

  • 成功建立链接
  • 内核等待网卡数据到位(涉及DMA技术)(这一步控制IO是否阻塞)
  • 内核缓冲区数据拷贝到用户空间(这里涉及mmap内存映射技术)(这一步控制IO是否同步)

DMA:网卡和磁盘数据拷贝到内存这个过程,如果需要CPU参与的话会占用大量的CPU运行时间,因为IO操作相对耗时非常高。而且CPU主要适用于进行运算,磁盘数据拷贝到内存这个过程并不涉及到运算操作而且流程固定,因此设计了DMA来专门进行上述拷贝操作,相当于在磁盘嵌入了一个DMA芯片(类似简易版的IO cpu)让它来专门负责上述拷贝操作,从而使得CPU不参与上述拷贝操作,使之专注于运算操作。

mmap的设计理念是:用户空间和内核空间映射同一块内存空间,从而达到省略将数据从内核缓冲区拷贝到用户空间的操作,用户空间通过映射直接操作内核缓冲区的数据。

同步阻塞IO

调用recv()后进程一直阻塞,等待内核数据到位后,进程继续阻塞,直到内核数据拷贝到用户空间。缺点:高并发时,服务端与客户端对等连接,线程多带来的问题:

  • CPU资源浪费,上下文切换。
  • 内存成本几何上升,JVM一个线程的成本约1MB。

同步非阻塞IO

应用进程一直轮训调用recv()查看内核缓冲区的数据是否准备好,内核立即给予答复,如果回复结果通知数据还未准备好,则接着轮训进行询问。

缺点:当进程有1000fds,代表用户进程轮询发生系统调用1000次kernel,来回的用户态和内核态的切换,成本几何上升。

I/O多路复用 - IO multiplexing

单个线程就可以同时处理多个网络连接。

内核负责轮询所有socket,当某个socket有数据到达了,就通知用户进程。

多路复用在Linux内核代码迭代过程中依次支持了三种调用,即SELECT、POLL、EPOLL三种多路复用的网络I/O模型。

select,poll,epoll有啥不同?epoll高效的原因是什么?

这些都是为了让服务器可以同时监控多个socket。(多个客户端与同一个服务端建立了连接,这个时候内核就会有多个socket,并且为它们分配多个fd文件描述符。它们收到网络数据后无法通过目的端口来直接匹配socket,还需要再通过源ip和端口来确定属于哪个socket。)

前置知识点:

  • 网卡数据接收大致流程:网线->网卡->I/O南桥芯片->内存

  • cpu如何知道接收了网络数据?:当网卡把数据写入到内存后,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据

  • 进程阻塞为什么不占用 CPU 资源?:阻塞是进程调度的关键一环,指的是进程在等待某个事件发生前的等待状态,recv,select,epoll都是阻塞方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //创建socket 
    int s = socket(AF_INET, SOCK_STREAM, 0);
    //绑定
    bind(s, ...)
    //监听
    listen(s, ...)
    //接受客户端连接
    int c = accept(s, ...)
    //接收客户端数据
    recv(c, ...);
    //将数据打印出来
    printf(...)

    这是一段基础的网络编程代码,先创建socket对象,依次绑定ip,端口和接受连接。当执行recv()时,进程会被阻塞,直到接收到数据才往下走。

简单方法:select实现

预先传入一个 Socket 列表,如果列表中的 Socket 都没有数据,挂起进程,直到有一个 Socket 收到数据,唤醒进程。这种方法很直接,也是 Select 的设计思想。

Select 的实现思路很直接,假如程序同时监视 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中,当任何一个 Socket 收到数据后,中断程序将唤起进程(就是将进程从所有的等待队列中移除,加入到工作队列里面)。

当进程 A被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。

这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:

  • 每次select都需要将进程加入到监视socket的等待队列,每次唤醒都要将进程从socket等待队列移除。这里涉及两次遍历操作,而且每次都要将FDS列表传递给内核,有一定的开销。
  • 进程被唤醒后,只能知道有socket接收到了数据,无法知道具体是哪一个socket接收到了数据,所以需要用户进程进行遍历,才能知道具体是哪个socket接收到了数据。

那么,有没有减少遍历的方法?有没有保存就绪 Socket 的方法?这两个问题便是 Epoll 技术要解决的。

epoll的设计思路

措施一:功能分离

select的添加等待队列和阻塞进程是合并在一起的,每次调用select()操作时都得执行一遍这两个操作,从而导致每次都要将fd[]传递到内核空间,并且遍历fd[]的每个fd的等待队列,将进程放入各个fd的等待队列中。

epoll优化的方案是:将添加等待队列和阻塞进程拆分成两个独立的操作,不用每次都去重新维护等待队列,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。

措施二:就绪列表

Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。

select会把整个fd[]返回给用户程序,让用户程序自己去遍历哪个fd有接收到网络数据。epoll只会把接收到网络数据的fd[]返回给用户程序,用户程序不用自己去进行遍历查询。

epoll实现代码大致结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int init_server(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr;
addr.sin_family = AF_INET;

bind(sockfd, &addr, sizeof(struct sockaddr_in));

listen(sockfd, 5);

return sockfd;
}

int main(){
int sockfd = init_server();

int epfd = epoll_create(1); //创建一个红黑树

struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;

epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); //将所有需要监听的socket添加到epfd中

struct epoll_event events[MAX_EVENTS];

while(1){ //事件循环
int n = epoll_wait(epfd, events, MAX_EVENTS, -1); //n为接收到数据的socket的数目
for(int i = 0; i != n; ++i){
if(events[i].data.fd == sockfd){//listen fd
int clientfd = accept(sockfd, );
}
else{
recv();

send();
}
}
}
}

epoll的原理与工作流程

创建Epoll对象

调用epoll_create方式时,会创建一个eventpoll对象(),eventpoll 对象也是文件系统中的一员,和 Socket 一样,它也会有等待队列。

创建一个代表该 Epoll 的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员。

维护监控对象

创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。eventpoll会通过一个红黑树来存储所有被监视的socket对象,实现快速查找,删除和添加。

接收数据

当 Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用。

给就绪列表添加引用

如上图展示的是 Sock2 和 Sock3 收到数据后,中断程序让 Rdlist 引用这两个 Socket。

eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。

当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。

阻塞和唤醒进程

假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。

当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态

也因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。

总结

select对socket是线性扫描,它只知道有I/O事件发生,却不知道具体是哪几个流,只会无差别地轮询所有流,找出能读/写数据的流进行操作。同时处理的流越多,无差别轮询事件越长(O(n))。因此当socket较多时,会浪费很多CPU事件

epoll模型修改主动轮询为被动通知,当有时间发生时,被动接收通知。所以epoll模型注册socket之后,主程序可做其他事情,当事件发生时,接收到通知后再去处理。

select,poll需自己主动不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但它是设备就绪时,调用回调函数,把就绪fd放入就绪链表,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但select和poll在“醒着”时要遍历整个fd集合,而epoll在“醒着”的时候只需判断就绪链表是否为空,节省大量CPU时间,这就是回调机制带来的性能提升

select为什么在socket连接很多的情况下性能不佳?

  • 将维护socket监控列表和阻塞进程的操作合并在了一起,每次select()调用都会触发这两个操作,从而导致每次调用select()都需要把全量的fd[]列表从用户空间传递到内核空间,内核线程在阻塞进程前需要遍历fd[]将待阻塞的进程放入到每个fd的等待队列里(第一次遍历)。
  • 当有网络数据到来时,并不知道网络数据属于具体哪个socket,只知道收到过网络数据,因此需要遍历fd[]唤醒等待队列里的阻塞进程(第二次遍历),并且把fd[]从内核空间拷贝到用户空间,让用户程序自己去遍历fd[]判别哪个socket收到了网络数据(第三次遍历,发生在用户空间)。
  • fd[]比较大的情况,大量的遍历操作会导致性能急剧下降,所以select会默认限制最大文件句柄数为1024,间接控制fd[]最大为1024。

poll其实内部实现基本跟select一样,区别在于它们底层组织fd[]的数据结构不太一样,从而实现了poll的最大文件句柄数量限制去除了

epoll是怎么优化select上述那些问题?

  • 引入了eventpoll这个中间结构,它通过红黑树(rbr)来组织所有待监控的socket对象,实现高效的查找,删除和添加。
  • 当收到网络数据时,会触发对应的fd的回调函数,这时不是去遍历各个fd的等待队列进行唤醒进程的操作了,而是把收到数据的socket加入到就绪列表(底层是一个双向链表)。
  • eventpoll有个单独的等待队列来维护待唤醒的进程,避免了像select那样每次需要遍历fd[]来查找各个fd的等待队列的进程。

epoll有这几个核心的数据结构:

  • 红黑树:存放所有待监听的socket (通过epoll_create创建)
  • 双向链表:存放收到网络数据的socket
  • 等待队列:待唤醒的等待线程

其他

网站如何承受高流量

集群处理、负载均衡

负载均衡(Load Balance)是集群技术(Cluster)的一种应用,可以将工作任务分摊到多个处理单元,从而提高并发处理能力,有利于提升中大型网站的性能。

http重定向协议实现负载均衡

  • 根据用户的http请求计算出一个真实的web服务器地址,并将该web服务器地址写入http重定向响应中返回给浏览器,由浏览器重新进行访问。
  • 优点:该方式比较简单
  • 缺点:性能较差,浏览器需要每次请求两次服务器才能拿完成一次访问。

【协议层】dns域名解析负载均衡

  • 在DNS服务器上配置多个域名对应IP的记录。
  • 优点:该方式直接将负载均衡的工作交给了DNS,为网站管理维护省掉了很多麻烦,访问速度快,有效改善性能。
  • 缺点:DNS可能缓存A记录,不受网站控制

【协议层】反向代理负载均衡

  • 反向代理服务器在提供负载均衡功能的同时,管理着一组web服务器,根据负载均衡算法将请求的浏览器访问转发到不同的web服务器处理,处理结果经过反向服务器返回给浏览器。
  • 优点:该方式部署简单
  • 缺点:反向代理服务器作为沟通桥梁是所有请求和相应的中转站,其性能可能称为瓶颈

【网络层】IP负载均衡

  • 在网络层通过修改目标地址进行负载均衡。
  • 优点:该方式在响应请求时速度较反向服务器负载均衡要快
  • 缺点:当请求数据较大(大型视频或文件)时,速度反应就会变慢。负载均衡的网卡带宽成为系统的瓶颈

【链路层】数据链路层负载均衡

  • 在数据链路层修改Mac地址进行负载均衡,负载均衡服务器的IP和它所管理的web 服务群的虚拟IP一致。它不需要负载均衡服务器进行地址的转换,但是对负载均衡服务器的网卡带宽要求较高。

F5

  • F5的全称是F5-BIG-IP-GTM,硬件负载均衡设备,其并发能力达到。该方式能够实现多链路的负载均衡和冗余,可以接入多条ISP链路,在链路之间实现负载均衡和高可用。

负载均衡算法

轮询

顾名思义将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

优点 缺点
服务器请求数据相同 服务器压力不同,不适合根据服务器配置不同的情况

随机

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

优点 缺点
使用简单 服务器压力不同,不适合根据服务器配置不同的情况

源地址哈希

源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

优点 缺点
将来自同一IP地址的请求,同一会话期内,转发到相同的服务器;实现会话粘滞。 目标服务器宕机后,会话会丢失

加权轮询法

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

优点 缺点
根据权重,调节转发服务器的请求数目 使用相对复杂

加权随机法

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

最小连接数法

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

优点 缺点
根据服务器当前的请求处理情况,动态分配 算法实现相对复杂,需要监控服务器请求连接数
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022 ZHU
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信