1. 介绍

    A pure stream http push technology for your Nginx setup.
    Comet made easy and really scalable.
    Supports EventSource, WebSocket, Long Polling, and Forever Iframe.
    nginx-push-stream-module是nginx的一个模块,用它可以轻易实现websocket服务器。
    以前我们实现websocket的服务器,不外乎两种方式,第一种是嵌入到web进程中,成为web进程的一部分,只是以路由的方式提供服务,还有一种是单独的进程,比如用puma来启动包含actioncable的rails项目。
    这两种方式多多少少跟web进程都有些关系,嵌入型的就不用多说,就算是单独的进程这种方式,也是用了另一种服务器去启动。
    现在我们来考虑另外一种方式,就是用完全独立于web进程的服务器,比如,之前我们的web是用ruby写的,可能会用puma或unicorn来启动,现在我们可以用c++启动websocket服务器,而web进程是通过http请求等方式来连接websocket服务器。
    当然,这一篇文章,我们是用nginx来启动一个websocket的服务器,nginx本身没有这样的功能,需要其中的一个模块,就是本章介绍的nginx-push-stream-module。
    现在我们先来跑一下流程,再来讲述一下它的原理以及为什么能够这样做。

    1. 使用

    首先得先安装一下这个模块。
    2.1 安装

    安装很简单,跟之前的模块安装一模一样的步骤,具体可以查看这篇文章nginx之编译第三方模块(六)。
    现在来列出一下大概的流程。

    1. $ git clone https://github.com/wandenberg/nginx-push-stream-module.git
    2. # 进入到nginx源码目录,--add-module后面接nginx-push-stream-module的源码目录
    3. $ ./configure --add-module=../nginx-push-stream-module
    4. # 编译
    5. $ make
    6. # 安装
    7. $ sudo make install
    8. # 结束老的nginx进程
    9. $ sudo nginx -s quit
    10. # 开启新的nginx进程
    11. $ sudo nginx

    接着我们来使用这个模块。
    2.2 配置

    在配置文件nginx.conf中添加下面这样的内容:

    1. http {
    2. push_stream_shared_memory_size 32M;
    3. server {
    4. location /channels-stats {
    5. # activate channels statistics mode for this location
    6. push_stream_channels_statistics;
    7. # query string based channel id
    8. push_stream_channels_path $arg_id;
    9. }
    10. location /pub {
    11. # activate publisher (admin) mode for this location
    12. push_stream_publisher admin;
    13. # query string based channel id
    14. push_stream_channels_path $arg_id;
    15. }
    16. location ~ /ws/(.*) {
    17. # activate websocket mode for this location
    18. push_stream_subscriber websocket;
    19. # positional channel path
    20. push_stream_channels_path $1;
    21. # message template
    22. push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
    23. push_stream_websocket_allow_publish on;
    24. # ping frequency
    25. push_stream_ping_message_interval 10s;
    26. }
    27. }
    28. }

    2.3 原理

    其中push_stream_shared_memory_size是添加在http下,其他的都在server下。
    push_stream_shared_memory_size我就不详说了,应该设置的是一个内存的容量,我对此细节并不了解,按照默认的就好了。
    我们来讲述一下server之下的三个location。

    1. /channels-stats
    2. /pub
    3. ~ /ws/(.*)

    第一个是关于websocket统计相关的东西,这个稍后再讲。
    另外两个是关于发布订阅的。
    其中客户端连接到服务器使用的是~ /ws/(.*)这个location,而服务器端向客户端推送消息是使用/pub这个location。
    至于客户端与服务器端是如何交互的,我们可以回顾一下。
    客户端,比如浏览器,发送new WebSocket给websocket服务器,表示要建立websocket请求,这个过程表示的是订阅,一直在等待服务器发送消息过来,一旦有消息过来,就会更改一些状态,比如DOM更新等。这个过程,不止只有一个客户端连接上服务器,可能有好多客户端同时连接。假如现在有了业务变化,服务器需要向所有的客户端推送消息,这个过程就是发布,广播消息。通过什么广播消息呢,这个机制可以自己实现,也可以用redis的pub/sub功能,比如,一旦客户端连接上服务器,就会订阅redis的一个channel,而发布的时候,就是往这个channel里推送消息,这样,所有的客户端都能接收到消息。
    nginx-push-stream-module不需要redis的pub/sub,它是自己实现的。
    2.4 测试

    现在我们开始来测试一下这个应用。
    还记得之前提到的/channels-stats这个location吗?它是统计信息用的。
    我们先来看下它的结果。

    1. $ curl -s -v 'http://localhost/channels-stats'

    输出的内容主要是下面的json信息:

    1. {"hostname": "macintoshdemacbook-air.local", "time": "2016-05-07T12:02:34", "channels": 0, "wildcard_channels": 0, "published_messages": 0, "stored_messages": 0, "messages_in_trash": 0, "channels_in_trash": 0, "subscribers": 0, "uptime": 19755, "by_worker": [
    2. {"pid": "21117", "subscribers": 0, "uptime": 19755}
    3. ]}

    上面的信息包括主机名,时间,通道的个数,消息的个数等,我们先不管。
    现在我们用浏览器建立一个连接到websocket服务器,也就是要请求~ /ws/(.*)这个location。

    1. ws = new WebSocket("ws://localhost/ws/ch1");
    2. ws.onmessage = function(evt){console.log(evt.data);};

    很简单,使用new WebSocket建立一个websocket请求,地址为ws://localhost/ws/ch1。
    ch1是通道的名称,push_stream_channels_path $1;这一行配置指的就是它。
    onmessage表示接收服务器端的消息,一旦有消息过来,就用console.log输出来。
    我们一直在关注着浏览器的输出。
    现在我们给客户端推送一条消息,自然是使用/pub这个location。

    1. $ curl http://localhost/pub\?id\=ch1 -d "Some Text"
    2. {"channel": "ch1", "published_messages": 1, "stored_messages": 0, "subscribers": 1}

    使用的是curl这个命令,ch1表示的是通道名,它可以以参数的形式来指定,这样就会灵活很多,不同类型的连接可以用不同的通道名。
    果然浏览器输出了信息了:

    1. {"id":1,"channel":"ch1","text":"Some Text"}

    id是消息的编号,默认从1开始,这个数字会自增,channel表示通道名,text是服务器端发送的信息。
    输出的内容,跟push_stream_message_template “{\”id\”:~id~,\”channel\”:\”~channel~\”,\”text\”:\”~text~\”}”;这里定义的模版有关。
    果然是推送了什么内容就是输出了什么内容。
    现在我们来看看统计内容的输出:

    1. $ curl -s -v 'http://localhost/channels-stats'
    2. {"hostname": "macintoshdemacbook-air.local", "time": "2016-05-07T12:24:13", "channels": 1, "wildcard_channels": 0, "published_messages": 1, "stored_messages": 0, "messages_in_trash": 0, "channels_in_trash": 0, "subscribers": 1, "uptime": 21054, "by_worker": [
    3. {"pid": "21117", "subscribers": 1, "uptime": 21054}
    4. ]}

    可以看到”channels”: 1表示有一个通道,之前是没有的,”published_messages”: 1表示发布的消息也多了一条了。
    我们可以发起多个new WebSocket或开多个浏览器进行测试,那样可以观看到更多的效果。
    之前通过curl工具,向/pub这个location发送了http请求,这个就间接向客户端发送数据,只是表现方式跟之前的不太一样。
    2.5 ruby

    在实际的编程中,我们可以会用ruby应用结合nginx的nginx-push-stream-module这个模块来做应用,总不至用curl这个工具,这个工具主要用于测试,我们现在试一下用ruby来代替curl。
    开启一个ruby的命令终端irb。

    1. require 'net/http'
    2. uri = URI("http://localhost/pub\?id\=ch1")
    3. http = Net::HTTP.new(uri.host, uri.port)
    4. req = Net::HTTP::Post.new(uri.to_s)
    5. req.body = 'Some Text'
    6. http.request(req)

    你会发现,效果是一样的。
    nginx-push-stream-module是个不错的工具,如果灵活运用它,肯定有意想不到的好处。
    完结。