Looking at the networking in Blackthorn, I'm still interested in
pursuing some portions of my unet library [1], but I'm not as married
to the UDP-only model that I had started for it.
[1]: http://nklein.com/software/unet/
Below is a brief overview/description of how I would envision the unet
library working. A good portion of something similar is already done
with UDP sockets. After a few days thought, I'm seeing how TCP could
work, too. I'd much appreciate hearing what you folks think about it
conceptually and whether it's the sort of network plumbing that
Blackthorn would want.
Create a unet instance to track the communications.
(make-instance 'unet :port port-number :protocol protocol :connection-
policy connection-policy)
If a `port-number` is specified, then incoming connections will be
allowed.
The `protocol` would be either `:reliable` or `:datagram`.
I'm not too fond of calling this class `unet`. Maybe `session`?
The
`connection-policy` would be a function which takes two arguments (the
unet instance and a peer (described later)). The `connection-policy`
function is called when a new peer tries to connect. The function
could then add the peer to various channels (described next) or close
the connection or what-have-you. The default `connection-policy`
would add the peer to all channels on the unet instance.
Next, create some number of channels to add to unet with the add-
channel method.
(add-channel unet (make-instance 'channel :channel-id channel-
id :peer-policy peer-policy))
(list-channels unet) => (channel1 channel2 ...)
(remove-channel unet channel)
The `peer-policy` would specify what to do when someone who is not
already part of a given channel sends a message on the channel. It
would be a function that takes two arguments (the channel and the peer
(described next)) and decides what to do with messages from that peer
on this channel. The default would be to add that peer to the
channel, but one could put them on an ignore list where any messages
they send for this channel are discarded, close the connection with
that peer altogether, what-have-you.... (actually, maybe the default
should be to add him to the ignore list since the connection policy
coulda added him to this channel if it had so desired).
One can retrieve a single message from a channel or retrieve all of
the messages from a given channel:
(get-message channel :check check-mode :timeout timeout)
=> (values message peer-address)
(get-all-messages channel :check check-mode :timeout timeout)
=> ((message1 peer-address1) (message2 peer-address2) ...)
The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.
Should there be a message struct/class that contains the
message data and the peer address?
I would expect the most common sort of loop would be something like:
(check-for-messages unet :check :if-empty :timeout :immediately)
... do some stuff ...
(mapc #'process-chat-message (get-all-messages chat-channel))
... do some other stuff ...
(mapc #'process-map-message (get-all-messages map-channel))
... do some other stuff ...
(bwhen (mp (get-message game-phase-channel))
(process-game-phase-message (first mp)))
Thoughts/complaints/suggestions/SEGA?
Thanks,
Patrick
On Thu, Jun 23, 2011 at 4:06 PM, Patrick Stein <p...@nklein.com> wrote:Create a unet instance to track the communications.
(make-instance 'unet :port port-number :protocol protocol :connection-
policy connection-policy)
If a `port-number` is specified, then incoming connections will be
allowed.If you don't specify port-number, then presumably you want to do outgoing connections? But if so then how do you connect?
I'm a little confused by the concept of channels. I think in the typical server-client case, the server will either want to send a message to a specific client, or will broadcast something to all clients. But I can see some other use cases, e.g. a multiplayer strategy team where you have one channel per team.
In Blackthorn, each machine keeps a symbolic mapping from node IDs to sockets. In a way, my node IDs are similar to channels where each channel has one peer. I even create a "broadcast" pseudo ID for sending to all hosts. But the key difference is that the mapping lives completely locally, and thus there is no way a remote host could send to (or even know about) any of the "channels" a local host is connected to. Maybe setting up many-to-many connections like this is more work, as each client needs it's own set of mappings. But the one-to-many case is entirely manageable.
(check-for-messages unet :check check-mode :timeout timeout)
(list-channels-with-messages unet :check check-mode :timeout
timeout)
I don't understand how this one is different from check-for-messages.
(get-message channel :check check-mode :timeout timeout)
=> (values message peer-address)
(get-all-messages channel :check check-mode :timeout timeout)
=> ((message1 peer-address1) (message2 peer-address2) ...)
The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.What exactly does ":never" mean? The timeout parameter is critical, but I can't think of a use case for check-mode.
Should there be a message struct/class that contains the
message data and the peer address?Or multiple return values.
Should a physical address be abstracted behind a symbolic identifier? Vectors and strings are not very nice as hash keys, which is a common enough operation to do with a remote address.
I think the API is missing some sort of method for handling disconnects. Any network operation could potentially result in a disconnect, and games usually have game-specific cleanup that needs to be done when this happens.
(check-for-messages unet :check check-mode :timeout timeout)(list-channels-with-messages unet :check check-mode :timeout
timeout)I don't understand how this one is different from check-for-messages.Ah... I was assuming 'check-for-messages' would just return a boolean as to whether there are some messages available on some channels. The `list-channels-with-messages` would return a list of all of the channels that have messages. Neither of these would actually return a message. They'd just let you know that there are messages available on some channel or which channels there are messages available on.
(get-message channel :check check-mode :timeout timeout)
=> (values message peer-address)
(get-all-messages channel :check check-mode :timeout timeout)
=> ((message1 peer-address1) (message2 peer-address2) ...)
The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.What exactly does ":never" mean? The timeout parameter is critical, but I can't think of a use case for check-mode.Suppose that I just started up and I called:(check-for-messages unet :check :always :timeout 0.1)This would wait 100ms for data to come in on any of the sockets that the unet instance knows about. It would return 't' if it had gotten some messages and put them on the channel queues.Now, suppose a bit later, we again call:(check-for-messages unet :check :always :timeout 0.1)This would again wait up to 100ms trying to read data from some socket that the unet instance knows about. It would return 't' if it got some new messages *or* if there were still some messages that weren't retrieved by `get-message` left over from the previous call.Now, suppose a bit later, we call with :never...(check-for-messages unet :check :never)This would return 't' if there are some messages still waiting in some channel queues.
Now, suppose a bit later, we call with :if-empty(check-for-messages unet :check :if-empty :timeout 0.1)If there are still some messages waiting in some channel queues, this would return 't' immediately. Otherwise, it does an :always.
And, multiple-return values is what I have as the return for `get-message`, but it is trickier with `get-all-messages`. I suppose it could return two lists.... one of messages and one of who sent it.Should there be a message struct/class that contains the
message data and the peer address?Or multiple return values.
Should a physical address be abstracted behind a symbolic identifier? Vectors and strings are not very nice as hash keys, which is a common enough operation to do with a remote address.The peer struct that I have reduces the hostname + port to a 32-bit IP address and a 16-bit IP address stored together in a 48-bit integer effectively. It makes a decent hash key. And, you can construct it once and tuck it into a variable if you want to refer to it later.True, that. There certainly should be a disconnect policy, too.I think the API is missing some sort of method for handling disconnects. Any network operation could potentially result in a disconnect, and games usually have game-specific cleanup that needs to be done when this happens.Hope this cleared up some stuff,Patrick
I suppose that I could see it debatable either way. At the very least, I'd think that under the hood there would be sort-of two channels... one for any communication that has to happen under-the-hood/behind-the-scenes/entirely-in-the-network-library and one for messages that the application wants to send to the application on the other side.It still seems kind of magical, e.g. in the current system it seems like the channels all live locally, which adds some possibility of error if the server and client have different names for things.
For example, take the team chat example. The server creates two channels, :team-chat-A and :team-chat-B. A given client is on team A, so it needs to be on :team-chat-A. But the client can't even know the name of the channel until the server tells it what that is. I think this is where things get confusing (e.g. "why isn't the client automatically in the channel after the server adds it?") and a potential place where new users could make mistakes with the API.
You could still have a single :team-chat channel and that the idea of their being multiple teams is kept in the game logic so the game logic knows that if it got a team chat message from user X then it needs to repeat that to all users on team A (except user X) on the team chat channel.Alternately, you could have both a :team-chat-A channel and a :team-chat-B channel. The server sends clients on team A a message (on :team-chat-A channel or on some other :server-directive channel or something) telling them to use :team-chat-A instead of :team-chat-B. The server doesn't add any of the users on team A to the :team-chat-B channel and thus implicitly ignores anything they send on that channel and never sends them something when it sends stuff to that channel (because they aren't a part of the channel).
I think the idea of inboxes is good, and the idea of tagging messages for which inbox they should go into is good. I'm not sure that the idea of "only some clients are allowed to use certain tags" is good, because it starts sounding more like IRC where "channels" exist in some global context and joining is a one-step operation (and e.g. other clients don't need to do anything when a sibling joins, even though the server has some work to do). The IRC analogy was actually the first thing I thought of when I saw channels, which is obviously not a very good match for what they actually do
.(check-for-messages unet :check check-mode :timeout timeout)(list-channels-with-messages unet :check check-mode :timeout
timeout)I don't understand how this one is different from check-for-messages.Ah... I was assuming 'check-for-messages' would just return a boolean as to whether there are some messages available on some channels. The `list-channels-with-messages` would return a list of all of the channels that have messages. Neither of these would actually return a message. They'd just let you know that there are messages available on some channel or which channels there are messages available on.I guess the advantage is that check-for-messages doesn't have to cons a return value? Otherwise I'd just capture and test the result of (list-channels-with-messages ...) and just do nothing when nil was returned.
Now, suppose a bit later, we call with :never...(check-for-messages unet :check :never)This would return 't' if there are some messages still waiting in some channel queues.How is this different from(check-for-messages unet :check :always :timeout :immediately)
it seems to me that this captures the exact meaning of(check-for-messages unet :check :never :timeout :immediately)
because either way no waiting can occur. And the other options for timeout, e.g.(check-for-messages unet :check :never :timeout 1.0)
Now, suppose a bit later, we call with :if-empty(check-for-messages unet :check :if-empty :timeout 0.1)If there are still some messages waiting in some channel queues, this would return 't' immediately. Otherwise, it does an :always.What is this use case for this?
In multi threaded mode, then I'd use your wrapper around this and never even see this function call.
And, multiple-return values is what I have as the return for `get-message`, but it is trickier with `get-all-messages`. I suppose it could return two lists.... one of messages and one of who sent it.Should there be a message struct/class that contains the
message data and the peer address?Or multiple return values.Or a list of lists where each list contains <message> <sender>? That has the advantage that you can do something like
(dolist (msg-and-stuff (get-all-messages ...))(apply #'my-handler-fn msg-and-stuff))