网络编程
主要介绍 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请求,看一些信息
# 向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&tpl=mn&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>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>* Connection #0 to host www.baidu.com left intactkevindeMacBook-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.jsconst http = require('http')const fs =require('fs')http.createServer((req, res) => {const { method, url } = reqconsole.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" + ')';//jsonpres.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,第二次才是带真实数据的请求
为什么会有预检请求
出于安全考虑,浏览器有同源策略,会限制跨域的请求,浏览器限制跨域有两种方式:
- 浏览器限制发起跨域请求
- 跨域请求可以正常发起,但返回的结果被浏览器拦截了
一般浏览器都是使用第二种方式限制跨域请求,跨域请求已经到达服务器,并可能对数据库里的数据进行了操作,但返回的结果被浏览器拦截了,对前端来讲这是一次失败的请求,但可能对数据库里的数据产生了影响,为了防止这种情况发生,对于可能对服务器数据产生副作用的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 } = reqconsole.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, // defaultaxios.defaults.withCredentials = true // 允许跨域请求发送cookie// OPTIONS需要加一个设置res.setHeader('Access-Control-Allow-Credentials', 'true')// 在接口响应时,写入Cookie,之后该域下每次发送的请求都会携带这个cookieres.setHeader('Set-Cookie', 'cookie1=test')// 之后发送的请求,请求头里会多出 Cookie: cookie1=test 这一项console.log(req.headers.cookie) // cookie1=test
服务器代理
除了上面将的跨域方法外,还有使用代理服务器的方法来跨域。就是请求同源服务器,通过该服务器转发请求到目标服务器,得到结果再转发给前端。
webpack,vue.config.js里面 devserver配置中就是使用这种方法。在测试时,使用代理。
正向代理与反向代理
正向代理:很早之前,网络带宽很小,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时可以直接访问indexapp.use(require('koa-static')(__dirname + '/'))app.use((ctx, next) => {const { req } = ctx.requestlet 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时可以直接访问indexapp.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/jsonaxios.post('/api/save', {a: 1, b: 2})// application/x-www-form-urlencodedaxios.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.jsconst 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 = appapp.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时可以直接访问indexapp.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.requestlet 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 = 0req.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 = 0fs.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, 服务端jqueryconsole.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.requestbody.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.messageconsole.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文档内集合了所有的资源
