通过实战学习HTTP(附demo)

213 阅读9分钟

学习以下知识点:

  • 1.跨越
  • 2.CORS预请求
  • 3.缓存
  • 4.cookie
  • 5.长链接
  • 6.重定向
  • 7.数据协商
  • 8.CSP

跨域

看下面代码

1.server.js

// server.js
const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  const html = fs.readFileSync('test.html', 'utf8')
  response.writeHead(200, {
    'Content-Type': 'text/html'
  })
  response.end(html)
}).listen(8888)
console.log('server listening on 8888')

2.server2.js

// server2.js
const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  response.end('123')
}).listen(8887)
console.log('server listening on 8887')

3.test.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    
</body>
<!-- cors1 -->
<script>
  var xhr = new XMLHttpRequest()
  xhr.open('GET', 'http://127.0.0.1:8887/')
  xhr.send()
</script>
</html>

从上面代码中可以看出,server.js中去读取test.html中内容,跑在8888端口

在test.html中,向8887端口发送http请求。

打开控制台看到:

这就是产生了跨域。

解决跨域

通过cors解决跨域,在server2.js中添加下面内容 Access-Control-Allow-Origin 头


const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
  })
  response.end('123')
}).listen(8887)
console.log('server listening on 8887')

'Access-Control-Allow-Origin': '*', 代表允许所有第三方请求。但是实际中不会,不安全。

重新打开控制台,不会出现报错了。

我们也可以通过jsonp来解决跨域问题。

修改下test.html 将请求放到script标签中的src属性中。这是因为浏览器允许script link这些标签跨域。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    
</body>
<script src='http://127.0.0.1:8887/'>
</script>
</html>

CORS预请求

  • 1.跨域的时候,默认允许的方法只有get post head请求。其他请求会有预请求。
  • 2.Content-Type中除了text/plain ,multipart/form-data, application/x-www-form-urlencoded ,其他都会有预请求。
  • 3.header头限制

我们自定义下header,修改下test.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    
</body>
<!-- cors1 -->
<!-- <script>
  fetch('http://localhost:8887', {
    method: 'POST',
    headers: {
      'X-Test-Cors': '123'
    }
  })
</script> -->
</html>

打开控制台如下报错:

意思是自定义header,不允许跨域。这也就是CORS预请求。

可以看请求,代码中发送的是POST请求,但是图中发送的是options请求

可以通过在服务端中添加对应的header头:


// server2.js
const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Headers': 'X-Test-Cors', // 添加自定义头
  })
  response.end('123')
}).listen(8887)
console.log('server listening on 8887')

重新运行,看下浏览器。此时发送了两个请求,先是options请求,再是post请求。控制台也不会报错了。

下面再试下请求方式的预请求。

我们修改下test.html中的请求方式,将POST改为PUT。重新允许,看下浏览器,控制台报错,只发送了options请求。

我们可以通过在服务端添加允许的请求方式头

// server2.js
const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  response.writeHead(200, {
    'Access-Control-Allow-Origin': 'http://127.0.0.1:8888',
    'Access-Control-Allow-Headers': 'X-Test-Cors',
    'Access-Control-Allow-Methods': 'POST, PUT, DELETE',
  })
  response.end('123')
}).listen(8887)
console.log('server listening on 8887')

重新运行。此时再看下浏览器。控制台不会报错。先有options请求,再有put请求。

下面我们再添加一个max-age头,如下:


const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  response.writeHead(200, {
    'Access-Control-Allow-Origin': 'http://127.0.0.1:8888',
    'Access-Control-Allow-Headers': 'X-Test-Cors',
    'Access-Control-Allow-Methods': 'POST, PUT, DELETE',
    'Access-Control-Max-Age': '1000'
  })
  response.end('123')
}).listen(8887)
console.log('server listening on 8887')

这个头代表,开始会先触发options请求,再触发put请求。1s过后,只有put请求。也就是告诉浏览器这个请求不用再预请求了。

缓存

可缓存性:Cache-Control

  • 1.public:整个http请求节点都可以缓存,包括代理缓存
  • 2.private:只有发起的浏览器才可以缓存
  • 3.no-cache:可以在本地缓存,但是需要服务器认证
  • 4.no-store:任何节点都不可缓存

到期:

  • max-age = 表示缓存多少s过期
  • s-max-age = 可以代替max-age 但是只在代理服务器上才生效。在代理服务器,两个同时存在,会读取s-max-age

下面看下max-age示例:

test.html如下:


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    
</body>
<script src="/script.js"></script>
</html>

server.js如下

const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html'
    })
    response.end(html)
  }
  if (request.url === '/script.js') {
    response.end('console.log("script loaded")')
  }
}).listen(8888)
console.log('server listening on 8888')

运行node server.js。打开控制台,可以看到:打印了 script loaded

我们在server.js中加入Cache-Control : max-age = 20 来缓存

const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html'
    })
    response.end(html)
  }
  if (request.url === '/script.js') {
    response.writeHead(200, {
      'Content-Type': 'text/javascript',
      'Cache-Control': 'max-age=20'
    })
    response.end('console.log("script loaded")')
  }
}).listen(8888)
console.log('server listening on 8888')

第一次刷新:看到size为228B,时间是3ms

过了20s后,再次刷新,可以看到这次是从memory cache中读取,而且时间是0ms

接下来,我看下打印的内容:

1.在不加入cacae-controle情况下,打印如下:

2.加入cache-controle,20s内,打印如下:

3.修改打印的内容,因为设置的max-age为20s,缓存时间过去了,所以打印新内容,打印如下:

4.我们将max-age改为200,将内容改回原先内容,重新运行打印如下:说明还是从缓存读取

实际工作中,cache-control是客户端缓存,一般将max-age设置很长时间。但是项目内容改变了,因为缓存没有更新。此时一般会使用时间戳或者内容hash来更新项目内容。

下面看下缓存的流程:

验证能否使用缓存的头:

1.Last-Modified

2.ETag

Last-Modified:

1.上次修改时间

2.配合If-Modified-Since使用

3.对比上次修改时间来验证资源是否需要更新

ETag:

1.数据签名

2.内容改变,数据签名改变,对比两个数据签名是否一样,从而判断是否缓存

3.配合If-Match使用

下面通过代码演示一下:

test.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    
</body>
<script src="/script.js"></script>
</html>

server.js


const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html'
    })
    response.end(html)
  }
  if (request.url === '/script.js') {
      response.writeHead(200, {
        'Content-Type': 'text/javascript',
        'Cache-Control': 'max-age=2000000, no-cache',
        'Last-Modified': '123',
        'Etag': '777'
      })
      response.end('console.log("script loaded twice")')
  }
}).listen(8888)
console.log('server listening on 8888')

代码中设置了Cache-Control: max-age=2000000, no-cache , 'Last-Modified': '123', 'Etag': '777'

第一次刷新页面看下,在response header中返回:

当第二次刷新的时候,request header就会分别带上对应的header,来判断是否命中缓存

可以看到,Etag和If-None-Match一样,Last-Modified 和If-Modified-Since 一样。

但是,第二次刷新还是请求了服务器,返回200

此时,可以通过在服务端判断Etag来确定是否可以读取缓存

修改server.js如下

const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html'
    })
    response.end(html)
  }
  if (request.url === '/script.js') {
    
    const etag = request.headers['if-none-match']
    if (etag === '777') {
      response.writeHead(304, {
        'Content-Type': 'text/javascript',
        'Cache-Control': 'max-age=2000000, no-cache',
        'Last-Modified': '123',
        'Etag': '777'
      })
      response.end()
    } else {
      response.writeHead(200, {
        'Content-Type': 'text/javascript',
        'Cache-Control': 'max-age=2000000, no-cache',
        'Last-Modified': '123',
        'Etag': '777'
      })
      response.end('console.log("script loaded twice")')
    }
  }
}).listen(8888)
console.log('server listening on 8888')

此时重新运行,如下:

代码中当命中Etag策略的时候,服务端没有返回内容,浏览器没有去请求服务器,而是返回304,内容确返回了缓存中的内容。

现在把server.js中的no-cache去掉,重新运行。

控制台打印如下:

当再次刷新:是从缓存中读取内容。

现在将no-cache改为no-store,打印如下:

当再次刷新:并没有从缓存中读取。这也就是no-cache和no-store的区别。

cookie

  • 1.通过Set-Cookie设置
  • 2.下次请求就会带上
  • 3.max-age和expires设置过期时间
  • 4.Secure只在https的时候发送
  • 5.HttpOnly 无法通过 document.cookie访问

下面代码演示下:

test.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div>Content</div>
</body>
<script>
  console.log(document.cookie)
</script>
</html>

server.js

const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html',
      'Set-Cookie': 'id=123'
    })
    response.end(html)
  }
}).listen(8888)
console.log('server listening on 8888')

在服务端通过Set-Cookie设置了id=123,当首次访问的时候,可以看到response header中显示:

同时控制台,也能打印出cookie

当第二次访问的时候,request header 就会自动带上cookie,如下:

也可以设置多个cookie,修改server.js中代码如下:

const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html',
      'Set-Cookie': ['id=123','abc=456']
    })
    response.end(html)
  }
}).listen(8888)
console.log('server listening on 8888')

重新运行,可以看到:设置了两个cookie

同样第二次访问的时候,request header自动带上所有cookie,多个cookie以分号隔开。

通过max-age给cookie设置过期时间,修改代码如:给id=123,设置过期时间为2s


const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    const html = fs.readFileSync('test.html', 'utf8')
    response.writeHead(200, {
      'Content-Type': 'text/html',
      'Set-Cookie': ['id=123; max-age=2', 'abc=456']
    })
    response.end(html)
  }
}).listen(8888)
console.log('server listening on 8888')

重新运行,第一次打开如下:

在2s内,重新刷新浏览器打开:此时有id=123

在2s后,重新刷新浏览器打开:此时只有abc=456,没有id=123了

我们知道,不同域名,cookie不能共享。但是可以做到二级域名共享cookie。使用domain。

在abc=456 这个cookie设置domain,表示二级域名是test.com,可以共享cookie

'Set-Cookie': ['id=123; max-age=2', 'abc=456;domain=test.com']

长链接

浏览器发送一个http请求,http1.0会直接关闭tcp链接。现在发送http请求,和服务端协商是否关闭TCP链接,

使用connection:keep-alive 来保持tcp不关闭。现在浏览器默认是keep-alive

代码演示:

test.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <img src="/test1.jpg" alt="">
  <img src="/test2.jpg" alt="">
  <img src="/test3.jpg" alt="">
  <img src="/test4.jpg" alt="">
  <img src="/test5.jpg" alt="">
  <img src="/test6.jpg" alt="">
  <img src="/test7.jpg" alt="">
  <img src="/test11.jpg" alt="">
  <img src="/test12.jpg" alt="">
  <img src="/test13.jpg" alt="">
  <img src="/test14.jpg" alt="">
  <img src="/test15.jpg" alt="">
  <img src="/test16.jpg" alt="">
  <img src="/test17.jpg" alt="">
  <img src="/test111.jpg" alt="">
  <img src="/test112.jpg" alt="">
  <img src="/test113.jpg" alt="">
  <img src="/test114.jpg" alt="">
  <img src="/test115.jpg" alt="">
  <img src="/test116.jpg" alt="">
  <img src="/test117.jpg" alt="">
</body>
</html>

server.js


const http = require('http')
const fs = require('fs')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  const html = fs.readFileSync('test.html', 'utf8')
  const img = fs.readFileSync('test.jpg')
  if (request.url === '/') {
    response.writeHead(200, {
      'Content-Type': 'text/html',
    })
    response.end(html)
  } else {
    response.writeHead(200, {
      'Content-Type': 'image/jpg',
      'Connection': 'keep-alive' // or close
    })
    response.end(img)
  }
}).listen(8888)
console.log('server listening on 8888')

首先把浏览器设置如下:

刷新浏览器:可以看到前两个connection ID一样,与网络环境也有关系。

将connection设置为close,即可关闭长链接。

重定向

301:永久重定向

302:临时重定向

通过location头设置重定向

代码演示:

const http = require('http')
http.createServer(function (request, response) {
  console.log('request come', request.url)
  if (request.url === '/') {
    response.writeHead(302, {  // or 301
      'Location': '/new'
    })
    response.end()
  }
  if (request.url === '/new') {
    response.writeHead(200, {
      'Content-Type': 'text/html',
    })
    response.end('<div>this is content</div>')
  }
}).listen(8888)
console.log('server listening on 8888')

打开浏览器可以看到: 先访问根路径,通过location,重定向到/new路径

再看下301和302,

设置为302,第一次访问浏览器,看到先访问根路径,再访问/new

刷新浏览器,同样是先访问根路径,再访问/new

设置为301,首次访问,先访问根路径,再访问/new

再次刷新,访问,直接访问/new路径

数据协商

Request header:

  • Accept:请求什么类型内容
  • Accept-Encoding:压缩方式
  • Accept-Language:语言
  • User-Agent:用户浏览器信息

Response Header:

  • Content:返回的内容
  • Content-Type:类型
  • Content-Encoding:压缩方式
  • Content-Language:语言

CSP

developer.mozilla.org/zh-CN/docs/…

以下内容代码放到了github:github.com/liangchaofe…

下载代码,可以直接运行测试。