Socket.IO打造基础聊天室

Socket.IO

01 Socket.io 简介

  • 一个100%由 JavaScript 实现、基于Node.js的用于实时通信、跨平台的开源框架,它包括了客户端的 JavaScript 库和 服务器端的 Node.js 服务。
  • 实现了对于其他语言的支持,如 Java、C++、Swift。
  • 提供了一个与 WebSocket 类似的通用 API:
Socket.io方法与事件

主要特点】:

(1) 可靠性(Reliability):依赖 Engine.IO, 首先建立长轮询,然后试着升级到更好的传输方式,如 WebSocket。
(2)自动重连(Auto-reconnection support):除非手动设置,否则当客户端断开连接时会一直尝试重连。
(3)心跳检测(Disconnection detection):在 Engine.IO 层面实现的心跳检测机制,允许服务器和客户端知道哪一方不再响应。

Engine.IO:为 Socket.IO 实现的基于传输、跨浏览器/跨设备的双向通信层。

(4)其它特点(如下图):

socket.io其它特点

02 工作流程

Socket.IO 底层是 Engine.IO,这个库实现了跨平台的双向通信,使用下面的传输方式封装了一套自己的 Socket 协议(EIO Socket)。

  • polling: XHR / JSONP polling transport
  • websocket: WebSocket transport

默认情况下,一个完整的 EIO Socket 包括多个 XHR 和 WebSocket 连接:

一个完整的EIO Socket连接(默认情况)
请求流程

EIO Socket 会首先发起XHR长轮询,然后服务端会返回以下字段:

  • 0:open标志
  • sid:当前连接的socket id
  • upgrade:表示可以把连接方式从长轮询升级到 websocket
  • pingInterval:心跳间隔
  • pingTimeout:心跳超时时间

前端收到握手的 upgrades 后,EIO 会检测浏览器是否支持 WebSocket,如果支持,就会启动一个 WebSocket 连接,然后通过这个 WebSocket 往服务器发一条内容为 probe, 类型为 ping 的数据。如果这时服务器返回了内容为 probe, 类型为 pong 的数据,前端就会把前面建立的 HTTP 长轮询停掉,后面只使用 WebSocket 通道进行收发数据。(socket.io 的详细工作流程是怎样的?

心跳检测:
EIO Socket生命周期内,会间隔一段时间 ping - pong 一次,用来测试网络是否正常

WebSocket消息帧

绿色:发送;白色:接收。
类型—> 2:ping,3:pong,4:message。

Socket.IO 在 Engine.IO 的基础上做了一些封装,比如 Socket.IO 里面这样的代码:

io.emit('add user', 'm') ;

在 Engine.io 里面是这样:

eio.send('message', '2["add user","m"]') ; // 2 是 socket.io 定义的包类型

因此,message的类型4后面有个2。

传输机制设置】:

Socket.io 为我们提供了选项,它的默认情况是以长轮询开始,我们也可以手动设置成只使用 websocket 方式来进行通信。

// 设置成只使用 websocket
const socket = io({ 
       transports: ['websocket'] 
}); 

// 重连时,重设选项
// 防止 websocket 可能因为代理、防火墙、浏览器等原因连接失败socket.on('reconnect_attempt', () => { 
       socket.io.opts.transports = ['polling', 'websocket']; 
}); 
只使用websocket

03 核心方法

Socket.io提供的方法

Socket.io 的核心函数:emiton

socket.emit(eventName[, ...args][, ack]):用来发射(触发)一个事件

  • eventName(string):事件名
  • args:要发送的数据
  • ack(Function):回调函数,一般省略,如需对方接受到信息后立即得到确认时需要用到
  • Returns Socket
socket.emit('ferret', 'tobi', (data) => {
  console.log(data); // data will be 'woot'
});

// server:
//  io.on('connection', (socket) => {
//    socket.on('ferret', (name, fn) => {
//      fn('woot');
//    });
//  });

socket.on(eventName, callback):用来监听一个 emit 发射的事件

  • eventName(string):监听的事件名
  • callback(Function):匿名函数,接收对方发来的数据,该匿名函数的第一个参数为接收的数据,若有第二个参数,则为要返回的函数
  • Returns Socket
socket.on('news', (data) => {
  console.log(data);
});

// with multiple arguments
socket.on('news', (arg1, arg2, arg3, arg4) => {
  // ...
});
// with callback
socket.on('news', (cb) => {
  cb(0);
});

Socket.io 提供了三种默认的事件(客户端和服务器都有):

  • connect:当与对方建立连接后自动触发;
  • message:当收到对方发来的数据后触发;
  • disconnect:当对方关闭链接后触发。

除了 socket.io 自身提供的事件之外,还支持自定义事件,丰富了通信:

// 如:
socket.on(‘new message’, function(data) {});
socket.emit(‘new message’, { message: message });

服务端广播的三种情况】:

服务器广播

客户端:

const socket = io();
// 监听事件
socket.on(‘message’, (data) => {});
// 触发事件
socket.emit(‘message’, { message }); 

服务端:

io.on(‘connection’, function(socket) {
        socket.on(‘message’, function(data) {
              // 1.广播给自己
              socket.emit(‘message’, data);
              // 2. 广播给除了自己的其它客户端
              socket.broadcast.emit(‘message’, data);
              // 3. 广播给所有客户端
              io.emit(‘message’, data);  // 等同于 io.sockets.emit()
        });
});

04 Rooms 和命名空间

【作用】:减少TCP连接数的同时区分不同的通信频道(在不同的路由层面能体现该作用,具体请参考 socket.io 中namespace 和 room的概念)、实现私聊

默认的命名空间:io.sockets、io

io.on('connection', function(socket){    
      socket.on('disconnect', function(){ });
}); 

自定义命名空间:

// 服务器端
var nsp = io.of('/my-namespace'); 
nsp.on('connection', function(socket){    
      socket.on('disconnect', function(){ });
}); 

// 客户端
var socket = io('/my-namespace'); 

Rooms:

// 自定义room
io.on('connection', function(socket){    
      socket.join('some room')); // 加入房间
      socket.leave('some room'); // 离开房间
}); 

// 向房间里的所有客户端发送消息
io.to('some room').emit('some event'); 

// 默认房间(每一个id一个room)
socket.on('say to someone', function(id, msg){    
       socket.broadcast.to(id).emit('my message', msg); 
}); 

获取房间信息:socket.adapter.rooms

rooms对象

默认情况下,每一个 id 便自成一个房间,房间名为 socket.id(指定命名空间之后,前面会带上命名空间);
自定义房间之后,原先的默认房间仍然存在;
房间为一个对象,包含当前进入房间的 sockets 以及长度。

05 打造基础聊天室

从官网最基础的聊天小例子入门,又分析了一下 Demos 中的 Chat demo 源码之后,自己试着用 react 实现了一遍,具体的功能及原理如下:

  • 最基础聊天功能

    【实现原理】:服务端运用 io.emit 进行广播给每个建立连接的客户端。

  • 登录(Chat Demo)

    【核心操作】:用户登录时(login 事件),服务端为当前的客户端存储 username。

    socket.username = username
    
  • 显示用户进入/离开

    【实现原理】:用户登录(login)时,触发 user joind 事件;用户断开连接(disconnection)时,触发 user left 事件—> 均通过 socket.broadcase.emit 广播给其它客户端。

  • 显示当前聊天室人数

    【实现原理】:操作 numUsers 变量—> 监听 connection 事件:++numUsers;监听 disconnection 事件:--numUsers 。

  • 显示各自昵称

    【实现原理】:触发 chat 事件的时候将当前连接的 username 通过 io.emit 广播给每个给客户端。

  • 提示对方正在输入

    【实现原理】:监听 typingstop typing 事件 —> 均通过 socket.broadcast.emit 广播给其它客户端。(typing 通过监听输入框的 onKeyPress 事件进行触发)

    注意:当发送完信息之后,需要清空“***正在输入”,客户端在监听到 chat 时可将提示置空。

  • 实现私人聊天

私人聊天

【实现原理】:运用 Room,通过 socket.adapter.rooms 获取当前 room 的信息,包括每个 room 中的 id。

// 加入房间
socket.join('some room'); 
// 离开房间
socket.leave('some room'); 

// 向房间里的所有客户端发送消息
io.to('some room').emit('some event'); 

// 向房间中的除了自己的客户端发送消息
socket.broadcast.to ('some room')
    .emit('my message', msg);
自娱自乐的实现版本:

github地址:ioChat

添加昵称+xxx正在输入
监听用户进入或离开/显示在线人数
Rooms 实现私人聊天

ps:下一次我会出一篇聊天室实现 emoji 表情发送的文章,敬请期待哟~~٩(๑>◡<๑)۶ ~~

§ 参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269