WebSocket API

优质
小牛编辑
130浏览
2023-12-01

市面上其实非常多 Socket.io 的文章,所以我写在这里其实是笔记居多,不嫌弃的话可以继续看下去这样。

WebSocket API

这一项技术其实在 w3c 上面还是 Draft 的状态,所以,其实你会听到大部分的人会说,用 Flash 来作会比较稳定一点。而其实 Socket.io 官方 wiki 上面也有提到 FlashScoket.IO 的东西(笑

这个东西是 HTML5 的新的协定,简单的来说,就是可以让浏览器与后端伺服器之间,经由一个握手(handshake)的动作,来连接一条两者之间的高速公路。这幺一来,我们就可以浏览器与后端伺服器之间,快速的传递一些资料。

维基百科上的 WebSocket 介绍。<http://zh.wikipedia.org/wiki/WebSocket>

其中有提到了目前的方式,大多是以轮巡(Polling)的方式来达成,还有一种是 Comet(我实在不知道该怎幺用中文来描述他),而因为 Comet 会在后端伺服器上面佔用连线,且若是非 non-blocking 的伺服器,像是 Apache,很容易会让 IO 爆炸。所以,后来就出现了长轮巡(Long Polling)与 iframe 改良式的 Comet。

以上的作法大多都以 AJAX(XHR)来去实做,而WebSocket 就解决了许多的问题,而且他是可以双向沟通的!

沟通的问题

目前其实最流行的方式,还是以 Long Polling 为主,最重要的原因是没有浏览器相容性的问题。

BUT!

如果你的后端伺服器不支援的话,那他就只是一个单纯的 Polling 而已。为什幺?

(function polling() {
    $.ajax({
        url: "http://server",
        type: "post",
        dataType: "json",
        timeout: 30000,
        success: function(data) {
            /* Do something */
        },
        complete: function() {
            /* Polling here. */
            polling();
        }
    });
})();

上面我做了一件事情,就是等待 30 秒后重複发送一个 ajax 的请求到后端伺服器去。而 Long Polling 的作法是,

前端送了一个请求给后端 后端收到后,回传资料给前端,并断开连线 前端收到后,执行 callback,并再次发送一个请求给后端 以上的方式就是一个无穷迴圈,所以,如果后端收到后,没有断开连线,那幺前端就只会每 30 秒断线重连,这样跟一般的 Polling 其实并没有两样。那,为什幺非 non-blocking 的后端伺服器不行?如果我送一个 ajax 给 Apache,那他把事情做完之后,丢一个回应给前端,也会达成 complete 的条件不是吗?

是!

<?php

/* 我在 php 睡了 10 秒,再吐资料给刚刚呼叫我的 ajax */
sleep(10);
echo json_encode(array('status' => 'ok'));

但是,当你的后端伺服器没有放开连线时,你只能等待前端 timeout 的时间到了,并且再次发送一次请求时,才能继续动作。而,届时后端的资料到底做完了没呢?答案是:不知道,所以,使用 non-blocking 的后端伺服器多少能避开这些问题。

以上,都是单向的沟通,也是目前流行的方式。

这里有两篇 Comet 文章可以看一下:

  • Comet Programming: Using Ajax to Simulate Server Push<http://www.webreference.com/programming/javascript/rg28/index.html>
  • Comet Programming: the Hidden IFrame Technique<http://www.webreference.com/programming/javascript/rg30/index.html>

Socket.IO

他做了一件事情,就是把那些沟通的方式全部整合起来,无论前端还是后端,他都帮你打包好。所以,你只要会用就可以了,这样是不是很佛心呢!

$ npm install socket.io

他所支援的传输方式有下列几种,

  • xhr-polling
  • xhr-multipart
  • htmlfile
  • websocket
  • flashsocket
  • jsonp-polling

除了字面上有 socket 的之外,都是 Polling 与其变种方式,其中 xhr-multipart 也是,他只是把资料拆成好几个部份来传送而已。而其中 htmlfile 貌似是 IE 底下的东西,我在大神上面问资料的时候,看到了 ActiveXObject 这几个字,我就不想理他了。

简单的后端应用方式,我们可以这样写(以下是官方範例)

var io = require('socket.io').listen(8080);

io.sockets.on('connection', function (socket) {
    socket.emit('news', { hello: 'world' });
    socket.on('my other event', function (data) {
        console.log(data);
    });
});

而前端是这个样子,

<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect('http://localhost:8080');
    socket.on('news', function (data) {
        console.log(data);
        socket.emit('my other event', { my: 'data' });
    });
</script>

我们没有特别去指定 Socket.IO 要用什幺方式来作传递,所以他会自己决定,透过目前你的浏览器能使用什幺方式,来传递我们所需要的资料。这幺说,我们也可以指定传递方式,

var io = require('socket.io').listen(8080);

io.configure('development', function() {
    io.set('transports', [
            'xhr-polling'
            , 'jsonp-polling'
        ]);
});

io.sockets.on('connection', function (socket) {
    socket.emit('news', { hello: 'world' });
    socket.on('my other event', function (data) {
        console.log(data);
    });
});

以上述的例子来说,他就会使用 xhr-polling 与 jsonp-polling 两种方式的其中一种,来传递我们的资料。

更多详细设定,在官方的 wiki 当中有相当详细的说明,

  • Configuring Socket.IO<https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO>

至于 Socket.IO 在握手(handshake)的处理的部份,在官方 wiki 也有说明,

  • Authorization and handshaking<https://github.com/LearnBoost/socket.io/wiki/Authorizing>

为什幺要作上述的动作呢?顾名思义就是为了认证的一些流程而衍生出来的需求。我可以在这个过程中查询 Session 的相关资料,也可以检查 Cookie,IP Address 或是其他需要处理的资料等等。当然,处理 Cookie 与 Session 则最为常见。

小插曲

我们在使用 Socket.IO 的时候,当然不可能将 listen 给绑在 port 80 上面,那是给一般伺服器使用的嘛。所以,我们就有可能会像上述的例子一样,把他绑在 port 8080 或是之类的额外的连接埠上面。

问题来了,如果绑在其他的连接埠,那幺前端的呼叫的位址就得加上埠号,否则你的动作是会失效的。怎幺解决呢?网路上有一个很玄妙的解法,利用改写 Socket.IO 的 xhr-polling 对于 XHRPolling 与 XHRPolling 的处理方式,来让前端不需要加上埠号就能动作,

io.configure(function() {
    io.set("transports", ["xhr-polling"]);
    io.set("polling duration", 10);

    var path = require('path');
    var HTTPPolling = require(path.join(
        path.dirname(require.resolve('socket.io')),'lib', 'transports','http-polling')
    );
    var XHRPolling = require(path.join(
        path.dirname(require.resolve('socket.io')),'lib','transports','xhr-polling')
    );

    XHRPolling.prototype.doWrite = function(data) {
        HTTPPolling.prototype.doWrite.call(this);

        var headers = {
            'Content-Type': 'text/plain; charset=UTF-8',
            'Content-Length': (data && Buffer.byteLength(data)) || 0
        };

        if (this.req.headers.origin) {
            headers['Access-Control-Allow-Origin'] = '*';
            if (this.req.headers.cookie) {
                headers['Access-Control-Allow-Credentials'] = 'true';
            }
        }

        this.response.writeHead(200, headers);
        this.response.write(data);
        this.log.debug(this.name + ' writing', data);
    };
});

有兴趣的人,原文在此,请参阅:How to make Socket.IO work behind nginx (mostly)

另外补上 Nginx 的相关设定,其实并不複杂,就依照一般的 Proxy 去设定即可,

user www-data;
worker_processes 4;
worker_rlimit_nofile 1024;

pid /var/run/nginx.pid;


events {
    worker_connections 1024;
    multi_accept on;
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    server_names_hash_bucket_size 128;
    server_name_in_redirect on;
    client_header_buffer_size 32k;
    large_client_header_buffers 4 32k;
    client_max_body_size 8m;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_disable "MSIE [1-6].(?!.*SV1)";
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
    limit_req zone=one burst=100 nodelay;

    upstream nodejs {
        ip_hash;
        server localhost:3000;
    }

    server {
        listen   80;
        server_name jsdc;

        root /var/www/mynode;
        index index.html index.htm;

        location / {
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://nodejs/;
        proxy_redirect off;
        }
    }
}