# HTTP协议原理

本文介绍 HTTP 的协议原理,会详细分析跨域的具体原理及其解决方案,后续将持续更新 HTTP2、HTTP3 的最新知识。

# 1、HTTP发展历史

HTTP 协议版本大致分为以下四个演进版本。

  1. HTTP/0.9
  • 只有一个命令GET
  • 没有HEADER等描述数据的信息
  • 服务器发送完毕,就关闭TCP连接
  1. HTTP/1.0
  • 增加了很多命令(POST/PUT等)
  • 增加status code和header
  • 多字符集支持、多部分发送、权限、缓存等
  1. HTTP/1.1
  • 持久连接
  • pipeline在同一个连接里发送多个请求
  • 增加host(同一个物理服务器可以跑多个web服务)和其他一些命令
  1. HTTP2
  • 所有数据以二进制传输(之前是字符串)
  • 同一个连接里面发送多个请求不再需要按照顺序来,可以同时多个请求数据
  • 头信息压缩(减少带宽使用)以及推送等提高效率的功能(服务端可以主动发起传输)

# 2、OSI七层模型

OSI模型是在协议开发前设计的,具有通用性。TCP/IP是先有协议集然后建立模型,不适用于非TCP/IP网络。

  1. 物理层

建立、维护、断开物理连接(定义物理设备如何传输数据)

  1. 数据链路层

建立逻辑连接、进行硬件地址寻址、差错校验等功能(通信的实体之间建立数据链路连接:0101之类的)

  1. 网络层

进行逻辑寻址,实现不同网络之间的路径选择(为数据在结点之间传输创建逻辑链路)

  1. 传输层

定义传输数据的协议端口号,以及流控和差错校验

协议有TCP(传输控制协议)和UDP(用户数据报协议,不可靠),数据包一旦离开网卡即进入网络传输层,向用户提供可靠的端到端(End-to-End)服务

  1. 会话层

建立、管理、终止会话。

  1. 表示层

数据的表示、安全和压缩。

  1. 应用层

网络服务与最终用户的一个接口; 为应用软件提供了很多服务; 构建于TCP协议之上; 屏蔽网络传输相关细节

协议有HTTP/FTP/SMTP/DNS/HTTPS/POP3

# 3、HTTP的三次握手

首先,HTTP是不存在连接这么个概念的,只有请求响应这个概念,都是数据包,那么这时候就需要一个传输的通道了,这个通道在哪里呢?就在TCP里了,在完成了三次握手之后,创建一个TCP connection,我们的http请求是在这个连接的基础上发送的。三次握手主要是为了规避网络传输中延迟导致的一些服务器开销问题。

TCP connection上面是可以发送多个请求的。

URI(统一资源标识符):用来唯一标识互联网上的信息资源,包含URL(统一资源定位器)和URN(永久统一资源定位符:在资源被移动后还能找到)。

长链接: Chrome的并发限制是6条,当一个页面有6个以上的请求发送时,会创建6个长链接TCP connection,在控制面板networkWaterfall中可以看出。

如果一个页面中有很多图片,下面的往往加载很慢,就是因为并发只有6个,后面的需要等待前面的执行完毕。同时,会复用前面的TCP连接Connection: keep-alive

# 4、HTTP/1.1中的状态码

  • 200 OK 正确处理
  • 201 CREATED 新建或更新数据成功。
  • 204 NO CONTENT 服务器接收的请求已成功处理,在返回的报文中不含主体的实体响应部分
  • 301 永久性重定向
  • 302 临时性重定向
  • 304 Not Modified 服务端资源维修改,从前端浏览器读取缓存
  • 400 BAD REQUEST 用户发出的请求有错误,该请求是幂等的。
  • 401 Unauthorized 表示用户没有认证,无法进行当前操作。比如,登录需要账号密码,只提供账号,没有密码,返回422
  • 403 Forbidden 由于服务端策略,当前资源被禁止访问
  • 404 Not Found 服务端没有找到请求的资源
  • 499 Client Aborted 这是Nginx对status的一个扩展,不产生实际响应,只记录在Nginx日志中。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
  • 502 Bad Gateway 当Web server充当proxy角色时,无法连接到上游服务
  • 503 Service Unavailable 服务器正在超负载,或者正在进行停机维护
  • 504 Gateway Timeout 当Web server充当proxy角色时,上游服务响应超时

502/504的对比,502表示与upstream在TCP层就无法连接,而504则是确定在应用层建立了连接,由于upstream的响应时间大于proxy的timeout阈值

# 5. HTTP协议常见请求/响应头

通过 curl -v 可以得到 HTTP 请求的完整报文。

  • Content-Type:请求的与实体对应的 MIME 信息
  • Accept:指定客户端能接受的内容类型
  • Origin:最初的请求来源于哪儿,主要用于POST请求

origin的提出,本身就是在HTML5中跨域操作所引入的。 其具体流程是,当一个链接或者XMLHttpRequest去请求跨域操作,浏览器事实上的确向目标服务器发起了连接请求,并且携带这origin。 当服务器返回时,浏览器将检查response中是否包含Access-Control-Allow-Origin字段,当缺少这个字段时,浏览器将abort,abort的意思是不显示,不产生事件,就好像没有请求过,甚至在network区域里面都看不到。 当存在这个header时,浏览器将检查当前请求所在域是否在这个access-control-allow-origin所允许的域内,如果是,继续下去,如果不存在,abort!

  • Cookie:发送给服务端的cookie的值

  • Cache-Control:指定缓存响应机制(public/private/no-cache)

    private是指,只允许发送请求的域名进行缓存,其他代理的不允许

  • User-Agent:用户信息

  • X-Forwarded-For:请求端真实的IP

  • Access-Control-Allow-Origin:允许特定的域名来访问(跨域使用)

  • Last-Modified:请求资源的最后响应时间,在服务端设置后,对应的在Request Headers中,会携带If-Modified-Since

  • Etag:通过数据签名,进行是否缓存验证,在服务端设置后,对应的在Request Headers中,会携带If-None-Match

  • Connection: 通知浏览器保持当前HTTP连接,以便在策略允许范围内进行连接复用

  • Keep-Alive: 保持连接的策略

const etag = req.headers['if-none-match']

if(etag === 'etag') {
  res.writeHead(304, {
    'Cache-Control': 'max-age=2000, no-cache',
    'Last-Modified': reqTime,
    'Etag': 'etag'
  }) 
  res.end('')
} else {
  res.writeHead(200, {
    'Cache-Control': 'max-age=2000, no-cache',
    'Last-Modified': reqTime,
    'Etag': 'etag'
  })
  res.end('res ok!')
}

# 6. HTTP协议的工作特点和工作原理

工作特点

  • 基于B/S模式

  • 通信开销小,简单快速,传输成本底

  • 使用灵活、可使用超文本传输协议

  • 节省传输时间

  • 无状态

工作原理

客户端发送请求到服务器,创建一个TCP连接,指定端口号,默认为80,连接到服务器,服务器监听浏览器请求,一旦监听到客户端请求,分析请求类型后,服务器会向客户端返回状态信息和数据内容。

# 7. CORS跨域请求

# 7.1 浏览器同域限制

如果服务端没有设置头信息 Access-Control-Allow-Origin,在前端跨域请求时,虽然客户端的请求还会被服务端正常接收,但是浏览器会把服务端返回的数据忽略掉,并在console中报错,这是浏览器的同源策略

一种解决方案是允许特定的请求域名:

  res.writeHead(200, {
    'Access-Control-Allow-Origin': "http://127.0.0.1:8888"
  })

但要注意,非浏览器环境是没有同域限制的,比如通过 curl 来获取数据,从 8888 端口向 8887 端口获取数据,是可以正常通信的。

---> curl 127.0.0.1:8887

<--- 123

另外,可以通过 jsonp 来实现跨域请求,其原理就是 script 标签没有同源限制,具体实现参考 Vue中的jsonp。另外需要知道JSONP只支持GET请求。

# 7.2 预请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。我们日常开发过程中,总是会先进行一次预请求主要就是因为 Content-Type 的值被设置为了 application/json;charset=UTF-8

比如:

GET /cors HTTP/1.1
Origin: http://www.baidu.com
Host: http:localhost:8888
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0

上面的头信息中,Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是 application/json

除了 Origin 字段也就是 Access-Control-Allow-Origin 外,预请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,比如 PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,比如 X-Custom-Header

app.all('*', function(req, res, next) {
    if( req.headers.origin == 'https://www.google.com' || req.headers.origin == 'https://www.baidu.com' ){
        res.header("Access-Control-Allow-Origin", req.headers.origin);
        res.header('Access-Control-Allow-Methods', 'POST, GET');
        res.header('Access-Control-Allow-Headers', 'X-Requested-With');
        res.header('Access-Control-Allow-Headers', 'Content-Type');
        res.header('Access-Control-Allow-Max-Age', '1000') // 在这个时间段内,只需要一次预请求
    }
    next();
});

# 8. Cookie和Session

cookie

  • 通过Set-Cookie设置
http.createServer((req, res) => {
  const html = fs.readFileSync('test.html', 'utf8')
  res.writeHead(200, {
    'Content-Type': 'text/html',
    'Set-Cookie': ['id=123; max-age=600','name=evan; HttpOnly']
  })
  res.end(html) 
}).listen(8888)
  • 下次请求会自动带上
  • 键值对,可以设置多个

cookie的属性

  • max-age和expire设置过期时间
  • Secure只在https的时候发送
  • HttpOnly: 禁止 JavaScript 通过 document.cookie 访问

# Redirect

http.createServer((req, res) => {
  if(req.url === '/'){
    res.writeHead(302, {  // 302 临时跳转,每次都经过'/'  301 永久重定向(慎用)
      'Location': '/newroute'
    })

    res.end('')
  }

  if(req.url === '/newroute'){
    res.writeHead(200, {
      'Content-Type': 'text/html'
    })
    res.end('<div>this is redirect content</div>')
  }
})