素人Web屋の備忘録

素人Web屋の備忘録。たまに雑記。

Node.js+Express+Azureでチャットアプリをデプロイしてみる

socket.ioとAzureの使い方を勉強したくて簡単なチャットアプリを作りました。
とはいえ全く知識ないのでとりあえずsocket.ioのGet-startedを利用しました。

自分がわかりやすいように元々のソースを少し触っています。
長くなりそうなので2回に分けて説明しようと思います。今回は前準備編。

サーバ側の設定

index.js

// var app = require('express')(); 分かりづらかったので分解
const express = require('express');
const app = express();

// const http = require('http').Server(app); 分かりづらかったので分解
const http = require('http');
const server = http.Server(app);

// const io = require('socket.io')(server); 分かりづらかったので分解
const socketio = require('socket.io');
const io = socketio.listen(server);

const PORT = process.env.PORT || 3000;

app.get(`/`, (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

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

server.listen(PORT, () => {
  console.log(`listening on *:${PORT}`);
});

クライアント側の設定

index.html

<body>
  <ul id="messages"></ul>
  <form action="">
    <input id="m" autocomplete="off" /><button>Send</button>
  </form>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://code.jquery.com/jquery-3.1.1.slim.js"   integrity="sha256-5i/mQ300M779N2OVDrl16lbohwXNUdzL/R2aVUXyXWA=" crossorigin="anonymous"></script>
  <script>
    const socket = io();
    $('form').submit(() => {
      socket.emit('chat message', $('#m').val());
      $('#m').val('');
      return false;
    });
    socket.on('chat message', (msg) => {
        $('#messages').append($('<li>').text(msg));
    });
  </script>
</body>

上のソースの解説

 サーバ側

サーバ側でまずやらないといけないことは、Websocket機能を持つサーバオブジェクトの用意です。  

そのためにまずExpressモジュールを呼び出してExpressオブジェクトをappに格納します。

const express = require('express');
const app = express();

次にServerオブジェクトを作成して引数にapp(つまりExpressオブジェクト)を渡します。これ、ExpressはappをHTTPサーバに渡せる関数ハンドラに初期化するってことらしいんだけどピンと来ないです。。

const http = require('http');
const server = http.Server(app);

そしてsocket.ioモジュールを呼び出して、ソケットとサーバを紐づけます。

const socketio = require('socket.io');
const io = socketio.listen(server);

これでソケット機能を持ったサーバが用意できました。

クライアント側

クライアント側では、Websocketに接続することが必要になります。
そのための記述をまずは書いていきます。

まずsocket.io.jsとjqueryを読ませます。

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.js"   integrity="sha256-5i/mQ300M779N2OVDrl16lbohwXNUdzL/R2aVUXyXWA=" crossorigin="anonymous"></script>

次にscriptタグ内で、以下の記述でsocket.ioに接続します。公式にはTIP: io() with no args does auto-discoveryって書いてあったけどこれはargなしで自動でsocket.ioを検出するということで合ってるだろうか。。

<script>
  const socket = io();
</script>

ここまで書けたら、サーバ側、クライアント側ともに通信時の処理を書いていきます。
簡単にまとめちゃうとonとemitでやりとりします。
クライアントが最初にemitでイベント発行→それをサーバ側のonで受け取り→続いてサーバ側でemit→クライアント側のonで受け取るって感じです。
詳しくは次回書きます。

そういえば、Websocket機能付きサーバを用意する過程で、色々console.logに出してみました。

//console.log(express);

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}
//console.log(app);

function (req, res, next) {
  app.handle(req, res, next);
}
//console.log(http);

{ _connectionListener: [Function: connectionListener],
  METHODS:
   [ 'ACL',
     'BIND',
     'CHECKOUT',
     'CONNECT',
     'COPY',
     'DELETE',
     'GET',
     'HEAD',
     'LINK',
     'LOCK',
     'M-SEARCH',
     'MERGE',
     'MKACTIVITY',
     'MKCALENDAR',
     'MKCOL',
     'MOVE',
     'NOTIFY',
     'OPTIONS',
     'PATCH',
     'POST',
     'PROPFIND',
     'PROPPATCH',
     'PURGE',
     'PUT',
     'REBIND',
     'REPORT',
     'SEARCH',
     'SUBSCRIBE',
     'TRACE',
     'UNBIND',
     'UNLINK',
     'UNLOCK',
     'UNSUBSCRIBE' ],
  STATUS_CODES:
   { '100': 'Continue',
     '101': 'Switching Protocols',
     '102': 'Processing',
     '200': 'OK',
     '201': 'Created',
     '202': 'Accepted',
     '203': 'Non-Authoritative Information',
     '204': 'No Content',
     '205': 'Reset Content',
     '206': 'Partial Content',
     '207': 'Multi-Status',
     '208': 'Already Reported',
     '226': 'IM Used',
     '300': 'Multiple Choices',
     '301': 'Moved Permanently',
     '302': 'Found',
     '303': 'See Other',
     '304': 'Not Modified',
     '305': 'Use Proxy',
     '307': 'Temporary Redirect',
     '308': 'Permanent Redirect',
     '400': 'Bad Request',
     '401': 'Unauthorized',
     '402': 'Payment Required',
     '403': 'Forbidden',
     '404': 'Not Found',
     '405': 'Method Not Allowed',
     '406': 'Not Acceptable',
     '407': 'Proxy Authentication Required',
     '408': 'Request Timeout',
     '409': 'Conflict',
     '410': 'Gone',
     '411': 'Length Required',
     '412': 'Precondition Failed',
     '413': 'Payload Too Large',
     '414': 'URI Too Long',
     '415': 'Unsupported Media Type',
     '416': 'Range Not Satisfiable',
     '417': 'Expectation Failed',
     '418': 'I\'m a teapot',
     '421': 'Misdirected Request',
     '422': 'Unprocessable Entity',
     '423': 'Locked',
     '424': 'Failed Dependency',
     '425': 'Unordered Collection',
     '426': 'Upgrade Required',
     '428': 'Precondition Required',
     '429': 'Too Many Requests',
     '431': 'Request Header Fields Too Large',
     '451': 'Unavailable For Legal Reasons',
     '500': 'Internal Server Error',
     '501': 'Not Implemented',
     '502': 'Bad Gateway',
     '503': 'Service Unavailable',
     '504': 'Gateway Timeout',
     '505': 'HTTP Version Not Supported',
     '506': 'Variant Also Negotiates',
     '507': 'Insufficient Storage',
     '508': 'Loop Detected',
     '509': 'Bandwidth Limit Exceeded',
     '510': 'Not Extended',
     '511': 'Network Authentication Required' },
  Agent:
   { [Function: Agent]
     super_:
      { [Function: EventEmitter]
        EventEmitter: [Object],
        usingDomains: false,
        defaultMaxListeners: [Getter/Setter],
        init: [Function],
        listenerCount: [Function] },
     defaultMaxSockets: Infinity },
  ClientRequest: { [Function: ClientRequest] super_: { [Function: OutgoingMessag
e] super_: [Object] } },
  globalAgent:
   Agent {
     domain: null,
     _events: { free: [Function] },
     _eventsCount: 1,
     _maxListeners: undefined,
     defaultPort: 80,
     protocol: 'http:',
     options: { path: null },
     requests: {},
     sockets: {},
     freeSockets: {},
     keepAliveMsecs: 1000,
     keepAlive: false,
     maxSockets: Infinity,
     maxFreeSockets: 256 },
  IncomingMessage:
   { [Function: IncomingMessage]
     super_:
      { [Function: Readable]
        ReadableState: [Function: ReadableState],
        super_: [Object],
        _fromList: [Function: fromList] } },
  OutgoingMessage:
   { [Function: OutgoingMessage]
     super_:
      { [Function: Stream]
        super_: [Object],
        Readable: [Object],
        Writable: [Object],
        Duplex: [Object],
        Transform: [Object],
        PassThrough: [Object],
        Stream: [Object],
        _isUint8Array: [Function: isUint8Array],
        _uint8ArrayToBuffer: [Function: _uint8ArrayToBuffer] } },
  Server: { [Function: Server] super_: { [Function: Server] super_: [Object] } }
,
  ServerResponse: { [Function: ServerResponse] super_: { [Function: OutgoingMess
age] super_: [Object] } },
  createServer: [Function: createServer],
  get: [Function: get],
  request: [Function: request] 
}
//console.log(server);

Server {
  domain: null,
  _events:
   { connection: [Function: connectionListener],
     close: [Function: bound ],
     listening: [Function: bound ],
     upgrade: [Function],
     request: [Function] },
  _eventsCount: 5,
  _maxListeners: undefined,
  _connections: 1,
  _handle:
   TCP {
     bytesRead: 0,
     _externalStream: [External],
     fd: -1,
     reading: false,
     owner: [Circular],
     onread: null,
     onconnection: [Function: onconnection],
     writeQueueSize: 0 },
  _usingSlaves: false,
  _slaves: [],
  _unref: false,
  allowHalfOpen: true,
  pauseOnConnect: false,
  httpAllowHalfOpen: false,
  timeout: 120000,
  keepAliveTimeout: 5000,
  _pendingResponseData: 0,
  maxHeadersCount: null,
  _connectionKey: '6::::3000',
  [Symbol(asyncId)]: 4 
}
//console.log(io);
//console.log(server)の内容も含んだサーバオブジェクトです。

Server {
  nsps:
   { '/':
      Namespace {
        name: '/',
        server: [Object],
        sockets: [Object],
        connected: [Object],
        fns: [],
        ids: 0,
        rooms: [],
        flags: {},
        adapter: [Object],
        _events: [Object],
        _eventsCount: 1 } 
    },
  parentNsps: Map {},
  _path: '/socket.io',
  _serveClient: true,
  parser:
   { protocol: 4,
     types:
      [ 'CONNECT',
        'DISCONNECT',
        'EVENT',
        'ACK',
        'ERROR',
        'BINARY_EVENT',
        'BINARY_ACK' ],
     CONNECT: 0,
     DISCONNECT: 1,
     EVENT: 2,
     ACK: 3,
     ERROR: 4,
     BINARY_EVENT: 5,
     BINARY_ACK: 6,
     Encoder: [Function: Encoder],
     Decoder: [Function: Decoder] 
    },
  encoder: Encoder {},
  _adapter: [Function: Adapter],
  _origins: '*:*',
  sockets:
   Namespace {
     name: '/',
     server: [Circular],
     sockets: { de5NSm3ugNqo1achAAAA: [Object] },
     connected: { de5NSm3ugNqo1achAAAA: [Object] },
     fns: [],
     ids: 0,
     rooms: [],
     flags: {},
     adapter:
      Adapter {
        nsp: [Object],
        rooms: [Object],
        sids: [Object],
        encoder: Encoder {} },
     _events: { connection: [Function] },
     _eventsCount: 1 
    },
  eio:
   Server {
     clients: { de5NSm3ugNqo1achAAAA: [Object] },
     clientsCount: 1,
     wsEngine: 'ws',
     pingTimeout: 5000,
     pingInterval: 25000,
     upgradeTimeout: 10000,
     maxHttpBufferSize: 100000000,
     transports: [ 'polling', 'websocket' ],
     allowUpgrades: true,
     allowRequest: [Function: bound ],
     cookie: 'io',
     cookiePath: '/',
     cookieHttpOnly: true,
     perMessageDeflate: { threshold: 1024 },
     httpCompression: { threshold: 1024 },
     initialPacket: [ '0' ],
     ws:
      WebSocketServer {
        domain: null,
        _events: {},
        _eventsCount: 0,
        _maxListeners: undefined,
        options: [Object] },
     _events: { connection: [Function: bound ] },
     _eventsCount: 1 
    },
  httpServer:
   Server {
     domain: null,
     _events:
      { connection: [Function: connectionListener],
        close: [Function: bound ],
        listening: [Function: bound ],
        upgrade: [Function],
        request: [Function] },
     _eventsCount: 5,
     _maxListeners: undefined,
     _connections: 1,
     _handle:
      TCP {
        bytesRead: 0,
        _externalStream: [External],
        fd: -1,
        reading: false,
        owner: [Object],
        onread: null,
        onconnection: [Function: onconnection],
        writeQueueSize: 0 },
     _usingSlaves: false,
     _slaves: [],
     _unref: false,
     allowHalfOpen: true,
     pauseOnConnect: false,
     httpAllowHalfOpen: false,
     timeout: 120000,
     keepAliveTimeout: 5000,
     _pendingResponseData: 0,
     maxHeadersCount: null,
     _connectionKey: '6::::3000',
     [Symbol(asyncId)]: 4 
    },
  engine:
   Server {
     clients: { de5NSm3ugNqo1achAAAA: [Object] },
     clientsCount: 1,
     wsEngine: 'ws',
     pingTimeout: 5000,
     pingInterval: 25000,
     upgradeTimeout: 10000,
     maxHttpBufferSize: 100000000,
     transports: [ 'polling', 'websocket' ],
     allowUpgrades: true,
     allowRequest: [Function: bound ],
     cookie: 'io',
     cookiePath: '/',
     cookieHttpOnly: true,
     perMessageDeflate: { threshold: 1024 },
     httpCompression: { threshold: 1024 },
     initialPacket: [ '0' ],
     ws:
      WebSocketServer {
        domain: null,
        _events: {},
        _eventsCount: 0,
        _maxListeners: undefined,
        options: [Object] },
     _events: { connection: [Function: bound ] },
     _eventsCount: 1 
    } 
}