简介

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

原生socket

socket是在http基础上,对http进行升级,让连接用socket来完成。

一个典型的Websocket握手请求如下:

客户端请求

  1. GET / HTTP/1.1
  2. Upgrade: websocket
  3. Connection: Upgrade
  4. Host: example.com
  5. Origin: http://example.com
  6. Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
  7. Sec-WebSocket-Version: 13

服务器回应

  1. HTTP/1.1 101 Switching Protocols
  2. Upgrade: websocket
  3. Connection: Upgrade
  4. Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
  5. Sec-WebSocket-Location: ws://example.com/
  • Connection 必须设置 Upgrade,表示客户端希望连接升级。
  • Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
  • Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
  • Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
  • Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。
  • 其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。

服务端

  1. const http = require('http');
  2. const net = require('net');//TCP 原生socket
  3. const crypto = require('crypto');
  4. let server = net.createServer(socket => {
  5. //握手只有一次
  6. socket.once('data',(data)=>{
  7. console.log('握手开始')
  8. let str = data.toString()
  9. let lines = str.split('\r\n')
  10. //舍弃第一行和最后两行
  11. lines = lines.slice(1,lines.length-2)
  12. //切开
  13. let headers ={}
  14. lines.forEach(line=>{
  15. let [key,value]= line.split(`: `)
  16. headers[key.toLowerCase()]=value
  17. })
  18. if(headers[`upgrade`]!='websocket'){
  19. console.log('其他协议',headers[`upgrade`])
  20. socket.end()
  21. }else if(headers[`sec-websocket-version`] != '13'){
  22. console.log('版本不对',headers[`upgrade`])
  23. socket.end()
  24. }else {
  25. let key = headers['sec-websocket-key']
  26. let mask = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
  27. let hash =crypto.createHash('sha1')
  28. hash.update(key+mask)
  29. let key2 =hash.digest('base64')
  30. socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${key2}\r\n\r\n`)
  31. //服务器响应,握手结束
  32. //真正的数据
  33. socket.on('data',data1 => {
  34. console.log(data1)//帧
  35. let FIN = data1[0]&0x001//位运算
  36. let opcode=data1[0]&0xF0
  37. })
  38. }
  39. })
  40. //断开
  41. socket.on('end',() =>{
  42. console.log('客户端断开')
  43. })
  44. });
  45. server.listen(8080);

客户端

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <script>
  7. let socket = new WebSocket('ws://localhost:8080/')
  8. socket.emit = function(name,...args){
  9. socket.send(JSON.stringify({name,data:args}))
  10. alert(JSON.stringify({name,data:args}))
  11. }
  12. socket.onopen = function (event) {
  13. console.log('WebSocket is open')
  14. socket.emit('msg',12,34)
  15. }
  16. socket.onmessage = function (event) {
  17. console.log('有消息过来')
  18. }
  19. socket.onclose = function () {
  20. console.log('断开连接')
  21. }
  22. </script>
  23. </head>
  24. <body>
  25. </body>
  26. </html>

socket.io

原生socket较复杂,一般都通过框架来使用websocket,socket.io封装了websocket。

socket.io文档

安装

  1. npm install socket.io -s

简单使用

服务端

  1. 创建服务端IO对象 io = require('socket.io')(httpServer);
  2. 监视连接 io.on('connection',function(socket))
  3. 通过emit 、 on
    • on(name,function(data){}) :绑定监听
    • emit(name,data): 发送消息
  1. let app = require('express')();
  2. let httpServer = require('http').createServer(app);
  3. //得到IO对象
  4. let io = require('socket.io')(httpServer);
  5. //监视连接,(当有一个客户连接上时回调) io关联多个socket
  6. io.on('connection',function (socket) {
  7. console.log('socketio connected');
  8. //绑定sendMsg监听,接受客户端发送的消息
  9. socket.on('sendMsg',function (data) {
  10. console.log('服务端接受到浏览器的信息');
  11. /*io.emit 发送给所有连接服务器的客户端,
  12. socket.emit 发送给当前连接的客户端*/
  13. socket.emit('receiveMsg',data.name + '_'+ data.date);
  14. console.log('服务器向浏览器发送消息',data)
  15. })
  16. })
  17. http.listen(4000,function(){
  18. console.log('listening on:4000')
  19. })

客户端

  1. 引入客户端socket.io-client
  2. io(url) 连接服务端,得到socket对象(如果不指定url,将会连接默认主机地址)
  3. 通过emit,on实现通信
  1. <script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
  2. <script>
  3. //连接服务器,得到代表连接的socket对象
  4. const socket = io('ws://localhost:4000');
  5. //绑定'receiveMessage的监听,来接受服务器发送的数据
  6. socket.on('receiveMsg',function(data){
  7. console.log('浏览器端接受消息');
  8. })
  9. //向服务器发送消息
  10. socket.emit('sendMsg',{name: 'Tom',date: Date.now()})
  11. console.log('浏览向服务器发送消息')
  12. </script>

实现一个简易的聊天室

上面服务端如果使用socket.emit 实现的是服务端和客户端的一对一发送数据,那么如何将服务端收到的数据发送给其他用户,来实现聊天室效果呢?

这里就需要io.emit 发送数据给当前连接此服务器的所有用户。

服务端

  1. let app = require('express')();
  2. let httpServer = require('http').createServer(app);
  3. //得到IO对象
  4. let io = require('socket.io')(httpServer);
  5. //监视连接.
  6. io.on('connection',function (socket) {
  7. socket.on('chat message', function(msg){
  8. console.log('connected',msg)
  9. io.emit('chat message', msg);
  10. });
  11. socket.on('disconnected',(res)=>{
  12. console.log('disconnected',res)
  13. })
  14. })
  15. http.listen(4000,function(){
  16. console.log('listening on:4000')
  17. })

客户端

  1. <head>
  2. <script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
  3. <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  4. <script>
  5. $(function () {
  6. let socket = io("ws://localhost:4000");
  7. $('form').submit(function(e){
  8. e.preventDefault(); //阻止刷新
  9. socket.emit('chat message', $('#m').val());
  10. $('#m').val('');
  11. return false;
  12. });
  13. socket.on('chat message', function(msg){
  14. $('#messages').append($('<li>').text(msg));
  15. });
  16. });
  17. </script>
  18. </head>
  19. <body>
  20. <ul id="messages"></ul>
  21. <form action="">
  22. <input id="m" autocomplete="off" /><button>Send</button>
  23. </form>
  24. </body>

接下来根据官网的方案进行优化

Here are some ideas to improve the application:

  • Broadcast a message to connected users when someone connects or disconnects.

在服务端,通过io.on('connection') 监听用户连接。

socket.on('disconnect') 监听用户断开。

通过回调向客户端传递提示信息。 socket.id 可以用来独一无二的表示当前会话的客户端id

  1. //服务端
  2. socket.on('add user',function (msg) { //在用户第一次加入时,触发add user,提示所有用户
  3. io.emit('user joined',{id: socket.id});
  4. })
  5. socket.on('disconnect',(reason)=>{
  6. console.log('disconnect',socket.id,reason);
  7. io.emit('user left',{id: socket.id})
  8. })
  9. //客户端
  10. socket.on('user joined',function(data){
  11. let {id} = data;
  12. $('#messages').append($('<li>').text(id+'加入聊天室').addClass('log'));
  13. })
  14. socket.on('user left',function(data){
  15. let {id} = data;
  16. $('#messages').append($('<li>').text(id+'离开聊天室').addClass('log'));
  17. })
  • Add support for nicknames.
  1. //在客户端提供添加昵称的输入框,当输入完信息后,传递昵称给服务端
  2. socket.emit('add user', username);
  3. //在服务端重构
  4. socket.on('add user', function (username) {
  5. socket.username = username;
  6. io.emit('user joined', {
  7. username: socket.username
  8. });
  9. })
  • Don’t send the same message to the user that sent it himself. Instead, append the message directly as soon as he presses enter.

通过监听keydown事件,判定 event.which 的值是否为 13(enter的Unicode码是13)。如果是则emit 消息

  • Add “{user} is typing” functionality.

通过监听input事件,来更新type信息

  1. //update
  2. $inputMessage.on('input', function () {
  3. updateTyping();
  4. });
  5. function updateTyping() {
  6. if (connected) {
  7. if (!typing) {//如果当前没在输入,则更改标志,并发送正在输入的消息消息
  8. typing = true;
  9. socket.emit('typing');
  10. }
  11. lastTypingTime = (new Date()).getTime();
  12. setTimeout(function () {
  13. var typingTimer = (new Date()).getTime();
  14. var timeDiff = typingTimer - lastTypingTime;
  15. if (timeDiff >= TYPING_TIMER_LENGTH && typing) {//如果停止输入超时,则发送停止消息
  16. socket.emit('stop typing');
  17. typing = false;
  18. }
  19. }, TYPING_TIMER_LENGTH);
  20. }
  21. }
  22. //服务端 传递当前正在输入或停止输入的用户名,用于让客户端显示或消失 is Typing的信息
  23. socket.on('typing', function () {
  24. io.emit('typing', {
  25. username: socket.username
  26. });
  27. });
  28. socket.on('stop typing', function () {
  29. io.emit('stop typing', {
  30. username: socket.username
  31. });
  • Show who’s online.
  • Add private messaging.

更多案例在官方仓库中查找

NameSpaces、rooms

namespace允许用户去分配路径,这个的好处是可以减少TCP资源,同时进行通道隔离

默认的namespace是/ 通过 of 方法可以自定义namespace

  1. //服务端
  2. const nsp = io.of('/my-namespace');
  3. nsp.on('connection', function(socket){
  4. console.log('someone connected');
  5. });
  6. nsp.emit('hi', 'everyone!');
  7. //客户端
  8. const socket = io('/my-namespace');

对于每个namespace,都可以定义多个频道,也就是room,用户可以 joinleft

  1. //服务端
  2. io.on('connection', function(socket){
  3. socket.join('some room');
  4. });
  5. //当要向某个房间传数据时,使用 to
  6. io.to('some room').emit('some event');

有的时候需要将数据从一个进程发送到令一个进程,可以通过redis adapter

  1. //一个服务端 可以应用redis adapter
  2. const io = require('socket.io')(3000);
  3. const redis = require('socket.io-redis');
  4. io.adapter(redis({ host: 'localhost', port: 6379 }));
  5. //另一个服务端可以通过连接给服务,从另一个进程的任意频道发送
  6. const io = require('socket.io-emitter')({ host: '127.0.0.1', port: 6379 });
  7. setInterval(function(){
  8. io.emit('time', new Date);
  9. }, 5000);

参考链接

原来你是这样的Websocket--抓包分析

SocketIO官方文档

socket.io简易教程(群聊,发送图片,分组,私聊)

socketIO官方案例的GitHub仓库

fork的chat案例

菜鸟教程 HTML5 WebSocket

WebSocket以及socketIO的使用的更多相关文章

  1. websocket和socketio的总结

    1.WebSocket是什么? WebScoket是一种让客户端和服务器之间能进行双向实时通信的技术.它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTP ...

  2. WebSocket和SocketIO总结

    1.WebSocket是什么? WebScoket是一种让客户端和服务器之间能进行双向实时通信的技术.它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTP ...

  3. 借助Nodejs探究WebSocket

    文章导读: 一.概述-what's WebSocket? 二.运行在浏览器中的WebSocket客户端+使用ws模块搭建的简单服务器 三.Node中的WebSocket 四.socket.io 五.扩 ...

  4. Nodejs之WebSocket

    文章导读: 一.概述-what's WebSocket? 二.运行在浏览器中的WebSocket客户端+使用ws模块搭建的简单服务器 三.Node中的WebSocket 四.socket.io 五.扩 ...

  5. 结合manage.py,在flask项目中使用websocket模块--- flask-socketio

    前言:       - 为什么我要使用 flask-socketio模块,而不是flask-sockets?       - 因为flask-socketio与前端流行的websocket库socke ...

  6. Cocos2d-JS/Ajax用Protobuf与NodeJS/Java通信

    原文地址:http://www.iclojure.com/blog/articles/2016/04/29/cocos2d-js-ajax-protobuf-nodejs-java Google的Pr ...

  7. Cocos2d-x网络通信

    Cocos2d-x示例提供了三种内置的网咯通信类 HttpClient,WebSocket,SocketIO. 其中第一个是简单的HTTP协议的使用,提供很多Http请求方式. 剩下的Socket*是 ...

  8. node.js浅见

    看过很多朋友node.js代码敲得很好,但是对于概念还是很生疏.个人认为,代码是树叶,树干搭起来才是王道. 1.简述node.js的适用场景: IIO密集而非计算密集的情景:高并发微数据(比如账号系统 ...

  9. python中socket、socketio、flask-socketio、WebSocket的区别与联系

    socket.socketio.flask-socketio.WebSocket的区别与联系 socket 是通信的基础,并不是一个协议,Socket是应用层与TCP/IP协议族通信的中间软件抽象层, ...

随机推荐

  1. IntelliJ IDEA编辑文件的时候CPU飙高问题的解决

    原文地址:https://www.javatang.com/archives/2018/04/26/25582403.html 上篇文章中说明了解决IntelliJ IDEA中文输入法无提示的问题,最 ...

  2. Excel.Application使用手册

    Excel.Application组件使用方法,适合应用于使用EXCEL组件做WEB应用开发. 转自http://bbs.xtjc.com/thread-376095-1-1.html 定制模块行为( ...

  3. SpringBoot实现简单的CRUD

    CRUD-员工列表 实验要求: 1).RestfulCRUD:CRUD满足Rest风格: URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作 2).实验的请求架构; 3).员工列表 ...

  4. kafka for centos7

    https://blog.csdn.net/wqh8522/article/details/79163467

  5. code review工具之codebrag安装使用

    code review之codebrag安装使用 1.说明 codebrag是一款审核代码的工具,安装部署很简单.现在网上有很多代码审核工具,收费的开源的一大堆,开源的比较好的是Facebook的ph ...

  6. Springboot | @RequestBody 接收到的参数对象属性为空

    背景 今天在调试项目的时候遇到一个坑,用Postman发送一个post请求,在Springboot项目使用@RequestBody接收时参数总是报不存在,但是多次检查postman上的请求格式以及项目 ...

  7. Java 加密/解密Excel

    概述 设置excel文件保护时,通常可选择对整个工作簿进行加密保护,打开文件时需要输入密码:或者对指定工作表进行加密,即设置表格内容只读,无法对工作表进行编辑.另外,也可以对工作表特定区域设置保护,即 ...

  8. HTML简介介绍

    网页概述 网页:纯文本格式的文件:(以村文本格式编写,后缀名改为HTML的文本文件) ---- 网站:多个网页的集合: ---- 主页:打开网站后显示的第一个页面: ---- 浏览器:将纯文本格式的文 ...

  9. 《ASP.NET Core 高性能系列》静态文件中间件

    一.概述 静态文件(如 HTML.CSS.图片和 JavaScript等文件)是 Web程序直接提供给客户端的直接加载的文件. 较比于程序动态交互的代码而言,其实原理都一样(走Http协议), ASP ...

  10. Linux用户在第一次登录时强制更改初始密码

    迫使用户更改密码 如果你想迫使用户更改其密码,请使用下面这个命令. $ sudo chage -d0 <user-name>   最初,“-d <N>”选项应该被设成密码的“有 ...