Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

⚡ Primus, the creator god of the transformers & an abstraction layer for real-time to prevent module lock-in.

License

NotificationsYou must be signed in to change notification settings

primus/primus

Repository files navigation

Version npmCICoverage Status

Primus, the creator god of transformers but now also known as universal wrapperfor real-time frameworks. There are a lot of real-time frameworks available forNode.js and they all have different opinions on how real-time should be done.Primus provides a common low level interface to communicate in real-time usingvarious real-time frameworks.

Advantages

  1. Effortless switching between real-time frameworks by changing one single lineof code. No more API rewrites needed when your project requirements change,the framework gets abandoned or simply breaks down.
  2. Built-in reconnect, it just works. The reconnect is controlled by arandomised exponential back-off algorithm to reduce server stress.
  3. Offline detection, Primus is smart enough to detect when users drop theirinternet connection (switching WIFI points/cell towers for example) andreconnects when they are back online.
  4. Automatically encodes and decodes messages using custom parsers. Can beeasily switched for binary encoding for example.
  5. A clean, stream-compatible interface for the client and server. You canjuststream#pipe data around. In addition to that, the client works onNode.js as well, write once, run it everywhere.
  6. Fixes various of bugs in the supported frameworks and additional stabilitypatches to improve real-time communication.
  7. Comes with an amazing plugin interface to keep the core library as fast andlean as possible while still allowing the server and the client to beextended.
  8. Last but not least, Primus is built with love, passion and dedication to thereal-time web.

Installation

Primus is released onnpm and can be installed using:

npm install primus --save

Before Starting

If you deploy your application behind a reverse proxy (Nginx, HAProxy, etc.) youmight need to add WebSocket specific settings to its configuration files. Ifyou intend to use WebSockets, please ensure that these settings have been added.There are some example configuration files available in theobserving/balancerbattlerepository.

Table of Contents

Getting started

Primus doesn't ship with real-time frameworks as dependencies, it assumes thatyou as user add them yourself as a dependency. This is done to keep the moduleas lightweight as possible. This works becauserequire in will walk throughyour directories searching fornode_module folders that have these matchingdependencies.

Primus needs to be "attached" to a HTTP compatible server. These includes thebuilt-inhttp andhttps servers but also thespdy module as it has thesame API as node servers. Creating a new Primus instance is relativelystraightforward:

'use strict';varPrimus=require('primus'),http=require('http');varserver=http.createServer(/* request handler */),primus=newPrimus(server,{/* options */});

The following options can be provided:

NameDescriptionDefault
authorizationAuthorization handlernull
pathnameThe URL namespace that Primus can own/primus
parserMessage encoder for all communicationJSON
transformerThe transformer we should use internallywebsockets
pluginThe plugins that should be applied{}
pingIntervalInterval at which heartbeats are sent30000
globalSet a custom client class / global namePrimus
compressionUse permessage-deflate / HTTP compressionfalse
maxLengthMaximum allowed packet size, in bytes10485760
transportTransformer specific configuration{}
idGeneratorCustom spark id generator functionundefined
originscors List of origins*
methodscors List of accepted HTTP methodsGET,HEAD,PUT,POST,DELETE,OPTIONS
credentialscors Allow sending of credentialstrue
maxAgecors Cache duration of CORS preflight30 days
headerscors Allowed headersfalse
exposedcors Headers exposed to the clientfalse

The options that are prefixed withcors are supplied to ouraccess-control module which handlesHTTP Access Control (CORS), so for a more detailed explanation of these optionscheck it out.

Thetransport option allows you to use any configuration option supported bythe underlying real-time framework. Its use is discouraged as these optionsare framework specific and no longer work if you change transformer. Our adviseis to use it only if you know what you are doing and if you need fine-grainedcontrol over the real-time framework. Please also keep in mind that some ofthese options are overriden by Primus.

ThepingInterval option specifies the interval at which heartbeats aretransmitted. It is possible to completely disable the heartbeats by setting thevalue of thepingInterval option tofalse.

TheidGenerator option can be used to define a function which will be calledto set eachspark.id. The generator function should returna unique string each time it is invoked. IfidGenerator is not defined, Primuswill try to use ids provided by the transformer. If the transformer does notprovide ids, Primus will usenanoid to generateSpark ids.

If you don't have a pre-existing server where you want or can attach your Primusserver to you can also use thePrimus.createServer convenience method. ThecreateServer method will automatically:

  • Setup a HTTP, HTTPS or SPDY server for you on the given port number.
  • Setup your Primus server with the given configuration.
  • Listen on the HTTP, HTTPS, SPDY server.
  • Attach aprimus.on('connection') listener.
  • Return the created Primus instance.
Primus.createServer(functionconnection(spark){},{port:8080,transformer:'websockets'});

In the above example we automatically create a HTTP server which will listenon port 8080, a primus instance with thewebsockets transformer and startlistening for incoming connections. The supplied function in thePrimus.createServer method is optional. You can just listen for incomingconnections your self using the returned Primus instance. If you want to listen toa HTTPS or SPDY server, which is recommended, you can directly pass the SPDY andHTTPS certs/keys/pfx files in the options object:

varprimus=Primus.createServer({port:443,root:'/folder/with/https/cert/files',cert:'myfilename.cert',key:'myfilename.cert',ca:'myfilename.ca',pfx:'filename.pfx',passphrase:'my super sweet password'});primus.on('connection',function(spark){spark.write('hello connnection');});

Primus.createServer returns a warning when it starts a HTTP server. Thewarning advises you to use a HTTPS server and can be disabled setting theoptioniknowhttpsisbetter totrue.

Client library

As most libraries come with their own client-side framework for making theconnection we've also created a small wrapper for this. The library can beretrieved using:

primus.library();

Which returns the client-side library as a string (which can then be minified oreven have more code added to it). It does not come pre-minified as that is outof the scope of this project. You can store this on a CDN or on your static server.Do whatever you want with it, but remember to regenerate it every time you changePrimus server options. This is important because some properties of the clientare set using the server configuration. For example if you change thepathname, the client should be regenerated to reflect that change and workcorrectly. We advise you to regenerate the library every time you redeploy soyou always have a client compatible with your back-end. To save the file youcan use:

primus.save(__dirname+'/primus.js');

This will store the compiled library in your current directory. If you want tosave it asynchronously, you can supply the method with a callback method:

primus.save(__dirname+'/primus.js',functionsave(err){});

But to make it easier for you during development we've automatically added anextra route to the supplied HTTP server, this will serve the library for you soyou don't have to save it. Please note, that this route isn't optimised forserving static assets and should only be used during development. In your HTMLpage add:

<scriptsrc="/primus/primus.js"></script>

As you can see, it will use the/primus pathname by default. Primus needs toown the whole path/namespace in order to function properly as it will forwardall other requests directly in to the transformers so they can work their magic.If you already have a static folder with the nameprimus you can change thepathname to something different and still make this work. But you would ofcourse need to update thesrc attribute of the script tag to set the correctlocation. It's always available at:

<protocol>://<server location>/<pathname>/primus.js

Here<pathname> is thepathname set in server options above. The clientis cross domain compatible so you don't have to serve it from thesame domain you're running Primus on. But please note, that the real-timeframework you're using might be tied to same domain restrictions.

Once you're all set up you can start listening for connections. Theseconnections are announced through theconnection event.

primus.on('connection',function(spark){// spark is the new connection.});

Disconnects are announced using adisconnection event:

primus.on('disconnection',function(spark){// the spark that disconnected});

Thespark argument is the actual real-time socket/connection. Sparks have areally low level interface and only expose a couple properties that are crossengine supported. The interface is modeled towards a Node.js stream compatibleinterface. So this will include all methods that are available on thestreaminterface includingSpark#pipe.

spark.headers

Thespark.headers property contains the headers of either the requestthat started a handshake with the server or the headers of the actual real-timeconnection. This depends on the module you are using.

Please note that sending custom headers from the client to the server isimpossible as not all transports that these transformers support can add customheaders to a request (JSONP for example). If you need to send custom data, use aquery string when connecting

spark.address

Thespark.address property contains theip andport of theconnection. If you're running your server behind a reverse proxy it willautomatically use thex-forwarded-for header. This way you will always havethe address of the connecting client and not the IP address of your proxy.

Please note that theport is probably out of date by the time you're goingto read it as it's retrieved from an old request, not the request that isactive at the time you access this property.

spark.query

Thespark.query contains the query string you used to connect to the server. It'sparsed as an object. Please note that this may not be available for all supportedtransformers.

spark.socket

Thespark.socket is set to the underlying socket of the transformer. This is notnecessarily a rawSocket and will differ from transformer to transformer.

spark.id

This is a unique id that we use to identify this single connection with. Normallythe frameworks refer to this as asessionid, which is confusing as it's onlyused for the duration of one single connection. You should not see this as a"session id", and rather expect it to change between disconnects and reconnects.

spark.request

Thespark.request gives you access to the HTTP request that was used toinitiate the real-time connection with the server. Please note that this requestis already answered and closed (in most cases) so do not attempt to write oranswer it anyway. But it might be useful to access methods that get added bymiddleware layers, etc.

spark.write(data)

You can use thespark.write method to send data over the socket. The data isautomatically encoded for you using theparser that you've set while creatingthe Primus server instance. This method always returnstrue on success andfalse on failure so back pressure isn't handled.

spark.write({foo:'bar'});

spark.end(data, options)

You can usespark.end to close the connection. This method takes two optionalarguments. The first, if provided, is thedata to send to the client beforeclosing the connection. The second is an options object used to customize thebehavior of the method. By default thespark.end method closes the connectionin a such way that the client knows it was intentional and it doesn't attempt areconnection.

spark.end();// the client doesn't reconnect automatically

You can change this behavior and trigger a client-side reconnection using thereconnect option.

spark.end(undefined,{reconnect:true});// trigger a client-side reconnection

spark.emits(event, parser)

This method is mostly used internally. It works similarly to the nativebindfunction, returning a function that emits the assignedevent every time it'scalled. If the last argument is a function, it will be used to parse thearguments of the returned function. Theparser is optional and always async,itsfirst argument is a callback that follows the usual error first pattern,all successive arguments are the ones to parse. Using theparser you canreduce the arguments down to a single value, remove them completely or preventthe event from being emitted. Seeemits fordetailed usage instructions.

spark.emits('event',functionparser(next,structure){next(undefined,structure.data);});

Please note that the data that is received here isn't decoded yet.

spark.on('data')

Thedata event is emitted when a message is received from the client. It'sautomatically decoded by the specified decoder.

spark.on('data',functionmessage(data){// the message we've received.});

spark.on('end')

Theend event is emitted when the client has disconnected.

primus.on('connection',function(spark){console.log('connection has the following headers',spark.headers);console.log('connection was made from',spark.address);console.log('connection id',spark.id);spark.on('data',function(data){console.log('received data from the client',data);//// Always close the connection if we didn't receive our secret imaginary// handshake.//if('foo'!==data.secrethandshake)spark.end();spark.write({foo:'bar'});spark.write('banana');});spark.write('Hello world');})

Connecting from the Browser

Primus comes with its client framework which can be compiled usingprimus.library() as mentioned above. To create a connection you can simplycreate a new Primus instance:

varprimus=newPrimus(url,{ options});//// But it can be easier, with some syntax sugar.//varprimus=Primus.connect(url,{ options});

The URL should confirm the following conditions:

  • It should include the protocol it needs to connect with. This can either behttp orhttps. We recommend that you're using HTTPS for all yourconnections as this prevents connection blocking by firewalls and anti-virusprograms.
  • The URL should not include a pathname. The pathname is configured by theserver (See:getting-started) and needs to be configuredthere as it will be compiled in to theprimus.js client file.

If nourl argument is passed, it will default to the current URL.

The following options can be provided:

NameDescriptionDefault
reconnectConfigures the exponential back off{}
timeoutConnect time out10000 ms
pingTimeoutMax time to wait for a server ping45000 ms
strategyOur reconnect strategies"disconnect,online,timeout"
manualManually open the connectionfalse
websocketsShould we use WebSocketsBoolean, is detected
networkUse nativeonline/offline detectionBoolean, is feature detected
transportTransport specific configuration{}
queueSizeNumber of messages that can be queuedInfinity

There are 2 important options that we're going to look a bit closer at.

Reconnect

When the connection goes down unexpectedly an automatic reconnect process isstarted. It uses a randomised exponential back-off algorithm to prevent clientsfrom DDoSing your server when you reboot as they will all be re-connecting atdifferent times. The reconnection can be configured using theoptions argumentinPrimus and you should add these options to thereconnect property:

NameDescriptionDefault
maxMaximum delay for a reconnection attemptInfinity
minMinimum delay for a reconnection attempt500 ms
retriesMaximum amount of attempts10
reconnect timeoutMaximum time for an attempt to complete30000 ms
factorExponential back off factor2
primus=Primus.connect(url,{reconnect:{max:Infinity// Number: The max delay before we try to reconnect.,min:500// Number: The minimum delay before we try reconnect.,retries:10// Number: How many times we should try to reconnect.}});

When you're going to customizemin please note that it will growexponentially e.g.500 -> 1000 -> 2000 -> 4000 -> 8000 and is randomizedso expect to have slightly higher or lower values.

Please note that when we reconnect, we will receive a newconnection event onthe server and a newopen event on the client, as the previous connection wascompletely dead and should therefore be considered a new connection.

If you are interested in learning more about the backoff algorithm you mightwant to readhttp://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html

Strategy

The strategy allows you to configure when you want areconnect operation tokick in. We're providing somesane defaults for this but we still want toprovide users with highest level of customization:

disconnect
Reconnect when we detect an unintentional disconnect in the connection.
online
Reconnect when the browser went from an offline event to an online event.
timeout
Reconnect when we failed to establish our initial connection. This can happen because we took too long to connect or because there was an error while we tried to connect (which happens when you connect to a dead server)

You can supply these options as a comma-separatedString:

varprimus=newPrimus(url,{strategy:'online, timeout, disconnect'})

Or as anArray:

varprimus=newPrimus(url,{strategy:['online','timeout','disconnect']});

We'll try to normalize everything as much as possible, wetoLowerCase everythingand join it back to a readable string so if you wrotedIsconNect it will getnormalized todisconnect.

If you are using authentication you should disable thetimeout strategy asthere is no way of detecting the difference between a failed authorization and afailed connect. If you leave this enabled with authorization every unauthorizedaccess will try to reconnect again.

We automatically disable this for you when you've set the authorization beforeyou save the library.

But there are always use cases where reconnection is not advised for yourapplication. In these cases we've provided a way to completely disable thereconnection, this is done by setting thestrategy tofalse:

varprimus=newPrimus(url,{strategy:false});

If you want to manually control the reconnection you can callprimus.end()to close the connection andprimus.open() to establish a new one.Be sureto useprimus.open() correctly, see below for details.

transport

The transport object allows you to add a transport specific configuration.We only recommend using this if you understand and accept the followingconsequences:

  • Primus will try to override configuration properties that are needed toensure a correct functioning.
  • We might start using options without any announcement or major version bump.
  • Expect your client and its connection to malfunction once you switch betweendifferent transports, as these configurations are specific to the bundledtransformer library/client.
  • Bugs and bug reports caused by using this functionality are closedimmediately.

Having that said, this gives you total freedom while still getting the benefitsof Primus.

primus.open()

This method opens a connection with the server. By default it is calledautomatically when the Primus instance is created, but there are cases whereit's desirable to open the connection manually. To do this set themanualoption totrue and when you have the Primus instance call the method:

primus.open();

When you callprimus.open() you should make sure that the connection istotally dead (e.g. after anend event) and primus isn't already trying orplanning to reconnect.

primus.write(message)

Once you've created your Primus instance you're ready to go. When you want towrite data to your server you can just call the.write method:

primus.write('message');

It automatically encodes your messages using the parser that you've specified onthe server. So sending objects back and forth between the server is nothingdifferent then just writing:

primus.write({foo:'bar'});

When you are sending messages to the server, you don't have to wait for theopen event to happen, the client will automatically buffer all the data you'vesend and automatically write it to the server once it's connected. The clientsupports a couple of different events.

primus.on('data')

Thedata event is the most important event of the whole library. It's emittedwhen we receive data from the server. The data that is received is alreadydecoded by the specified parser.

primus.on('data',functionmessage(data){console.log('Received a new message from the server',data);});

primus.on('open')

Theopen event is emitted when we've successfully created a connection withthe server. It will also be emitted when we've successfully reconnected after theconnection goes down unintentionally.

primus.on('open',functionopen(){console.log('Connection is alive and kicking');});

primus.on('error')

Theerror event is emitted when something breaks that is out of our control.Unlike Node.js, we do not throw an error if noerror event listener isspecified. In general, when there is an active connection, it is not directlyclosed when anerror event is emitted. The cause of an error, in fact, couldbe that the parser failed to encode or decode a message. In this case we onlyemit the error, discard the message and keep the connection alive. Anerrorevent can also be emitted when a connection fails to establish. When thishappens the client automatically tries to reconnect, unless the connection getsclosed for some other reason. The only exception is when there is anauthorization hook. If we get an error when connecting to a server whereauthorization is required, we simply close the connection, as we can'tdeterminate if the error is the result of an unauthorized access or not.

primus.on('error',functionerror(err){console.error('Something horrible has happened',err.stack);});

primus.on('reconnect')

Thereconnect event is emitted when we're attempting to reconnect to theserver. This all happens transparently and it's just a way for you to know whenthese reconnects are actually happening.

primus.on('reconnect',function(opts){console.log('Reconnection attempt started');});

primus.on('reconnect scheduled')

Looks a lot like thereconnect event mentioned above, but it's emitted whenwe've detected that connection went/is down and we're going to start a reconnectoperation. This event would be ideal to update your application's UI when theconnection is down and you are trying to reconnect in x seconds.

primus.on('reconnect scheduled',function(opts){console.log('Reconnecting in %d ms',opts.scheduled);console.log('This is attempt %d out of %d',opts.attempt,opts.retries);});

primus.on('reconnected')

The client successfully reconnected with the server.

primus.on('reconnected',function(opts){console.log('It took %d ms to reconnect',opts.duration);});

primus.on('reconnect timeout')

Thereconnect timeout event is emitted when a reconnection attempt takes toomuch time. This can happen for example when the server does not answer a requestin a timely manner.

primus.on('reconnect timeout',function(err,opts){console.log('Timeout expired: %s',err.message);});

After this event a whole new reconnection procedure is automatically started, soyou don't have to worry about it.

primus.on('reconnect failed')

This event is emitted when the reconnection failed, for example when allattempts to reconnect have been unsuccessful.

primus.on('reconnect failed',function(err,opts){console.log('The reconnection failed: %s',err.message);});

primus.on('end')

Theend event is emitted when we've closed the connection. When this event isemitted you should consider your connection to be fully dead with no way ofreconnecting. But it's also emitted when the server closes the connection.

primus.on('end',function(){console.log('Connection closed');});

primus.end()

When you want to close the connection you can call theprimus.end() method.After this the connection should be considered dead and a new connection needsto be made usingPrimus.connect(url) orprimus = new Primus(url) if you wantto talk with the server again.

primus.end();

primus.destroy()

This method literally destroys theprimus instance. Internally it calls theprimus.end() method but it also frees some potentially heavy objects likethe underlying socket, the timers, the message transformers, etc. It alsoremoves all the event listeners but before doing that it emits a finaldestroyevent. Keep in mind that once this method is executed, you can no longer useprimus.open() on the sameprimus instance.

primus.on('destroy',function(){console.log('Feel the power of my lasers!');});primus.destroy();

primus.emits(event, parser)

This method is analogous to thespark.emits method.It returns a function that emits the given event every time it's called. Seeemits for detailed usage instructions.

primus.emits('event',functionparser(next,structure){next(undefined,structure.data);});

primus.id(callback)

There are cases where it is necessary to retrieve thespark.idfrom the client. To make this easier, we added aprimus.id() method thattakes a callback function to which the id will be passed.

primus.id(function(id){console.log(id);});

Connecting from the server

There are two ways of creating a server side client.

  1. When you've created yourprimus instance you can access theSocketproperty on it. ThisSocket is automatically configured to connect to thecorrect pathname, using the sametransformer andparser that you'vespecified when you created yourprimus instance.

    varprimus=newPrimus(server,{transformer:transformer,parser:parser}),Socket=primus.Socket;varclient=newSocket('http://localhost:8080');//// It has the same interface as the client, so you can just socket.write or// listen for the `open` events etc.//
  2. You might need to connect from a different node process where you don't haveaccess to yourprimus instance and the compatibleSocket instance. Forthese cases there a specialcreateSocket method where you can specify thetransformer,parser,plugin that you are using on your server to createanother compatible socket.

    varPrimus=require('primus')// Primus library from npm install primus,Socket=Primus.createSocket({transformer:transformer,parser:parser}),client=newSocket('http://localhost:8080');

When you are using plugins with Primus make sure you add thembefore youreference theprimus.Socket or it will compile a client without your plugins.If you're using thePrimus.createSocket api you can directly supply theplugins as part of the options as it supportsplugin object:

varSocket=Primus.createSocket({transformer:transformer,parser:parser,plugin:{'my-emitter':require('my-emitter'),'substream':require('substream')}});

The constructor returned byprimus.Socket orPrimus.createSocket has thesame signature of the constructor used to connect from the browser. Thismeans that you can use all the options mentioned in the previoussection:

varSocket=Primus.createSocket(),client=newSocket('http://localhost:8080',{ options});

If you do not know which transformer and parser are used on the server, weexpose a small JSON "spec" file that exposes this information. The specificationcan be reached on the/<pathname>/spec and will output the following JSONdocument:

{"version":"2.4.0","pathname":"/primus","parser":"json","transformer":"websockets"}

Authorization

Server

Primus has a built-in auth hook that allows you to leverage the basic authheader to validate the connection. To setup the optional auth hook, use thePrimus#authorize method:

varauthParser=require('basic-auth-parser');//// Add hook on server//primus.authorize(function(req,done){varauth;try{auth=authParser(req.headers['authorization'])}catch(ex){returndone(ex)}//// Do some async auth check//authCheck(auth,done);});primus.on('connection',function(spark){//// You only get here if you make it through the auth hook!//});

In this particular case, if an error is passed todone byauthCheck orthe exception handler then the connection attempt will never make it to theprimus.on('connection') handler.

The error you pass can either be a string or an object. If an object, it canhave the following properties which affect the response sent to the client:

  • statusCode: The HTTP status code returned to the client. Defaults to 401.
  • authenticate: If set andstatusCode is 401 then aWWW-Authenticateheader is added to the response, with a value equal to theauthenticateproperty's value.
  • message: The error message returned to the client. The response body will be{error: message}, JSON-encoded.

If the error you pass is a string then a 401 response is sent to the clientwith noWWW-Authenticate header and the string as the error message.

For example to send 500 when an exception is caught, 403 for forbidden usersand details of the basic auth scheme being used when authentication fails:

primus.authorize(function(req,done){varauth;if(req.headers.authorization){try{auth=authParser(req.headers.authorization)}catch(ex){ex.statusCode=500;returndone(ex);}if((auth.scheme==='myscheme')&&checkCredentials(auth.username,auth.password)){if(userAllowed(auth.username)){returndone();}else{returndone({statusCode:403,message:'Go away!'});}}}done({message:'Authentication required',authenticate:'Basic realm="myscheme"'});});

Please note that the auth hook is run each and every time a request is made tothe server.

Client

Unfortunately, the amount of detail you get in your client when authorizationfails depends on the transformer in use. Most real-time frameworks supportedby Primus don't expose the status code, headers or response body.

The WebSocket transformer's underlying transport socket will fire anunexpected-response event with the HTTP request and response:

primus.on('outgoing::open',function(){primus.socket.on('unexpected-response',function(req,res){console.error(res.statusCode);console.error(res.headers['www-authenticate']);//// It's up to us to close the request (although it will time out).//req.abort();//// It's also up to us to emit an error so primus can clean up.//primus.socket.emit('error','authorization failed: '+res.statusCode);});});

If you want to read the response body then you can do something like this:

primus.on('outgoing::open',function(){primus.socket.on('unexpected-response',function(req,res){console.error(res.statusCode);console.error(res.headers['www-authenticate']);vardata='';res.on('data',function(v){data+=v;});res.on('end',function(){//// Remember error message is in the 'error' property.//primus.socket.emit('error',newError(JSON.parse(data).error));});});});

Ifunexpected-response isn't caught (because the WebSocket transformer isn'tbeing used or you don't listen for it) then you'll get anerror event:

primus.on('error',functionerror(err){console.error('Something horrible has happened',err.stack);});

As noted above,err won't contain any details about the authorization failureso you won't be able to distinguish it from other errors.

Broadcasting

Broadcasting allows you to write a message to every connectedSpark on your server.There are 2 different ways of doing broadcasting in Primus. The easiest way is touse thePrimus#write method which will write a message to every connected user:

primus.write('message');

There are cases where you only want to broadcast a message to a smaller group ofusers. To make it easier to do this, we've added aPrimus#forEach method whichallows you to iterate over all active connections.

primus.forEach(function(spark,id,connections){if(spark.query.foo!=='bar')return;spark.write('message');});

The method can be also used asynchronously. To enable the asynchronous iterationyou have to callPrimus#forEach with two arguments. The first is the iteratorfunction that is called on every step. The iterator is called with a connectionfrom the list and a callback for when it has finished. The second argument isthe main callback and is called when the iteration has finished.

primus.forEach(function(spark,next){//// Do something and call next when done//next();},function(err){console.log('We are done');});

There are also cases where you want to select a singleSpark. To do this youcan use thePrimus#spark method.

// Get a spark by its idvarspark=primus.spark(id);spark.write('message');

This method returns aSpark orundefined if the given id doesn't match anyof the activeSpark ids on the server.

Destruction

In rare cases you might need to destroy the Primus instance you've created. Youcan use theprimus.destroy() orprimus.end() method for this. This methodaccepts an Object which allows you to configure the destruction process:

  • close Close the HTTP server that Primus received. Defaults totrue.
  • reconnect Automatically reconnect the clients. Defaults tofalse.
  • timeout Close all active connections and clean up the Primus instance afterthe specified amount of timeout. Defaults to0.

The timeout is especially useful if you want gracefully shutdown your server butreally don't want to wait an infinite amount of time.

primus.destroy({timeout:10000});

Events

Primus is built upon the Stream and EventEmitter interfaces. This is a summaryof the events emitted by Primus.

EventUsageLocationDescription
outgoing::reconnectprivateclientTransformer should reconnect.
reconnect scheduledpublicclientWe're scheduling a reconnect.
reconnectpublicclientReconnect attempt is about to be made.
reconnectedpublicclientSuccessfully reconnected.
reconnect timeoutpublicclientReconnect attempt took too much time.
reconnect failedpublicclientFailed to reconnect.
timeoutpublicclientFailed to connect to server.
outgoing::openprivateclient/sparkTransformer should connect.
incoming::openprivateclient/sparkTransformer has connected.
openpublicclientConnection is open.
destroypublicclientThe instance has been destroyed.
incoming::errorprivateclientTransformer received an error.
errorpublicclient/sparkAn error happened.
incoming::dataprivateclient/serverTransformer received data.
outgoing::dataprivateclient/sparkTransformer should write data.
datapublicclient/sparkWe received data.
incoming::endprivateclient/sparkTransformer closed the connection.
outgoing::endprivateclient/sparkTransformer should close connection.
endpublicclient/sparkThe connection has ended.
closepublicclient/serverThe connection has closed, we might reconnect. / The server has been destroyed.
connectionpublicserverWe received a new connection.
disconnectionpublicserverWe received a disconnection.
initialisedpublicserverThe server is initialised.
pluginpublicserverA new plugin has been added.
plugoutpublicserverA plugin has been removed.
incoming::pingprivateclientWe received a ping message.
outgoing::pingprivatesparkWe're sending a ping message.
incoming::pongprivatesparkWe received a pong message.
outgoing::pongprivateclientWe're sending a pong message.
heartbeatpublicsparkWe've received a response to a heartbeat.
onlinepublicclientWe've regained a network connection.
offlinepublicclientWe've lost our internet connection.
logpublicserverLog messages.
readyStateChangepublicclient/sparkThe readyState has changed.
outgoing::urlprivateclientThe options used to construct the URL.

As a rule of thumb assume that every event that is prefixed withincoming:: oroutgoing:: is reserved for internal use only and that emitting such events yourself will most likely result in chaos and destruction.

To make it easier for developers to emit events on Primus itself, we've added asmall helper function that checks if the event you want to emit is reserved forPrimus only. This would be allincoming:: andoutgoing:: prefixed events andthe events listed above. This method is called<class>.reserved() and it'simplemented on theSpark:

primus.on('connection',functionconnection(spark){spark.on('data',function(data){//// Just imagine that we receive an array of arguments from the client which// first argument is the name of the event that we need to emit and the// second argument are the arguments for function.//if(spark.reserved(data.args[0]))return;spark.emit.apply(spark,data.args[0]);});});

But also the client:

varprimus=newPrimus('http://example.bar');primus.on('data',function(data){if(primus.reserved(data.args[0]))return;primus.emit.apply(primus,data.args);});

And of course thePrimus instance as well.

Heartbeats and latency

Heartbeats are used in Primus to figure out if we still have an active, workingand reliable connection with the server. These heartbeats are sent from theserver to the client as shown in the following diagram.

     client will disconnect       if not recv within          `pingTimeout`     primus:pong:{timestamp}    +----------------------+    |                      |+---v----+            +---------+| server |            |  client |+--------+            +----^----+    |                      |    +----------------------+     primus:ping:{timestamp}      sent at `pingInterval`      server will disconnect      if no response since           last ping

The heartbeat message that we send over the connection isprimus::ping::<timestamp>. Upon receipt of this message, the client will sendback aprimus::pong::<timestamp> message with the same<timestamp> itreceived from the server.This allows to calculate the latency between messages by simply gettingthe<timestamp> and comparing it with the local time.

Supported Real-time Frameworks

The following transformers/transports are supported in Primus:

BrowserChannel

BrowserChannel was the original technology that GMail used for their real-timecommunication. It's designed for same domain communication and does not useWebSockets. To use BrowserChannel you need to install thebrowserchannelmodule:

npm install browserchannel --save

And tellPrimus that you want to usebrowserchannel as transformer:

varprimus=newPrimus(server,{transformer:'browserchannel'});

Thebrowserchannel transformer comes with built-in node client support and can beaccessed using:

varSocket=primus.Socket,socket=newSocket('url');

Please note that you should use at least version1.0.6 which contains supportfor query strings.

Engine.IO

Engine.IO is the low level transport functionality of Socket.IO 1.0. It supportsmultiple transports for creating a real-time connection. It uses transportupgrading instead of downgrading which makes it more resilient to blockingproxies and firewalls. To enableengine.io you need to install theengine.iomodule:

npm install engine.io --save

And tellPrimus that you want to useengine.io as transformer:

varprimus=newPrimus(server,{transformer:'engine.io'});

If you want to use the client interface inside of Node.js you also need toinstall theengine.io-client:

npm install engine.io-client --save

And then you can access it from your server instance:

varSocket=primus.Socket,socket=newSocket('url');

Faye

Faye is a WebSocket only transformer. It uses thefaye-websocket module whichis part of theFaye project and supports allprotocol specifications. To use this you need to install thefaye-websocketmodule:

npm install faye-websocket --save

And tellPrimus that you want to usefaye as transformer:

varprimus=newPrimus(server,{transformer:'faye'});

Thefaye transformer comes with built-in node client support and can beaccessed using:

varSocket=primus.Socket,socket=newSocket('url');

SockJS

SockJS is a real-time server that focuses on cross-domain connections and doesthis by using multiple transports. To use SockJS you need to install thesockjs module:

npm install sockjs --save

And tellPrimus that you want to usesockjs as transformer:

varprimus=newPrimus(server,{transformer:'sockjs'});

If you want to use the client interface inside of Node.js you also need toinstall thesockjs-client module:

npm install sockjs-client --save

And then you can access it from your server instance:

varSocket=primus.Socket,socket=newSocket('url');

uws

uws is a WebSocket only transformer. It uses theuws module which is probablythe fastest WebSocket server available in Node.js. To use uws you have toinstall theuws module:

npm install uws --save

And tellPrimus that you want to useuws as transformer:

varprimus=newPrimus(server,{transformer:'uws'});

If you want to use the client interface inside of Node.js you also need toinstall thews module:

npm install ws --save

And then you can access it from your server instance:

varSocket=primus.Socket,socket=newSocket('url');

WebSockets

If you are targeting a high end audience or maybe just need something forinternal uses you can use a pure WebSocket server. This transformer uses thepopularws module which is battle tested and supports all protocolspecifications. To use WebSockets you need to install thews module:

npm install ws --save

And tellPrimus that you want to useWebSockets as transformer:

varprimus=newPrimus(server,{transformer:'websockets'});

TheWebSockets transformer comes with built-in node client support and can beaccessed using:

varSocket=primus.Socket,socket=newSocket('url');

As you can see from the examples above, it doesn't matter how you write the nameof the transformer, we justtoLowerCase() everything.

Transformer inconsistencies

  • BrowserChannel does not give you access to theremotePort of the incomingconnection. So when you accessspark.address theport property will be setto1337 by default.
  • BrowserChannel is the only transformer that does not support cross domainconnections.
  • BrowserChannel and SockJS are written in CoffeeScript and this can makedebugging harder when their internals fail.
  • Engine.IO and SockJS do not ship their client-side library with their serverside component. We're bundling a snapshot of these libraries inside of Primus.We will always be targeting the latest version of these transformers when webundle the library.

Parsers

In addition to support different frameworks we've also made it possible to usecustom encoding and decoding libraries. We're usingJSON by default but youcould also usebinary orEJSON for example (but these parsers need to besupported by Primus, so check out the parser folder for examples). To specifythe parser to use you can supply aparser configuration option:

varprimus=newPrimus(server,{parser:'JSON'});

All parsers have anasync interface for error handling.

Middleware

Primus has two ways of extending the functionality. We havepluginsbut also support middleware. And there is an important difference between these.The middleware layers allows you to modify the incoming requestsbefore theyare passed in to the transformers. Plugins allow you to modify and interact withthe sparks. The middleware layer is only run for the requests that are handledby Primus.

We support 2 kind of middleware,async andsync middleware. The maindifference between these kinds is that sync middleware doesn't require acallback, it is completely optional. In Primus, we eat our own dog food. Variousof components in Primus are implemented through middleware layers:

  • cors: Adds the Access Control headers.
  • primus.js: It serves ourprimus.js client file.
  • spec: It outputs the server specification (version, transformer, path).
  • authorization: Our authorization handler, which is implemented as a middleware.
  • no-cache: Add no-cache headers to every HTTP request.
  • x-xss: AddX-XSS-Protection headers to every HTTP request.

Primus.use(name, fn, options, index)

Theprimus.use method is how you add middleware layers to your system. Allmiddleware layers need to be named. This allows you to also enable, disable andremove middleware layers. The supplied function can either be a pre-configuredfunction that is ready to answer request/response or an unconfiguredmiddleware. An unconfigured middleware is a function with less then 2 arguments.We execute this function automatically withPrimus as context of the functionand optionally, the options that got provided:

primus.use('name',function(){varprimus=this;returnfunction(req,res){res.end('foo');}},{foo:'bar'});

As you can see in the example above, we assume that you return the actualmiddleware layer. If you don't need any pre-configuration you can just supplythe function directly:

// sync middlewareprimus.use('name',function(req,res){});// async middlewareprimus.use('name',function(req,res,next){doStuff();});

You need to be aware that these middleware layers are running for HTTP requestsbut also for upgrade requests. Certain middleware layers should only run forHTTP or Upgrade requests. To make it possible you can add ahttp orupgradeproperty to the middleware function and set it tofalse if you don't want itto be triggered.

primus.use('name',function(){functionmiddleware(req,res,next){}middleware.upgrade=false;// Don't run this middleware for upgradesreturnmiddleware;});

By default a new middleware layer is added after the previous one, but thereare cases where you need to add a middleware at a specified index inthe stack. To accomplish this you can use the optional 0 basedindexargument.

// add a middleware after the first two in the stackprimus.use('name',function(req,res){},2);

Primus.remove(name)

This method allows you to remove configured middleware. This worksfor the middleware layers that you added but also the middleware layers that weadd by default. If you want to use a different way to serve theprimus.jsfile you can simply:

primus.remove('primus.js');

And add your own middleware instead.

Primus.disable(name)

In addition to removing middleware layers, it's also possible to disable them sothey are skipped when we iterate over the middleware layers. It might be usefulto just disable certain middleware layers in production.

primus.disable('name');

Primus.enable(name)

Of course, when you can disable middleware there also needs to be way to enablethem again. This is exactly what this method does. Re-enable a disabledmiddleware layer.

primus.enable('name');

Plugins

Primus was built as a low level interface where you can build your applicationsupon. At it's core, it's nothing more than something that passes messages backand forth between the client and server. To make it easier for developers toswitch to Primus we've developed a simple but effective plugin system thatallows you to extend Primus's functionality.

Plugins are added on the server side in the form of anObject:

//// Require a plugin directly.//primus.plugin('name',require('metroplex'));//// Or supply it manually with the required object structure//primus.plugin('name',{server:function(primus,options){},client:function(primus,options){},library:'client side library'});

Or you can pass the pluginObject directly into the constructor:

varprimus=newPrimus(server,{plugin:{name:{server:function(primus,options){},client:function(primus,options){},library:'client side library'}}})

And last but not least, you can also supply the constructor with a comma orspace separated list of plugin names which will be required automatically:

varprimus=newPrimus(server,{plugin:'metroplex, primus-emit'})

To remove added plugins you can use theplugout method:

primus.plugin('name',require('metroplex'));primus.plugout('name');// returns true/false indicating successful removal.

The server function is only executed on the server side and receives 2arguments:

  1. A reference to the initialised Primus server.
  2. The options that were passed in thenew Primus(server, { options })constructor. So the plugin can be configured through the same interface.

The client receives the same arguments:

  1. A reference to the initialised Primus client.
  2. The options that were passed in thenew Primus(url, { options })constructor. So the plugin can be configured through the same interface.

The only thing you need to remember is that the client is stored in the libraryusingtoString() so it cannot have any references outside the client'sclosure. But luckily, there's alibrary property that will also be included onthe client side when it's specified. Thelibrary property should be anabsolute path to the library file.

Intercepting theconnection events

Theconnection event is emitted using aasync emitter. It checks if yoursupplied event emitter function has extra callback function. When it detectsthis it will wait with the execution of the other assigned listeners until thecallback has been called. Please note that the order of assigning eventlisteners is still respected so if you've assigned aconnection listenerbefore an async connection listener it will still be executed first.

primus.on('connection',function(spark){console.log('first call, i have no spark.newproperty',spark.newproperty);});primus.on('connection',function(spark,next){longrunningasynmethod(spark.query,function(err,data){spark.newproperty=data;console.log('second call, i added the new property');next(err);});});primus.on('connection',function(spark){console.log('third call, i can read the ',spark.newproperty);});

When an error argument is supplied it will automatically end the connection andemit anerror event on the spark. If you are coming from Socket.IO 1.0 >=,this will basically work the same way as their middleware system.

Extending the Spark / Socket

The server has a.Spark property that can be extended. This allows you toeasily add new functionality to the socket. For example adding join roomfunction would be as easy as:

primus.plugin('rooms',{server:function(primus){varSpark=primus.Spark;Spark.prototype.join=function(){// implement room functionality.};}});

Transforming and intercepting messages

Intercepting and transforming messages is something that a lot of pluginsrequire. When you're building anEventEmitter plugin or something else youprobably don't want the defaultdata event to be emitted but your customevent. There are 2 different types of messages that can be transformed:

  1. incoming These messages are being received by the server.
  2. outgoing These messages are being sent to the client.

The transformer is available on both the client and the server and share, likeyou would have expected the same identical API. Adding a new transformer isrelatively straightforward:

primus.transform('incoming',function(packet){//// The packet.data contains the actual message that either received or// transformed.//// This would transform all incoming messages to foo;packet.data='foo';// If you are handling the message and want to prevent the `data` event from// happening, simply `return false` at the end of your function. No new// transformers will be called, and the event won't be emitted.});

These transformations can easily be done in the plugins:

primus.plugin('name',{server:function(primus){primus.transform('outgoing',function(packet){packet.data='foo';});primus.transform('incoming',function(packet){if(packet.data==='foo')packet.data='bar';});},client:function(primus){primus.transform('outgoing',function(packet){packet.data='foo';});primus.transform('incoming',function(packet){if(packet.data==='foo')packet.data='bar';});}});

We also expose asynchronous interfaces for these transformers. If your functionaccepts 2 arguments we automatically assume it's async and that the lastargument is the callback variable:

primus.transform('outgoing',function(packet,next){asyncprocess(packet.data,function(err,data){//// If you return an error here, it will be emitted as `error` on the// spark/client and no `data` event will be emitted.//if(err)returnnext(err);//// If you just wanted to ignore this message instead of emitting an error// you can do://if(err)returnnext(undefined,false);//// To update the data, just re-assign the `data` property on the packet you// received and call the next callback.//packet.data=data;next();});});

Primus project plugins

The following plugins are part of the Primus project.

fortess-maximus
Fortress Maximus validates every incoming message on your Primus server as all user input should be seen as a potential security risk.
NPM versionBuild Status
metroplex
Metroplex is a Redis based spark/connection registry for Primus. It stores the sparks and their server address. So you can cluster multiple primus's together with Metroplex and Omega Supreme
NPM versionBuild Status
mirage
Mirage generates and validates persistent session IDs.
NPM versionBuild Status
omega-supreme
Omega Supreme allows you to broadcast messages to Primus using a regular HTTP request. These messages can be broacasted to every spark, single spark or a collection of sparks.
NPM versionBuild Status
primus-analytics
Integrates Primus with Google Analytics.
NPM versionBuild Status
primus-emit
The emit module adds client -> server and server -> client event emitting to Primus.
NPM versionBuild Status
substream
Substream is an opinionated but stream compatible connection multiplexer on top of the Primus connections. These streams can be created without pre-defining them on the server or client.
NPM versionBuild Status

Community plugins

These are also plugins created by our amazing community. If you want your moduleto be listed here, please open a pull request.

backbone.primus
Bind primus.io events to backbone models and collections.
Build Status
hapi_primus_sessions
A hapi and primus plugin which extends primus' spark with a `getSession(cb)` method which returns the current hapi session object.
NPM version
primus-cluster
Scale Primus across multiple servers or with node cluster.
NPM versionBuild Status
primus-emitter
A module that adds emitter capabilities to Primus.
NPM versionBuild Status
primus-express-session
Share a user session between Express and Primus.
NPM versionBuild Status
primus-multiplex
A module that adds multiplexing capabilities to Primus.
NPM versionBuild Status
primus-redis
primus-redis is a Redis store for Primus. It takes care of distributing messages to other instances using Redis Pub/Sub.
NPM versionBuild Status
primus-redis-rooms
primus-redis-rooms is a Redis store for Primus and primus-rooms.
NPM versionBuild Status
primus-resource
Define resources with auto-bound methods that can be called remotely on top of Primus.
NPM versionBuild Status
primus-responder
Client and server plugin that adds a request/response cycle to Primus.
NPM versionBuild Status
primus-rooms
A module that adds rooms capabilities to Primus. It's based on the rooms implementation of Socket.IO.
NPM versionBuild Status
primus-rooms-redis-adapter
A redis adapter for primus-rooms module. Supports integration with metroplex and omega-supreme.
NPM versionBuild Status
primus-spark-latency
Adds a latency property to primus sparks server-side.
NPM versionBuild Status

Community

Deployed Primus to production or built an awesome demo using the technology?We've set up a specialwiki page for it where you can show your awesomecreations or learn from demo and example applications how to use Primus.Checkout the wiki page at:

https://github.com/primus/primus/wiki/Production

FAQ

What is the best way to scale Primus

Scaling Primus is as simple as sticking it behind a load balancer that supportssticky sessions and run multipleversions of your application. This is a vital feature that your load balancerneeds to support. This ensures that the incoming requests always go back to thesame server. If your load balancer does not support sticky sessions, get anotherone. I highly recommendHAProxy. According to my owntesting it is the fastest and best proxy available that supports WebSockets. Seehttps://github.com/observing/balancerbattle for more detailed information.

The reason for which sticky-sessions are so important is that a lot of frameworksthat use polling transports require to save a state in the node process in orderto work correctly. This state contains times, sessions ids, handshake data etc.If a request from the same client does not enter the same node process it willbe treated as anunknown request and your real-time connection will be closed.

If you want more advanced scaling and messaging please take a look at the variousplugins we've written for this scope. Plugins like metroplex, omega-supreme andprimacron can be time savers.

Can I use cluster?

Note: The following only applies to websocket emulation transformers likesockjs or engine.io. If you are usingws,uws orfaye-websocket, thereis no need for sticky sessions, and thus no issue.

Thecluster module that ships with Node.js does not implement sticky sessions.

There are projects likestick-session which attempt to implementsticky-sessions in cluster, but the problem with this specific approach is thatit uses theremoteAddress of the connection. For some people this isn't aproblem but when you add this behind a load balancer the remote address will beset to the address of the load balancer that forwards the requests. So all in allit only causes more scalability problems instead of solving them. This is whywe've opted to warn people about the risks ofcluster when we detect that thePrimus library is run in a worker environment.USE IT AT YOUR OWN RISK.

To turn off the cluster warning in your Primus instance you can set the optioniknowclusterwillbreakconnections totrue.

How do I use Primus with Express

Express'express() instance isn't a valid HTTP server. In order to make itwork withPrimus and other real-time transformers you need to feed the instanceto a realhttp server and supply this server. See example below:

'use strict';varexpress=require('express'),Primus=require('primus'),app=express();//// Do your express magic.//varserver=require('http').createServer(app),primus=newPrimus(server,{ options});server.listen(port);

Is require.js supported

Require.js is supported to a certain degree. Theprimus.js core file should becompatible with require.js but it could be that the transformer of your choosingisn't compatible with require.js. For exampleengine.io usescomponent whichintroduces it's ownrequire function that causes issues. In addition to that,there are plugins which might use these modules that break require.js. Thegeneral advice for this is to drop require.js in favour of plain script loadingor use of browserify where possible. If you feel strong about require.js we acceptpull requests that improve this behaviour or helps us save guard against theseissues.

Can I send custom headers to the server

It is not possible to send custom headers from the client to the server. This isbecause these headers need to be set by the actual transports that thetransformers are using. The only transport that would support this would be AJAXpolling. To send custom data to the server use a query string in your connectionURL, as this is something that all transports support.

varprimus=newPrimus('http://localhost:8080/?token=1&name=foo');

Versioning

History

You can discover the version history and change logs on theReleases page

Convention

All0.x.x releases should be considered unstable and not ready for production.The version number is laid out as:major.minor.patch and tries to followsemver as closely as possible but this is how we use our version numbering:

major

A major and possible breaking change has been made in the primus core. These changes are not backwards compatible with older versions.

minor

New features are added or a big change has happened with one of the real-time libraries that we're supporting.

patch

A bug has been fixed, without any major internal and breaking changes.

Release cycle

There isn't a steady or monthly release cycle. We usually release a new versionwhen:

  1. A critical bug is discovered.
  2. There have been a lot of minor changes.
  3. A framework did an incompatible update.
  4. A new framework is added.
  5. People ask for it.

Other languages

These projects are maintained by our valuable community and allow you to usePrimus in a different language than #"https://github.com/seegno/primus-objc">primus-objc

A client written in Objective-C for the Primus real-time framework with initial support for web sockets (via SocketRocket) and socket.io (via socket.IO-objc). Easily switch between different real-time Objective-C frameworks without any code changes.
Build Status
primus-android
A Primus client written in Java for Android with initial support for web sockets viaAndroidAsync.

Want to have your project listed here? Add it using a pull-request!

Protocol

Primus uses some internal protocol messages in order to keep the connection openand stable between a client and a server. If you are planning on implementingPrimus in another language you must handle the followingprimus::* prefixedmessages:

  • primus::ping::<ping>server -> client, The ping type contains the timein EPOCH. Ping messages are needed to keep the connection open as certain loadbalancers, proxies and browsers will close connections automatically whenthere is inactivity.
  • primus::pong::<ping>client -> server, The pong is the response to theping packet. It echoes back the exact value that it received.
  • primus::server::closeserver -> client, Indication that the serverintentionally closed the connection and that no reconnection/connection shouldbe made.
  • primus::id::client -> server, Request of the internalspark.idthat's assigned to the connection.
  • primus::id::<spark.id>server -> client, The internalid that we usedon the server to identify the connection as we do not sync this information bydefault and requires aprimus.id() call on the client.

Any other message that is prefixed withprimus:: should be ignored and notemitted to the user.

License

MIT

About

⚡ Primus, the creator god of the transformers & an abstraction layer for real-time to prevent module lock-in.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors60

Languages


[8]ページ先頭

©2009-2025 Movatter.jp