Let’s talk about the message bus. In my previous post, I described the basic architecture of the MUD engine we are trying to build. There were three layers, the Portal, the Message Bus, and the World. In order for the portal to communicate with the world, it needs to be able to send messages to the message bus. The message bus then sends the message to the world.
Choosing a Message Bus
There are a number of message buses available. From RabbitMQ, to Kafka, to NATS, each provides a different set of features. In this case I’ve chosen to use NATS as our message bus due to the simplicity of integrating it with Go.
Docker and the NATS Server
I love using docker for managing background processes. It’s an easy way to manage a server in the background, and it’s virtually effortless to get started on a project that uses it correctly. Instructions for installing Docker can be found here, and while we’re at it let’s grab docker-compose.
docker-compose is a tool for defining and running multi-container Docker applications.
Once the dependencies are ready we can create a simple docker-compose.yml
file in our example
game directory:
version: "3"
services:
nats:
image: nats:latest
ports:
- "4222:4222"
Now when we call docker-compose up
, the NATS server will be started listening on port 4222.
Connecting NATS
We only really need to manage one connection to the NATS server, and we can set that up in the engine
package. First
we run ` go get github.com/nats-io/nats.go to get the NATS client, and we update our
engine.Init()` function to use it.
But first we need a place to store the connection in our state:
type state struct {
// ... state ...
nats *nats.EncodedConn
}
NATS has the ability to automatically marshall and unmarshall from JSON, this is what nats.EncodedConn
does. With that
in place, we can create a function to connect to the NATS server:
func connectToNats() {
n, err := nats.Connect(nats.DefaultURL)
if err != nil {
os.Exit(1)
}
c, err := nats.NewEncodedConn(n, nats.JSON_ENCODER)
if err != nil {
os.Exit(1)
}
state.nats = c
}
If the connection fails, we shouldn’t probably continue, so we exit. Finally call that function in our engine.Init()
function:
func Init() {
// ... init ...
connectToNats()
}
Creating the Publisher
Now we need to publish events to the bus:
// PublishEvent publishes an event to the NATS event bus.
func PublishEvent(topic string, event interface{}) error {
err := state.nats.Publish(topic, event)
if err != nil {
return err
}
return nil
}
The topic
is the name of the topic to publish to. The event
is the event data to publish to the topic.
Go is a typed language, but has the flexibility to declare a variable as an unknown type using the
interface{}
type, meaning the variable can be any type.
Creating the Subscriber
With the PublishEvent()
function defined, we can create a function to subscribe to an event published to the bus:
// SubscribeToEvent subscribes to an event on the NATS event bus.
func SubscribeToEvent(topic string, handler nats.Handler) (*nats.Subscription, error) {
sub, err := state.nats.Subscribe(topic, handler)
if err != nil {
return nil, err
}
return sub, nil
}
Again the SubscribeToEvent()
function a topic
, and this time we have a handler
function that can be passed any
kind of event struct.
Our First Event
Within our world
package we are going to create our first event. Events will be represented as structs that
define the event payload, with the addition of a function that returns the topic of the event. I chose to use a function
as it is a flexible way create topic names dynamically in a consistent way:
package events
type AssertSession struct {
UUID string
ConnectedAt int64
LastInputAt int64
RemoteAddr string
}
func AsserSessionTopic() string {
return "world.assert_session"
}
This event will be used to message the world that a new connection has been established and that it should create a new session. We’ll pass some additional information about the connection for the World to utilize.
Publishing the AssertSession Event
The AssertSession
event is published any time a new connection is received by the server. This will be used later
by the World. To do this we add the following to connection.Start()
in the telnet_portal
package:
func (c *connection) Start() {
// ... start ...
err = engine.PublishEvent(events.AssertSessionTopic(), &events.AssertSession{
UUID: c.uuid,
ConnectedAt: c.connectedAt,
LastInputAt: c.lastInputAt,
RemoteAddr: c.remoteAddr,
})
if err != nil {
c.Stop()
return
}
// ... start ...
}
With this in place, we can now start building the world
portion of our MUD engine, and set it up to interact with the
portal.