Saturday, February 27, 2010

In being dragged across the grocery store yet again! I spent some time contemplating what I was thinking about last night, as I was finishing up part of my games net code. Wouldn't it be practical, to just implement a simple Virtual File System? It would make adapting the code base to different uses easier, since pathes and I/O could then be defined through a set of VFS_routines, but on the downside, making it pluggable would push greater overhead on all those I/O routines at every use.

The zpkg, system input/output, and network modules present very similar interfaces. Main differences being that zpkg doesn't have write support (an unneeded non-trivial feature), and seeking between positions in a socket, just doesn't make the same sense as with local files. If a virtual file system layer was built on top of it, it would be rather easy to define a "Plugin" data structure providing the necessary function pointers as needed, and simply use a hash table to handle the mappings between paths. Of course that leads to the bugger of a look up operation \o/.

Really, most of the places where it matters wouldn't impact game play, since most I/O can be divided between startup phase, loading stuff, client/server communication, and shutdown phase; the chatter between client and server obviously being implemented directly on top of the networking module, and therefore a moot point. It would make it possible for the resource loading code to become more flexible at run time; i.e. being able to load game assets both out of zpkg files and local system files without a recompile or a  restrictive version of the VFS.

I think it would be worth while, as an added plus, it would even allow splitting the path argument to Zpkg_Open, and pulling out the interesting bits into the VFS adapter function, which would be replacing that feature of the zpkg module.

For today however, my primary goal is to port the networking code (almost) completed last night, from the BSD Sockets API over to the Windows Sockets API. That way I can replace the less appropriate network code in my RvS admin program with it, and save having to complicate its design to make the most of Qts networking API. All while improving my games code base ^_^.

Although WinSock was based on the old Berkeley interface, Winsock has arguably grown more over the last decade, then the Unix interface has over the last 20 years. Not that there was much need beyond adding IPv6 capability, which the common Unix interface already grew ages ago. I personally dislike both the Berkeley and Windows interfaces immensely, why? Because in my humble opinion, the proper way would have been something like:

int s;

if (s = open("/net/tcp/host:port", O_CREAT | O_RDRW | O_EXLOCK)) == -1) {
 perror("Unable to open a connection to host:port");

/* do usual stuff here, like read() or write() */

where /net would be an arbitrary mount point for a special file system, in which file system operations reflect their corresponding network operations. Flags for system calls like open() and fcntl() could have been suitably extended to cope, and others like accept() implemented as needed. In the light of things like FUSE, it would be even more interesting to do it that way today, then it would have been in the 1980s.

Instead of that simple form, whenever we want to create a socket: we have to setup suitable socket-specific data structures, which and how many depending on the operations to be conducted; common practice is to zero over the most important (sockaddr_in / sockaddr_in6) structures before using them, and leave it to the compilers optimizer whether it actually happens at run time; look up in the system manuals what pre processor definitions correspond to type of network connection we want (let's say TCP over IP) for the socket() call; initialise the field structures, using the same pre pre processor flags, and even converting details like the port # into suitable formats, all by ourselves. After which we might finally get around to doing the socket I/O ourselves, pardoning any intervening system calls needed for your particular task.

 * Assumes open flags in previous theoretical example corresponded to a connect
 * operation, rather then a bind() and listen() operation. Like wise for the
* sake of terseness, I'm "Skipping" support for rather then IPs.

int s;
struct sockaddr_in addr;

memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, "", &addr.sin_addr) != 1) {
 /* handle AF_INET, address parsing, or unknown errors here.
  * error is indicated by return value.

if ((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
 perror("Unable to create socket");
 goto CLEANUP;
/* use system calls and a data structures as needed to setup things as desired */

if (connect(s, (const struct sockaddr *)&addr,
     sizeof(struct sockaddr_in)) == -1)
 perror("Unable to connect socket");
 goto CLEANUP;

/* do I/O here: send/recev, or read/write */

 shutdown(s, SHUT_RDWR);

I reckon the Berkeley interface was the right choice, for portability between systems (including non-unix ones; making it easy to write stuff like WinSock), and probably easier to graft onto the 4.3BSD kernel, but it's damn inconvenient for a UNIX programmer. That ain't changed after about 25-30 years.

Oh wells, at least it works pretty darn near every where, give or take a few kinks.

No comments:

Post a Comment