# nginx-push-stream-module 模塊
#### 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](https://github.com/wandenberg/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`。
現在我們先來跑一下流程,再來講述一下它的原理以及為什么能夠這樣做。
#### 2. 使用
首先得先安裝一下這個模塊。
##### 2.1 安裝
安裝很簡單,跟之前的模塊安裝一模一樣的步驟,具體可以查看這篇文章[nginx之編譯第三方模塊(六)](http://www.rails365.net/articles/nginx-zhi-bian-yi-di-san-fang-mo-kuai-liu)。
現在來列出一下大概的流程。
```
$ git clone https://github.com/wandenberg/nginx-push-stream-module.git
# 進入到nginx源碼目錄,--add-module后面接nginx-push-stream-module的源碼目錄
$ ./configure --add-module=../nginx-push-stream-module
# 編譯
$ make
# 安裝
$ sudo make install
# 結束老的nginx進程
$ sudo nginx -s quit
# 開啟新的nginx進程
$ sudo nginx
```
接著我們來使用這個模塊。
##### 2.2 配置
在配置文件`nginx.conf`中添加下面這樣的內容:
```
http {
push_stream_shared_memory_size 32M;
server {
location /channels-stats {
# activate channels statistics mode for this location
push_stream_channels_statistics;
# query string based channel id
push_stream_channels_path $arg_id;
}
location /pub {
# activate publisher (admin) mode for this location
push_stream_publisher admin;
# query string based channel id
push_stream_channels_path $arg_id;
}
location ~ /ws/(.*) {
# activate websocket mode for this location
push_stream_subscriber websocket;
# positional channel path
push_stream_channels_path $1;
# message template
push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
push_stream_websocket_allow_publish on;
# ping frequency
push_stream_ping_message_interval 10s;
}
}
}
```
##### 2.3 原理
其中`push_stream_shared_memory_size`是添加在`http`下,其他的都在`server`下。
`push_stream_shared_memory_size`我就不詳說了,應該設置的是一個內存的容量,我對此細節并不了解,按照默認的就好了。
我們來講述一下`server`之下的三個`location`。
- /channels-stats
- /pub
- ~ /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`嗎?它是統計信息用的。
我們先來看下它的結果。
```
$ curl -s -v 'http://localhost/channels-stats'
```
輸出的內容主要是下面的json信息:
```
{"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": [
{"pid": "21117", "subscribers": 0, "uptime": 19755}
]}
```
上面的信息包括主機名,時間,通道的個數,消息的個數等,我們先不管。
現在我們用瀏覽器建立一個連接到websocket服務器,也就是要請求`~ /ws/(.*)`這個`location`。
```
ws = new WebSocket("ws://localhost/ws/ch1");
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`。
```
$ curl http://localhost/pub\?id\=ch1 -d "Some Text"
{"channel": "ch1", "published_messages": 1, "stored_messages": 0, "subscribers": 1}
```
使用的是curl這個命令,`ch1`表示的是通道名,它可以以參數的形式來指定,這樣就會靈活很多,不同類型的連接可以用不同的通道名。
果然瀏覽器輸出了信息了:
```
{"id":1,"channel":"ch1","text":"Some Text"}
```
`id`是消息的編號,默認從1開始,這個數字會自增,`channel`表示通道名,`text`是服務器端發送的信息。
輸出的內容,跟`push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";`這里定義的模版有關。
果然是推送了什么內容就是輸出了什么內容。
現在我們來看看統計內容的輸出:
```
$ curl -s -v 'http://localhost/channels-stats'
{"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": [
{"pid": "21117", "subscribers": 1, "uptime": 21054}
]}
```
可以看到`"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`。
```
require 'net/http'
uri = URI("http://localhost/pub\?id\=ch1")
http = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Post.new(uri.to_s)
req.body = 'Some Text'
http.request(req)
```
你會發現,效果是一樣的。
`nginx-push-stream-module`是個不錯的工具,如果靈活運用它,肯定有意想不到的好處。
完結。