给skynet增加websocket模块

最近迷上了skynet,代码质量很高,算开源游戏服务器框架中的佼佼者,不管是Python的firefly,C++/Python的kbengine,C#的scut,还是nodejs的pomelo,skynet在并发上和商业应用都有很大的优势,根据http://thislinux.com/blog/5_panic.html描述,skynet能支持单机3w在线用户,性能很是给力。

最近做的都是一些h5小游戏,用tornado/django基本上也都绰绰有余,下个小游戏打算试试skynet,skynet没有带websocket库,于是就很happy的去造轮子去了,虽然有lua-resty-websocket这个nginx扩展库,有2个原因我不喜欢。

1.lua-resty-websocket实在太老了,现在已经是lua53的时代了

2.还是喜欢tornado websocket的基于回调的方式,当然我写的既可使用回调方式,也可使用lua-resty-websocket

基于直接recv的方式

其实解析websocket还是比较简单的,比较复杂点的是websocket 的close操作。和握手一样,close也是需要客户端-服务器

端确认的。

local skynet = require "skynet"
local string = require "string"
local crypt = require "crypt"
local socket = require "socket"
local httpd = require "http.httpd"
local sockethelper = require "http.sockethelper"
local urllib = require "http.url"

local ws = {}
local ws_mt = { __index = ws }

local function response(id, ...)
    return httpd.write_response(sockethelper.writefunc(id), ...)
end

local function write(id, data)
    socket.write(id, data)
end

local function read(id, sz)
    return socket.read(id, sz)
end

local function challenge_response(key, protocol)
    local accept = crypt.base64encode(crypt.sha1(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
    return string.format("HTTP/1.1 101 Switching Protocols\r\n" ..
                        "Upgrade: websocket\r\n" ..
                        "Connection: Upgrade\r\n" ..
                        "Sec-WebSocket-Accept: %s\r\n" ..
                        "%s\r\n", accept, protocol or "")

end

local function accept_connection(header, check_origin, check_origin_ok)
    -- Upgrade header should be present and should be equal to WebSocket
    if not header["upgrade"] or header["upgrade"]:lower() ~= "websocket" then
        return 400, "Can \"Upgrade\" only to \"WebSocket\"."
    end

    -- Connection header should be upgrade. Some proxy servers/load balancers
    -- might mess with it.
    if not header["connection"] or not header["connection"]:lower():find("upgrade", 1,true) then
        return 400, "\"Connection\" must be \"Upgrade\"."
    end

    -- Handle WebSocket Origin naming convention differences
    -- The difference between version 8 and 13 is that in 8 the
    -- client sends a "Sec-Websocket-Origin" header and in 13 it‘s
    -- simply "Origin".
    local origin = header["origin"] or header["sec-websocket-origin"]
    if origin and check_origin and not check_origin_ok(origin, header["host"]) then
        return 403, "Cross origin websockets not allowed"
    end

    if not header["sec-websocket-version"] or header["sec-websocket-version"] ~= "13" then
        return 400, "HTTP/1.1 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n"
    end

    local key = header["sec-websocket-key"]
    if not key then
        return 400, "\"Sec-WebSocket-Key\" must not be  nil."
    end

    local protocol = header["sec-websocket-protocol"]
    if protocol then
        local i = protocol:find(",", 1, true)
        protocol = "Sec-WebSocket-Protocol: " .. protocol:sub(1, i or i-1)
    end

    return nil, challenge_response(key, protocol)

end

local H = {}

function H.check_origin_ok(origin, host)
    return urllib.parse(origin) == host
end

function H.on_open(ws)

end

function H.on_message(ws, message)

end

function H.on_close(ws, code, reason)

end

function H.on_pong(ws, data)
    -- Invoked when the response to a ping frame is received.

end

function ws.new(id, header, handler, conf)
    local conf = conf or {}
    local handler = handler or {}

    setmetatable(handler, { __index = H })

    local code, result = accept_connection(header, conf.check_origin, handler.check_origin_ok)

    if code then
        response(id, code, result)
        socket.close(id)
    else
        write(id, result)
    end

    local self = {
        id = id,
        handler = handler,
        mask_outgoing = conf.mask_outgoing,
        check_origin = conf.check_origin
    }

    self.handler.on_open(self)

    return setmetatable(self, ws_mt)
end

function ws:send_frame(fin, opcode, data)
    if fin then
        finbit = 0x80
    else
        finbit = 0
    end

    frame = string.pack("B", finbit | opcode)
    l = #data

    if self.mask_outgoing then
        mask_bit = 0x80
    else
        mask_bit = 0
    end

    if l < 126 then
        frame = frame .. string.pack("B", l | mask_bit)
    elseif l < 0xFFFF then
        frame = frame .. string.pack("!BH", 126 | mask_bit, l)
    else
        frame = frame .. string.pack("!BL", 127 | mask_bit, l)
    end

    if self.mask_outgoing then
    end

    frame = frame .. data

    write(self.id, frame)

end

function ws:send_text(data)
    self:send_frame(true, 0x1, data)
end

function ws:send_binary(data)
    self:send_frame(true, 0x2, data)
end

function ws:send_ping(data)
    self:send_frame(true, 0x9, data)
end

function ws:send_pong(data)
    self:send_frame(true, 0xA, data)
end

function ws:close(code, reason)
    -- 1000  "normal closure" status code
    if code == nil and reason ~= nil then
        code = 1000
    end
    local data = ""
    if code ~= nil then
        data = string.pack(">H", code)
    end
    if reason ~= nil then
        data = data .. reason
    end
    self:send_frame(true, 0x8, data)
end

function ws:recv()
    local data = ""
    while true do
        local success, final, message = self:recv_frame()
        if not success then
            return success, message
        end
        if final then
            data = data .. message
            break
        else
            data = data .. message
        end
    end
    self.handler.on_message(self, data)
    return data
end

local function websocket_mask(mask, data, length)
    umasked = {}
    for i=1, length do
        umasked[i] = string.char(string.byte(data, i) ~ string.byte(mask, (i-1)%4 + 1))
    end
    return table.concat(umasked)
end

function ws:recv_frame()
    local data, err = read(self.id, 2)

    if not data then
        return false, nil, "Read first 2 byte error: " .. err
    end

    local header, payloadlen = string.unpack("BB", data)
    local final_frame = header & 0x80 ~= 0
    local reserved_bits = header & 0x70 ~= 0
    local frame_opcode = header & 0xf
    local frame_opcode_is_control = frame_opcode & 0x8 ~= 0

    if reserved_bits then
        -- client is using as-yet-undefined extensions
        return false, nil, "Reserved_bits show using undefined extensions"
    end

    local mask_frame = payloadlen & 0x80 ~= 0
    payloadlen = payloadlen & 0x7f

    if frame_opcode_is_control and payloadlen >= 126 then
        -- control frames must have payload < 126
        return false, nil, "Control frame payload overload"
    end

    if frame_opcode_is_control and not final_frame then
        return false, nil, "Control frame must not be fragmented"
    end

    local frame_length, frame_mask

    if payloadlen < 126 then
        frame_length = payloadlen

    elseif payloadlen == 126 then
        local h_data, err = read(self.id, 2)
        if not h_data then
            return false, nil, "Payloadlen 126 read true length error:" .. err
        end
        frame_length = string.pack("!H", h_data)

    else --payloadlen == 127
        local l_data, err = read(self.id, 8)
        if not l_data then
            return false, nil, "Payloadlen 127 read true length error:" .. err
        end
        frame_length = string.pack("!L", l_data)
    end

    if mask_frame then
        local mask, err = read(self.id, 4)
        if not mask then
            return false, nil, "Masking Key read error:" .. err
        end
        frame_mask = mask
    end

    --print(‘final_frame:‘, final_frame, "frame_opcode:", frame_opcode, "mask_frame:", mask_frame, "frame_length:", frame_length)

    local  frame_data = ""
    if frame_length > 0 then
        local fdata, err = read(self.id, frame_length)
        if not fdata then
            return false, nil, "Payload data read error:" .. err
        end
        frame_data = fdata
    end

    if mask_frame and frame_length > 0 then
        frame_data = websocket_mask(frame_mask, frame_data, frame_length)
    end

    if not final_frame then
        return true, false, frame_data
    else
        if frame_opcode  == 0x1 then -- text
            return true, true, frame_data
        elseif frame_opcode == 0x2 then -- binary
            return true, true, frame_data
        elseif frame_opcode == 0x8 then -- close
            local code, reason
            if #frame_data >= 2 then
                code = string.unpack(">H", frame_data:sub(1,2))
            end
            if #frame_data > 2 then
                reason = frame_data:sub(3)
            end
            self:close()
            socket.close(self.id)
            self.handler.on_close(self, code, reason)
        elseif frame_opcode == 0x9 then --Ping
            self:send_pong()
        elseif frame_opcode == 0xA then -- Pong
            self.handler.on_pong(self, frame_data)
        end

        return true, true, nil
    end

end

function ws:start()
    while true do
        local message, err = self:recv()
        if not message then
            --print(‘recv eror:‘, message, err)
            socket.close(self.id)
        end
    end
end

return ws

是用方法也很简单,基于回调的方式用起来真是舒服

local skynet = require "skynet"
local socket = require "socket"
local string = require "string"
local websocket = require "websocket"
local httpd = require "http.httpd"
local urllib = require "http.url"
local sockethelper = require "http.sockethelper"

local handler = {}
function handler.on_open(ws)
    print(string.format("%d::open", ws.id))
end

function handler.on_message(ws, message)
    print(string.format("%d receive:%s", ws.id, message))
    ws:send_text(message .. "from server")
end

function handler.on_close(ws, code, reason)
    print(string.format("%d close:%d  %s", ws.id, code, reason))
end

local function handle_socket(id)
    -- limit request body size to 8192 (you can pass nil to unlimit)
    local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
    if code then

        if url == "/ws" then
            local ws = websocket.new(id, header, handler)
            ws:start()
        end
    end

end

skynet.start(function()
    local address = "0.0.0.0:8001"
    skynet.error("Listening "..address)
    local id = assert(socket.listen(address))
    socket.start(id , function(id, addr)
       socket.start(id)
       pcall(handle_socket, id)
    end)
end)

详细信息请到 https://github.com/Skycrab/skynet_websocket

时间: 2024-11-03 22:42:27

给skynet增加websocket模块的相关文章

为nginx增加nginx_http_concat模块

为nginx增加nginx_http_concat模块 时间 2013-06-05 22:14:56  我行我思 原文  http://www.fanjun.me/?p=562 主题 Nginx 缘由 最近在做的一个项目引入的js库文件比较多,所以导致的问题就是感觉速度会比较慢,而很多库文件都是拿的开源的库,基本上不会改动,所以想是否合并一下来下载. 合并JS方式很多,一般要么是服务器端合并要么是客户端合并,如果是以前我可能会选择客户端合并,但是现在nginx上面有比较成熟的模块 nginx-h

为Phonegap Android平台增加websocket支持,使默认成为socket.io首选通

为Phonegap Android平台增加websocket支持,使默认成为socket.io首选通道选择 广而告之 使用socket.io作为跨浏览器平台的实时推送首选,经测试在各个主流浏览器上测试都确实具有良好的下实时表现.这里为推广socketio-netty服务器端实现哈,做次广告,同时预热一下: socketio-netty : 又一款socket.io服务器端实现,兼容0.9-1.0版本~ 示范目的 我们要构建一个在市面上常见浏览器上都可以正常运行的集体聊天应用,保证在IE6+,Fi

nginx增加第三方模块

增加第三方模块 ============================================================ 一.概述nginx文件非常小但是性能非常的高效,这方面完胜apache.nginx文件小的一个原因之一是nginx自带的功能相对较少,好在nginx允许第三方模块,第三方模块使得nginx越发的强大. nginx已支持动态加载模块 二.安装第三方模块./configure --prefix=源安装目录 --add-module=/第三方模块解压目录 以安装ng

nginx增加新模块

以gzip这个模块为例,讲述一下,在nginx中如何安装新的模块1.首先查看nginx已经安装了哪些模块.nginx –V2.发现没有gzip模块,安装进入nginx的安装目录中,不是nginx的软件目录.在已有模块种写上要安装的模块,执行下边的命令./configure \--prefix=/usr/local/ywgh/nginx \--http-client-body-temp-path=/tmp/clientbody \--http-proxy-temp-path=/tmp/proxy

tengine-2.3.1 增加ngx_http_upstream_check_module 模块

该模块在Tengine-1.4.0版本以前没有默认开启,它可以在配置编译选项的时候开启:./configure --with-http_upstream_check_module 但是在1.4.0之后编译时默认开启的,但是在2.3.1版本时候默认取消了,需要以增加模块方式编译进去 ./configure --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --prefix=/usr/loc

使用phpize增加php模块

一,phpize的好处 什么时候我们要用phpize呢?我们在安装php时: ./configure --prefix=/apps/product/php --with-config-file-path=/apps/product/php/etc --with-iconv-dir --with-freetype-dir --with-png-dir --with-zlib --with-libxml-dir=/usr --enable-xml --enable-discard-path --en

Zepto.js touch,tap增加 touch模块深入分析

1. touch库实现了什么和引入背景 click事件在移动端上会有 300ms 的延迟,同时因为需要 长按 , 双触击 等富交互,所以我们通常都会引入类似 zepto 这样的库.zepto 中touch库实现了 'swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap' 这样一些功能. 2.touch库实现'swipe', 'swipeLeft',

我的应用里自定义增加功能模块

现在 tui 表里增加一行数据 然后在语言表 tui_lang_cs 里增加语言数据

PHP提供Oracle支持增加oci8模块

环境: CentOS7 x86_64 PHP 7.1.2(安装路径:/usr/local/php7.1.2) Oracle 11G R2 1.下载Oracle客户端:(http://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html) instantclient-basic-linux.x64-11.2.0.4.0.zip instantclient-sdk-linux.x64-11.2.0.4.0.zip 2.下载php