Создание многопользовательской веб-игры в жанре .io

Minimal working example

If you are new to the Node.js ecosystem, please take a look at the Get Started guide, which is ideal for beginners.


Else, let’s start right away! The server library can be installed from NPM:

$ npm install socket.io

More information about the installation can be found in the Server installation page.

Then, let’s create an file, with the following content:

const content = require('fs').readFileSync(__dirname + '/index.html', 'utf8');const httpServer = require('http').createServer((req, res) => {  res.setHeader('Content-Type', 'text/html');  res.setHeader('Content-Length', Buffer.byteLength(content));  res.end(content);});const io = require('socket.io')(httpServer);io.on('connect', socket => {  console.log('connect');});httpServer.listen(3000, () => {  console.log('go to http://localhost:3000');});

Here, a classic Node.js is started to serve the file, and the Socket.IO server is attached to it. Please see the Server initialization page for the various ways to create a server.

Let’s create the file next to it:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Minimal working example</title></head><body>    <ul id="events"></ul>    <script src="/socket.io/socket.io.js"></script>    <script>        const $events = document.getElementById('events');        const newItem = (content) => {          const item = document.createElement('li');          item.innerText = content;          return item;        };        const socket = io();        socket.on('connect', () => {          $events.appendChild(newItem('connect'));        });    </script></body></html>

Finally, let’s start our server:

$ node index.js

And voilà!

The object on both sides extends the EventEmitter class, so:

  • sending an event is done with:
  • receiving an event is done by registering a listener:

To send an event from the server to the client

Let’s update the file (server-side):

io.on('connect', socket => {  let counter = ;  setInterval(() => {    socket.emit('hello', ++counter);  }, 1000);});

And the file (client-side):

const socket = io();socket.on('connect', () => {  $events.appendChild(newItem('connect'));});socket.on('hello', (counter) => {  $events.appendChild(newItem(`hello - ${counter}`));});

Demo:

To send a message from the client to the server

Let’s update the file (server-side):

io.on('connect', socket => {  socket.on('hey', data => {    console.log('hey', data);  });});

And the file (client-side):

const socket = io();socket.on('connect', () => {  $events.appendChild(newItem('connect'));});let counter = ;setInterval(() => {  ++counter;  socket.emit('hey', { counter }); }, 1000);

Demo:

Now, let’s detail the features provided by Socket.IO.

Importing an SioClient

Lets copy the SioClient into the QT project under the subfolder

Edit to configure paths and compile options by simply adding:

SOURCES += ./sioclient/src/sio_client.cpp           ./sioclient/src/sio_packet.cppHEADERS  += ./sioclient/src/sio_client.h            ./sioclient/src/sio_message.hINCLUDEPATH += $$PWD/sioclient/lib/rapidjson/includeINCLUDEPATH += $$PWD/sioclient/lib/websocketpp

Add two additional compile options:

CONFIG+=no_keywordsCONFIG+=c++11

The flag prevents from treating some function names as as the keyword for the signal-slot mechanism.


Use to ask for C++11 support.

socket.join(room[, callback])

  • (String)
  • (Function)
  • Returns for chaining

Adds the client to the , and fires optionally a callback with signature (if any).

io.on('connection', (socket) => {  socket.join('room 237', () => {    let rooms = Object.keys(socket.rooms);    console.log(rooms);     io.to('room 237').emit('a new user has joined the room');   });});

The mechanics of joining rooms are handled by the that has been configured (see above), defaulting to socket.io-adapter.

For your convenience, each socket automatically joins a room identified by its id (see ). This makes it easy to broadcast messages to other sockets:

io.on('connection', (socket) => {  socket.on('say to someone', (id, msg) => {    socket.to(id).emit('my message', msg);  });});

NginX configuration

Within the section of your file, you can declare a section with a list of Socket.IO process you want to balance load between:

http {  server {    listen 3000;    server_name io.yourhost.com;    location / {      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;      proxy_set_header Host $host;      proxy_pass http://nodes;      proxy_http_version 1.1;      proxy_set_header Upgrade $http_upgrade;      proxy_set_header Connection "upgrade";    }  }  upstream nodes {    ip_hash;    server app01:3000;    server app02:3000;    server app03:3000;  }}

Notice the instruction that indicates the connections will be sticky.

Make sure you also configure in the topmost level to indicate how many workers NginX should use. You might also want to look into tweaking the setting within the block.

How to use

A standalone build of is exposed automatically by the socket.io server as . Alternatively you can serve the file found at the root of this repository.

<scriptsrc="/socket.io/socket.io.js"><script><script>var socket =io('http://localhost');socket.on('connect',function(){});socket.on('event',function(data){});socket.on('disconnect',function(){});</script>

Add to your and then:

var socket =require('socket.io-client')('http://localhost');socket.on('connect',function(){});socket.on('event',function(data){});socket.on('disconnect',function(){});

Overview

In this tutorial well look at creating a small iOS app that demonstrates socket.io and iOS. If you learn better from looking at code you can look at it here. The point of the tutorial is not to explain developing an iOS app, but to demonstrate how you can incorporate into your projects! So it is assumed you have a basic knowledge of XCode.Note: This example uses Swift 1.2. However, 1.2 isnt much different from Swift 1.1, and the library has branches for Swift 1.1 and 1.2. The only difference in this guide is I use 1.2s expanded construct to avoid nesting.Note 2: While this library is written in, and meant for, Swift applications, it can be used with Objective-C projects, but will require some extra work (youll probably need to create a Swift class that can interface with your Objective-C code, as not all methods in the client will be available to Objective-C i.e emit, onAny). See for more information.

Handling Socket.IO Events

We’ll need to handle socket.io events in our functions they are bound to.

For example, we need to show received messages in the list view.

void MainWindow::OnNewMessage(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp){    if(data->get_flag() == message::flag_object)    {        std::string msg = data->get_map()->get_string();        std::string name = data->get_map()->get_string();        QString label = QString::fromUtf8(name.data(),name.length());        label.append(':');        label.append(QString::fromUtf8(msg.data(),msg.length()));        QListWidgetItem *item= new QListWidgetItem(label);        Q_EMIT RequestAddListItem(item);    }}

Broadcasting

The next goal is for us to emit the event from the server to the rest of the users.

In order to send an event to everyone, Socket.IO gives us the method.

io.emit('some event', { someProperty: 'some value', otherProperty: 'other value' }); 

If you want to send a message to everyone except for a certain emitting socket, we have the flag for emitting from that socket:

io.on('connection', (socket) => {  socket.broadcast.emit('hi');});

In this case, for the sake of simplicity we’ll send the message to everyone, including the sender.

io.on('connection', (socket) => {  socket.on('chat message', (msg) => {    io.emit('chat message', msg);  });});

And on the client side when we capture a event we’ll include it in the page. The total client-side JavaScript code now amounts to:

<script>  $(function () {    var socket = io();    $('form').submit(function(e){      e.preventDefault();       socket.emit('chat message', $('#m').val());      $('#m').val('');      return false;    });    socket.on('chat message', function(msg){      $('#messages').append($('<li>').text(msg));    });  });</script>

And that completes our chat application, in about 20 lines of code! This is what it looks like:

Setting up the Socket

For single-window applications, simply let class hold the object by declaring a member of the and several event handling functions in .

private:    void OnNewMessage(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnUserJoined(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnUserLeft(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnTyping(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnStopTyping(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnLogin(std::string const& name,message::ptr const& data,bool hasAck,message::ptr &ack_resp);    void OnConnected();    void OnClosed(client::close_reason const& reason);    void OnFailed();    std::unique_ptr<client> _io;

Initialize and setup event bindings for the default in the constructor.

We also need to handle connectivity and disconnect events.


Add the following to the constructor:

MainWindow::MainWindow(QWidget *parent) :    QMainWindow(parent),    ui(new Ui::MainWindow),    _io(new client()){    ui->setupUi(this);    using std::placeholders::_1;    using std::placeholders::_2;    using std::placeholders::_3;    using std::placeholders::_4;    socket::ptr sock = _io->socket();    sock->on("new message",std::bind(&MainWindow::OnNewMessage,this,_1,_2,_3,_4));    sock->on("user joined",std::bind(&MainWindow::OnUserJoined,this,_1,_2,_3,_4));    sock->on("user left",std::bind(&MainWindow::OnUserLeft,this,_1,_2,_3,_4));    sock->on("typing",std::bind(&MainWindow::OnTyping,this,_1,_2,_3,_4));    sock->on("stop typing",std::bind(&MainWindow::OnStopTyping,this,_1,_2,_3,_4));    sock->on("login",std::bind(&MainWindow::OnLogin,this,_1,_2,_3,_4));    _io->set_socket_open_listener(std::bind(&MainWindow::OnConnected,this,std::placeholders::_1));    _io->set_close_listener(std::bind(&MainWindow::OnClosed,this,_1));    _io->set_fail_listener(std::bind(&MainWindow::OnFailed,this));    connect(this,SIGNAL(RequestAddListItem(QListWidgetItem*)),this,SLOT(AddListItem(QListWidgetItem*)));}

new Manager(url[, options])

  • (String)
  • (Object)
  • Returns

Available options:

Option Default value Description
name of the path that is captured on the server side
whether to reconnect automatically
number of reconnection attempts before giving up
how long to initially wait before attempting a new reconnection. Affected by +/- , for example the default initial delay will be between 500 to 1500ms.
maximum amount of time to wait between reconnections. Each attempt increases the reconnection delay by 2x along with a randomization factor.
0 <= randomizationFactor <= 1
connection timeout before a and events are emitted
by setting this false, you have to call whenever you decide it’s appropriate
additional query parameters that are sent when connecting a namespace (then found in object on the server-side)
the parser to use. Defaults to an instance of the that ships with socket.io. See socket.io-parser.

Available options for the underlying Engine.IO client:

Option Default value Description
whether the client should try to upgrade the transport from long-polling to something better.
forces JSONP for polling transport.
determines whether to use JSONP when necessary for polling. If disabled (by settings to false) an error will be emitted (saying “No transports available”) if no other transports are available. If another transport is available for opening a connection (e.g. WebSocket) that transport will be used instead.
forces base 64 encoding for polling transport even when XHR2 responseType is available and WebSocket even if the used standard supports binary.
enables XDomainRequest for IE8 to avoid loading bar flashing with click sound. default to because XDomainRequest has a flaw of not sending cookie.
whether to add the timestamp with each transport request. Note: polling requests are always stamped unless this option is explicitly set to
the timestamp parameter
port the policy server listens on
a list of transports to try (in order). always attempts to connect directly with the first one, provided the feature detection test for it passes.
hash of options, indexed by transport name, overriding the common options for the given transport
If true and if the previous websocket connection to the server succeeded, the connection attempt will bypass the normal upgrade process and will initially try websocket. A connection attempt following a transport error will use the normal upgrade process. It is recommended you turn this on only when using SSL/TLS connections, or if you know that your network does not block websockets.
whether transport upgrades should be restricted to transports supporting binary data
timeout for xhr-polling requests in milliseconds () (only for polling transport)
a list of subprotocols (see ) (only for websocket transport)

Node.js-only options for the underlying Engine.IO client:

Option Default value Description
the to use
Certificate, Private key and CA certificates to use for SSL.
Private key to use for SSL.
A string of passphrase for the private key or pfx.
Public x509 certificate to use.
An authority certificate or array of authority certificates to check the remote host against.
A string describing the ciphers to use or exclude. Consult the for details on the format.
If true, the server certificate is verified against the list of supplied CAs. An ‘error’ event is emitted if verification fails. Verification happens at the connection level, before the HTTP request is sent.
parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to to disable.
Headers that will be passed for each request to the server (via xhr-polling and via websockets). These values then can be used during handshake or for special proxies.
Uses NodeJS implementation for websockets — even if there is a native Browser-Websocket available, which is preferred by default over the NodeJS implementation. (This is useful when using hybrid platforms like nw.js or electron)
the local IP address to connect to

How to use

A standalone build of is exposed automatically by the socket.io server as . Alternatively you can serve the file found at the root of this repository.

<scriptsrc="/socket.io/socket.io.js"><script><script>var socket =io('http://localhost');socket.on('connect',function(){});socket.on('event',function(data){});socket.on('disconnect',function(){});</script>

Add to your and then:

var socket =require('socket.io-client')('http://localhost');socket.on('connect',function(){});socket.on('event',function(data){});socket.on('disconnect',function(){});

Listening on events

Like I mentioned earlier, Socket.IO is bidirectional, which means we can send events to the server, but also at any time during the communication the server can send events to us.

We then can make the socket listen an event on lifecycle callback.

@Overridepublic void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    mSocket.on("new message", onNewMessage);    mSocket.connect();}

With this we listen on the event to receive messages from other users.

import com.github.nkzawa.emitter.Emitter;private Emitter.Listener onNewMessage = new Emitter.Listener() {    @Override    public void call(final Object... args) {        getActivity().runOnUiThread(new Runnable() {            @Override            public void run() {                JSONObject data = (JSONObject) args[];                String username;                String message;                try {                    username = data.getString("username");                    message = data.getString("message");                } catch (JSONException e) {                    return;                }                addMessage(username, message);            }        });    }};

This is what looks like. A listener is an instance of and must be implemented the method. Youll notice that inside of is wrapped by , that is because the callback is always called on another thread from Android UI thread, thus we have to make sure that adding a message to view happens on the UI thread.

Adding UI Refresh Signals/Slots

The callbacks are not in the UI thread. However, the UI must be updated with those callbacks, so we need a signal for the non-UI thread to request the functions in the UI thread. To signal that has been added, insert the following:

Q_SIGNALS:    void RequestAddListItem(QListWidgetItem *item);private Q_SLOTS:    void AddListItem(QListWidgetItem *item);
void MainWindow::AddListItem(QListWidgetItem* item){    this->findChild<QListWidget*>("listView")->addItem(item);}

Then connect them in the constructor.

connect(this,SIGNAL(RequestAddListItem(QListWidgetItem*)),this,SLOT(AddListItem(QListWidgetItem*)));

Connection

const client = io('https://myhost.com');

The following steps take place:

  • on the client-side, a new instance is created

  • the instance tries to establish a transport

GET https://myhost.com/socket.io/?EIO=3&transport=polling&t=ML4jUwU&b64=1with:  "EIO=3"                 "transport=polling"     "t=ML4jUwU&b64=1"     

the engine.io server responds with:

{  "type": "open",  "data": {    "sid": "36Yib8-rSutGQYLfAAAD",      "upgrades": ,          "pingInterval": 25000,              "pingTimeout": 5000               }}

the content is encoded by the engine.io-parser as:

'96:0{"sid":"hLOEJXN07AE0GQCNAAAB","upgrades":,"pingInterval":25000,"pingTimeout":5000}2:40'with:  "96"    ":"     "0"     '{"sid":"hLOEJXN07AE0GQCNAAAB","upgrades":,"pingInterval":25000,"pingTimeout":5000}'   "2"     ":"     "4"     "0"   
  • the content is then decoded by the on the client-side

  • an event is emitted at the level

  • a event is emitted at the level

new Server(httpServer[, options])

  • (http.Server) the server to bind to.
  • (Object)

Works with and without :

const io = require('socket.io')();const Server = require('socket.io');const io = new Server();

Available options:

Option Default value Description
name of the path to capture
whether to serve the client files
the adapter to use. Defaults to an instance of the that ships with socket.io which is memory based. See socket.io-adapter
the allowed origins
the parser to use. Defaults to an instance of the that ships with socket.io. See socket.io-parser.

Available options for the underlying Engine.IO server:

Option Default value Description
how many ms without a pong packet to consider the connection closed
how many ms before sending a new ping packet
how many ms before an uncompleted transport upgrade is cancelled
how many bytes or characters a message can be, before closing the session (to avoid DoS).
A function that receives a given handshake or upgrade request as its first parameter, and can decide whether to continue or not. The second argument is a function that needs to be called with the decided information: , where is a boolean value where false means that the request is rejected, and err is an error code.
transports to allow connections to
whether to allow transport upgrades
parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to to disable.
parameters of the http compression for the polling transports (see api docs). Set to to disable.
name of the HTTP cookie that contains the client sid to send as part of handshake response headers. Set to to not send one.
path of the above option. If false, no path will be sent, which means browsers will only send the cookie on the engine.io attached path (). Set false to not save io cookie on all requests.
if HttpOnly io cookie cannot be accessed by client-side APIs, such as JavaScript. This option has no effect if or is set to .
what WebSocket server implementation to use. Specified module must conform to the interface (see ws module api docs). Default value is . An alternative c++ addon is also available by installing module.

Among those options:

  • The and parameters will impact the delay before a client knows the server is not available anymore. For example, if the underlying TCP connection is not closed properly due to a network issue, a client may have to wait up to ms before getting a event.

  • The order of the array is important. By default, a long-polling connection is established first, and then upgraded to WebSocket if possible. Using means there will be no fallback if a WebSocket connection cannot be opened.

const server = require('http').createServer();const io = require('socket.io')(server, {  path: '/test',  serveClient: false,  pingInterval: 10000,  pingTimeout: 5000,  cookie: false});server.listen(3000);

Using socket in Activity and Fragment

First, we have to initialize a new instance of Socket.IO as follows:

import com.github.nkzawa.socketio.client.IO;import com.github.nkzawa.socketio.client.Socket;private Socket mSocket;{    try {        mSocket = IO.socket("http://chat.socket.io");    } catch (URISyntaxException e) {}}@Overridepublic void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    mSocket.connect();}

returns a socket for with the default options. Notice that the method caches the result, so you can always get a same instance for an url from any Activity or Fragment.And we explicitly call to establish the connection here (unlike the JavaScript client). In this app, we use lifecycle callback for that, but it actually depends on your application.

Sending messages from the outside-world

In some cases, you might want to emit events to sockets in Socket.IO namespaces / rooms from outside the context of your Socket.IO processes.

There are several ways to tackle this problem, like implementing your own channel to send messages into the process.

To facilitate this use case, we created two modules:

  • socket.io-redis
  • socket.io-emitter

By implementing the Redis :

const io = require('socket.io')(3000);const redis = require('socket.io-redis');io.adapter(redis({ host: 'localhost', port: 6379 }));

you can then messages from any other process to any channel

const io = require('socket.io-emitter')({ host: '127.0.0.1', port: 6379 });setInterval(function(){  io.emit('time', new Date);}, 5000);

Upgrade

Once all the buffers of the existing transport (XHR polling) are flushed, an upgrade gets tested on the side by sending a probe.

GET wss://myhost.com/socket.io/?EIO=3&transport=websocket&sid=36Yib8-rSutGQYLfAAADwith:  "EIO=3"                       "transport=websocket"         "sid=36Yib8-rSutGQYLfAAAD"  
  • a “ping” packet is sent by the client in a WebSocket frame, encoded as by the , with being the “ping” message type.

  • the server responds with a “pong” packet, encoded as , with being the “pong” message type.

  • upon receiving the “pong” packet, the upgrade is considered complete and all following messages go through the new transport.

Emitting events

The main idea behind Socket.IO is that you can send and receive any events you want, with any data you want. Any objects that can be encoded as JSON will do, and is supported too.

Let’s make it so that when the user types in a message, the server gets it as a event. The section in should now look as follows:

<script src="/socket.io/socket.io.js"></script><script src="https://code.jquery.com/jquery-3.4.1.min.js"></script><script>  $(function () {    var socket = io();    $('form').submit(function(e) {      e.preventDefault();       socket.emit('chat message', $('#m').val());      $('#m').val('');      return false;    });  });</script>

And in we print out the event:

io.on('connection', (socket) => {  socket.on('chat message', (msg) => {    console.log('message: ' + msg);  });});

The result should be like the following video:


С этим читают