--- 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 ©);
+ NetworkSocket &operator= (const NetworkSocket ©);
/**
* Reset bound+poll