Anyway, it kinda works now. Shitloads of FIXMEs and TODOs, but it's working some of the time now, on some types of requests. :-)
Instead of forking off a thread per client (which would've been easy, but I'm a sucker for pain), I did all async IO with callbacks. And because .NET's async IO is done with threads (thread pools), the callback can be done in another thread, I get to deal with synchronization issues as well! More fun.
At this point you're probably thinking: uh, if async IO is done with threads, why didn't you just use threads in the first place with blocking IO? I wondered the same after discovering that, and still am a little, but I observed that it's really efficient with its thread pools. There isn't a thread for each connection... it just uses a pooled thread to do IO whenever necessary. And mono is even using Linux 2.6 syscalls... saw it doing futex(). Go mono!
Um, C# wizards.... anybody know the interactions between doing async IO (BeginWrite) on a BufferedStream wrapping a NetworkStream? The callback never seems to be run after myBufferedStream.BeginWrite(...). I'd expect the BufferedSteam to at least invoke my callback saying, "Yo, I added your crap to my buffer, even if I'm not telling you whether or not I'm pushing it out to the NetworkStream yet." And I don't even get the callback(s) after doing myBufferedStream.Flush(). I mean, the whole program runs and I get the data in my test HTTP client, but I never got the callbacks to all my writes to the client. Mysterious.
Eh... I can probably just not use the BufferedStream class since I'm pretty much doing its job already, but I want to understand it.
Oh, and as an update to my earlier rant about working with strings without encodings, and doing regular expressions on buffers, ignoring but preserving 8 bit data: The Encoding class only has static instances of Encoding.ASCII, .UTF8, .UTF7 and some other Unicode-ish ones, so I didn't think to just do: Encoding.GetEncoding("ISO-8859-1"). Or any other encoding with 256 unique byte/char mappings. So I can take Mr. Anonymous Unknown8bit buffer, assume it's Latin-1, fiddle with it as a String ignoring high bytes, then go back to a byte buffer using the same encoding, even if it was actually Russian or something.
After learning C#'s networking and IO classes a bit more, next step is abstracting out the backend node selection class. (round robin vs. random vs. custom)