• WebSocket以及socketIO的使用


    简介

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

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

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

    websocket轮询与ajax轮询

    原生socket

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

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

    客户端请求

    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: http://example.com
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    

    服务器回应

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    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 中使用。

    服务端

    const http = require('http');
    const net = require('net');//TCP 原生socket
    const crypto = require('crypto');
    
    let server = net.createServer(socket => {
    
        //握手只有一次
        socket.once('data',(data)=>{
            console.log('握手开始')
            let str = data.toString()
            let lines = str.split('
    ')
    
            //舍弃第一行和最后两行
            lines = lines.slice(1,lines.length-2)
    
            //切开
            let headers ={}
            lines.forEach(line=>{
               let [key,value]= line.split(`: `)
    
                headers[key.toLowerCase()]=value
    
            })
    
            if(headers[`upgrade`]!='websocket'){
                console.log('其他协议',headers[`upgrade`])
                socket.end()
            }else if(headers[`sec-websocket-version`] != '13'){
                console.log('版本不对',headers[`upgrade`])
                socket.end()
            }else {
                let key  = headers['sec-websocket-key']
                let mask = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    
                let hash =crypto.createHash('sha1')
                hash.update(key+mask)
                let key2 =hash.digest('base64')
    
                socket.write(`HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: ${key2}
    
    `)
                //服务器响应,握手结束
    
                //真正的数据
                socket.on('data',data1 => {
                    console.log(data1)//帧
    
                    let FIN = data1[0]&0x001//位运算
                    let opcode=data1[0]&0xF0
    
                })
            }
        })
    
        //断开
        socket.on('end',() =>{
            console.log('客户端断开')
        })
    });
    
    server.listen(8080);
    
    

    客户端

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
      <script>
        let socket = new WebSocket('ws://localhost:8080/')
    
        socket.emit = function(name,...args){
            socket.send(JSON.stringify({name,data:args}))
            alert(JSON.stringify({name,data:args}))
        }
        
        socket.onopen = function (event) {
            console.log('WebSocket is open')
            socket.emit('msg',12,34)
        }
        
        socket.onmessage = function (event) {
            console.log('有消息过来')
        }
        
        socket.onclose = function () {
            console.log('断开连接')
        }
      </script>
    </head>
    <body>
    
    </body>
    </html>
    

    socket.io

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

    socket.io文档

    安装

    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): 发送消息
    let app = require('express')();
    let httpServer = require('http').createServer(app);
    
    
    //得到IO对象
    let io = require('socket.io')(httpServer);
        //监视连接,(当有一个客户连接上时回调) io关联多个socket
        io.on('connection',function (socket) {
            console.log('socketio connected');
            
            //绑定sendMsg监听,接受客户端发送的消息
            socket.on('sendMsg',function (data) {
                console.log('服务端接受到浏览器的信息');
                
                /*io.emit 发送给所有连接服务器的客户端, 
                socket.emit 发送给当前连接的客户端*/
                socket.emit('receiveMsg',data.name + '_'+ data.date);
                console.log('服务器向浏览器发送消息',data)
            })
        })
    
    http.listen(4000,function(){
        console.log('listening on:4000')
    })
    

    客户端

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

    实现一个简易的聊天室

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

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

    服务端

    let app = require('express')();
    let httpServer = require('http').createServer(app);
    
    
    //得到IO对象
    let io = require('socket.io')(httpServer);
        //监视连接.
        io.on('connection',function (socket) {
           	socket.on('chat message', function(msg){
                console.log('connected',msg)
                io.emit('chat message', msg);
            });
            socket.on('disconnected',(res)=>{
                console.log('disconnected',res)
            })
        })
    
    http.listen(4000,function(){
        console.log('listening on:4000')
    })
    

    客户端

    <head>
       	<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
        <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
        <script>
        $(function () {
        	let socket = io("ws://localhost:4000");
        	$('form').submit(function(e){
          		e.preventDefault(); //阻止刷新
          		socket.emit('chat message', $('#m').val());
          		$('#m').val('');
          		return false;
        });
        socket.on('chat message', function(msg){
          $('#messages').append($('<li>').text(msg));
        });
      });
        </script>
    </head>
        <body>
            <ul id="messages"></ul>
            <form action="">
              <input id="m" autocomplete="off" /><button>Send</button>
            </form>
          </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

    //服务端
    socket.on('add user',function (msg) { //在用户第一次加入时,触发add user,提示所有用户
                io.emit('user joined',{id: socket.id});
            })
    socket.on('disconnect',(reason)=>{
                console.log('disconnect',socket.id,reason);
                io.emit('user left',{id: socket.id})
            })
    
    
    //客户端
     socket.on('user joined',function(data){
            let {id} = data;
            $('#messages').append($('<li>').text(id+'加入聊天室').addClass('log'));
        })
        socket.on('user left',function(data){
            let {id} = data;
            $('#messages').append($('<li>').text(id+'离开聊天室').addClass('log'));
        })
    
    • Add support for nicknames.
    //在客户端提供添加昵称的输入框,当输入完信息后,传递昵称给服务端 
    socket.emit('add user', username);
    
    //在服务端重构
     socket.on('add user', function (username) {
         socket.username = username;
         io.emit('user joined', {
                username: socket.username
          });
     })
    
    • 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信息

    //update
    $inputMessage.on('input', function () {
                    updateTyping();
                });
    
    function updateTyping() {
         if (connected) {
            if (!typing) {//如果当前没在输入,则更改标志,并发送正在输入的消息消息
               typing = true;
               socket.emit('typing');
             }
          lastTypingTime = (new Date()).getTime();
    
          setTimeout(function () {
          	var typingTimer = (new Date()).getTime();
          	var timeDiff = typingTimer - lastTypingTime;
          	if (timeDiff >= TYPING_TIMER_LENGTH && typing) {//如果停止输入超时,则发送停止消息
              socket.emit('stop typing');
              typing = false;
              }
          }, TYPING_TIMER_LENGTH);
                }
    }
    
    //服务端  传递当前正在输入或停止输入的用户名,用于让客户端显示或消失 is Typing的信息
            socket.on('typing', function () {
                io.emit('typing', {
                    username: socket.username
                });
            });
    
            socket.on('stop typing', function () {
                io.emit('stop typing', {
                    username: socket.username
        });
    
    • Show who’s online.
    • Add private messaging.

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

    NameSpaces、rooms

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

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

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

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

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

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

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

    参考链接

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

    SocketIO官方文档

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

    socketIO官方案例的GitHub仓库

    fork的chat案例

    菜鸟教程 HTML5 WebSocket

  • 相关阅读:
    gridcontrol中使用右健菜单popupMenu1
    无线路由信号增强办法
    sql server2008企业版和标准版
    使用apache设置绑定多个域名或网站
    VirtualBox 虚拟机复制
    Mysql 性能优化7【重要】sql语句的优化 慢查询
    Mysql 性能优化6【重要】 索引优化
    Mysql 性能优化7【重要】sql语句的优化 浅谈MySQL中优化sql语句查询常用的30种方法(转)
    mysql 高可用架构
    Mysql 复制工作原理
  • 原文地址:https://www.cnblogs.com/deus/p/12355605.html
Copyright © 2020-2023  润新知