如何在Node.js的WebSocket服务器中区分客户端?

116

我正在尝试在node.js中使用sockets,我成功了,但我不知道如何在我的代码中区分客户端。与sockets相关的部分是这样的:

var WebSocketServer = require('ws').Server, 
    wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws) {
    ws.on('message', function(message) {
        console.log('received: %s', message); 
        ws.send(message);
    });
    ws.send('something');
});

这段代码在我的客户端js上能正常运行。

但我想向特定用户或所有在我的服务器上打开套接字的用户发送消息。

在我的情况下,我作为客户端发送一条消息并收到响应,但其他用户则什么都没有看到。

例如,我希望用户1通过WebSocket向服务器发送一条消息,并向其打开套接字的用户2发送通知。


你如何定义哪个连接是“user1”,哪个是“user2”? - lanzz
我不太确定,我想在连接时为用户提供一种socketSession。 - Ajouve
5
一旦您知道连接的用户身份(例如,当您的用户将其用户名作为消息发送时),您可以在字典中存储一个命名引用(例如,connections[username] = ws),然后在其他地方,您可以执行类似于 connections[username].send(message) 的操作。 - lanzz
如果您的需求不是针对单个用户,而是针对一组用户(可能是1组)隔离到“房间”中,您可以使用socket join()broadcast()方法。在这里查看一些讨论:https://dev59.com/nGw15IYBdhLWcg3wFHpZ - meetamit
谢谢,它很好地将我所有的ws注册到了一个数组中 :) - Ajouve
13个回答

141
在Node.js中,您可以直接修改ws客户端并为每个客户端单独添加自定义属性。此外,您还有一个全局变量wss.clients,可在任何地方使用。请尝试使用至少两个已连接的客户端运行以下代码:
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({
    server: httpsServer
});


wss.getUniqueID = function () {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
    }
    return s4() + s4() + '-' + s4();
};

wss.on('connection', function connection(ws, req) {
    ws.id = wss.getUniqueID();

    wss.clients.forEach(function each(client) {
        console.log('Client.ID: ' + client.id);
    });
});

您还可以直接在客户端连接的 URL 中传递参数:

https://myhost:8080?myCustomParam=1111&myCustomID=2222

在连接函数中,您可以获取这些参数并将它们直接分配给您的 ws 客户端:

wss.on('connection', function connection(ws, req) {

    const parameters = url.parse(req.url, true);

    ws.uid = wss.getUniqueID();
    ws.chatRoom = {uid: parameters.query.myCustomID};
    ws.hereMyCustomParameter = parameters.query.myCustomParam;
}

有没有一种方法可以在不改变 ws 的情况下完成这个操作?当执行 ws.clients.values() 时,有没有一种方法可以从该列表中获取请求? - Kevin Ghadyani
2
添加支持它的参考资料: https://github.com/websockets/ws/issues/859翻译: - Maoritzio
这太棒了,谢谢!我正在使用websocket库(而不是ws),所以我必须使用req.resourceURL。这包括对象的解析查询字符串,因此您只需使用req.resourceURL.query.myCustomParam即可。 - Chris Broski
1
在TypeScript中,我遇到了“属性'id'在类型'WebSocket'上不存在”的错误。有没有一种方法可以不像这里所描述的那样强制转换为any来解决这个问题? - Omar Sharaki
如果有新手不清楚,url.parse() 中的 url 是一个内置的 Nodejs 模块。为了让这行代码正常工作,请在你的脚本中添加 const url = require('url') - A__

70

您可以将用户ID分配给一个名为CLIENTS[]的数组,其中将包含所有用户。您可以按照以下方式直接向所有用户发送消息:

您可以简单地将用户ID分配给一个名为CLIENTS[]的数组,其中将包含所有用户。您可以按照以下方式直接向所有用户发送消息:

var WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({port: 8080}),
    CLIENTS=[];

wss.on('connection', function(ws) {
    CLIENTS.push(ws);
    ws.on('message', function(message) {
        console.log('received: %s', message);
        sendAll(message);
    });
    ws.send("NEW USER JOINED");
});

function sendAll (message) {
    for (var i=0; i<CLIENTS.length; i++) {
        CLIENTS[i].send("Message: " + message);
    }
}

1
如果你来回发送客户的唯一ID,你也可以获得消息的源客户端。 - Mehmet AVŞAR
47
这个解决方案无法扩展,因为变量CLIENTS存在于NodeJS的单个进程中。即使我们通过fork多个进程利用多个核心,CLIENTS也不是共享内存分配,每个进程都维护自己的CLIENTS列表。因此,需要后续连接与相同的进程通信。 - Khanh Hua
13
@KhanhHua,你能否提供一个可扩展的解决方案? - vgoklani
9
你可以使用Redis的发布/订阅功能,创建一个“发布”频道,让你的每个服务器都监听它。每当在该频道上发布一条消息时,每个服务器都会向其所有客户端进行广播。就这么简单。 - Nepoxx
8
根据文档WebSocketServer 有一个 clients 属性,可以跟踪当前的客户端。文档还提供了一个示例,用于遍历该集合以向每个客户端发送广播消息。 - Coderer
显示剩余11条评论

17

您可以使用请求头 'sec-websocket-key'

wss.on('connection', (ws, req) => {
  ws.id = req.headers['sec-websocket-key']; 

  //statements...
});

那 sec-websocket-key 重复的情况怎么样? - SerG
太棒了!非常感谢你! - undefined

11

这个 Worlize 服务器的代码片段真的对我帮助很大。即使你正在使用 ws,这段代码也应该很容易适应。我在这里选择了重要的部分:

// initialization
var connections = {};
var connectionIDCounter = 0;

// when handling a new connection
connection.id = connectionIDCounter ++;
connections[connection.id] = connection;
// in your case you would rewrite these 2 lines as
ws.id = connectionIDCounter ++;
connections[ws.id] = ws;

// when a connection is closed
delete connections[connection.id];
// in your case you would rewrite this line as
delete connections[ws.id];

现在,您可以根据链接中的代码轻松创建broadcast()和sendToConnectionId()函数。
希望能对您有所帮助。

2
需要使用for in循环来广播,而不是标准的for循环,因为(关联)数组中存在“空洞”。 - Tobiq

8
取决于您使用的Websocket。例如,在此处找到的最快的Websocket:https://github.com/websockets/ws,可以通过以下方法进行广播:
var WebSocketServer = require('ws').Server,
   wss = new WebSocketServer({host:'xxxx',port:xxxx}),
   users = [];
wss.broadcast = function broadcast(data) {
wss.clients.forEach(function each(client) {
  client.send(data);
 });
};

稍后在您的代码中,您可以使用wss.broadcast(message)向所有人发送消息。要向个人用户发送私信,我执行以下操作:

(1) 在我发送到服务器的消息中包含用户名 (2) 然后,在onMessage中,我将带有该用户名的WebSocket保存在数组中,稍后通过用户名检索它:

wss.on('connection', function(ws) {

  ws.on('message', function(message) {

      users[message.userName] = ws;

(3) 要发送给特定的用户,您可以执行users[userName].send(message);。


你能提供任何包含用户名的代码链接吗? - johannesMatevosyan
在客户端,当您发送消息时,可以创建一个JSON对象,然后像这样对其进行字符串化:var sendObject = {userName:"Tim",pm:true,message:"how are you?"}; ws.send(JSON.stringify(sendObject)); 此外,在Einaros中,您可以在标头中发送json有效负载,例如:WS.open(uri, [JSON.stringify({userName:"Tim"})]); 然后,您可以使用ws.upgradeReq.headers在服务器上解析它,并通过JSON.parse将字符串转换回其对象。我已经使用此方法创建了用户组,但也可以用于存储用户数组。 - droid-zilla
我有一个类似的问题,但是无法解决,你能看一下吗?https://dev59.com/c5Tfa4cB1Zd3GeqPO050 - johannesMatevosyan
如果您认为我的答案是正确的,您能否请点个赞。谢谢。我已经在您的Plunk上进行了评论,并发现可能会影响事情的一个拼写错误。 - droid-zilla
当然,我会的,但首先我需要明天检查一下。 - johannesMatevosyan
请展示您如何将数据推送到users[]数组中,否则这个回答就不能解决问题。它只是展示了如何向所有客户端广播,而不是单独的客户端。-1 - Spankied

6
我正在使用ws对象中的fd。它应该对每个客户端都是唯一的。
var clientID = ws._socket._handle.fd;

当我打开一个新的浏览器选项卡时,我得到了不同的数字。

第一个选项卡显示11,下一个则显示12。


13
如果一个用户离开,另一个用户加入时,我认为文件描述符(fd)是可以被重复利用的... 本质上它们可能会被循环使用。 - user772401
1
如果用户离开了,您可以从会话中删除它,即使有相同的编号加入,也会在您的会话表中留下未填充的键。 - Anton Krug
1
可能会出现问题,当断开连接时,_handle 将消失,您无法从表中删除它,也许您可以将 handle 保存在 ws 中? - Anton Krug

3

您可以检查连接对象。每个连接的客户端都有内置的标识; 您可以在此处找到:

let id=ws._ultron.id;
console.log(id);

你怎么知道Ultron.id是唯一的?我找不到任何相关的信息。 - adelriosantiago
尝试使用不同的连接自行操作。 - rrkjonnapalli
1
在去年1月的5.1、5.0和4.1版本中,ultron已被移除作为websockets/ws的依赖项。当使用这些(或更高版本)时,ws._ultron将不再存在。 - Jochem Schulenklopper

2

这里的一个可能的解决方案是,在用户ID前附加设备ID,这样我们就可以区分在不同设备上使用相同用户ID的多个用户。

ws://xxxxxxx:9000/userID/<<deviceId>>


1

使用全局计数变量,并为每个新连接分配其值:

const wss = new WebSocket.Server({server});
let count_clients = 0;
wss.on('connection', function connection(ws){
    ws.id=count_clients++;
    console.log(`new connection, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);
    ws.on('close', req => {console.log(`disconnected, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);});
...

0
这是我做的:
* on connect, server generate an unique id (e.g uuid) for the connection,
    * save it in memory, (e.g as key of map),
    * send back to client in response,
    * 
* 
* client save the id, on each request will also send the id as part of request data,
* then server identify the client by id, on receive further request,
* 
* server maintain client, e.g cleanup on close/error,
* 

我已经实现了这个想法,它可以很好地识别客户端。
而且,我还基于这个想法实现了组/主题广播,这需要服务器维护额外的信息。


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