├── .gitignore ├── LICENSE ├── README.md ├── app ├── emitor.lua ├── init_worker.lua ├── web_socket.lua └── ws_access.lua ├── conf └── nginx.conf ├── logs └── error.log └── vendor └── dispatcher.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Timmy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # orpush 2 | 3 | 基于OpenResty的websocket推送框架 4 | 5 | ### 启动 6 | 7 | ```shell 8 | nginx -p `pwd`/ -c conf/nginx.conf 9 | ``` 10 | 11 | ### 连接 12 | 13 | ```javascript 14 | var s = new WebSocket('ws://localhost:8080/ws'); 15 | 16 | s.onmessage = function(v){ 17 | console.log(v.data); 18 | } 19 | ``` 20 | 21 | ### 推送 22 | 23 | ```shell 24 | curl -X POST \ 25 | http://localhost:8080/emitor \ 26 | -H 'content-type: application/json' \ 27 | -d '{ 28 | "session_id": 100001, 29 | "message": "Hello, World!" 30 | }' 31 | ``` -------------------------------------------------------------------------------- /app/emitor.lua: -------------------------------------------------------------------------------- 1 | local json = require("cjson") 2 | local dispatcher = require("dispatcher") 3 | 4 | local method = ngx.req.get_method() 5 | if method ~= 'POST' then 6 | return ngx.exit(ngx.HTTP_NOT_ALLOWED) 7 | end 8 | 9 | ngx.req.read_body() 10 | local body = ngx.req.get_body_data() 11 | 12 | local ok, data = pcall(json.decode, body) 13 | if not ok then 14 | return ngx.exit(ngx.HTTP_BAD_REQUEST) 15 | end 16 | 17 | local session_id = data.session_id 18 | local message = data.message 19 | 20 | if not session_id or not message then 21 | return ngx.exit(ngx.HTTP_BAD_REQUEST) 22 | end 23 | 24 | if type(message) == 'table' then 25 | message = json.encode(message) 26 | end 27 | 28 | dispatcher:dispatch(session_id, message) 29 | 30 | ngx.exit(ngx.HTTP_NO_CONTENT) -------------------------------------------------------------------------------- /app/init_worker.lua: -------------------------------------------------------------------------------- 1 | local ngx = ngx 2 | local ngx_log = ngx.log 3 | local ngx_ERR = ngx.ERR 4 | local ngx_timer_at = ngx.timer.at 5 | local require = require 6 | 7 | local dispatcher = require("dispatcher") 8 | 9 | local delay = 1 10 | local loop_message 11 | 12 | loop_message = function(premature) 13 | if premature then 14 | ngx_log(ngx_ERR, "timer was shut: ", err) 15 | return 16 | end 17 | 18 | dispatcher:loop_message() 19 | 20 | local ok, err = ngx_timer_at(delay, loop_message) 21 | 22 | if not ok then 23 | ngx_log(ngx_ERR, "failed to create the timer: ", err) 24 | return 25 | end 26 | end 27 | 28 | loop_message() -------------------------------------------------------------------------------- /app/web_socket.lua: -------------------------------------------------------------------------------- 1 | local server = require("resty.websocket.server") 2 | local dispatcher = require("dispatcher") 3 | 4 | local wbsocket, err = server:new{ 5 | timeout = 30000, 6 | max_payload_len = 65535 7 | } 8 | 9 | if not wbsocket then 10 | ngx.log(ngx.ERR, "failed to new websocket: ", err) 11 | return ngx.exit(ngx.HTTP_CLOSE) 12 | end 13 | 14 | local session_id = dispatcher:gen_session_id() 15 | local send_semaphore = dispatcher:get_semaphore(session_id) 16 | local close_flag = false 17 | 18 | local function _push_thread_function() 19 | while not close_flag do 20 | local ok, err = send_semaphore:wait(300) 21 | 22 | if ok then 23 | local messages = dispatcher:get_messages(session_id) 24 | 25 | for i, message in ipairs(messages) do 26 | local bytes, err = wbsocket:send_text(message) 27 | if not bytes then 28 | close_flag = true 29 | ngx.log(ngx.DEBUG, 'send text failed session: '..session_id, err) 30 | break 31 | end 32 | end 33 | end 34 | end 35 | 36 | dispatcher:destory(session_id) 37 | end 38 | 39 | local push_thread = ngx.thread.spawn(_push_thread_function) 40 | 41 | while not close_flag do 42 | local data, typ, err = wbsocket:recv_frame() 43 | 44 | while err == "again" do 45 | local cut_data 46 | cut_data, typ, err = wbsocket:recv_frame() 47 | data = (data or '') .. cut_data 48 | end 49 | 50 | if not data then 51 | local bytes, err = wbsocket:send_ping() 52 | if not bytes then 53 | ngx.log(ngx.DEBUG, 'send ping failed session: '..session_id, err) 54 | close_flag = true 55 | send_semaphore:post(1) 56 | break 57 | end 58 | elseif typ == 'close' then 59 | close_flag = true 60 | send_semaphore:post(1) 61 | ngx.log(ngx.DEBUG, 'close session: '..session_id, err) 62 | break 63 | elseif typ == 'ping' then 64 | local bytes, err = wbsocket:send_pong(data) 65 | if not bytes then 66 | close_flag = true 67 | send_semaphore:post(1) 68 | ngx.log(ngx.DEBUG, 'send pong failed session: '..session_id, err) 69 | break 70 | end 71 | elseif typ == 'pong' then 72 | elseif typ == 'text' then 73 | -- your receive function handler 74 | elseif typ == 'continuation' then 75 | elseif typ == 'binary' then 76 | end 77 | 78 | end 79 | 80 | ngx.thread.wait(push_thread) 81 | wbsocket:send_close() -------------------------------------------------------------------------------- /app/ws_access.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/orpush/d91a78830008ea9f8afa895bfcd99c7a6bacf1bb/app/ws_access.lua -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | error_log logs/error.log debug; 3 | events { 4 | worker_connections 65535; 5 | } 6 | 7 | http { 8 | 9 | lua_package_path '$prefix/app/?.lua;$prefix/conf/?.lua;$prefix/vendor/?.lua;;'; 10 | lua_code_cache on; 11 | lua_socket_log_errors off; 12 | 13 | lua_shared_dict message_1 32m; 14 | lua_shared_dict message_2 32m; 15 | lua_shared_dict message_3 32m; 16 | lua_shared_dict message_4 32m; 17 | 18 | init_worker_by_lua_file app/init_worker.lua; 19 | 20 | server { 21 | listen 8080; 22 | location /ws { 23 | # access_by_lua_file app/ws_access.lua; 24 | content_by_lua_file app/web_socket.lua; 25 | } 26 | 27 | location /emitor 28 | { 29 | content_by_lua_file app/emitor.lua; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /logs/error.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/orpush/d91a78830008ea9f8afa895bfcd99c7a6bacf1bb/logs/error.log -------------------------------------------------------------------------------- /vendor/dispatcher.lua: -------------------------------------------------------------------------------- 1 | local semaphore = require "ngx.semaphore" 2 | 3 | local _M = { _VERSION = '0.01' } 4 | 5 | local ngx_worker_id = ngx.worker.id() 6 | 7 | local _messages = {} 8 | local _semaphores = {} 9 | local _incr_id = 0 10 | local _base_num = 100000 11 | local _shared_dicts = { 12 | ngx.shared.message_1, 13 | ngx.shared.message_2, 14 | ngx.shared.message_3, 15 | ngx.shared.message_4 16 | } 17 | 18 | local current_shared = _shared_dicts[ngx_worker_id + 1] 19 | local current_message_id = 0 20 | 21 | current_shared:set('id', 0) 22 | 23 | local function _gen_session_id() 24 | _incr_id = _incr_id + 1 25 | return (ngx_worker_id + 1) * _base_num + math.fmod(_incr_id, _base_num) 26 | end 27 | 28 | local function _get_shared_id(session_id) 29 | return math.floor(session_id/_base_num) 30 | end 31 | 32 | local function _get_message_key(message_id) 33 | return "message." .. message_id 34 | end 35 | 36 | local function _wake_up(session_id, message) 37 | if _messages[session_id] then 38 | table.insert(_messages[session_id], message) 39 | _semaphores[session_id]:post(1) 40 | else 41 | ngx.log(ngx.DEBUG, 'invalid session: '..session_id) 42 | end 43 | end 44 | 45 | function _M:gen_session_id() 46 | local session_id = _gen_session_id() 47 | while _messages[session_id] do 48 | session_id = _gen_session_id() 49 | end 50 | _messages[session_id] = {} 51 | ngx.log(ngx.DEBUG,'new session: '..session_id) 52 | return session_id 53 | end 54 | 55 | function _M:get_semaphore(session_id) 56 | if not _semaphores[session_id] then 57 | _semaphores[session_id] = semaphore.new(0) 58 | end 59 | return _semaphores[session_id] 60 | end 61 | 62 | function _M:get_messages(session_id) 63 | local messages = _messages[session_id] 64 | _messages[session_id] = {} 65 | return messages 66 | end 67 | 68 | function _M:destory(session_id) 69 | ngx.log(ngx.DEBUG,'destory session: '..session_id) 70 | _messages[session_id] = nil 71 | _semaphores[session_id] = nil 72 | end 73 | 74 | function _M:loop_message() 75 | local message, session_id 76 | local max_message_id, _ = current_shared:get("id") 77 | while current_message_id < max_message_id do 78 | current_message_id = current_message_id + 1 79 | message, session_id = current_shared:get(_get_message_key(current_message_id)) 80 | if message then 81 | _wake_up(session_id, message) 82 | current_shared:delete(_get_message_key(current_message_id)) 83 | else 84 | ngx.log(ngx.ERR, 'Error message session: '..session_id..' message_id: '..current_message_id) 85 | end 86 | end 87 | end 88 | 89 | function _M:dispatch(session_id, message) 90 | if _messages[session_id] then 91 | _wake_up(session_id, message) 92 | else 93 | local shared_id = _get_shared_id(session_id) 94 | local message_shared = _shared_dicts[shared_id] 95 | local message_id = message_shared:incr("id", 1, 0) 96 | message_shared:set(_get_message_key(message_id), message, 60, session_id) 97 | end 98 | end 99 | 100 | return _M --------------------------------------------------------------------------------