The OpenD Programming Language

arsd.fibersocket

Fiber-based socket i/o built on Phobos' std.socket and Socket.select without any other dependencies.

More...

Public Imports

std.socket
public import std.socket;
Undocumented in source.

Members

Classes

FiberManager
class FiberManager

The FiberManager is responsible for running your socket event loop and dispatching events to your fibers. It is your main point of interaction with this library.

FiberSocket
class FiberSocket

Subclass of Phobos' socket that basically works the same way, except it yields back to the FiberManager when it would have blocked.

Functions

allowBroadcast
void allowBroadcast(Socket socket)

just because I forget how to enable this, trivial helper function

sendAll
ptrdiff_t sendAll(Socket s, const(void)[] data)

Convenience function to loop and send until it it all sent or an error occurs.

Detailed Description

This is meant to be a single-threaded event-driven basic network server.

1 void main() {
2 	auto fm = new FiberManager();
3 	// little tcp echo server
4 	// exits when it gets "QUIT" on the socket.
5 	Socket listener;
6 	listener = fm.listenTcp6(6660, (Socket conn) {
7 		while(true) {
8 			char[128] buffer;
9 			auto ret = conn.receive(buffer[]);
10 			// keeps the Phobos interface so...
11 			if(ret <= 0) // ...still need to check return values
12 				break;
13 			auto got = buffer[0 .. ret];
14 			if(got.length >= 4 && got[0 .. 4] == "QUIT") {
15 				listener.close();
16 				break;
17 			} else {
18 				conn.send(got);
19 			}
20 		}
21 		conn.close();
22 	});
23 
24 	// simultaneously listen for and echo UDP packets
25 	fm.makeFiber( () {
26 		auto sock = fm.bindUdp4(9999);
27 		char[128] buffer;
28 		Address addr;
29 		while(true) {
30 			auto ret = sock.receiveFrom(buffer[], addr);
31 			if(ret <= 0)
32 				break;
33 			import std.stdio;
34 			auto got = buffer[0 .. ret];
35 			// print it to the console
36 			writeln("Received UDP ", got);
37 			// send the echo
38 			sock.sendTo(got, addr);
39 
40 			if(got.length > 4 && got[0 .. 4] == "QUIT") {
41 				break; // stop processing udp when told to quit too
42 			}
43 		}
44 	}).call(); // need to call it the first time ourselves to get it started
45 
46 	// run the events. This keeps going until there are no more registered events;
47 	// so when all registered sockets are closed or abandoned.
48 	//
49 	// So this will return when both QUIT messages are received and all clients disconnect.
50 	import std.stdio;
51 	writeln("Entering.");
52 
53 	fm.run();
54 
55 	writeln("Exiting.");
56 }

Note that DNS address lookups here may still block the whole thread, but other methods on Socket are overridden in the subclass (FiberSocket) to yield appropriately, so you should be able to reuse most existing code that uses Phobos' Socket with little to no modification. However, since it keeps the same interface as the original object, remember you still need to check your return values!

There's two big differences:

  1. You should not modify the blocking flag on the Sockets. It is already set for you and changing it will... probably not hurt, but definitely won't help.
  2. You shouldn't construct the Sockets yourself, nor call connect or listen on them. Instead, use the methods in the FiberManager class. It will ensure you get the right objects initialized in the right way with the minimum amount of blocking.

    The listen family of functions accept a delegate that is called per each connection in a fresh fiber. The connect family of functions can only be used from inside an existing fiber - if you do it in a connection handler from listening, it is already set up. If it is from your main thread though, you'll get an assert error unless you make your own fiber ahead of time. FiberManager.makeFiber can construct one for you, or you can call new Fiber(...) from import core.thread.fiber yourself. Put all the work with the connection inside that fiber so the manager can do its work most efficiently.

There's several convenience functions to construct addresses for you too, or you may simply do getAddress or new InternetAddress and friends from std.socket yourself.

Conceptual Overview

A socket is a common programming object for communication over a network. Phobos has support for the basics and you can read more about that in my blog socket tutorial: http://dpldocs.info/this-week-in-d/Blog.Posted_2019_11_11.html

A lot of things describe fibers as lightweight threads, and that's not wrong, but I think that actually overcomplicates them. I prefer to think of a fiber as a function that can pause itself. You call it like a function, you write it like a function, but instead of always completing and returning, it can yield, which is putting itself on pause and returning to the caller. The caller then has a chance to resume the function when it chooses to simply by calling it again, and it picks up where it left off, or the caller can reset the fiber function to the beginning and start over.

Fiber-based async i/o thus isn't as complicated as it sounds. The basic idea is you just write an ordinary function in the same style as if you were doing linear, blocking i/o calls, but instead of actually blocking, you register a callback to be woken up when the call can succeed, then yield yourself. This callback you register is simply your own fiber resume method; the event loop picks up where you left off.

With Phobos sockets (and most Unix i/o functions), you then retry the operation that would have blocked and carry on because the callback is triggered when the operation is ready. If you're using another async system, like Windows' Overlapped I/O callbacks, it is actually even easier, since that callback happens when the operation has already completed. In those cases, you register the fiber's resume function as the event callback, then yield. When you wake up, you can immediately carry on.

When a fiber is woken up, it continues executing from the last yield call. Just think of yield as being a pause button you press.

Understanding how it works means you can translate any callback-based i/o system to use fibers, since it would always follow that same pattern: register the fiber resume method, then yield. If it is a callback when the operation is ready, try it again when you wake up (so right after yield, you can loop back to the call), or if it is a callback when the operation is complete, you can immediately use the result when you wake up (so right after yield, you use it).

How does the event loop work? How do you know what fiber runs next? See, this is where the "lightweight thread" explanation complicates things. With a thread, the operating system is responsible for scheduling them and might even run several simultaneously. Fibers are much simpler: again, think of them as just being a function that can pause itself. Like with an ordinary function, just one runs at a time (in your thread anyway, of course adding threads can complicate fibers like it can complicate any other function). Like with an ordinary function, YOU choose which one you want to call and when. And when a fiber yields, it is very much like an ordinary function returning - it passes control back to you, the caller. The only difference is the Fiber object remembers where the function was when it yielded, so you can ask it to pick up where it left off.

The event loop therefore doesn't look all that special. If you've used Socket.select before, you'll recognize most of it. (select can be tricky to use though, epoll based code is actually simpler and more efficient... but this module only wanted to use Phobos' std.socket on its own. Besides, select still isn't that complicated, is cross-platform, and performs well enough for most tasks anyway.) It has a list of active sockets that it adds to either a read or write set, it calls the select function, then it loops back over and handles the events, if set. The only special thing is the event handler resumes the fiber instead of some other action.

I encourage you to view the source of this file and try to follow along. It isn't terribly long and can hopefully help to introduce you to a new world of possibilities. You can use Fibers in other cases too, for example, the game I'm working on uses them in enemy scripts. It sets up their action, then yields and lets the player take their turn. When it is the computer's turn again, the script fiber resumes. Same principle, simple code once you get to know it.

Limitations

Socket.select has a limit on the number of pending sockets at any time, and since you have to loop through them each iteration, it can get slow with huge numbers of concurrent connections. I'd note that you probably will not see this problem, but it certainly can happen. Similarly, there's new allocations for each socket and virtual calls throughout, which, again, probably will be good enough for you, but this module is not C10K+ "web scale".

It also cannot be combined with other event loops in the same thread. But, since the FiberManager only uses the thread you give it, you might consider running it here and other things along side in their own threads.

Meta

History

Written December 26, 2020. First included in arsd-official dub release 9.1.

License

BSL-1.0, same as Phobos

Credits

vibe.d is the first time I recall even hearing of fibers and is the direct inspiration for this.