├── .github └── workflows │ └── test.yml ├── LICENSE ├── Readme.md ├── conf.lua ├── main.lua ├── test.lua └── websocket.lua /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: setup luajit 16 | run: sudo apt install luajit 17 | - name: test 18 | run: luajit ./test.lua 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 flaribbit 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 | # websocket client pure lua implement for love2d 2 | 3 | Event-driven websocket client for love2d in pure lua, which aims to be simple and easy to use. 4 | 5 | Not all websocket features are implemented, but it works fine. Tested with aiohttp(python) and ws(nodejs) library. 6 | 7 | ## Quick start 8 | Just copy `websocket.lua` to your project directory, and write code as the following example. 9 | 10 | ```lua 11 | local client = require("websocket").new("127.0.0.1", 5000) 12 | function client:onmessage(message) 13 | print(message) 14 | end 15 | function client:onopen() 16 | self:send("hello from love2d") 17 | self:close() 18 | end 19 | function client:onclose(code, reason) 20 | print("closecode: "..code..", reason: "..reason) 21 | end 22 | 23 | function love.update() 24 | client:update() 25 | end 26 | ``` 27 | 28 | ## WSS connection 29 | If you need wss connection(websocket with TLS), you can use [LuaSec](https://github.com/brunoos/luasec) with this library, or just use [löve-ws](https://github.com/holywyvern/love-ws). 30 | 31 | ## API 32 | * `websocket.new(host: string, port: int, path?: string) -> client` 33 | * `function client:onopen()` 34 | * `function client:onmessage(message: string)` 35 | * `function client:onerror(error: string)` 36 | * `function client:onclose(code: int, reason: string)` 37 | * `client.status -> int` 38 | * `client:send(message: string)` 39 | * `client:close(code?: int, reason?: string)` 40 | * `client:update()` 41 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.modules.window = false 3 | end 4 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local client = require("websocket").new("127.0.0.1", 5000) 2 | function client:onmessage(s) 3 | print(s) 4 | end 5 | function client:onopen() 6 | self:send("hello from love2d") 7 | self:close() 8 | end 9 | function client:onerror(e) 10 | print(e) 11 | end 12 | function client:onclose(code, reason) 13 | print("closecode: "..code..", reason: "..reason) 14 | end 15 | 16 | function love.update() 17 | client:update() 18 | end 19 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | package.preload["socket"] = function()end 2 | local ws = require"websocket" 3 | local client = { 4 | socket = {}, 5 | _buffer = "", 6 | _length = 2, 7 | _head = nil, 8 | } 9 | local res, head, err 10 | local function receive(t) 11 | return function(_, n) 12 | if #t>0 then 13 | local ret = t[1] 14 | if n<#ret then 15 | ret, t[1] = ret:sub(1,n), ret:sub(n+1) 16 | else 17 | table.remove(t, 1) 18 | end 19 | return ret, nil, nil 20 | else 21 | return nil, "timeout", nil 22 | end 23 | end 24 | end 25 | 26 | --空消息 27 | client.socket.receive = receive{"\x81", "\x00"} 28 | res, head, err = ws.read(client) 29 | assert(res==nil and head==nil and err=="buffer length less than 2") 30 | res, head, err = ws.read(client) 31 | assert(res=="" and head==0x81 and err==nil) 32 | 33 | --1字节消息 34 | client.socket.receive = receive{"\x81\x01"} 35 | res, head, err = ws.read(client) 36 | assert(res==nil and head==nil and err==nil) 37 | client.socket.receive = receive{"\x31"} 38 | res, head, err = ws.read(client) 39 | assert(res=="1" and head==0x81 and err==nil) 40 | 41 | --5字节消息 42 | client.socket.receive = receive{"\x81\x05", "12", "345"} 43 | res, head, err = ws.read(client) 44 | assert(res==nil and head==nil and err=="buffer length less than 5") 45 | res, head, err = ws.read(client) 46 | assert(res=="12345" and head==0x81 and err==nil) 47 | 48 | --200字节消息 49 | local s = "" for i=1,100 do s=s..i%5 end 50 | client.socket.receive = receive{"\x81\x7e", "\x00", "\xc8", s, s} 51 | res, head, err = ws.read(client) 52 | assert(res==nil and head==nil and err=="buffer length less than 4") 53 | res, head, err = ws.read(client) 54 | assert(res==nil and head==nil and err=="buffer length less than 200") 55 | res, head, err = ws.read(client) 56 | assert(res==s..s and head==0x81 and err==nil) 57 | 58 | print(ws.read(client)) 59 | -------------------------------------------------------------------------------- /websocket.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | websocket client pure lua implement for love2d 3 | by flaribbit 4 | 5 | usage: 6 | local client = require("websocket").new("127.0.0.1", 5000) 7 | function client:onmessage(s) print(s) end 8 | function client:onopen() self:send("hello from love2d") end 9 | function client:onclose() print("closed") end 10 | 11 | function love.update() 12 | client:update() 13 | end 14 | ]] 15 | 16 | local socket = require"socket" 17 | local bit = require"bit" 18 | local band, bor, bxor = bit.band, bit.bor, bit.bxor 19 | local shl, shr = bit.lshift, bit.rshift 20 | local seckey = "osT3F7mvlojIvf3/8uIsJQ==" 21 | 22 | local OPCODE = { 23 | CONTINUE = 0, 24 | TEXT = 1, 25 | BINARY = 2, 26 | CLOSE = 8, 27 | PING = 9, 28 | PONG = 10, 29 | } 30 | 31 | local STATUS = { 32 | CONNECTING = 0, 33 | OPEN = 1, 34 | CLOSING = 2, 35 | CLOSED = 3, 36 | TCPOPENING = 4, 37 | } 38 | 39 | ---@class wsclient 40 | ---@field socket table 41 | ---@field url table 42 | ---@field _head integer|nil 43 | local _M = { 44 | OPCODE = OPCODE, 45 | STATUS = STATUS, 46 | } 47 | _M.__index = _M 48 | function _M:onopen() end 49 | function _M:onmessage(message) end 50 | function _M:onerror(error) end 51 | function _M:onclose(code, reason) end 52 | 53 | ---create websocket connection 54 | ---@param host string 55 | ---@param port integer 56 | ---@param path string 57 | ---@return wsclient 58 | function _M.new(host, port, path) 59 | local m = { 60 | url = { 61 | host = host, 62 | port = port, 63 | path = path or "/", 64 | }, 65 | _continue = "", 66 | _buffer = "", 67 | _length = 0, 68 | _head = nil, 69 | status = STATUS.TCPOPENING, 70 | socket = socket.tcp(), 71 | } 72 | m.socket:settimeout(0) 73 | m.socket:connect(host, port) 74 | setmetatable(m, _M) 75 | return m 76 | end 77 | 78 | local mask_key = {1, 14, 5, 14} 79 | local function send(sock, opcode, message) 80 | -- message type 81 | sock:send(string.char(bor(0x80, opcode))) 82 | 83 | -- empty message 84 | if not message then 85 | sock:send(string.char(0x80, unpack(mask_key))) 86 | return 0 87 | end 88 | 89 | -- message length 90 | local length = #message 91 | if length>65535 then 92 | sock:send(string.char(bor(127, 0x80), 93 | 0, 0, 0, 0, 94 | band(shr(length, 24), 0xff), 95 | band(shr(length, 16), 0xff), 96 | band(shr(length, 8), 0xff), 97 | band(length, 0xff))) 98 | elseif length>125 then 99 | sock:send(string.char(bor(126, 0x80), 100 | band(shr(length, 8), 0xff), 101 | band(length, 0xff))) 102 | else 103 | sock:send(string.char(bor(length, 0x80))) 104 | end 105 | 106 | -- message 107 | sock:send(string.char(unpack(mask_key))) 108 | local msgbyte = {message:byte(1, length)} 109 | for i = 1, length do 110 | msgbyte[i] = bxor(msgbyte[i], mask_key[(i-1)%4+1]) 111 | end 112 | return sock:send(string.char(unpack(msgbyte))) 113 | end 114 | 115 | ---read a message 116 | ---@return string|nil res message 117 | ---@return number|nil head websocket frame header 118 | ---@return string|nil err error message 119 | function _M:read() 120 | local res, err, part 121 | ::RECIEVE:: 122 | res, err, part = self.socket:receive(self._length-#self._buffer) 123 | if err=="closed" then return nil, nil, err end 124 | if part or res then 125 | self._buffer = self._buffer..(part or res) 126 | else 127 | return nil, nil, nil 128 | end 129 | if not self._head then 130 | if #self._buffer<2 then 131 | return nil, nil, "buffer length less than 2" 132 | end 133 | local length = band(self._buffer:byte(2), 0x7f) 134 | if length==126 then 135 | if self._length==2 then self._length = 4 goto RECIEVE end 136 | if #self._buffer<4 then 137 | return nil, nil, "buffer length less than 4" 138 | end 139 | local b1, b2 = self._buffer:byte(3, 4) 140 | self._length = shl(b1, 8) + b2 141 | elseif length==127 then 142 | if self._length==2 then self._length = 10 goto RECIEVE end 143 | if #self._buffer<10 then 144 | return nil, nil, "buffer length less than 10" 145 | end 146 | local b5, b6, b7, b8 = self._buffer:byte(7, 10) 147 | self._length = shl(b5, 24) + shl(b6, 16) + shl(b7, 8) + b8 148 | else 149 | self._length = length 150 | end 151 | self._head, self._buffer = self._buffer:byte(1), "" 152 | if length>0 then goto RECIEVE end 153 | end 154 | if #self._buffer>=self._length then 155 | local ret, head = self._buffer, self._head 156 | self._length, self._buffer, self._head = 2, "", nil 157 | return ret, head, nil 158 | else 159 | return nil, nil, "buffer length less than "..self._length 160 | end 161 | end 162 | 163 | ---send a message 164 | ---@param message string 165 | function _M:send(message) 166 | send(self.socket, OPCODE.TEXT, message) 167 | end 168 | 169 | ---send a ping message 170 | ---@param message string 171 | function _M:ping(message) 172 | send(self.socket, OPCODE.PING, message) 173 | end 174 | 175 | ---send a pong message (no need) 176 | ---@param message any 177 | function _M:pong(message) 178 | send(self.socket, OPCODE.PONG, message) 179 | end 180 | 181 | ---update client status 182 | function _M:update() 183 | local sock = self.socket 184 | if self.status==STATUS.TCPOPENING then 185 | local url = self.url 186 | local _, err = sock:connect(url.host, url.port) 187 | self._length = self._length+1 188 | if err=="already connected" then 189 | sock:send( 190 | "GET "..url.path.." HTTP/1.1\r\n".. 191 | "Host: "..url.host..":"..url.port.."\r\n".. 192 | "Connection: Upgrade\r\n".. 193 | "Upgrade: websocket\r\n".. 194 | "Sec-WebSocket-Version: 13\r\n".. 195 | "Sec-WebSocket-Key: "..seckey.."\r\n\r\n") 196 | self.status = STATUS.CONNECTING 197 | self._length = 2 198 | elseif self._length>600 then 199 | self:onerror("connection failed") 200 | self.status = STATUS.CLOSED 201 | end 202 | elseif self.status==STATUS.CONNECTING then 203 | local res = sock:receive("*l") 204 | if res then 205 | repeat res = sock:receive("*l") until res=="" 206 | self:onopen() 207 | self.status = STATUS.OPEN 208 | end 209 | elseif self.status==STATUS.OPEN or self.status==STATUS.CLOSING then 210 | while true do 211 | local res, head, err = self:read() 212 | if err=="closed" then 213 | self.status = STATUS.CLOSED 214 | return 215 | elseif res==nil then 216 | return 217 | end 218 | local opcode = band(head, 0x0f) 219 | local fin = band(head, 0x80)==0x80 220 | if opcode==OPCODE.CLOSE then 221 | if res~="" then 222 | local code = shl(res:byte(1), 8) + res:byte(2) 223 | self:onclose(code, res:sub(3)) 224 | else 225 | self:onclose(1005, "") 226 | end 227 | sock:close() 228 | self.status = STATUS.CLOSED 229 | elseif opcode==OPCODE.PING then self:pong(res) 230 | elseif opcode==OPCODE.CONTINUE then 231 | self._continue = self._continue..res 232 | if fin then self:onmessage(self._continue) end 233 | else 234 | if fin then self:onmessage(res) else self._continue = res end 235 | end 236 | end 237 | end 238 | end 239 | 240 | ---close websocket connection 241 | ---@param code integer|nil 242 | ---@param message string|nil 243 | function _M:close(code, message) 244 | if code and message then 245 | send(self.socket, OPCODE.CLOSE, string.char(shr(code, 8), band(code, 0xff))..message) 246 | else 247 | send(self.socket, OPCODE.CLOSE, nil) 248 | end 249 | self.status = STATUS.CLOSING 250 | end 251 | 252 | return _M 253 | --------------------------------------------------------------------------------