ELB(CLB) で WebSocket 通信

ALB では標準で WebSocket プロトコル機能に対応しているため、HTTP/HTTPS リスナーで受けて HTTP/1.1 から Upgrade で WebSocket 通信を開始することができます。 しかしながら、CLB, NLB では、ELB の機能としては対応しておりませんが、TCP リスナーを経由してバックエンドで WebSocket の機能に対応することで実現可能です。TCP レイヤーではスティッキーセッションのようなセッション維持機能を利用することができないので、nginx でプロキシし、その際、接続元の IP アドレスの情報は Proxy Protocol を利用する形で、WebSocket 通信を行います。 ALB では、Upgrade からそれに続く WebSocket 通信の間でスティッキーセッション機能を利用することもできますが、WebSocket が1つの TCP コネクションを利用するため、スティッキーセッション機能を利用することなく一つのバックエンドに割り振られます。 ただし、CLB ではアイドルタイムアウトもあり、60秒間アイドル状態が続くとコネクションが切断されてしまいます。そのため、サーバ側から定期的に Ping を定期的に行い、Pong の応答を得てコネクションを維持する方法があります。

設定

Node.js のインストール

$ sudo yum install -y gcc-c++ make openssl-devel
$ curl -sL https://rpm.nodesource.com/setup_8.x | sudo bash -
$ sudo yum install -y nodejs

インストールされていることの確認

$ node -v
v8.11.3

npm のインストール

$ curl -L http://npmjs.org/install.sh | sudo sh
$ vim ~/.npmrc
$ vim ~/.bashrc
$ source ~/.bashrc

.npmrc

root = ~/.npm/libraries
binroot = ~/.npm/bin
manroot = ~/.npm/man

.bashrc に以下を追記

export PATH=$HOME/.npm/bin:$PATH
export NODE_PATH=$HOME/.npm/libraries:$NODE_PATH
export MANPATH=$HOME/.npm/man:$MANPATH

インストールされていることを確認

$ npm -v
6.3.0

Socket.IO 等のインストール

$ npm install socket.io
$ npm install express

nginx のインストール

$ sudo rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
$ sudo yum install -y nginx
$ sudo service nginx start
$ sudo chkconfig nginx on

プログラムの作成

$ vim index.js
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

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

io.on('connection', function(socket){
  socket.broadcast.emit('hi');
  socket.on('chat message', function(msg){
    console.log('message: ' + msg);
    io.emit('chat message', msg);
  });
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});
$ vim index.html
<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <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-1.11.1.js"></script>
    <script>
        $(function () {
            var socket = io();
            $('form').submit(function(){
            socket.emit('chat message', $('#m').val());
            $('#m').val('');
             return false;
        });
      });
    </script>
  </body>
</html>
$ vim /etc/nginx/nginx.conf
http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

:
:

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 80 proxy_protocol;
        real_ip_header proxy_protocol;
        # listen       80 default_server;
        # listen       [::]:80 default_server;
        # server_name  localhost;
        # root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            proxy_http_version "1.1";
            proxy_pass http://localhost:3000/;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header X-Forwarded-For $proxy_protocol_addr;
            proxy_set_header X-Forwarded-Host $http_host;
        }

ELBのProxy Protocolの設定

$ aws elb create-load-balancer-policy \
  --load-balancer-name WebSocketCLB \
  --policy-name EnableProxyProtocol \
  --policy-type-name ProxyProtocolPolicyType \
  --policy-attributes \
    AttributeName=ProxyProtocol,AttributeValue=true \
  --region us-west-2

$ aws elb set-load-balancer-policies-for-backend-server \
  --load-balancer-name WebSocketCLB \
  --instance-port 80 \
  --policy-names EnableProxyProtocol \
  --region us-west-2


$ aws elb describe-load-balancers \
  --load-balancer-name WebSocketCLB \
  --region us-west-2

動作確認

ELB の FQDN に対してブラウザからアクセス http://.us-west-2.elb.amazonaws.com/ Socket.IOのアプリケーション起動

$ node index.js
listening on *:3000
message: abc
message: def

参考

  • https://qiita.com/you21979@github/items/4c9c382b9536effc590d
  • http://d.hatena.ne.jp/lettas0726/20110406/1302041546
  • http://d.hatena.ne.jp/lettas0726/20110406
  • https://socket.io/get-started/chat
  • https://qiita.com/yamamaijp/items/84da15b81ec5c16ffdd0
  • https://debiancdn.wordpress.com/2012/03/03/elb-loadbalancer-websocket/

My Twitter & RSS

Leave a Reply

Your email address will not be published. Required fields are marked *