lib/socket: intro `TCPSocket`
[nit.git] / lib / socket / socket.nit
index c28a354..7e9018f 100644 (file)
 # Socket services
 module socket
 
-import socket_c
+private import socket_c
 intrude import standard::stream
 
-# A general TCP socket, either a `TCPStream` or a `TCPServer`
+# A general Socket, either TCP or UDP
 abstract class Socket
 
        # Underlying C socket
-       private var socket: NativeSocket is noinit
+       private var native: NativeSocket is noinit
+
+       # Is this socket closed?
+       var closed = false
+
+end
+
+# A general TCP socket, either a `TCPStream` or a `TCPServer`
+abstract class TCPSocket
+       super Socket
 
        # Port used by the socket
        var port: Int
@@ -33,17 +42,14 @@ abstract class Socket
        #
        # Formatted as xxx.xxx.xxx.xxx.
        var address: String is noinit
-
-       # Is this socket closed?
-       var closed = false
 end
 
 # Simple communication stream with a remote socket
 class TCPStream
-       super Socket
-       super BufferedIStream
-       super OStream
-       super PollableIStream
+       super TCPSocket
+       super BufferedReader
+       super Writer
+       super PollableReader
 
        # Real canonical name of the host to which `self` is connected
        var host: String
@@ -57,36 +63,60 @@ class TCPStream
        # Creates a socket connection to host `host` on port `port`
        init connect(host: String, port: Int)
        do
-               _buffer = new FlatBuffer
+               _buffer = new NativeString(1024)
                _buffer_pos = 0
-               socket = new NativeSocket.socket(new NativeSocketAddressFamilies.af_inet,
+               native = new NativeSocket.socket(new NativeSocketAddressFamilies.af_inet,
                        new NativeSocketTypes.sock_stream, new NativeSocketProtocolFamilies.pf_null)
-               if socket.address_is_null then
+               if native.address_is_null then
+                       end_reached = true
+                       closed = true
+                       return
+               end
+               if not native.setsockopt(new NativeSocketOptLevels.socket, new NativeSocketOptNames.reuseaddr, 1) then
                        end_reached = true
                        closed = true
                        return
                end
-               socket.setsockopt(new NativeSocketOptLevels.socket, new NativeSocketOptNames.reuseaddr, 1)
-               var hostname = socket.gethostbyname(host)
-               addrin = new NativeSocketAddrIn.with_hostent(hostname, port)
 
-               address = addrin.address
-               init(addrin.port, hostname.h_name)
+               var hostent = sys.gethostbyname(host.to_cstring)
+               if hostent.address_is_null then
+                       # Error in name lookup
+                       var err = sys.h_errno
+                       last_error = new IOError(err.to_s)
+
+                       closed = true
+                       end_reached = true
+
+                       return
+               end
+
+               addrin = new NativeSocketAddrIn
+               addrin.fill_from_hostent hostent
+               addrin.port = port
+
+               address = addrin.address.to_s
+               init(addrin.port, hostent.h_name.to_s)
 
                closed = not internal_connect
                end_reached = closed
+               if closed then
+                       # Connection failed
+                       last_error = new IOError(errno.strerror)
+               end
+
+               prepare_buffer(1024)
        end
 
        # Creates a client socket, this is meant to be used by accept only
        private init server_side(h: SocketAcceptResult)
        do
-               _buffer = new FlatBuffer
-               _buffer_pos = 0
-               socket = h.socket
+               native = h.socket
                addrin = h.addr_in
-               address = addrin.address
+               address = addrin.address.to_s
 
                init(addrin.port, address)
+
+               prepare_buffer(1024)
        end
 
        redef fun poll_in do return ready_to_read(0)
@@ -98,7 +128,7 @@ class TCPStream
        # timeout : Time in milliseconds before stopping listening for events on this socket
        private fun pollin(event_types: Array[NativeSocketPollValues], timeout: Int): Array[NativeSocketPollValues] do
                if end_reached then return new Array[NativeSocketPollValues]
-               return socket.socket_poll(new PollFD(socket.descriptor, event_types), timeout)
+               return native.socket_poll(new PollFD(native.descriptor, event_types), timeout)
        end
 
        # Easier use of pollin to check for something to read on all channels of any priority
@@ -106,8 +136,8 @@ class TCPStream
        # timeout : Time in milliseconds before stopping to wait for events
        fun ready_to_read(timeout: Int): Bool
        do
-               if _buffer_pos < _buffer.length then return true
-               if eof then return false
+               if _buffer_pos < _buffer_length then return true
+               if end_reached then return false
                var events = [new NativeSocketPollValues.pollin]
                return pollin(events, timeout).length != 0
        end
@@ -133,49 +163,74 @@ class TCPStream
        private fun internal_connect: Bool
        do
                assert not closed
-               return socket.connect(addrin) >= 0
+               return native.connect(addrin) >= 0
        end
 
        # If socket.end_reached, nothing will happen
-       redef fun write(msg: Text)
+       redef fun write(msg)
+       do
+               if closed then return
+               native.write(msg.to_cstring, msg.length)
+       end
+
+       redef fun write_byte(value)
        do
                if closed then return
-               socket.write(msg.to_s)
+               native.write_byte value
+       end
+
+       redef fun write_bytes(bytes) do
+               if closed then return
+               var s = bytes.to_s
+               native.write(s.to_cstring, s.length)
        end
 
        fun write_ln(msg: Text)
        do
-               if end_reached then return
-               write(msg.to_s)
-               write("\n")
+               if closed then return
+               write msg.to_s
+               write "\n"
        end
 
        redef fun fill_buffer
        do
-               _buffer.clear
-               _buffer_pos = 0
                if not connected then return
-               var read = socket.read
-               if read.length == 0 then
+
+               var read = native.read(_buffer, _buffer_capacity)
+               if read == -1 then
                        close
                        end_reached = true
                end
-               _buffer.append(read)
+
+               _buffer_length = read
+               _buffer_pos = 0
+       end
+
+       fun enlarge(len: Int) do
+               if _buffer_capacity >= len then return
+               _buffer_capacity = len
+
+               var ns = new NativeString(_buffer_capacity)
+               _buffer.copy_to(ns, _buffer_length - _buffer_pos, _buffer_pos, 0)
+               _buffer = ns
        end
 
        redef fun close
        do
                if closed then return
-               if socket.close >= 0 then
+               if native.close >= 0 then
                        closed = true
+                       end_reached = true
                end
        end
 
        # Send the data present in the socket buffer
        fun flush
        do
-               socket.setsockopt(new NativeSocketOptLevels.tcp, new NativeSocketOptNames.tcp_nodelay, 1)
-               socket.setsockopt(new NativeSocketOptLevels.tcp, new NativeSocketOptNames.tcp_nodelay, 0)
+               if not native.setsockopt(new NativeSocketOptLevels.tcp, new NativeSocketOptNames.tcp_nodelay, 1) or
+                  not native.setsockopt(new NativeSocketOptLevels.tcp, new NativeSocketOptNames.tcp_nodelay, 0) then
+                       closed = true
+               end
        end
 end
 
@@ -183,19 +238,27 @@ end
 #
 # Create streams to communicate with clients using `accept`.
 class TCPServer
-       super Socket
+       super TCPSocket
 
        private var addrin: NativeSocketAddrIn is noinit
 
        # Create and bind a listening server socket on port `port`
        init
        do
-               socket = new NativeSocket.socket(new NativeSocketAddressFamilies.af_inet,
+               native = new NativeSocket.socket(new NativeSocketAddressFamilies.af_inet,
                        new NativeSocketTypes.sock_stream, new NativeSocketProtocolFamilies.pf_null)
-               assert not socket.address_is_null
-               socket.setsockopt(new NativeSocketOptLevels.socket, new NativeSocketOptNames.reuseaddr, 1)
-               addrin = new NativeSocketAddrIn.with(port, new NativeSocketAddressFamilies.af_inet)
-               address = addrin.address
+               assert not native.address_is_null
+               if not native.setsockopt(new NativeSocketOptLevels.socket, new NativeSocketOptNames.reuseaddr, 1) then
+                       closed = true
+                       return
+               end
+
+               addrin = new NativeSocketAddrIn
+               addrin.family = new NativeSocketAddressFamilies.af_inet
+               addrin.port = port
+               addrin.address_any
+
+               address = addrin.address.to_s
 
                # Bind it
                closed = not bind
@@ -205,14 +268,14 @@ class TCPServer
        #
        # Returns whether the socket has been be bound.
        private fun bind: Bool do
-               return socket.bind(addrin) >= 0
+               return native.bind(addrin) >= 0
        end
 
        # Sets the socket as ready to accept incoming connections, `size` is the maximum number of queued clients
        #
        # Returns `true` if the socket could be set, `false` otherwise
        fun listen(size: Int): Bool do
-               return socket.listen(size) >= 0
+               return native.listen(size) >= 0
        end
 
        # Accepts an incoming connection from a client
@@ -225,7 +288,7 @@ class TCPServer
        fun accept: nullable TCPStream
        do
                assert not closed
-               var native = socket.accept
+               var native = native.accept
                if native == null then return null
                return new TCPStream.server_side(native)
        end
@@ -236,7 +299,7 @@ class TCPServer
                # We use the opposite from the native version as the native API
                # is closer to the C API. In the Nity API, we use a positive version
                # of the name.
-               socket.non_blocking = not value
+               native.non_blocking = not value
        end
 
        # Close this socket
@@ -245,36 +308,54 @@ class TCPServer
                # FIXME unify with `SocketStream::close` when we can use qualified names
 
                if closed then return
-               if socket.close >= 0 then
+               if native.close >= 0 then
                        closed = true
                end
        end
 end
 
+# A simple set of sockets used by `SocketObserver`
 class SocketSet
-       var sset = new NativeSocketSet
-       fun set(s: TCPSocket) do sset.set(s.socket) end
-       fun is_set(s: TCPSocket): Bool do return sset.is_set(s.socket) end
-       fun zero do sset.zero end
-       fun clear(s: TCPSocket) do sset.clear(s.socket) end
+       private var native = new NativeSocketSet
+
+       init do clear
+
+       # Add `socket` to this set
+       fun add(socket: Socket) do native.set(socket.native)
+
+       # Remove `socket` from this set
+       fun remove(socket: Socket) do native.clear(socket.native)
+
+       # Does this set has `socket`?
+       fun has(socket: Socket): Bool do return native.is_set(socket.native)
+
+       # Clear all sockets from this set
+       fun clear do native.zero
 end
 
+# Service class to manage calls to `select`
 class SocketObserver
-       private var observer: NativeSocketObserver
-       var readset: nullable SocketSet = null
-       var writeset: nullable SocketSet = null
-       var exceptset: nullable SocketSet = null
-       init(read :Bool, write :Bool, except: Bool)
-       do
-               if read then readset = new SocketSet
-               if write then writeset = new SocketSet
-               if except then exceptset = new SocketSet
-               observer = new NativeSocketObserver
+       private var native = new NativeSocketObserver
+
+       var read_set: nullable SocketSet = null
+
+       var write_set: nullable SocketSet = null
+
+       var except_set: nullable SocketSet = null
+
+       init(read: Bool, write: Bool, except: Bool)
+       is old_style_init do
+               if read then read_set = new SocketSet
+               if write then write_set = new SocketSet
+               if except then except_set = new SocketSet
        end
-       fun select(max: TCPSocket, seconds: Int, microseconds: Int): Bool
+
+       fun select(max: Socket, seconds: Int, microseconds: Int): Bool
        do
+               # FIXME this implementation (see the call to nullable attributes below) and
+               # `NativeSockectObserver::select` is not stable.
+
                var timeval = new NativeTimeval(seconds, microseconds)
-               return observer.select(max.socket, readset.sset, writeset.sset, readset.sset, timeval) > 0
+               return native.select(max.native, read_set.native, write_set.native, except_set.native, timeval) > 0
        end
 end
-