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.

Previous Post In This Series