socket 即时通讯

682 阅读6分钟

全双工和单工:
全双工(Full Duplex)是通讯传输的一个术语,通信允许数据在两个方向上同事传输,他的能力相当于两个单双工通信方式的结合 。全双工指可以同时进行信号的传输(A-B && B-A),A→B的同时B→A,是瞬时同步的;
单工、半双工(Half Duplex),所谓半双工就是指一个时间段内只有一个动作的发生,比如一条单行道;

Socket.io server

  1. 服务器信息传输

//发送到当前请求的客户端
socket.emit('message','Lbxin'); io.sockets.emit('message','Lbxin');
//发送到除发送者以外的所有客户端
socket.broadcast.emit('message','Lbxin');
//发送到test房间的除发送者以外的所有用户
socket.broadcast.to('game').emit('message','Lbxin'); io.sockets.in('game').emit('message','Lbxin');
//发送到指定socketid的客户端
io.sockets.socket(socketid).emit('message','Lbxin')

Socket.io Client

  1. 客户端创建socket对象,io()的第一个参数是连接服务器的URl,默认情况下是window.location;第二个参数是配置项;io.connect(uri, {options})
this.options = {
    port: 80,
    secure: false,
    document: 'document' in global ? document : false,
    resource: 'socket.io',
    //['websocket', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling', 'jsonp-polling']
    //默认支持的链接方式(顺序敏感)
    transports: io.transports,
    'connect timeout': 10000,
    // 当连接超时后是否允许 socket.io 以其他连接方式尝试连接
    'try multiple transports': true,
    //当连接终止后,是否允许Socket.io自动进行重连,设置 false 后将会采用用户自定义的事件机制进行重连,本身会进入到对应的失败事件(error,disconnect)
    'reconnect': true,
    //为重连设置一个时间间隔,内部会在尝试重连时采用该值来进行重连,避免性能损耗
    'reconnection delay': 500,
    'reconnection limit': Infinity,
    'reopen delay': 3000,
    //设置一个重连的最大尝试次数,超过这个值后Socket.io会使用所有允许的其他连接方式尝试重连,直到最终失败。
    'max reconnection attempts': 10,
    'sync disconnect on unload': false,
    'auto connect': true,
    'flash policy port': 10843,
    'manualFlush': false
};

Socket 心跳机制 so_keeplive简介

SO_KEEPALIVE用于检测对方主机是否崩溃,避免服务器长时间阻塞TCP连接的输入;设置该选项后,在不配置的情况下会在两小时内没有数据交换的情况下TCP自动会给对方发送一个保持存活探测分节,对方在一切正常的情况下是必须得返回数据的。

  1. 对方接受一切正常,以期望值ACK进行响应,两小时后TCP将发出另一个探测节点;
  2. 对方崩溃且已经重新启动;以RST进行响应,套接字的待处理错误被设置为ECONNRESET,套接字本身则被关闭;
  3. 对方无任何响应,TCP发送另外8个探测分节,每隔75s发送一个,试图得到一个响应在发送完探测分节后如还无任何响应就放弃,套接字的待处理错误被处理为ETIMEOUT,套接字本身被关闭,如果ICMP的错误是host unreachable(主机不可达),说明对方机器没有崩溃,只是不可达,这种情况下待处理错误被设置为EHOSTUNREACH

SO_KEEPALIVE的参数详解:
发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测直到放弃探测确定连接断开的时间,大约为11min;
只有设置了SO_KEEPALIVE套接口选项后才会发送保活探测消息; socket中存在ping pong检测时该方案就没有存在的必要了,因为不会存在长时间客户端和服务端不进行数据交换的情况,也就不会发sing探测包的情况了

  1. tcp_keepalive_intvl,保活探测消息的发送频率。默认值为75s;
  2. tcp_keepalive_probes,TCP发送保活探测消息以确定连接是否已断开的次数。默认值为9(次);
  3. tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测消息的时间,即允许的持续空闲时间。默认值为7200s(2h)

前后端权限校验

  1. 正常前后端交互会在请求头里携带token,一般在axios请求拦截中统一全局处理;在socket上也离不开token校验,在登录后拿到token会携带在socket中,而在前端项目中一般只会维护一个socket实例,所以需要单独进行处理;

一般是在登录请求获取到token后进行socket token的相关校验注入,token的校验需要在前端进行相关校验,没有token强制用户进行登录,之后才能进入连接,这样可以避免服务端的频繁校验损耗不必要的性能;

前端在首次socket连接时需要携带token到服务器端,服务器收到token后进行相关的校验后判断是否通过,通过则返回一个固定的client-id,后续的所有请求都以这个为唯一凭证信息;

socketio-jwt进行相关的安全认证;有时需要将相关请求发送到同一个socket连接上,可以使用nginxip_hash方法,该方法将同一个 IP段的请求分发到了同一个实例,但该方法可能导致一台服务器负载过大;

  1. 在前端socket项目中一般会引用套件进行相关的集成拓展,比如vue-socket.io-entended,使用套件后的作用是会将$socket挂载到Vue的原型上,其次在使用socket时就可以想写vue一样,在datamethods同级下写sockets,定义所有socket-io的相关事件了;
  2. 服务端在收到消息后的处理方式与客户端有所不同,在客户端是一对一,服务对象只有服务端,只需要向服务端发送相关消息事件;而在服务端是多对一,有多个客户端连接自己,需要对不同的客户端进行相关的标识分类,其中消息的分发也分了三类:
    • 向当前连接的客户端发送事件通知;socket.emit()
    • 向除当前连接的客户端发送事件通知;socket.broadcast.emit()
    • 向包含当前客户端的所有用户发送消息;io.sockets.emit()
  3. 用户进入房间后服务端需要将房间的相关信息返回给用户,一个基本的房间信息有:
    • 历史用户聊天信息;
    • 房间信息、房间公告、其他房间的自定义配置等;
    • 当前所有在线用户列表,其中包含了用户的基础信息;

客户端的@操作在服务端的实现是通过正则进行匹配判断,当有指定字符如@ 后进行私聊逻辑的执行,用户在连接成功后,会在io.on('connection', socket => {})中产生一个唯一的socket标识,其中就包含用户信息和id标识,这时就需要将信息分别存储下来,在后续的私聊和个人信息判断中可以进行参考引用;比如私聊中的通过用户名进行查找和对不同房间中的用户根据id进行匹配。
客户端的@则是通过点击用户头像名称进行判断,当点击后如果获取到用户信息就进行输入框信息的替换(替换内容需要和服务端进行统一)input.value = @${user}

用户心如房间后的相关操作如发送消息礼物等,在服务端的操作是:
1、消息存入到db数据库中,当做历史记录消息,也可以通过正常的HTTP请求进行存储;
2、通知所有人,然后所有客户端收到消息通知后把新来的消息push到当前的消息列表中,客户端只做push的操作即可;

socket 精准单用户推送

根据用户的 socketID可以定向进行消息推送,获取方式为可以生成一个哈希数组,key 为 username,value 为 socket.id,这样就可以通过用户名获取对应的 id,进而定向向 client 推送消息。 io.sockets.socket(socketid).emit('message', 'Lbxin message')

socket 的跨域问题

  1. 可以使用cors组件库进行跨域的解决;
const express = require('express');
const app = express();
const http = require('http').Server(app);
let path = require('path');
var cors = require('cors');

app.use(cors());

const io = require('socket.io')(http, {
  cors: {
    origin: "http://127.0.0.1:8081",
    methods: ["GET", "POST"],
    credentials: true,
    allowEIO3: true
  },
  transport: ['websocket']
});

io.on('connection', function (socket) {
  console.log('A user connected');

  //Whenever someone disconnects this piece of code executed
  socket.on('disconnect', function () {
    console.log('A user disconnected');
  });

  socket.on('login', function (obj) {
    console.log(obj.username);
    // 发送数据
    socket.emit('relogin', {
      msg: `你好${obj.username}`,
      code: 200
    });
  });


});

http.listen(3000, function () {
  console.log('listening on *:3000');
});