网络编程

主要介绍 http https http2 websocket

OSI模型

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 物理层

http协议

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

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

执行结果

  1. kevindeMacBook-Air:ts_init kevin$ curl -v http://www.baidu.com
  2. * Rebuilt URL to: http://www.baidu.com/
  3. * Trying 14.215.177.38...
  4. * TCP_NODELAY set
  5. * Connected to www.baidu.com (14.215.177.38) port 80 (#0)
  6. > GET / HTTP/1.1
  7. > Host: www.baidu.com
  8. > User-Agent: curl/7.54.0
  9. > Accept: */*
  10. >
  11. < HTTP/1.1 200 OK
  12. < Accept-Ranges: bytes
  13. < Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
  14. < Connection: keep-alive
  15. < Content-Length: 2381
  16. < Content-Type: text/html
  17. < Date: Sat, 11 Jan 2020 14:17:27 GMT
  18. < Etag: "588604dd-94d"
  19. < Last-Modified: Mon, 23 Jan 2017 13:27:57 GMT
  20. < Pragma: no-cache
  21. < Server: bfe/1.0.8.18
  22. < Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
  23. <
  24. <!DOCTYPE html>
  25. <!--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;京ICP030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
  26. * Connection #0 to host www.baidu.com left intact
  27. 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中

创建接口

  1. // app.js
  2. const http = require('http')
  3. const fs =require('fs')
  4. http.createServer((req, res) => {
  5. const { method, url } = req
  6. console.log(url)
  7. if (method === 'GET' && url === '/') {
  8. fs.readFile('./index.html', (err, data) => {
  9. res.setHeader('Content-Type', 'text/html')
  10. res.end(data)
  11. })
  12. } else if (mtthod === 'GET' && url === '/api/users') {
  13. res.setHeader('Content-Type', 'application/json')
  14. res.end(JSON.stringify([{name: 'Tom', age: 20}]))
  15. }
  16. }).listen(3000, () => {
  17. console.log('服务已开启与3000端口')
  18. })

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

  1. <body>
  2. <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  3. <script>
  4. (async () => {
  5. let res = await axios.get('/api/users')
  6. console.log(JSON.stringify(res, null, 2))
  7. // 一种埋点方式
  8. let img = new Image()
  9. img.src="/api/users?button=123"
  10. })()
  11. // res 返回的数据
  12. // {
  13. // "data": [
  14. // {
  15. // "name": "Tom",
  16. // "age": 20
  17. // }
  18. // ],
  19. // "status": 200,
  20. // "statusText": "OK",
  21. // "headers": {
  22. // "connection": "keep-alive",
  23. // "content-length": "25",
  24. // "content-type": "application/json",
  25. // "date": "Sun, 12 Jan 2020 06:27:54 GMT"
  26. // },
  27. // "config": {
  28. // "url": "/api/users",
  29. // "method": "get",
  30. // "headers": {
  31. // "Accept": "application/json, text/plain, */*"
  32. // }
  33. // ...
  34. // }
  35. // }
  36. </script>
  37. </body>

前端跨域问题

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

  • 协议 http
  • host 127.0.0.1
  • 端口 3000

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

CORS 跨域资源共享

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

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

其他跨域JSONP与img

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

  1. <body>
  2. <!-- 客户端代码 -->
  3. <input id="jsonpclick" type="button" value="jsonp test">
  4. <script>
  5. function handleRes(response) {
  6. console.log(response)
  7. // 这里可以接收到对应的数据
  8. }
  9. var jsonpclick = document.getElementById('jsonpclick');
  10. jsonpclick.onclick = function(){
  11. console.log('开始测试');
  12. var script = document.createElement('script');
  13. script.type="text/javascript"
  14. script.src = "http://127.0.0.1:8088/gzh_test?callback=handleRes"
  15. document.body.insertBefore(script, document.body.firstChild)
  16. }
  17. </script>
  18. </body>

node服务端代码

  1. function gzhM_test(app, data, req, res) {
  2. console.log('开始执行gzhm_test');
  3. console.log(req.query)
  4. if (req.query && req.query.callback) {
  5. //console.log(params.query.callback);
  6. var str = req.query.callback + '(' + "a=2" + ')';//jsonp
  7. res.end(str);
  8. } else {
  9. res.end('b=2');//普通的json
  10. }
  11. return;
  12. }

preflight 预检请求

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

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

加了请求头后,发现不能跨域了,请求一直在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请求跨域,所以需要加上对应的处理:

  1. const http = require('http')
  2. const fs =require('fs')
  3. http.createServer((req, res) => {
  4. const { method, url } = req
  5. console.log(url)
  6. if (method === 'GET' && url === '/') {
  7. fs.readFile('./index.html', (err, data) => {
  8. res.setHeader('Content-Type', 'text/html')
  9. res.end(data)
  10. })
  11. } else if ((method === 'GET' || method === 'POST') && url === '/api/users') {
  12. // 如果是4000端口发过来的请求,允许跨域
  13. // res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:4000')
  14. res.setHeader('Access-Control-Allow-Origin', '*')
  15. res.setHeader('Content-Type', 'application/json')
  16. res.setHeader('Set-Cookie', 'cookie1=test')
  17. res.end(JSON.stringify([{name: 'Tom', age: 20}]))
  18. } else if (method === 'OPTIONS' && url === '/api/users') {
  19. // 预检
  20. res.writeHead(200, {
  21. 'Access-Control-Allow-Origin': 'http://127.0.0.1:4000',
  22. 'ACCESS-Control-Allow-Headers': 'X-token',
  23. // 'ACCESS-Control-Allow-Headers': 'X-token,Content-Type',
  24. // 'Access-Control-Allow-Methods': 'PUT'
  25. })
  26. res.end()
  27. }
  28. }).listen(3000, () => {
  29. console.log('服务已开启与3000端口')
  30. })

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

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

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

  1. // axios 跨域请求 默认不发送cookie,如果想发送的请求携带cookie需要将withCredentials设置为true
  2. // `withCredentials` indicates whether or not cross-site Access-Control requests
  3. // should be made using credentials
  4. // withCredentials: false, // default
  5. axios.defaults.withCredentials = true // 允许跨域请求发送cookie
  6. // OPTIONS需要加一个设置
  7. res.setHeader('Access-Control-Allow-Credentials', 'true')
  8. // 在接口响应时,写入Cookie,之后该域下每次发送的请求都会携带这个cookie
  9. res.setHeader('Set-Cookie', 'cookie1=test')
  10. // 之后发送的请求,请求头里会多出 Cookie: cookie1=test 这一项
  11. console.log(req.headers.cookie) // cookie1=test

参考:前端 | 浅谈preflight request

服务器代理

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

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

正向代理与反向代理

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

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

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

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

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

利用反向代理跨域

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

示例代码

  1. const express = require('express')
  2. const app = new express()
  3. const proxy = require('http-proxy-middleware')
  4. // webpack devServer里proxy就是使用的这个包
  5. app.use(express.static(__dirname + '/'))
  6. app.use('/api', proxy({
  7. target: 'http://127.0.0.1:3000',
  8. changeOrigin: true
  9. }))
  10. app.listen(4000)

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

  1. module.export = {
  2. devServer: {
  3. disableHostCheck: true,
  4. compress: true,
  5. port: 5000,
  6. proxy: {
  7. '/api': {
  8. tartget: 'http://127.0.0.1:4000',
  9. changeOrigin: true
  10. }
  11. }
  12. }
  13. }

nginx 代理配置

  1. server {
  2. listen 80;
  3. location / {
  4. root /var/www/html;
  5. index index.html index.htm;
  6. try_files $uri $uri/ /index.html
  7. }
  8. }
  9. location /api {
  10. proxy_pass http://127.0.0.1:3000;
  11. proxy_redirect off;
  12. proxy_set_header Host $host;
  13. proxy_set_header X-Real-IP $remote_addr;
  14. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  15. }

Bodyparser

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

node接收post发送的数据

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

  1. // 前端发送post请求 index.html
  2. // <form action="/api/save" method="POST">
  3. // <input name="title" value="abc">
  4. // <input type="submit" value="save">
  5. // </form>
  6. const Koa = require('koa')
  7. const app = new Koa()
  8. // 静态服务,让访问 127.0.0.1:3000时可以直接访问index
  9. app.use(require('koa-static')(__dirname + '/'))
  10. app.use((ctx, next) => {
  11. const { req } = ctx.request
  12. let reqData = []
  13. let size = 0
  14. // 处理post请求发送的数据
  15. req.on('data', data => {
  16. console.log('>>>req on', data) // >>>req on <Buffer 74 69 74 6c 65 3d 61 62 63>
  17. reqData.push(data)
  18. size += data.length
  19. })
  20. req.on('end', () => {
  21. console.log('end')
  22. const data = Buffer.concat(reqData, size)
  23. console.log('data:', size, data.toString()) // data: 9 title=abc
  24. })
  25. })
  26. app.listen(3000, () => { console.log('服务开启于3000端口') })

bodyparser中间件

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

  1. const Koa = require('koa')
  2. const app = new Koa()
  3. // 静态服务,让访问 127.0.0.1:3000时可以直接访问index
  4. app.use(require('koa-static')(__dirname + '/'))
  5. app.use(require('koa-bodyparser')())
  6. app.use((ctx, next) => {
  7. console.log(ctx.request.body) // { title: 'abc' }
  8. ctx.body = ctx.request.body
  9. })
  10. app.listen(3000, () => { console.log('服务开启于3000端口') })

axios发送数据Content-Type

  1. // application/json
  2. axios.post('/api/save', {a: 1, b: 2})
  3. // application/x-www-form-urlencoded
  4. axios.post('/api/save', 'a=1&n=2', {
  5. headers: {
  6. 'Content-Type': 'aplication/x-www-form-urlencoded'
  7. }
  8. })

文件上传与下载

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

文件下载

前端demo

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

下载demo

  1. // http.js
  2. const http = require("http");
  3. const fs = require("fs");
  4. const app = http
  5. .createServer((req, res) => {
  6. const { method, url } = req;
  7. if (method == "GET" && url == "/") {
  8. fs.readFile("./index.html", (err, data) => {
  9. res.setHeader("Content-Type", "text/html");
  10. res.end(data);
  11. });
  12. } else if (method === "GET" && url === "/api/download") {
  13. fs.readFile("./file.pdf", (err, data) => {
  14. res.setHeader("Content-Type", "application/pdf");
  15. const fileName = encodeURI('中文')
  16. res.setHeader('Content-Disposition' ,`attachment; filename="${fileName}.pdf"`)
  17. res.end(data);
  18. });
  19. }
  20. })
  21. // module.exports = app
  22. app.listen(3000)

文件上传

前端demo

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

node获取上传的图片

  1. // 文件上传 3 种处理方式
  2. const Koa = require('koa')
  3. const app = new Koa()
  4. const path = require('path')
  5. const fs = require('fs')
  6. // 静态服务,让访问 127.0.0.1:3000时可以直接访问index
  7. app.use(require('koa-static')(__dirname + '/'))
  8. app.use(require('koa-bodyparser')())
  9. app.use((ctx, next) => {
  10. // 如果是上传文件,Content-Type为 multipart/form-data,bodyparser会无效
  11. console.log(ctx.request.body)
  12. let { req } = ctx.request
  13. let fileName = req.headers['file-name'] ? req.headers['file-name'] : '123.png'
  14. const outputFile = path.resolve(__dirname, fileName)
  15. // 1.使用流的方法
  16. // const fis = fs.createWriteStream(outputFile)
  17. // console.log(req.pipe)
  18. // req.pipe(fis)
  19. // 2.使用buffer方法
  20. let chunk = []
  21. let size = 0
  22. req.on('data', data => {
  23. chunk.push(data)
  24. size += data.length;
  25. console.log('data: ', data, size)
  26. })
  27. req.on('end', () => {
  28. console.log('end..')
  29. const buffer = Buffer.concat(chunk, size)
  30. size = 0
  31. fs.writeFileSync(outputFile, buffer)
  32. })
  33. // 3.流事件写入
  34. // const fis = fs.createWriteStream(outputFile)
  35. // req.on('data', data => {
  36. // console.log('data:', data)
  37. // fis.write(data)
  38. // })
  39. // req.on('end', () => {
  40. // fis.end()
  41. // })
  42. ctx.body = 'hello'
  43. })
  44. app.listen(3000, () => { console.log('服务开启于3000端口') })

利用node来爬取网站内容

  1. const originRequest = require("request");
  2. const cheerio = require("cheerio");
  3. const iconv = require("iconv-lite");
  4. function request(url, callback) {
  5. const options = {
  6. url: url,
  7. encoding: null
  8. };
  9. originRequest(url, options, callback);
  10. }
  11. for (let i = 100553; i < 100563; i++) {
  12. const url = `https://www.dy2018.com/i/${i}.html`;
  13. request(url, function (err, res, body) {
  14. const html = iconv.decode(body, "gb2312");
  15. const $ = cheerio.load(html); // 解析html, 服务端jquery
  16. console.log($(".title_all h1").text(), i);
  17. });
  18. }
  19. // 2019年英国欧美剧《午夜狂飙/宵禁第一季》连载至5 100561
  20. // 2019年美国欧美剧《叛徒/叛国者第一季》连载至6 100554
  21. // 2018年奥地利6.6分剧情片《动物》BD中英双字 100558
  22. // 2018年法国6.7分剧情片《高潮》BD中字 100559
  23. // 2019年西班牙欧美剧《碰撞第一季》连载至10 100556

实现一个即时通讯IM

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

socket实现

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

  1. const net = require('net')
  2. const chatServer = net.createServer()
  3. const clientList = []
  4. chatServer.on('connection', client => {
  5. client.write('Hi\n')
  6. clientList.push(client)
  7. client.on('data', data => {
  8. console.log('recive:', data.toString())
  9. clientList.forEach(v => {
  10. v.write(data)
  11. })
  12. })
  13. })
  14. chatServer.listen(9000, () => {
  15. console.log('服务开启在9000端口')
  16. })

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

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

http实现

如果单纯的使用http协议,服务端无法向客户端发送消息,那只有客户端每隔1s请求接口,来获聊天内容, 下面是前端demo,index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <!-- vue -->
  8. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  9. <!-- axios -->
  10. <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  11. <title>Document</title>
  12. </head>
  13. <body>
  14. <div id="app">
  15. <div>
  16. <input type="text" v-model="message">
  17. <input type="button" value="发送" @click="send">
  18. <input type="button" value="清除" @click="clear">
  19. </div>
  20. <div>
  21. <ul>
  22. <li v-for="item in msgList">{{ item }}</li>
  23. </ul>
  24. </div>
  25. </div>
  26. <script>
  27. let vm = new Vue({
  28. el: '#app',
  29. data: {
  30. message: '',
  31. msgList: [],
  32. },
  33. mounted() {
  34. setInterval(() => {
  35. this.getMsgList()
  36. }, 1000)
  37. },
  38. methods: {
  39. async send() {
  40. try {
  41. let res = await axios.post('/api/send', { msg: this.message })
  42. } catch(e) {
  43. console.log(e)
  44. }
  45. },
  46. async clear() {
  47. try {
  48. let res = await axios.post('/api/clear')
  49. } catch(e) {
  50. console.log(e)
  51. }
  52. },
  53. async getMsgList() {
  54. try {
  55. let res = await axios.get('/api/list')
  56. console.log(res)
  57. this.msgList = res.data
  58. } catch(e) {
  59. console.log(e)
  60. }
  61. }
  62. }
  63. })
  64. </script>
  65. </body>
  66. </html>

index.js

  1. const Koa = require('koa')
  2. const app = new Koa()
  3. const Router = require('koa-router')
  4. const router = new Router()
  5. app.use(require('koa-static')(__dirname + '/'))
  6. app.use(require('koa-bodyparser')())
  7. let msgList = []
  8. router.post('/api/send', (ctx, next) => {
  9. let { body } = ctx.request
  10. body.msg && msgList.push(body.msg)
  11. ctx.body = 'success'
  12. })
  13. router.post('/api/clear', (ctx, next) => {
  14. msgList = []
  15. ctx.body = 'success'
  16. })
  17. router.get('/api/list', (ctx, next) => {
  18. ctx.body = msgList
  19. })
  20. app.use(router.routes())
  21. 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

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Document</title>
  8. <script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
  9. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  10. </head>
  11. <body>
  12. <div id="app">
  13. <div>
  14. <input type="text" v-model="message">
  15. <input type="button" value="发送" @click="send">
  16. <!-- <input type="button" value="清除" @click="clear"> -->
  17. </div>
  18. <div>
  19. <ul>
  20. <li v-for="item in msgList">{{ item }}</li>
  21. </ul>
  22. </div>
  23. </div>
  24. <script>
  25. let vm = new Vue({
  26. el: '#app',
  27. data: {
  28. message: '',
  29. msgList: [],
  30. },
  31. mounted() {
  32. this.socket = io()
  33. this.socket.on('chat message', (msg) => {
  34. console.log(msg)
  35. this.msgList.push(msg)
  36. })
  37. },
  38. methods: {
  39. async send() {
  40. try {
  41. const msg = this.message
  42. console.log('send msg', msg)
  43. this.socket.emit('chat message', msg)
  44. this.message = ''
  45. } catch(e) {
  46. console.log(e)
  47. }
  48. }
  49. }
  50. })
  51. </script>
  52. </body>
  53. </html>

后端demo index.js

  1. const Koa = require('koa')
  2. const app = new Koa()
  3. const server = require('http').Server(app.callback());
  4. const io = require('socket.io')(server);
  5. app.use(require('koa-static')(__dirname + '/'))
  6. let users = []
  7. io.on('connection', (socket) => {
  8. console.log('a user connect')
  9. console.log(socket.id) // 每个链接都是一个新的连接
  10. users.push(socket.id)
  11. console.log('在线人数', users.length)
  12. // 接收到消息
  13. socket.on('chat message', (msg) => {
  14. console.log('chat msg', socket.id + ': ' + msg)
  15. // 广播给所有人
  16. io.emit('chat message', socket.id + ': ' + msg)
  17. // 广播给除了发送者的所有人
  18. // socket.broadcast.emit('chat message', socket.id + ': ' + msg)
  19. })
  20. // 如果有连接离线
  21. socket.on('disconnect', () => {
  22. console.log(socket.id + '已离线')
  23. users.splice(users.indexOf(socket.id), 1)
  24. console.log('在线人数', users.length)
  25. })
  26. })
  27. 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文档内集合了所有的资源