使用socket.io和node.js向特定客户端发送消息

241

我正在使用socket.io和node.js进行开发,目前看起来还不错。但我不知道如何从服务器发送消息到一个特定的客户端,类似于这样:

client.send(message, receiverSessionId)

但是,.send().broadcast()方法似乎都不能满足我的需求。

我找到的一个可能的解决方案是,.broadcast()方法接受第二个参数作为不发送消息的SessionIds数组,因此我可以传递一个包含连接到服务器的所有SessionIds的数组,但不包括我希望发送消息的SessionId,但我感觉还必须有更好的解决方案。

有什么想法吗?

12个回答

203

在Socket.io 0.9中,Ivo Wetzel的答案似乎不再有效。

简而言之,现在您必须保存socket.id,并使用io.sockets.socket(savedSocketId).emit(...)向其发送消息。

这是我在集群化的Node.js服务器上使其工作的方法:

首先,您需要将Redis存储设置为存储,以便消息可以跨进程传递:

var express = require("express");
var redis = require("redis");
var sio = require("socket.io");

var client = redis.createClient()
var app = express.createServer();
var io = sio.listen(app);

io.set("store", new sio.RedisStore);


// In this example we have one master client socket 
// that receives messages from others.

io.sockets.on('connection', function(socket) {

  // Promote this socket as master
  socket.on("I'm the master", function() {

    // Save the socket id to Redis so that all processes can access it.
    client.set("mastersocket", socket.id, function(err) {
      if (err) throw err;
      console.log("Master socket is now" + socket.id);
    });
  });

  socket.on("message to master", function(msg) {

    // Fetch the socket id from Redis
    client.get("mastersocket", function(err, socketId) {
      if (err) throw err;
      io.sockets.socket(socketId).emit(msg);
    });
  });

});

我这里省略了聚合代码,因为它会让内容变得杂乱无章,但是将其添加到工作代码中非常简单。更多文档请参见http://nodejs.org/api/cluster.html


5
谢谢,这很有帮助。我只需要使用一个数组即可: io.of('/mynamespace').sockets[socketID].emit(...) (不知道是不是因为我在使用命名空间)。 - Adrien Schuler
在集群环境中,我如何确保套接字所属的正确进程正在发送消息? - Gal Ben-Haim
@Gal Ben-Haim,使用NGINX或HAProxy提供的粘性会话怎么样? - matanster
var holder = new socketio.RedisStore; ^类型错误:未定义的函数 在 Object.<anonymous> (C:\Users\Dev\Desktop\nouty-server\server.js:108:14) 在 Module._compile (module.js:460:26) 在 Object.Module._extensions..js (module.js:478:10) 在 Module.load (module.js:355:32) 在 Function.Module._load (module.js:310:12) 在 Function.Module.runMain (module.js:501:10) 在 startup (node.js:129:16) 在 node.js:814:3 - Lucas Bertollo
2
在socket.io 1.0中,io.sockets.socket(socket_id)已被移除。详情请参见https://github.com/socketio/socket.io/issues/1618#issuecomment-46151246。 - ImMathan
这是让socket.io和redis一起工作以实现更高效的内存处理的正确方式吗? - Spencer Bigum

147

11
这是最佳答案,适用于较新版本的socket.io。这里有一张很好的备忘单: https://dev59.com/OGkw5IYBdhLWcg3wNX32 - blented
6
请注意,这是“广播”方式发出事件。因此,如果您尝试为此设置回调函数,则会出现错误。如果您需要向特定的套接字发送带有回调的事件,请使用@PHPthinking的答案并使用io.sockets.connected [socketid] .emit();。在1.4.6版上进行了测试。 - butcarudev
但是我得到了这个错误:io.to["JgCoFX9AiCND_ZhdAAAC"].emit("socketFromServe‌​r", info); ^ TypeError: 无法读取未定义的 'emit' 属性。 - Raz
这对我有用,但必须使用 io.to(id).emit("keyword", data)。 - DORRITO
似乎不再起作用,文档页面也被删除了。 - undefined

104

最简单、最优雅的方法

已验证可与socket.io v3.1.1一起使用

只需要这样:

client.emit("your message");

就是这样。好的,但它是如何工作的?

最小工作示例

这是一个简单的客户端-服务器交互示例,其中每个客户端定期接收包含序列号的消息。对于每个客户端都有一个唯一的序列号,这就是“我需要发送消息到特定客户端”的地方。

服务器

server.js

const
    {Server} = require("socket.io"),
    server = new Server(8000);

let
    sequenceNumberByClient = new Map();

// event fired every time a new client connects:
server.on("connection", (socket) => {
    console.info(`Client connected [id=${socket.id}]`);
    // initialize this client's sequence number
    sequenceNumberByClient.set(socket, 1);

    // when socket disconnects, remove it from the list:
    socket.on("disconnect", () => {
        sequenceNumberByClient.delete(socket);
        console.info(`Client gone [id=${socket.id}]`);
    });
});

// sends each client its current sequence number
setInterval(() => {
    for (const [client, sequenceNumber] of sequenceNumberByClient.entries()) {
        client.emit("seq-num", sequenceNumber);
        sequenceNumberByClient.set(client, sequenceNumber + 1);
    }
}, 1000);
服务器开始监听8000端口的传入连接。一旦建立新连接,该客户端将被添加到保持其序列号的映射中。服务器还监听“断开”事件,以便在客户端离开时从映射中删除该客户端。
每秒钟,计时器都会触发。当它发生时,服务器遍历映射,并向每个客户端发送一个消息,其中包含其当前序列号,并立即递增该序列号。这就是全部内容。非常简单易行。
客户端部分甚至更简单。它只需连接到服务器并监听“seq-num”消息,每次收到消息都将其打印到控制台上。
"client.js"
const
    io = require("socket.io-client"),
    ioClient = io.connect("http://localhost:8000");

ioClient.on("seq-num", (msg) => console.info(msg));

运行示例

安装所需的库:

npm install socket.io@3.1.1 socket.io-client@3.1.1

运行服务器:

node server

通过运行以下命令,打开其他终端窗口并生成任意数量的客户端:

Open other terminal windows and spawn as many clients as you want by running:

node client

我还准备了一个包含完整代码的 gist,在这里


在生产环境中,端口8000是socketio的标准吗? - pyfork
1
抱歉我回复这么晚,@ingo。在测试websockets或HTTP服务器时,8000是Node社区常用的端口(3000也很常见)。当然你可以在生产环境中使用它,但我不确定那有多常见...无论如何,你实际上可以使用任何端口,只要你的网关/负载均衡器等已经做好准备。 - Lucio Paiva
我是Python/PHP程序员,对套接字和节点不太熟悉。我的问题是,为什么我们需要每秒递增同一用户的序列号?这只是为了演示吗? - Umair Ayub
1
@Umair 这只是为了演示目的,没有真正需要递增序列号。这只是为了表明您可以继续向每个客户端发送内容,他们将收到它。 - Lucio Paiva
我在这个例子中没有看到如何让一个特定的客户端仅仅向另一个客户端发送消息。每个客户端只知道他们自己的序列号,就像你为演示命名和创建它一样,并且没有从这些序列号到套接字ID的映射,这是直接向具有该套接字ID的客户端发送消息所需的。 - sçuçu
@Işık,客户端之间没有直接发送消息的方法,因为websocket协议不允许这样做。此外,问题是关于服务器向客户端发送消息的。如果您需要在两个客户端之间通信,建议您从客户端A发送一条消息到服务器,然后由服务器将其中继到客户端B。互联网上有几个聊天应用程序的示例展示了如何做到这一点,但如果您需要帮助,也可以在SO上提出具体问题。 - Lucio Paiva

99

好吧,你需要获取客户端(惊喜),你可以采取简单的方式:

var io = io.listen(server);
io.clients[sessionID].send()
可能会出现问题,虽然我不确定,但io.clients可能会被更改,因此请小心使用上述方法。
或者你可以自己跟踪客户端,因此在connection监听器中将它们添加到自己的clients对象中,并在disconnect监听器中将它们删除。
我会使用后者,因为根据您的应用程序,您可能希望在客户端上有更多状态,因此类似于clients[id] = {conn: clientConnect, data: {...}}这样的代码可能会完成任务。

9
Ivo,你能否举出一个更完整的例子或详细解释一下吗?我很想了解这种方法,但我不确定你在这个例子中使用的变量/对象。在 clients[id] = {conn: clientConnect, data: {...}} 中,clients[id] 是否是上面 io.clients[sessionID] 中看到的 io 对象的一部分?此外,clientConnect 对象是什么?谢谢。 - AndrewHenderson
@ivo-wetzel 你好,能帮我解决这个问题吗?http://stackoverflow.com/questions/38817680/nodejs-could-not-detect-online-socket - mahdi pishguy

42

您可以使用以下代码向客户端发送消息:

//仅向发送者客户端发送消息

socket.emit('message', '请查看此信息');

//或将消息发送给所有监听器,包括发送者

io.emit('message', '请查看此信息');

//将消息发送给除发送者之外的所有监听器

socket.broadcast.emit('message', '这是一条消息');

//或者您可以将消息发送到聊天室

socket.broadcast.to('chatroom').emit('message', '这是发给所有人的消息');


1
对我来说起作用了,我也需要在我的server.js文件中添加“const socket = require('socket.io')(http);”。 - iPzard

41

在1.0中,您应该使用:

io.sockets.connected[socketid].emit();

1
是的!!!之前io.sockets.sockets[socketid].emit()可以工作,但在新版本的socket.io中会出现未定义对象错误。更改为io.sockets.connected即可解决问题。 - Fraggle
对于使用TypeScript的人来说,根据类型定义文件,这是目前的“规范”API。 - Avi Cherry
但是我收到了一个错误:io.sockets.connected["JgCoFX9AiCND_ZhdAAAC"].emit("socketFromServer", info); ^类型错误:无法读取未定义的属性'emit'。 - Raz
2
这可能是由于 API 的更新,而 io.sockets.connected["something"] 对象及其 emit 方法已经不存在了。 - sçuçu

15

无论我们使用哪个版本,如果我们在服务器端的nodejs代码中使用了“io”对象(例如:io.on('connection', function(socket) {...});),只需console.log()一下该对象,就可以看到“io”只是一个json对象,其中有许多子对象存储着socket id和socket对象。

顺便说一下,我使用的是socket.io 1.3.5 版本。

如果我们查看io对象,它包含:

 sockets:
  { name: '/',
    server: [Circular],
    sockets: [ [Object], [Object] ],
    connected:
     { B5AC9w0sYmOGWe4fAAAA: [Object],
       'hWzf97fmU-TIwwzWAAAB': [Object] },

在这里,我们可以看到socketids“B5AC9w0sYmOGWe4fAAAA”等。那么,我们可以执行以下操作:

io.sockets.connected[socketid].emit();

再次仔细检查,我们可以看到类似以下的片段:

 eio:
  { clients:
     { B5AC9w0sYmOGWe4fAAAA: [Object],
       'hWzf97fmU-TIwwzWAAAB': [Object] },

那么,我们可以通过执行以下操作从这里检索套接字

io.eio.clients[socketid].emit();

此外,在引擎下我们还有:

engine:
 { clients:
    { B5AC9w0sYmOGWe4fAAAA: [Object],
      'hWzf97fmU-TIwwzWAAAB': [Object] },

所以,我们也可以写成:

io.engine.clients[socketid].emit();

所以,我猜我们可以通过上述三种方式中的任何一种来实现我们的目标:

  1. io.sockets.connected[socketid].emit(); 或者
  2. io.eio.clients[socketid].emit(); 或者
  3. io.engine.clients[socketid].emit();

但是我得到了这个错误: io.eio.clients["JgCoFX9AiCND_ZhdAAAC"].emit("socketFromServer", info); ^类型错误:无法读取未定义的属性'emit'。 - Raz
目前(版本2.1.1),这仅适用于sockets.connected,而不适用于另外两个。 - James
有用的答案,让我更好地理解了套接字客户端-服务器结构的工作原理。谢谢Suman。 - beyondtdr

13
你可以在服务器上完成此操作。
global.io=require("socket.io")(server);

io.on("connection",function(client){
    console.log("client is ",client.id);
    //This is handle by current connected client 
    client.emit('messages',{hello:'world'})
    //This is handle by every client
    io.sockets.emit("data",{data:"This is handle by every client"})
    app1.saveSession(client.id)

    client.on("disconnect",function(){
        app1.deleteSession(client.id)
        console.log("client disconnected",client.id);
    })

})

    //And this is handle by particular client 
    var socketId=req.query.id
    if(io.sockets.connected[socketId]!=null) {
        io.sockets.connected[socketId].emit('particular User', {data: "Event response by particular user "});
    }

在客户端上,处理起来非常容易。

var socket=io.connect("http://localhost:8080/")
    socket.on("messages",function(data){
        console.log("message is ",data);
        //alert(data)
    })
    socket.on("data",function(data){
        console.log("data is ",data);
        //alert(data)
    })

    socket.on("particular User",function(data){
        console.log("data from server ",data);
        //alert(data)
    })

7

从版本1.4.5开始,在使用io.to()时,请确保您提供一个正确的带前缀的socketId。

我曾经用客户端登录的socketId进行调试,但由于没有前缀,所以我一直在搜索,直到最终找到解决方法!因此,如果您拥有的ID没有前缀,您可能需要这样做:

io.to('/#' + socketId).emit('myevent', {foo: 'bar'});

我实际上必须删除前缀才能使其工作!感谢你的回答。 - Ebdulmomen1

6
在v0.9版本中,io.sockets.sockets[socket.id].emit(...)对我有效。

欢迎来到Stack Overflow。这个答案似乎与现有的答案没有太多关联。一旦您拥有更多的声望,您就可以评论其他人的帖子。这似乎更适合作为评论。 - jerry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接