Редактиране: В Socket.IO 1.0+, вместо да настроите магазин с множество клиенти на Redis, вече може да се използва по-опростен адаптер на Redis.
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
Примерът, показан по-долу, би изглеждал по-скоро така:
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
io.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
Ако имате главен възел, който трябва да публикува в други процеси на Socket.IO, но не приема самите връзки на сокет, използвайте socket.io-emitter вместо socket.io-redis.
Ако имате проблеми с мащабирането, стартирайте вашите Node приложения с DEBUG=*
. Socket.IO вече прилага отстраняване на грешки, което също ще отпечатва съобщения за отстраняване на грешки на адаптера Redis. Примерен изход:
socket.io:server initializing namespace / +0ms
socket.io:server creating engine.io instance with opts {"path":"/socket.io"} +2ms
socket.io:server attaching client serving req handler +2ms
socket.io-parser encoding packet {"type":2,"data":["event","payload"],"nsp":"/"} +0ms
socket.io-parser encoded {"type":2,"data":["event","payload"],"nsp":"/"} as 2["event","payload"] +1ms
socket.io-redis ignore same uid +0ms
Ако и главният, и дъщерният процес показват едни и същи съобщения за анализатор, значи приложението ви е правилно мащабирано.
Не би трябвало да има проблем с вашата настройка, ако излъчвате от един работник. Това, което правите, е излъчване от четирите работници и поради публикуването/абонирането на Redis, съобщенията не се дублират, а се записват четири пъти, както поискахте от приложението. Ето една проста диаграма на това, което Redis прави:
Client <-- Worker 1 emit --> Redis
Client <-- Worker 2 <----------|
Client <-- Worker 3 <----------|
Client <-- Worker 4 <----------|
Както можете да видите, когато емитирате от работник, той ще публикува emit в Redis и ще бъде огледален от други работници, които са се абонирали за базата данни на Redis. Това също означава, че можете да използвате множество сокет сървъри, свързани към един и същи екземпляр, и излъчване на един сървър ще бъде задействано на всички свързани сървъри.
При клъстер, когато клиент се свърже, той ще се свърже с един от четирите ви работници, а не с всичките четири. Това също означава, че всичко, което излъчвате от този работник, ще бъде показано само веднъж на клиента. Така че да, приложението се мащабира, но начинът, по който го правите, излъчвате и от четирите работници, а базата данни Redis го прави така, сякаш го извиквате четири пъти на един работник. Ако клиент действително се свърже с всичките ви четири екземпляра на сокета, той ще получава шестнадесет съобщения в секунда, а не четири.
Видът на манипулирането на сокета зависи от типа на приложението, което ще имате. Ако ще се справяте с клиентите поотделно, тогава не би трябвало да имате проблем, защото събитието за връзка ще се задейства само за един работник на един клиент. Ако имате нужда от глобален "сърдечен ритъм", тогава бихте могли да имате манипулатор на сокет във вашия главен процес. Тъй като работниците умират, когато главният процес умира, трябва да компенсирате натоварването на връзката от главния процес и да оставите децата да се справят с връзките. Ето един пример:
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster) {
// we create a HTTP server, but we do not use listen
// that way, we have a socket.io server that doesn't accept connections
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
setInterval(function() {
// all workers will receive this in Redis, and emit
io.sockets.emit('data', 'payload');
}, 1000);
for (var i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
}
if (cluster.isWorker) {
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var redis = require('socket.io/node_modules/redis');
io.set('store', new RedisStore({
redisPub: redis.createClient(),
redisSub: redis.createClient(),
redisClient: redis.createClient()
}));
io.sockets.on('connection', function(socket) {
socket.emit('data', 'connected to worker: ' + cluster.worker.id);
});
app.listen(80);
}
В примера има пет екземпляра на Socket.IO, единият е главен, а четири са децата. Главният сървър никога не извиква listen()
така че няма излишни разходи за връзка за този процес. Въпреки това, ако извикате emit на главния процес, той ще бъде публикуван в Redis и четирите работни процеса ще извършат предаването на своите клиенти. Това компенсира натоварването на връзката към работниците и ако работникът умре, логиката на основното ви приложение ще бъде недокосната в главния.
Имайте предвид, че с Redis всички излъчвания, дори в пространство от имена или стая, ще бъдат обработени от други работни процеси, сякаш сте задействали излъчването от този процес. С други думи, ако имате две копия на Socket.IO с един екземпляр на Redis, извикване на emit()
на сокет в първия работник ще изпрати данните на своите клиенти, докато работник два ще направи същото, както ако извикате emit от този работник.