# HG changeset patch # User Tero Marttila # Date 1232133885 -7200 # Node ID c7295b72731ac8a6cf17e1d7b076c52ddb52e56c # Parent 306825786fba01f2af13c3d127574b1f78d482f6 documentation work on Network diff -r 306825786fba -r c7295b72731a src/Network/Address.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 -#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: diff -r 306825786fba -r c7295b72731a src/Network/Buffer.cc --- 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); diff -r 306825786fba -r c7295b72731a src/Network/Buffer.hh --- 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 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 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 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 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 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 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 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 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 diff -r 306825786fba -r c7295b72731a src/Network/Endpoint.hh --- 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 - #include -#else - #error "This network code won't compile on win32 :)" -#endif + +#include "../Error.hh" +#include "Platform.hh" #include @@ -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: diff -r 306825786fba -r c7295b72731a src/Network/Platform.h --- /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 + + // NetworkEndpoint + #include + #include + + // NetworkSocket + #include + #include + #include + #include + #define closesocket close + +#else + #error "This network code won't compile on win32 :)" +#endif + + +#endif diff -r 306825786fba -r c7295b72731a src/Network/Socket.cc --- 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; } diff -r 306825786fba -r c7295b72731a src/Network/Socket.hh --- 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 - #include - #include - #include - - #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 ©); + NetworkSocket &operator= (const NetworkSocket ©); /** * Reset bound+poll