--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file socket.lua -- -- define module local socket = socket or {} local _instance = _instance or {} -- load modules local io = require("base/io") local libc = require("base/libc") local bytes = require("base/bytes") local table = require("base/table") local string = require("base/string") local scheduler = require("base/scheduler") -- the socket types socket.TCP = 1 socket.UDP = 2 socket.ICMP = 3 -- the socket families socket.IPV4 = 1 socket.IPV6 = 2 socket.UNIX = 3 -- the socket events, @see tbox/platform/socket.h socket.EV_RECV = 1 socket.EV_SEND = 2 socket.EV_CONN = socket.EV_SEND socket.EV_ACPT = socket.EV_RECV -- the socket control code socket.CTRL_SET_RECVBUFF = 2 socket.CTRL_SET_SENDBUFF = 4 -- new a socket function _instance.new(socktype, family, sock) local instance = table.inherit(_instance) instance._SOCK = sock instance._TYPE = socktype instance._FAMILY = family setmetatable(instance, _instance) return instance end -- get socket type function _instance:type() return self._TYPE end -- get socket family function _instance:family() return self._FAMILY end -- get cdata of socket function _instance:cdata() return self._SOCK end -- get poller object type, poller.OT_SOCK function _instance:otype() return 1 end -- get socket rawfd function _instance:rawfd() -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return nil, errors end -- get rawfd local result, errors = io.socket_rawfd(self:cdata()) if not result and errors then errors = string.format("%s: %s", self, errors) end return result, errors end -- get socket peer address function _instance:peeraddr() -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return nil, errors end -- get peer address local result, errors = io.socket_peeraddr(self:cdata()) if not result and errors then errors = string.format("%s: %s", self, errors) end return result, errors end -- control socket function _instance:ctrl(code, value) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- control it local ok, errors = io.socket_ctrl(self:cdata(), code, value) if not ok and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- bind socket to address and port -- -- @param addr the bind address -- @param port the bind port -- @return true on success, or false and error info -- function _instance:bind(addr, port) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- bind it local ok, errors = io.socket_bind(self:cdata(), addr, port, self:family()) if not ok and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- bind socket from the unix address function _instance:bind_unix(addr, opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- must be unix socket if self:family() ~= socket.UNIX then return -1, string.format("%s: must be unix socket!", self) end -- bind it opt = opt or {} local ok, errors = io.socket_bind(self:cdata(), addr, opt.is_abstract, self:family()) if not ok and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- listen for incoming connections -- -- @param backlog the maximum pending connections -- @return true on success, or false and error info -- function _instance:listen(backlog) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- listen it local ok, errors = io.socket_listen(self:cdata(), backlog or 10) if not ok and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- accept an incoming connection -- -- @param opt the options (optional) -- @return the client socket, or nil on timeout -- function _instance:accept(opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- accept it local sock, errors = io.socket_accept(self:cdata()) if not sock and not errors then opt = opt or {} local events, waiterrs = _instance.wait(self, socket.EV_ACPT, opt.timeout or -1) if events == socket.EV_ACPT then sock, errors = io.socket_accept(self:cdata()) else errors = waiterrs end end if not sock and errors then errors = string.format("%s: %s", self, errors) end if sock then sock = _instance.new(self:type(), self:family(), sock) end return sock, errors end -- connect to remote address and port -- -- @param addr the remote address -- @param port the remote port -- @param opt the options (optional) -- @return 1 on success, 0 on timeout, -1 on error -- function _instance:connect(addr, port, opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- connect it local ok, errors = io.socket_connect(self:cdata(), addr, port, self:family()) if ok == 0 then opt = opt or {} -- trace socket if socket._is_tracing_socket() then print(string.format("%s: connecting %s:%d, timeout: %d", self, addr, port, opt.timeout or -1)) end local events, waiterrs = _instance.wait(self, socket.EV_CONN, opt.timeout or -1) if events == socket.EV_CONN then ok, errors = io.socket_connect(self:cdata(), addr, port, self:family()) else errors = waiterrs end end if ok < 0 and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- connect socket from the unix address function _instance:connect_unix(addr, opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- must be unix socket if self:family() ~= socket.UNIX then return -1, string.format("%s: must be unix socket!", self) end -- connect it opt = opt or {} local ok, errors = io.socket_connect(self:cdata(), addr, opt.is_abstract, self:family()) if ok == 0 then local events, waiterrs = _instance.wait(self, socket.EV_CONN, opt.timeout or -1) if events == socket.EV_CONN then ok, errors = io.socket_connect(self:cdata(), addr, opt.is_abstract, self:family()) else errors = waiterrs end end if ok < 0 and errors then errors = string.format("%s: %s", self, errors) end return ok, errors end -- send data to socket -- -- @param data the data to send (string or bytes) -- @param opt the options, e.g. {block = true} -- @return the real sent size, or -1 on error -- function _instance:send(data, opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- get data address and size for bytes and string if type(data) == "string" then data = bytes(data) end local datasize = data:size() local dataaddr = data:caddr() -- init start and last opt = opt or {} local start = opt.start or 1 local last = opt.last or datasize if start < 1 or start > datasize then return -1, string.format("%s: invalid start(%d)!", self, start) end if last < start - 1 or last > datasize + start - 1 then return -1, string.format("%s: invalid last(%d)!", self, last) end -- send it local send = 0 local real = 0 local wait = false local errors = nil if opt.block then local size = last + 1 - start while start <= last do real, errors = io.socket_send(self:cdata(), dataaddr + start - 1, last + 1 - start) if real > 0 then send = send + real start = start + real wait = false elseif real == 0 and not wait then local events, waiterrs = _instance.wait(self, socket.EV_SEND, opt.timeout or -1) if events == socket.EV_SEND then wait = true elseif events == 0 then os.raise("%s: send timeout!", self) else errors = waiterrs break end else break end end if send ~= size then send = -1 end else send, errors = io.socket_send(self:cdata(), dataaddr + start - 1, last + 1 - start) if send < 0 and errors then errors = string.format("%s: %s", self, errors) end end return send, errors end -- send file data to socket (zero-copy) -- -- @param file the file object -- @param opt the options (optional) -- @return the real sent size, or -1 on error -- function _instance:sendfile(file, opt) -- ensure the socket opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- ensure the file opened local ok, errors = file:_ensure_opened() if not ok then return -1, errors end -- empty file? if file:size() == 0 then return -1, string.format("%s: send empty file!", self) end -- init start and last opt = opt or {} local start = opt.start or 1 local last = opt.last or file:size() -- check start and last if start > last or start < 1 then return -1, string.format("%s: invalid start(%d) and last(%d)!", self, start, last) end -- send it local send = 0 local real = 0 local wait = false local errors = nil if opt.block then local size = last + 1 - start while start <= last do real, errors = io.socket_sendfile(self:cdata(), file._FILE, start, last) if real > 0 then send = send + real start = start + real wait = false elseif real == 0 and not wait then local events, waiterrs = _instance.wait(self, socket.EV_SEND, opt.timeout or -1) if events == socket.EV_SEND then wait = true elseif events == 0 then os.raise("%s: sendfile timeout!", self) else errors = waiterrs break end else break end end if send ~= size then send = -1 end else send, errors = io.socket_sendfile(self:cdata(), file._FILE, start, last) if send < 0 and errors then errors = string.format("%s: %s", self, errors) end end return send, errors end -- receive data from socket -- -- @param buff the buffer to receive data -- @param size the max receive size -- @param opt the options, e.g. {block = true} -- @return the real received size, or -1 on error -- function _instance:recv(buff, size, opt) assert(buff) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- check buffer size = size or buff:size() if buff:size() < size then return -1, string.format("%s: too small buffer!", self) end -- check size if size == 0 then return 0 elseif size == nil or size < 0 then return -1, string.format("%s: invalid size(%d)!", self, size) end -- init start in buffer opt = opt or {} local start = opt.start or 1 local pos = start - 1 if start >= buff:size() or start < 1 then return -1, string.format("%s: invalid start(%d)!", self, start) end -- recv it local recv = 0 local real = 0 local wait = false local data_or_errors = nil if opt.block then while recv < size do real, data_or_errors = io.socket_recv(self:cdata(), buff:caddr() + pos + recv, math.min(buff:size() - pos - recv, size - recv)) if real > 0 then recv = recv + real wait = false elseif real == 0 and not wait then local events, waiterrs = _instance.wait(self, socket.EV_RECV, opt.timeout or -1) if events == socket.EV_RECV then wait = true elseif events == 0 then os.raise("%s: recv timeout!", self) else data_or_errors = waiterrs break end else break end end if recv == size then data_or_errors = buff:slice(start, recv) else recv = -1 end else recv, data_or_errors = io.socket_recv(self:cdata(), buff:caddr() + pos, math.min(buff:size() - pos, size)) if recv > 0 then data_or_errors = buff:slice(start, recv) end end if recv < 0 and data_or_errors then data_or_errors = string.format("%s: %s", self, data_or_errors) end return recv, data_or_errors end -- send UDP data to peer -- -- @param data the data to send -- @param addr the peer address -- @param port the peer port -- @param opt the options (optional) -- @return the real sent size, or -1 on error -- function _instance:sendto(data, addr, port, opt) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- only for udp if self:type() ~= socket.UDP then return -1, string.format("%s: sendto() only for udp socket!", self) end -- check address if not addr or not port then return -1, string.format("%s: sendto empty address!", self) end -- get data address and size for bytes and string if type(data) == "string" then data = bytes(data) end local datasize = data:size() local dataaddr = data:caddr() -- init start and last opt = opt or {} local start = opt.start or 1 local last = opt.last or datasize if start < 1 or start > datasize then return -1, string.format("%s: invalid start(%d)!", self, start) end if last < start - 1 or last > datasize + start - 1 then return -1, string.format("%s: invalid last(%d)!", self, last) end -- send it local send = 0 local wait = false local errors = nil if opt.block then while true do send, errors = io.socket_sendto(self:cdata(), dataaddr + start - 1, last + 1 - start, addr, port, self:family()) if send == 0 and not wait then local events, waiterrs = _instance.wait(self, socket.EV_SEND, opt.timeout or -1) if events == socket.EV_SEND then wait = true else errors = waiterrs break end else break end end else send, errors = io.socket_sendto(self:cdata(), dataaddr + start - 1, last + 1 - start, addr, port, self:family()) if send < 0 and errors then errors = string.format("%s: %s", self, errors) end end return send, errors end -- receive UDP data from peer -- -- @param buff the buffer to receive data -- @param size the max receive size -- @param opt the options (optional) -- @return the real received size, the peer address, the peer port -- function _instance:recvfrom(buff, size, opt) assert(buff) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- only for udp if self:type() ~= socket.UDP then return -1, string.format("%s: sendto() only for udp socket!", self) end -- check buffer size = size or buff:size() if buff:size() < size then return -1, string.format("%s: too small buffer!", self) end -- check size if size == 0 then return 0 elseif size == nil or size < 0 then return -1, string.format("%s: invalid size(%d)!", self, size) end -- init start in buffer opt = opt or {} local start = opt.start or 1 local pos = start - 1 if start >= buff:size() or start < 1 then return -1, string.format("%s: invalid start(%d)!", self, start) end -- recv it local recv = 0 local wait = false local data_or_errors = nil if opt.block then while true do recv, data_or_errors, addr, port = io.socket_recvfrom(self:cdata(), buff:caddr() + pos, math.min(buff:size() - pos, size)) if recv > 0 then data_or_errors = buff:slice(start, recv) break elseif recv == 0 and not wait then local events, waiterrs = _instance.wait(self, socket.EV_RECV, opt.timeout or -1) if events == socket.EV_RECV then wait = true else recv = -1 data_or_errors = waiterrs break end else break end end else recv, data_or_errors, addr, port = io.socket_recvfrom(self:cdata(), buff:caddr() + pos, math.min(buff:size() - pos, size)) if recv > 0 then data_or_errors = buff:slice(start, recv) end end if recv < 0 and data_or_errors then data_or_errors = string.format("%s: %s", self, data_or_errors) end return recv, data_or_errors, addr, port end -- wait for socket events -- -- @param events the events to wait, e.g. socket.EV_RECV, socket.EV_SEND -- @param timeout the timeout in milliseconds, -1 for infinite -- @return the received events, or 0 on timeout -- function _instance:wait(events, timeout) -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return -1, errors end -- wait events local result = -1 local errors = nil if scheduler:co_running() then result, errors = scheduler:poller_wait(self, events, timeout or -1) else result, errors = io.socket_wait(self:cdata(), events, timeout or -1) end if result < 0 and errors then errors = string.format("%s: %s", self, errors) end return result, errors end -- kill socket function _instance:kill() -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return false, errors end -- kill it io.socket_kill(self:cdata()) return true end -- close the socket -- -- @return true on success -- function _instance:close() -- ensure opened local ok, errors = self:_ensure_opened() if not ok then return false, errors end -- cancel socket events from the scheduler if scheduler:co_running() then ok, errors = scheduler:poller_cancel(self) if not ok then return false, errors end end -- close it ok = io.socket_close(self:cdata()) if ok then self._SOCK = nil end return ok end -- ensure the socket is opened function _instance:_ensure_opened() if not self:cdata() then return false, string.format("%s: has been closed!", self) end return true end -- tostring(socket) function _instance:__tostring() local rawfd = self:cdata() and self:rawfd() or "closed" local types = {"tcp", "udp", "icmp"} return string.format("", types[self:type()], self:family() == socket.IPV6 and "6" or "4", rawfd) end -- gc(socket) function _instance:__gc() if self:cdata() and io.socket_close(self:cdata()) then self._SOCK = nil end end -- trace socket for profile(stuck,trace)? function socket._is_tracing_socket() local is_tracing = socket._IS_TRACING_SOCKET if is_tracing == nil then local profile = os.getenv("XMAKE_PROFILE") if profile then profile = profile:trim() if profile == "trace" or profile == "stuck" then is_tracing = true end end is_tracing = is_tracing or false socket._IS_TRACING_SOCKET = is_tracing end return is_tracing end -- open a socket -- -- @param socktype the socket type, e.g. tcp, udp, icmp -- @param family the address family, e.g. ipv4, ipv6 -- -- @return the socket instance -- function socket.open(socktype, family) socktype = socktype or socket.TCP family = family or socket.IPV4 local sock, errors = io.socket_open(socktype, family) if sock then return _instance.new(socktype, family, sock) else return nil, errors or string.format("failed to open socket(%s/%s)!", socktype, family) end end -- open tcp socket -- -- @param opt the options, e.g. {family = socket.IPV6} -- @return the socket instance, or nil and error info -- function socket.tcp(opt) opt = opt or {} return socket.open(socket.TCP, opt.family or socket.IPV4) end -- open udp socket -- -- @param opt the options, e.g. {family = socket.IPV6} -- @return the socket instance, or nil and error info -- function socket.udp(opt) opt = opt or {} return socket.open(socket.UDP, opt.family or socket.IPV4) end -- open unix domain socket -- -- @return the socket instance, or nil and error info -- function socket.unix(opt) return socket.open(socket.TCP, socket.UNIX) end -- open and bind tcp socket -- -- @param addr the bind address, e.g. "0.0.0.0", "127.0.0.1" -- @param port the bind port -- @param opt the options (optional) -- @return the bound socket, or nil and error info -- function socket.bind(addr, port, opt) local sock, errors = socket.tcp(opt) if not sock then return nil, errors end local ok, errors = sock:bind(addr, port) if not ok then sock:close() return nil, string.format("bind %s:%s failed, %s!", addr, port, errors or "unknown reason") end return sock end -- open and bind tcp socket from the unix address -- -- @param addr the unix socket path -- @param opt the options (optional) -- @return the bound socket, or nil and error info -- function socket.bind_unix(addr, opt) local sock, errors = socket.unix(opt) if not sock then return nil, errors end local ok, errors = sock:bind_unix(addr, opt) if not ok then sock:close() return nil, string.format("bind unix://%s failed, %s!", addr, errors or "unknown reason") end return sock end -- open and connect tcp socket -- -- @param addr the server address -- @param port the server port -- @param opt the options (optional) -- @return the connected socket, or nil and error info -- function socket.connect(addr, port, opt) local sock, errors = socket.tcp(opt) if not sock then return nil, errors end local ok, errors = sock:connect(addr, port, opt) if ok <= 0 then sock:close() return nil, errors end return sock end -- open and connect tcp socket from the unix address -- -- @param addr the unix socket path -- @param opt the options (optional) -- @return the connected socket, or nil and error info -- function socket.connect_unix(addr, opt) local sock, errors = socket.unix(opt) if not sock then return nil, errors end local ok, errors = sock:connect_unix(addr, opt) if ok <= 0 then sock:close() return nil, errors end return sock end -- return module return socket