# 网络编程

主要介绍 http https http2 websocket

# OSI模型

# http协议

http协议是基于请求响应的无状态协议,可以使用curl发送http请求,看一些信息

# 向baidu.com发送http请求
curl -v http://www.baidu.com

执行结果

kevindeMacBook-Air:ts_init kevin$ curl -v http://www.baidu.com
* Rebuilt URL to: http://www.baidu.com/
*   Trying 14.215.177.38...
* TCP_NODELAY set
* Connected to www.baidu.com (14.215.177.38) port 80 (#0)
> GET / HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: keep-alive
< Content-Length: 2381
< Content-Type: text/html
< Date: Sat, 11 Jan 2020 14:17:27 GMT
< Etag: "588604dd-94d"
< Last-Modified: Mon, 23 Jan 2017 13:27:57 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
< 
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
* Connection #0 to host www.baidu.com left intact
kevindeMacBook-Air:ts_init kevin$ 

# 特点

无连接、无状态(http协议本身无法保存鉴权、登录状态)、简单快速、灵活

# 请求部分(request)

上行,curl > 部分内容,向服务器发送请求信息

# 请求行

  • Method GET
    • GET 请求获取url所标识的资源
    • POST 在url所标识的资源里附加新的数据
    • HEAD 请求获取url所标识资源的响应消息报头,请求回应的部分里,http头部信息与通过get请求所得到的信息一致,利用head,不必传输整个资源内容,就可以获取url所标识资源的信息,常用于测试超链接的有效性,是否可以访问,以及是否有更新
    • PUT 请求服务器存储一个资源,并用url作为标识
    • DELETE 请求服务器删除url所标识的资源
    • TRACE 请求服务器回送收到的请求消息,主要用于测试或诊断
    • CONNECT 保留将来使用
    • OPTIONS 请求查询服务器的性能,或者查询与资源相关选项和需求
  • RequestUrl www.baidu.com
  • HttpVersion / HTTP/1.1

# 消息包头

  • Accept 指定客户端接受那些类型的消息/MIME
    • text/html html文本
    • image/gif gif图片
  • Accept-Chartset 客户端接受的字符集
    • gb2312 中文字符
    • iso-8859-1 西文字符
    • utf-8 多语言字符
  • Accept-Encoding 可接受的内容编码
    • gzip,deflate 压缩类型
    • identify 默认
  • Accept-Language 浏览器语言 zh-cn
  • Authorization 证明客户端有权查看某个资源(已经很少用了)
  • Host 指定被请求资源的internet主机和端口号 www.baidu.com:8080
  • User-Agent 用户代理 curl/7.54.0
    • 操作系统及版本
    • CPU类型
    • 浏览器版本
    • 浏览器渲染引擎
    • 浏览器语言
    • 浏览器插件
  • Content-Type Body(请求正文)的编码方式

# 请求正文

根据头部的Content-Type确定

  • application/x-www-form-urlencoded
    • title=test&a=2
    • 默认的数据编码方式
  • multipart/form-data
    • 既有文本数据,又有文件等二进制数据
    • 允许在数据中包含整个文件,常用于文件上传
  • application/json 序列化后的JSON字符串,ajax
  • text/xml xml作为编码方式的远程调用规范
  • text/plain 纯文本数据

# 响应部分(response)

下行,curl < 部分内容,从服务器接收数据

# 状态行

HTTP协议版本(HTTP-Version) 状态码(Status-code) 状态码文本描述(Reason-Phrase) CRLF

< HTTP/1.1 200 OK

状态码

  • 1xx 指示信息 -- 表示请求已接收, 继续处理
  • 2xx 成功 -- 表示请求已被成功接收,理解、接受
    • 200 OK 请求成
    • 200 客户端发送一个带Range头的GET请求 服务器完成
  • 3xx 重定向 -- 一般用于url变更,为了seo或不影响原入口,当访问原url时重定向到新的url
    • 301 表示资源永久性移动到新URL
    • 302 表示资源暂时重定向到新URL
  • 4xx 客户端错误 -- 请求有语法错误或请求无法实现
    • 400 Bad Request 客户端请求有语法错误,不能被服务器所理解
    • 401 Unauthorized 请求未经授权
    • 403 Forbidden 服务器收到请求,但拒绝提供服务
    • 404 Not Found 请求资源部存在,比如输错了url
  • 5xx 服务端错误 -- 服务端未能实现合法的请求
    • 500 Internal Server Error 服务器发生不可预期的错误
    • 503 Server Unavailable 服务器当前不能处理客户端请求,一段时间后可能恢复正常

# 消息报头

  • 响应报头
    • Location 重定向接受者到一个新的位置
    • WWW-Authenticate 包含在401(未授权)响应消息中,客户端接收到401响应时,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报文就包含该报头域。
    • Server 包含了服务器用来处理请求的软件信息 如:Apache-Coyote/1.1
  • 实体报头
  • Content-Encoding 媒体类型的修饰符 gzip
  • Content-Language 资源所用的语言
  • Content-Length 正文的长度,以字节方式存储的十进制数字表示
  • Conent-Type 正文媒体类型
    • text/html
    • application/json
  • Expirse 响应过期日期和时间 为了让代理服务器或浏览器在一段时间以后更新缓存数据

# 响应正文

服务器返回的资源、内容

# GET和POST区别

  • GET回退无害,POST会再次提交
  • GET产生URL地址收藏,POST不可以起
  • GET请求会被浏览器主动缓存
  • GET请求需要URL编码
  • GET请求长度限制
  • GET参数通过URL传递,POST在request Body中

# 创建接口

// app.js
const http = require('http')
const fs =require('fs')
http.createServer((req, res) => {
  const { method, url } = req
  console.log(url)
  if (method === 'GET' && url === '/') {
    fs.readFile('./index.html', (err, data) => {
      res.setHeader('Content-Type', 'text/html')
      res.end(data)
    })
  } else if (mtthod === 'GET' && url === '/api/users') {
    res.setHeader('Content-Type', 'application/json')
    res.end(JSON.stringify([{name: 'Tom', age: 20}]))
  }
}).listen(3000, () => {
  console.log('服务已开启与3000端口')
})

index.html 里使用axios请求接口,通过http://127.0.0.1:3000 可以访问

<body>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script>
    (async () => {
      let res = await axios.get('/api/users')
      console.log(JSON.stringify(res, null, 2))

      // 一种埋点方式
      let img = new Image()
      img.src="/api/users?button=123"
    })()
    // res 返回的数据
    // {
    //   "data": [
    //     {
    //       "name": "Tom",
    //       "age": 20
    //     }
    //   ],
    //   "status": 200,
    //   "statusText": "OK",
    //   "headers": {
    //     "connection": "keep-alive",
    //     "content-length": "25",
    //     "content-type": "application/json",
    //     "date": "Sun, 12 Jan 2020 06:27:54 GMT"
    //   },
    //   "config": {
    //     "url": "/api/users",
    //     "method": "get",
    //     "headers": {
    //       "Accept": "application/json, text/plain, */*"
    //     }
    //     ...
    //   }
    // }
  </script>
</body>

# 前端跨域问题

http://127.0.0.1:3000,跨域是浏览器同源策略引起的接口调用问题,只针对XMLHttpRequest发出的请求

  • 协议 http
  • host 127.0.0.1
  • 端口 3000

协议、端口、host 三者有一个不同就会跨域,假设接口地址为 http://127.0.0.1/api/users 下面的三种请求都是跨域:

  • 前端发出的请求URL为 https://127.0.0.1/api/users 协议不同
  • 前端发出的请求URL为 https://192.168.1.2/api/users host不同
  • 前端发出的请求URL为 http://127.0.0.1:3000/api/users 端口不同,默认端口为80

# CORS 跨域资源共享

Cross Origin Resource Sharing, 后端设置响应头,参考之前的笔记:https://www.yuque.com/guoqzuo/js_es6/fcw53h#ac35dda4

// 如果是4000端口发过来的请求,允许跨域
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:4000')
// 允许所有跨域
res.setHeader('Access-Control-Allow-Origin', '*')

# 其他跨域JSONP与img

使用script标签或img标签以加载资源的方式来发送请求,而非xhr(XMLHttpRequest)请求,所以不会跨域, 参考之前的笔记:https://www.yuque.com/guoqzuo/js_es6/fcw53h#JSONP

<body>
  <!-- 客户端代码 -->
  <input id="jsonpclick" type="button" value="jsonp test">
  <script>
    function handleRes(response) {
      console.log(response)
      // 这里可以接收到对应的数据
    }
    var jsonpclick = document.getElementById('jsonpclick');
    jsonpclick.onclick = function(){
      console.log('开始测试');
      var script = document.createElement('script');
      script.type="text/javascript" 
      script.src = "http://127.0.0.1:8088/gzh_test?callback=handleRes"
      document.body.insertBefore(script, document.body.firstChild)
    }

  </script>
</body>

node服务端代码

function gzhM_test(app, data, req, res) {
    console.log('开始执行gzhm_test');
    console.log(req.query)
    if (req.query && req.query.callback) {
        //console.log(params.query.callback);
        var str =  req.query.callback + '(' + "a=2" + ')';//jsonp
        res.end(str);
    } else {
        res.end('b=2');//普通的json
    }
    return;
}

# preflight 预检请求

/priː'flaɪt/, 在CORS跨域方法中,设置了允许跨域,就可以跨域访问了,现在在请求里加一个请求头试试

(async () => {
  axios.defaults.baseURL = 'http://127.0.0.1:3000';
  let res = await axios.get('/api/users', {headers: {'X-Token': 'test'}})
  console.log(JSON.stringify(res, null, 2))
})()

加了请求头后,发现不能跨域了,请求一直在pedding,这就涉及到CORS的 preflight(预检)了。在跨域时,有些情况会发送两次请求,第一次为预检,请求方式为OPTIONS,第二次才是带真实数据的请求

# 为什么会有预检请求

出于安全考虑,浏览器有同源策略,会限制跨域的请求,浏览器限制跨域有两种方式:

  1. 浏览器限制发起跨域请求
  2. 跨域请求可以正常发起,但返回的结果被浏览器拦截了

一般浏览器都是使用第二种方式限制跨域请求,跨域请求已经到达服务器,并可能对数据库里的数据进行了操作,但返回的结果被浏览器拦截了,对前端来讲这是一次失败的请求,但可能对数据库里的数据产生了影响,为了防止这种情况发生,对于可能对服务器数据产生副作用的HTTP请求方法,浏览器必先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许跨域请求:如果允许,就发送带真实的数据请求,如果不允许,则阻止带数据的真实请求。

# 什么情况会发触发CORS预检请求

  • 使用了PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH请求方法
  • 人为设置了CORS安全的请求头之外的其他请求头,下面是安全的请求头列表
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    • Content-Type值为 application/x-www-form-urlencoded、multipart/form-data、text/plain

# 加上对CORS预检请求的处理

上面的例子中,加了一个CORS非安全的请求头: X-Token,所以发触发CORS预检,但我们并没有允许预检的OPTIONS请求跨域,所以需要加上对应的处理:

const http = require('http')
const fs =require('fs')
http.createServer((req, res) => {
  const { method, url } = req
  console.log(url)
  if (method === 'GET' && url === '/') {
    fs.readFile('./index.html', (err, data) => {
      res.setHeader('Content-Type', 'text/html')
      res.end(data)
    })
  } else if ((method === 'GET' || method === 'POST') && url === '/api/users') {
    // 如果是4000端口发过来的请求,允许跨域
    // res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:4000')
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Content-Type', 'application/json')
    res.setHeader('Set-Cookie', 'cookie1=test')
    res.end(JSON.stringify([{name: 'Tom', age: 20}]))
  } else if (method === 'OPTIONS' && url === '/api/users') {
    // 预检
    res.writeHead(200, {
      'Access-Control-Allow-Origin': 'http://127.0.0.1:4000',
      'ACCESS-Control-Allow-Headers': 'X-token',
      // 'ACCESS-Control-Allow-Headers': 'X-token,Content-Type',
      // 'Access-Control-Allow-Methods': 'PUT'
    })
    res.end()
  }
}).listen(3000, () => {
  console.log('服务已开启与3000端口')
})

如果我们把请求改为POST,axios发送json数据时,Cotent-Type为application/json,非CORS安全请求头,需要允许对应的请求头

'ACCESS-Control-Allow-Headers': 'X-token,Content-Type',
// 或者
'ACCESS-Control-Allow-Headers': '*'

如果请求携带cookie信息,则请求变为credential请求

// axios 跨域请求 默认不发送cookie,如果想发送的请求携带cookie需要将withCredentials设置为true
// `withCredentials` indicates whether or not cross-site Access-Control requests
// should be made using credentials
// withCredentials: false, // default
axios.defaults.withCredentials = true // 允许跨域请求发送cookie

// OPTIONS需要加一个设置
res.setHeader('Access-Control-Allow-Credentials', 'true')

// 在接口响应时,写入Cookie,之后该域下每次发送的请求都会携带这个cookie
res.setHeader('Set-Cookie', 'cookie1=test')
// 之后发送的请求,请求头里会多出 Cookie: cookie1=test 这一项
console.log(req.headers.cookie) // cookie1=test

参考:前端 | 浅谈preflight request (opens new window)

# 服务器代理

除了上面将的跨域方法外,还有使用代理服务器的方法来跨域。就是请求同源服务器,通过该服务器转发请求到目标服务器,得到结果再转发给前端。

webpack,vue.config.js里面 devserver配置中就是使用这种方法。在测试时,使用代理。

# 正向代理与反向代理

参考:https://www.cnblogs.com/xuepei/p/10437114.html

正向代理:很早之前,网络带宽很小,64k 网络专线,那么多电脑想要访问网络,采用的方式是,统一访问一台代理服务器来上网。客户端通过代理服务器访问目标服务器内容,就是正向代理。比如科学上网,对于墙屏蔽的网站,我们会采用ss代理服务器来访问。这就是正向代理。

反向代理:对于比较大流量的站点,一台服务器顶不住,需要在前面有个代理服务器,将请求分发代理到其他服务器来处理。一台服务器将前端发送的请求,转发到另一台服务器处理,再将处理好的数据传给前端,这种情况就是反向代理。一般用于负载均衡。

正向代理与反向代理的区别:

  • 正向代理是客户端的代理,帮助客户端访问其无法访问的服务器资源。反向代理则是服务器代理,帮助服务器做负载均衡、安全防护等。
  • 正向代理一般是客户端架设的,比如在电脑上装一个ss客户端。反向代理一般在服务端架设,比如在集群中部署一个反向代理服务器
  • 正向代理中服务端不知道真正的客户端是谁,以为访问自己的就是真实的客户端。反向代理中,客户端不知道真正的服务器是谁,以为自己访问的就是真实的服务器
  • 正向代理和反向代理的作用和目的不同:正向代理主要用于解决访问限制问题,反向代理主要提供负载均衡,安全防护功能。二者均能提高访问速度。

# 利用反向代理跨域

# 使用 http-proxy-middleware 将请求代理到目标服务器
npm i http-proxy-middleware --save-dev

示例代码

const express = require('express')
const app = new express()
const proxy = require('http-proxy-middleware') 
// webpack devServer里proxy就是使用的这个包

app.use(express.static(__dirname + '/'))
app.use('/api', proxy({
  target: 'http://127.0.0.1:3000',
  changeOrigin: true
}))

app.listen(4000)

webpack devServer vue.config.js里面的配置

module.export = {
  devServer: {
    disableHostCheck: true,
    compress: true,
    port: 5000,
    proxy: {
      '/api': {
        tartget: 'http://127.0.0.1:4000',
        changeOrigin: true
      }
    }
  }
}

nginx 代理配置

server {
  listen 80;
  location / {
    root /var/www/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html
  }
}

location /api {
  proxy_pass http://127.0.0.1:3000;
  proxy_redirect off;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# Bodyparser

为什么需要使用bodyparser,bodyparser是用来做什么的?先看看一个例子

# node接收post发送的数据

node接收post发送的数据时,默认处理方式

// 前端发送post请求 index.html
// <form action="/api/save" method="POST">
//   <input name="title" value="abc">
//   <input type="submit" value="save">
// </form>
const Koa = require('koa')
const app = new Koa()

// 静态服务,让访问 127.0.0.1:3000时可以直接访问index
app.use(require('koa-static')(__dirname + '/'))

app.use((ctx, next) => {
  const { req } = ctx.request
  let reqData = []
  let size = 0

  // 处理post请求发送的数据
  req.on('data', data => {
    console.log('>>>req on', data) // >>>req on <Buffer 74 69 74 6c 65 3d 61 62 63>
    reqData.push(data)
    size += data.length
  })

  req.on('end', () => {
    console.log('end')
    const data = Buffer.concat(reqData, size)
    console.log('data:', size, data.toString()) //  data: 9 title=abc
  })
})

app.listen(3000, () => { console.log('服务开启于3000端口') })

# bodyparser中间件

bodyparser就是将获取post请求内容封装成了一个中间件,后面就可以直接通过ctx.request.body就可以拿到post请求的数据了

const Koa = require('koa')
const app = new Koa()

// 静态服务,让访问 127.0.0.1:3000时可以直接访问index
app.use(require('koa-static')(__dirname + '/'))

app.use(require('koa-bodyparser')())
app.use((ctx, next) => {
  console.log(ctx.request.body) // { title: 'abc' }
  ctx.body = ctx.request.body
})

app.listen(3000, () => { console.log('服务开启于3000端口') })

# axios发送数据Content-Type

// application/json
axios.post('/api/save', {a: 1, b: 2})

// application/x-www-form-urlencoded
axios.post('/api/save', 'a=1&n=2', {
  headers: {
    'Content-Type': 'aplication/x-www-form-urlencoded'
  }
})

# 文件上传与下载

主要介绍node处理文件的上传和下载

# 文件下载

前端demo

<a href="/api/download" target="_blank">download</a>

下载demo

// http.js
const http = require("http");
const fs = require("fs");

const app = http
  .createServer((req, res) => {
      const { method, url } = req;
      if (method == "GET" && url == "/") {
          fs.readFile("./index.html", (err, data) => {
              res.setHeader("Content-Type", "text/html");
              res.end(data);
          });

      } else if (method === "GET" && url === "/api/download") {        
          fs.readFile("./file.pdf", (err, data) => {
              res.setHeader("Content-Type", "application/pdf");
              const fileName = encodeURI('中文')
              res.setHeader('Content-Disposition' ,`attachment; filename="${fileName}.pdf"`)
              res.end(data);
          });

      }


  })
// module.exports = app
app.listen(3000)

# 文件上传

前端demo

<input id='file1' type="file" />
<script>
  window.onload=function(){
      var files = document.getElementsByTagName('input'),
      len = files.length,
      file;
      for (var i = 0; i < len; i++) {
          file = files[i];
          if (file.type !== 'file') continue; // 不是文件类型的控件跳过
          file.onchange = function() {
              console.log('change')
              var _files = this.files;
              if (!_files.length) return;
              if (_files.length === 1) { // 选择单个文件
                  var xhr = new XMLHttpRequest();
                  xhr.open('POST', '/api/upload');
                  var filePath = files[0].value;
                  console.log(filePath) // C:\fakepath\2_koa中间件洋葱圈模型.png
                  // 'setRequestHeader' on 'XMLHttpRequest': Value is not a valid ByteString.
                  // 请求的头信息中不能出现中文或UTF-8码的字符
                  xhr.setRequestHeader('file-name', filePath.substring(filePath.lastIndexOf('\\') + 1));
                  xhr.send(_files[0]);
              } else { }
          };
      }
  };
</script>

node获取上传的图片

// 文件上传 3 种处理方式 
const Koa = require('koa')
const app = new Koa()
const path = require('path')
const fs = require('fs')

// 静态服务,让访问 127.0.0.1:3000时可以直接访问index
app.use(require('koa-static')(__dirname + '/'))
app.use(require('koa-bodyparser')())

app.use((ctx, next) => {
  // 如果是上传文件,Content-Type为 multipart/form-data,bodyparser会无效
  console.log(ctx.request.body)
  let { req } = ctx.request
  let fileName = req.headers['file-name'] ? req.headers['file-name'] : '123.png'
  const outputFile = path.resolve(__dirname, fileName)

  // 1.使用流的方法
  // const fis = fs.createWriteStream(outputFile)
  // console.log(req.pipe)
  // req.pipe(fis)

  // 2.使用buffer方法
  let chunk = []
  let size = 0
  req.on('data', data => {
    chunk.push(data)
    size += data.length;
    console.log('data: ', data, size)
  })
  req.on('end', () => {
    console.log('end..')
    const buffer = Buffer.concat(chunk, size)
    size = 0
    fs.writeFileSync(outputFile, buffer)
  })

  // 3.流事件写入
  // const fis = fs.createWriteStream(outputFile)
  // req.on('data', data => {
  //   console.log('data:', data)
  //   fis.write(data)
  // })
  // req.on('end', () => {
  //   fis.end()
  // })

  ctx.body = 'hello'
})

app.listen(3000, () => { console.log('服务开启于3000端口') })

# 利用node来爬取网站内容

const originRequest = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");

function request(url, callback) {
    const options = {
        url: url,
        encoding: null
    };
    originRequest(url, options, callback);
}

for (let i = 100553; i < 100563; i++) {
    const url = `https://www.dy2018.com/i/${i}.html`;
    request(url, function (err, res, body) {
        const html = iconv.decode(body, "gb2312");
        const $ = cheerio.load(html); // 解析html, 服务端jquery
        console.log($(".title_all h1").text(), i);
    });
}

// 2019年英国欧美剧《午夜狂飙/宵禁第一季》连载至5 100561
// 2019年美国欧美剧《叛徒/叛国者第一季》连载至6 100554
// 2018年奥地利6.6分剧情片《动物》BD中英双字 100558
// 2018年法国6.7分剧情片《高潮》BD中字 100559
// 2019年西班牙欧美剧《碰撞第一季》连载至10 100556

# 实现一个即时通讯IM

tcp协议和udp协议的区别:TCP协议需要传统的三次握手,而UDP不需要,所以速度会快点

# socket实现

原理:Net模块能够创建一个基于流的TCP服务器,客户端与服务端建立连接后,服务器可以获得一个全双工Socket对象,服务器可以保存socket对象列表,在接收到某个客户端消息时,再推送给其他客户端。

const net = require('net')
const chatServer = net.createServer()
const clientList = []

chatServer.on('connection', client => {
  client.write('Hi\n')
  clientList.push(client)
  client.on('data', data => {
    console.log('recive:', data.toString())
    clientList.forEach(v => {
      v.write(data)
    })
  })
})

chatServer.listen(9000, () => {
  console.log('服务开启在9000端口')
})

使用 telnet 连接到服务器,发送消息,其他连接上的客户端就可以收到消息了

# mac安装telnet:brew install telnet
# telnet是最早的远程控制工具,由于是明文传输,不安全,现在都改为ssh了
# 使用命链接到服务器
telnet localhost 9000

# http实现

如果单纯的使用http协议,服务端无法向客户端发送消息,那只有客户端每隔1s请求接口,来获聊天内容, 下面是前端demo,index.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">
  <!-- vue -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <!-- axios -->
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div>
      <input type="text" v-model="message">
      <input type="button" value="发送" @click="send">
      <input type="button" value="清除" @click="clear">
    </div>
    <div>
      <ul>
        <li v-for="item in msgList">{{ item }}</li>
      </ul>
    </div>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        message: '',
        msgList: [],
      },
      mounted() {
        setInterval(() => {
          this.getMsgList()
        }, 1000)
      },
      methods: {
        async send() {
          try {
            let res = await axios.post('/api/send', { msg: this.message })
          } catch(e) {
            console.log(e)
          }
        },
        async clear() {
          try {
            let res = await axios.post('/api/clear')
          } catch(e) {
            console.log(e)
          }
        },
        async getMsgList() {
          try {
            let res = await axios.get('/api/list')
            console.log(res)
            this.msgList = res.data
          } catch(e) {
            console.log(e)
          }
        }
      }
    })
  </script>
</body>
</html>

index.js

const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const router = new Router()

app.use(require('koa-static')(__dirname + '/'))
app.use(require('koa-bodyparser')())

let msgList = []

router.post('/api/send', (ctx, next) => {
  let { body } = ctx.request
  body.msg && msgList.push(body.msg)
  ctx.body = 'success'
})

router.post('/api/clear', (ctx, next) => {
  msgList = []
  ctx.body = 'success'
})

router.get('/api/list', (ctx, next) => {
  ctx.body = msgList
})

app.use(router.routes())
app.listen(3000, () => console.log('服务开启成功,3000端口'))

# socket.io 实现

socket.io 官方文档:https://socket.io/docs/server-api/

前后端都可以使用socket.io来进行socket通信,socket.io特点:

  • 源于HTML5标准
  • 支持优雅降级
    • WebSocket
    • WebSocket over Flash
    • XHR Polling
    • XHR Multipart Streaming
    • Forever Iframe
    • JSONP Polling

前端demo index.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>
  <script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <div>
      <input type="text" v-model="message">
      <input type="button" value="发送" @click="send">
      <!-- <input type="button" value="清除" @click="clear"> -->
    </div>
    <div>
      <ul>
        <li v-for="item in msgList">{{ item }}</li>
      </ul>
    </div>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        message: '',
        msgList: [],
      },
      mounted() {
        this.socket = io()
        this.socket.on('chat message', (msg) => {
          console.log(msg)
          this.msgList.push(msg)
        })
      },
      methods: {
        async send() {
          try {
            const msg = this.message
            console.log('send msg', msg)
            this.socket.emit('chat message', msg)
            this.message = ''
          } catch(e) {
            console.log(e)
          }
        }
      }
    })
  </script>
</body>
</html>

后端demo index.js

const Koa = require('koa')
const app = new Koa()
const server = require('http').Server(app.callback());
const io = require('socket.io')(server);

app.use(require('koa-static')(__dirname + '/'))

let users = []

io.on('connection', (socket) => {
  console.log('a user connect')
  console.log(socket.id) // 每个链接都是一个新的连接
  users.push(socket.id)
  console.log('在线人数', users.length)

  // 接收到消息
  socket.on('chat message', (msg) => {
    console.log('chat msg', socket.id + ': ' + msg)
    // 广播给所有人
    io.emit('chat message', socket.id + ': ' + msg)
    // 广播给除了发送者的所有人
    // socket.broadcast.emit('chat message', socket.id + ': ' + msg)
  })
  // 如果有连接离线
  socket.on('disconnect', () => {
    console.log(socket.id + '已离线')
    users.splice(users.indexOf(socket.id), 1)
    console.log('在线人数', users.length)
  })
})

server.listen(3000, () => console.log('服务开启成功,3000端口'))

# http2

  • 多路复用 - 雪碧图、多域名CDN、接口合并
    • 官方演示Demo: https://http2.akamai.com/demo 通过请求接口载入378张小图片,http/1.1载入耗时28.82s,而http/2 耗时4.25s
    • 多路复用允许同时通过单一的HTTP/2连接发起多重的请求/响应消息;而HTTP/1.1协议中,浏览器客户端在同一时间、同一域名下的请求有一定的数量限制。超过限制数目的请求会被阻塞
  • 头部压缩
    • HTTP/1.1的header由于cookie和user-agent很容易膨胀,且每次都要重新发送。HTTP/2使用encoder(编码)来减少需要传输的header大小,通讯双方各自缓存一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量,从而降低延迟
  • 服务器推送
    • HTTP/2中,服务器可以对客户端的一个请求发送多个响应。例如:如果一个请求请求的是index.html,服务器很可能同时响应index.html、logo.jpg、css和js文件,因为客户端可能会需要用到这些东西。相当于一个HTML文档内集合了所有的资源
OSI模型 实际应用
7 应用层 应用层: TELNET,SSH,HTTP,SMTP,POP,SSL/TLS,FTP,MIME,HTML,SNMP,MIB,SIP,RTP...
6 表示层
5 会话层
4 传输层 传输层: TCP,UDP,UDP-Lite,SCTP,DCCP
3 网络层 网络层: ARP,IPv4,IPv6,ICMP,IPsec
2 数据链路层 以太网,无线LAN,PPP... (双文线电缆、无线、光纤...)
1 物理层
上次更新: 2020/10/29 22:59:19