documentation work on Network
authorTero Marttila <terom@fixme.fi>
Fri, 16 Jan 2009 21:24:45 +0200
changeset 399 c7295b72731a
parent 398 306825786fba
child 400 d64bf28c4340
documentation work on Network
src/Network/Address.hh
src/Network/Buffer.cc
src/Network/Buffer.hh
src/Network/Endpoint.hh
src/Network/Platform.h
src/Network/Socket.cc
src/Network/Socket.hh
--- a/src/Network/Address.hh	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Address.hh	Fri Jan 16 21:24:45 2009 +0200
@@ -1,22 +1,27 @@
 #ifndef NETWORK_ADDRESS_H
 #define NETWORK_ADDRESS_H
 
+/**
+ * @file
+ *
+ * Binary NetworkAddress
+ */
+
+#include "Platform.hh"
 #include "Endpoint.hh"
 
-/*
- * Platform-specific includes
- */
-#ifndef WIN32
-    // linux
-    #include <netinet/in.h>
-#else
-    #error "This network code won't compile on win32 :)"
-#endif
-
 /**
- * This represents a `struct sockaddr` as used by the socket API.
+ * A NetworkAddress represents a single network address in the form of a `struct sockaddr`, as used by the OS's socket
+ * API. A NetworkAddress's value may only be constructed/manipulated in sockaddr form, and the class then emulates the
+ * get_addrinfo (by building our own addrinfo struct value) and get_hostname()/get_service() (by using libc's
+ * getnameinfo() when the address is updated).
  *
- * It can be used like a NetworkEndpoint, but it's also suitable for use with recvfrom/sendto
+ * This means that a NetworkAddress can be used like a NetworkEndpoint (like a literal IP address), but can also be
+ * used in a more specifc fashion like NetworkSocket::send or NetworkSocket::accept, where a definitive address is
+ * required.
+ *
+ * XXX: currently, there is no way to form a NetworkAddress from a NetworkEndpoint for e.g. sending UDP packets without
+ * an associated TCP connection.
  */
 class NetworkAddress : public NetworkEndpoint {
     protected:
--- a/src/Network/Buffer.cc	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Buffer.cc	Fri Jan 16 21:24:45 2009 +0200
@@ -26,7 +26,8 @@
 void NetworkBufferBase::resize (size_t item_size) {
     size_t new_size = size;
 
-    // make sure that new_size isn't zero, because zero times two is zero, even if you do that in an infinite loop :)
+    // make sure that new_size isn't zero, because zero times two is zero, even if you try and calculate that in an
+    // infinite loop :)
     if (new_size == 0)
         new_size = 1;
 
@@ -36,14 +37,21 @@
     
     // grow if needed
     if (new_size != size) {
+        char *temp = buf;
+
         // realloc buffer
-        if ((buf = (char *) realloc((void *) buf, new_size)) == NULL)
+        if ((buf = (char *) realloc((void *) buf, new_size)) == NULL) {
+            // restore temp, but the buffer may be corrupted now
+            buf = temp;
+            
+            // raise error
             throw NetworkBufferError("realloc failed");
+        }
 
         // update size
         size = new_size;
 
-    } else if (new_size > (offset + item_size) * 4) {
+    } else if (size > (offset + item_size) * 4) {
         // XXX: shrink?
     }
 }
@@ -176,7 +184,8 @@
         
 bool NetworkBufferOutput::write_prefix (char *buf, uint16_t prefix) {
     uint16_t nval = htons(prefix);
-
+    
+    // XXX: not exception-safe
     push_write((char*) &nval, sizeof(uint16_t)); 
     push_write(buf, prefix);
 
@@ -187,6 +196,7 @@
 bool NetworkBufferOutput::write_prefix (char *buf, uint32_t prefix) {
     uint32_t nval = htonl(prefix);
 
+    // XXX: not exception-safe
     push_write((char*) &nval, sizeof(uint32_t)); 
     push_write(buf, prefix);
     
--- a/src/Network/Buffer.hh	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Buffer.hh	Fri Jan 16 21:24:45 2009 +0200
@@ -1,6 +1,12 @@
 #ifndef NETWORK_BUFFER_HH
 #define NETWORK_BUFFER_HH
 
+/**
+ * @file
+ *
+ * Buffering of network streams
+ */
+
 #include "Socket.hh"
 #include "../Error.hh"
 
@@ -20,7 +26,12 @@
 };
 
 /**
- * Base buffer-manipulation operations for buffered socket send/recv
+ * Base buffer-manipulation operations for buffered socket send/recv.
+ *
+ * This implements a simple linear in-memory buffer which can contain data that must be buffered for send/recv.
+ * New data can be written to the end of the buffer, and data can be read/discarded from the beginning.
+ *
+ * A buffer is associated with a NetworkSocket, and takes care of all send/recv activity.
  */
 class NetworkBufferBase {
     protected:
@@ -30,7 +41,7 @@
         /** The buffer itself */
         char *buf;
 
-        /** Buffer size and current read/write offset */
+        /** Buffer size and current write offset */
         size_t size, offset;
     
     public:
@@ -54,7 +65,11 @@
     protected:
         /**
          * Resize the buffer, allocating enough new space to hold <item_size> bytes at the end, and leaving any
-         * existing data at the beginning in-place
+         * existing data at the beginning in-place.
+         *
+         * This is done by doubling the buffer size until item_size fits, and then reallocing (if needed).
+         * Currently, the buffer is never shrunk, although it probably should be to adapt better to peak load (e.g.
+         * when a player joins and the map data is transmitted...).
          *
          * @param item_size the number of bytes that must fit at the end of the buffer
          */
@@ -63,6 +78,9 @@
         /**
          * Trim the buffer, discarding <prefix_size> bytes at the beginning. Updates offset to match.
          *
+         * Currently, this is implemented as a simple memmove() of all the remaining data in the buffer, which may be
+         * slow under some load patterns.
+         *
          * @param prefix_size the number of bytes to discard
          */
         void trim (size_t prefix_size);
@@ -70,6 +88,10 @@
 
 /**
  * Buffered prefix-len socket input
+ *
+ * This handles calling recv() on the socket, and is specialized to handle a stream of messages prefixed with a length
+ * header (uint16_t or uint32_t). Use the peek_data() method to receive these messages when NetworkSocket::sig_read() 
+ * indicates that more data is available.
  */
 class NetworkBufferInput : public NetworkBufferBase {
     public:
@@ -110,77 +132,46 @@
         // @} 
         
         /**
-         * This attempts to read a length-prefix of the given type (using peek_prefix), and then the associated data.
-         * If succesful, this sets prefix to the length of the data, and buf_ref to point at the data inside our buffer
-         * and returns true, else false.
+         * This attempts to locate a full message in the buffer (calling recv as nessecary), prefixed with a length
+         * prefix of the given type (using peek_prefix). If succesfull (we have a full message in the buffer), 
+         * \a prefix will be updated to the length of the message (not including the prefix), and \a buf_ref will be
+         * updated to point at the message of \a prefix bytes (not including the prefix) in our internal buffer memory.
+         * If a full message could not be received (recv would block), this will return false.
          *
-         * This will try and consume data from the buffer, or recv if needed.
+         * Once you have processed the message, call flush_data() to remove the unused message from the buffer.
          *
-         * @param prefix stores the data length here
-         * @param buf_ref stores a pointer to the data here
-         * @return bool true if we have the full data, false if we need to wait for more data on the socket
+         * @param prefix updated to the message length
+         * @param buf_ref updated to point at the message data
+         * @return bool true if we have a full message, false if we need to wait for more data on the socket
          *
          * @see peek_prefix
          * @see flush_data
          */
-        template <typename PrefixType> bool peek_data (PrefixType &prefix, char *&buf_ref) {
-            size_t missing = 0;
-            
-            do {    
-                // do we have the prefix?
-                if (peek_prefix(prefix)) {
-                    // do we already have the payload?
-                    if (offset >= sizeof(PrefixType) + prefix) {
-                        break;
-
-                    } else {
-                        missing = (sizeof(PrefixType) + prefix) - offset;
-                    }
-
-                } else {
-                    missing = sizeof(PrefixType);
-                }
-
-                // sanity-check
-                // XXX: a zero-prefix will trigger this
-                assert(missing);
-                
-                // try and read the missing data
-                if (try_read(missing) == false) {
-                    // if unable to read what we need, return zero.
-                    return false;
-                }
-                
-                // assess the situation again
-            } while (true);
-            
-            // update the buf_ref to point past the prefix-length
-            buf_ref = buf + sizeof(PrefixType);
-
-            // return
-            return true;
-        }
+        template <typename PrefixType> bool peek_data (PrefixType &prefix, char *&buf_ref);
           
         /**
-         * This flushes a prefix-length worth of data from the buffer, i.e. it first reads the prefix, and then trims
-         * the prefix and the data away. Don't call this unless you *know* that the buffer contains enough data.
+         * This flushes the current message from the buffer, as returned when peek_data returns true. It is a bug to
+         * call flush_data when there is no full message present, the behaviour is unspecified (most likely an
+         * assert()).
          *
          * @see peek_data
          */
-        template <typename PrefixType> void flush_data (void) {
-            PrefixType prefix;
-            
-            // we *must* have a valid prefix
-            if (!peek_prefix(prefix))
-                assert(false);
-
-            // trim the bytes out
-            trim(sizeof(PrefixType) + prefix);
-        }
+        template <typename PrefixType> void flush_data (void);
 };
 
 /**
- * Buffered prefix-len socket output
+ * Buffered prefix-len socket output.
+ *
+ * This handles calling send() on the socket, and is specialized to handle a stream of message prefixed with a length
+ * header (uint16_t or uint32_t). You can write messages using write_prefix(), and they will be buffered if needed.
+ * If write_prefix() returns true (socket buffer full), then you must register the socket for
+ * NetworkSocket::set_poll_write(), and call flush_write() once NetworkSocket::sig_write() is triggered. The socket's
+ * poll_write should be unregistered once flush_write returns false (no more buffered data remaining to be sent), or
+ * an many processor cycles will be wasted, for the socket will remain ready for write until its buffer fills up again.
+ *
+ * Data is not buffered needlessly; if the socket's buffer has room, write_prefix will not have to touch our internal
+ * buffer. In fact, write_prefix will rarely return false except under heavy network congestion with high levels of
+ * traffic on the socket.
  */
 class NetworkBufferOutput : public NetworkBufferBase {
     public:
@@ -191,7 +182,7 @@
 
     private:
         /**
-         * Write the given data to the socket, either now of later. 
+         * Write the given data to the socket, either now or later. 
          *
          * If our buffer is empty, fast-path the given buf_ptr directly to send(), then copy the remaining portion to
          * our buffer for later use with flush_write.
@@ -203,15 +194,22 @@
    
     public:    
         /**
-         * If we have data in our buffer, flush it out using send().
+         * If we have data in our buffer, flush it out using send(). This should be called once the socket indicates it
+         * is ready for write again after a call to write_prefix that returned false. Once this returns false, the
+         * NetworkSocket::set_poll_write() flag should be unset again to avoid needless calls to this.
          *
-         * @return true if there's still buffered data left to write, false otherwise
+         * @return true if there's still buffered data left to write, false otherwise (buffer is empty)
          */
         bool flush_write (void);
         
         // @{
         /**
-         * Write out the given data, writing first the prefix, and then the data itself, using push_write.
+         * Write out the given message, writing first the prefix, and then the data itself, using push_write.
+         *
+         * Returns true if the data was passed on to the socket API directly, false if a portion of it had to be
+         * buffered (if there is already data buffered, all subsequent write_prefix's will buffer the full message
+         * until our buffer is empty again). If this method returns false, you must register the socket for
+         * poll_write and call flush_write later.
          * 
          * @param buf the data to write
          * @param prefix the amount of data
@@ -222,4 +220,59 @@
         // @}
 };
 
+
+/*
+ * NetworkBufferInput template method implementation
+ */
+template <typename PrefixType> bool NetworkBufferOutput::peek_data (PrefixType &prefix, char *&buf_ref) {
+    size_t missing = 0;
+    
+    do {    
+        // do we have the prefix?
+        if (peek_prefix(prefix)) {
+            // do we already have the payload?
+            if (offset >= sizeof(PrefixType) + prefix) {
+                break;
+
+            } else {
+                missing = (sizeof(PrefixType) + prefix) - offset;
+            }
+
+        } else {
+            missing = sizeof(PrefixType);
+        }
+
+        // sanity-check (above >= and - should never happen like this)
+        assert(missing);
+        
+        // try and read the missing data
+        if (try_read(missing) == false) {
+            // if unable to read what we need, return zero.
+            return false;
+        }
+        
+        // assess the situation again
+    } while (true);
+    
+    // update the buf_ref to point past the prefix-length
+    buf_ref = buf + sizeof(PrefixType);
+
+    // return message
+    return true;
+}
+
+template <typename PrefixType> void NetworkBufferOutput::flush_data (void) {
+    PrefixType prefix;
+    
+    // we *must* have a valid prefix
+    if (!peek_prefix(prefix))
+        assert(false);
+
+    // ensure that we have the data to trim...
+    assert(offset >= sizeof(PrefixType) + prefix);
+
+    // trim the bytes out
+    trim(sizeof(PrefixType) + prefix);
+}
+
 #endif
--- a/src/Network/Endpoint.hh	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Endpoint.hh	Fri Jan 16 21:24:45 2009 +0200
@@ -1,18 +1,14 @@
 #ifndef NETWORK_ENDPOINT_HH
 #define NETWORK_ENDPOINT_HH
 
-#include "../Error.hh"
-
-/*
- * Platform-specific includes
+/**
+ * @file
+ *
+ * Textual NetworkEndpoint addresses
  */
-#ifndef WIN32
-    // linux
-    #include <sys/types.h>
-    #include <netdb.h>
-#else
-    #error "This network code won't compile on win32 :)"
-#endif
+
+#include "../Error.hh"
+#include "Platform.hh"
 
 #include <string>
 
@@ -22,12 +18,19 @@
 #endif
 
 /**
- * Length of a network address
+ * Length of a network address in text form
  */
 const socklen_t NETWORK_ADDRESS_LENGTH = INET6_ADDRSTRLEN;
 
 /**
- * We use ClanLib's IPAddress API, but with our own name
+ * NetworkEndpoint is mostly used to pass addresses from the (human) user to NetworkSocket's bind()/connect(). The 
+ * constructor accepts a textual service name (port) and hostname (literal IP adddress or hostname), and can then
+ * provide a list of resolved addresses for use by NetworkSocket (using the libc getaddrinfo). Additionally,
+ * methods/operators are defined for textual output of the address.
+ *
+ * It may be of value to note that the hostname/service is only interpreted by the get_addrinfo() method, which means
+ * that invalid/non-existant hostnames/services will only raise an error once the NetworkEndpoint is passed to
+ * NetworkSocket.
  */ 
 class NetworkEndpoint {
     protected:
@@ -98,7 +101,7 @@
 std::ostream& operator<< (std::ostream &s, const NetworkEndpoint &addr);
 
 /**
- *
+ * Errors raised by NetworkEndpoint, so e.g. DNS errors (hostname not found etc.)
  */
 class NetworkAddressError : public Error {
     protected:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Network/Platform.h	Fri Jan 16 21:24:45 2009 +0200
@@ -0,0 +1,30 @@
+#ifndef NETWORK_PLATFORM_H
+#define NETWORK_PLATFORM_H
+
+/**
+ * @file
+ *
+ * Takes care of platform-specific imports and #defines as regards the Network code
+ */
+
+#ifndef WIN32
+    // NetworkAddress
+    #include <netinet/in.h>
+    
+    // NetworkEndpoint
+    #include <sys/types.h>
+    #include <netdb.h>
+    
+    // NetworkSocket
+    #include <sys/types.h>
+    #include <sys/socket.h>
+    #include <unistd.h>
+    #include <fcntl.h>
+    #define closesocket close
+
+#else
+    #error "This network code won't compile on win32 :)"
+#endif
+
+
+#endif
--- a/src/Network/Socket.cc	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Socket.cc	Fri Jan 16 21:24:45 2009 +0200
@@ -47,7 +47,8 @@
 }
 
 void NetworkSocket::lazy_socket (int family, int socktype, int protocol) {
-    // if we already have a socket, exit
+    // if we already have a socket, good
+    // XXX: should we check family/socktype/protocol against sock_type?
     if (fd >= 0)
         return;
     
@@ -248,7 +249,8 @@
     // EAGAIN? 
     if (ret < 0) {
         // set want_write so we get a sig_write
-        want_write = true;
+        // XXX: this is the job of the user
+        // want_write = true;
 
         return 0;
     }
--- a/src/Network/Socket.hh	Fri Jan 16 01:05:34 2009 +0200
+++ b/src/Network/Socket.hh	Fri Jan 16 21:24:45 2009 +0200
@@ -1,30 +1,49 @@
 #ifndef NETWORK_SOCKET_HH
 #define NETWORK_SOCKET_HH
 
+/**
+ * @file
+ *
+ * Network sockets
+ */
+
 // forward-declare
 class NetworkSocket;
 
 #include "../Error.hh"
+#include "Platform.hh"
 #include "Address.hh"
 #include "Reactor.hh"
 
-/*
- * Platform-specific includes
- */
-#ifndef WIN32
-    // linux
-    #include <sys/types.h>
-    #include <sys/socket.h>
-    #include <unistd.h>
-    #include <fcntl.h>
-    
-    #define closesocket close
-#else
-    #error "This network code won't compile on win32 :)"
-#endif
-
 /**
- * We use ClanLib's Socket API, but with our own extensions...
+ * This is a socket class that wraps an OS socket filedescriptor and provides the more important socket operations
+ * as methods. The implementation aims to be address-family agnostic, and should thence work with both IPv6 and IPv4.
+ *
+ * Network addresses are abstracted into the NetworkEndpoint and derived NetworkAddress classes. The bind() and
+ * connect() methods accept a NetworkEndpoint as an argument, which allows them to handle hosts with multiple
+ * addresses (such as all dual-stack IPv6/IPv4 hosts). Other methods such as accept(), recv() and send() require a
+ * NetworkAddress, which encodes a single specific address. Note how these are also useable as arguments to
+ * connect()/bind().
+ *
+ * The constructor accepts family/socktype/protocol arguments (as passed to the socket() syscall) which can be used to
+ * limit which kinds of addresses/sockets will be used. Usually, family can be specified as AF_UNSPEC and socktype as
+ * either SOCK_STREAM or SOCK_DGRAM - this will let bind()/connect() pick the best IPv6/IPv4 address for use.
+ *
+ * Note however that a call to bind()/connect() can result to multiple calls to the socket() syscall - this interaction
+ * is slightly complicated. On a succesfull bind() operation, the resulting socket will be "locked down", meaning that
+ * a later connect() operation will use the same local socket - this restricts the remote address families that are valid.
+ *
+ * The behaviour of send/recv differs from the behaviour of the similarly named syscalls. On failure,
+ * NetworkSocketErrno is thrown, and on EOF, NetworkSocketEOFError. Zero is returned if the syscall returns EAGAIN - in
+ * other words - if a socket is nonblocking and the operation would block. Otherwise, the return value is the same as
+ * for the syscalls - the number of bytes send/received.
+ *
+ * NetworkSockets also support polling for non-blocking operations using NetworkReactor. A socket is associated with a
+ * specific reactor (passed to the constructor, defaults to NetworkReactor::current). This is inherited by accept()'d
+ * sockets. For read/write, NetworkSocket provides a sig_read()/sig_write() signal which will be fired if a socket is
+ * registered using set_poll_read(true)/set_poll_write(true) and the NetworkReactor::poll is run. The internal logic
+ * does not manipulate the poll states. Usually, sig_read will always be enabled (except to throttle incoming traffic),
+ * and sig_write should be enabled after send() returns zero (XXX: provide an automatic mechanism for this?).
  */
 class NetworkSocket {
     private:
@@ -51,7 +70,7 @@
         /** The file descriptor */
         int fd;
 
-        /** Our current type, intialized via constructor, but updated by lazy_socket */
+        /** Our current type as used for fd, intialized via constructor, but updated by lazy_socket */
         socket_type type;
 
         /** 
@@ -102,7 +121,9 @@
         ~NetworkSocket (void);
 
     private:
-        // XXX: nocopy
+        // nocopy
+        NetworkSocket (const NetworkSocket &copy);
+        NetworkSocket &operator= (const NetworkSocket &copy);
         
         /**
          * Reset bound+poll