1
0
This repository has been archived on 2023-12-27. You can view files and clone it, but cannot push or open issues or pull requests.
beginning-go/message.go

190 lines
6.3 KiB
Go

package main
import (
"bytes"
"compress/zlib"
"encoding/json"
"fmt"
"strings"
"github.com/clockworksoul/smudge"
)
// messageType is an alias for int8. Whenever you see "messageType" in the code,
// think int8. Using this alias however allows us to increase code clarity and
// safety beyond simply passing meaningless numbers around.
type messageType int8
const (
// Go does not have any native support for enum types, however there is an
// idiomatic way to accomplish the same goal.
// https://golang.org/ref/spec#Iota
messageTypeChat messageType = iota + 1
messageTypeUsernames
messageTypeUsernameReq
)
// message represents the structure of the contents in a smudge.Broadcast. We
// can use the Type to determine what the Body will contain.
type message struct {
// The text in backticks after each field here is called a Struct Tag.
// The standard JSON marshaller uses the "json" struct tag to name fields.
//
// https://golang.org/pkg/reflect/#StructTag
// https://golang.org/pkg/encoding/json/#Marshal
//
// The JSON marshaller can only interact with "exported fields", therefore
// these field names must start with an uppercase letter.
// Type informs us what to expect in the body of this message, and what
// action to take on it.
Type messageType `json:"type"`
// Body contains the bulk of the message
Body string `json:"body"`
// Usernames is filled only in a messageTypeUsernames. It contains a map
// of the address->username pairings know by the sending client.
Usernames map[NodeAddress]string `json:"usernames"`
}
// Encode converts the message into a form which can be sent to other clients
// through Smudge (a []byte, pronounced byte slice).
//
// Notice how this func has an extra set of parens before the name? This is how
// methods are defined in Go. Unlike a function, methods are not in the global
// namespace and must be called on an instance of an object. That object being
// called on is in the first set of parens, and is called the receiver. This of
// this as "self" in python, or "this" in many other languages.
// More info: https://tour.golang.org/methods/1
func (m *message) Encode() []byte {
// There is a lot happening here in a pretty small space. We first create an
// empty buffer in which we can temporarily store some bytes. This buffer
// implements the io.Writer interface, but we want to write compressed
// bytes, so we wrap that writer in the zlib writer which also implements
// the io.Writer interface. Finally we create a json encoder which will
// output the json format of m into the zlib writer.
var b bytes.Buffer
w := zlib.NewWriter(&b)
err := json.NewEncoder(w).Encode(m)
if err != nil {
printError("Failed to marshal a chat message to send: %s", err)
}
err = w.Close() // The bytes might not actually be written until closed (or flushed)
if err != nil {
printError("Failed to close the encoding writer: %s", err)
}
// read out the contents from our temporary buffer, and return them
return b.Bytes()
}
// Messenger contains all the messages which we know have been sent in the past.
// Additionally, it provides the interface for sending and receiving new
// messages.
//
// To send and receive messages we must implement the Broadcast functionality in
// the smudge library.
//
// https://godoc.org/github.com/clockworksoul/smudge#BroadcastListener
// https://godoc.org/github.com/clockworksoul/smudge#BroadcastString
type Messenger struct {
// clients is the list of all known and alive clients. Maintaining a
// reference here will allow us to update status based on broadcasts.
clients ClientList
}
// Decode converts the byte slice received from a broadcast into a usable
// message. This is the reverse of the Encode() operation.
//
// Just like how the encode method above uses the json and zlib packages to
// json marshal and then compress a message, here we are doing the reverse.
func (m *message) Decode(data []byte) error {
bb := bytes.NewReader(data)
r, err := zlib.NewReader(bb)
if err != nil {
return fmt.Errorf("Failed to decompress message: %s", err)
}
// msg is what the decompressed bytes will be un-json-marshalled into
err = json.NewDecoder(r).Decode(m)
if err != nil {
return fmt.Errorf("Failed to decode message: %s", err)
}
return nil
}
// OnBroadcast is the only method defined on the smudge.BroadcastListener
// interface. By implementing this method on the Messenger struct, that struct
// will satisfy the interface and we can register it with smudge.
//
// When another node in the gossip cluster sends a broadcast message, this
// function will be called.
func (m *Messenger) OnBroadcast(b *smudge.Broadcast) {
senderAddr := NodeAddress(b.Origin().Address())
printDebug("Received %d bytes", len(b.Bytes()))
var msg message
err := msg.Decode(b.Bytes())
if err != nil {
printError("Failed to receive message from %s: %s", senderAddr, err)
return
}
switch msg.Type {
case messageTypeUsernames:
printDebug("Received a broadcast containing usernames")
if msg.Usernames == nil || len(msg.Usernames) == 0 {
printError("Received an empty username list")
return
}
err := m.clients.AddUsernames(msg.Usernames)
if err != nil {
printError("Failed to process received usernames: %s", err)
}
case messageTypeUsernameReq:
printDebug("Received a broadcast requesting %s send usernames, my localAddress is %s", msg.Body, localAddress)
if msg.Body == string(localAddress) {
// The request targeted us...
// Let's send all the usernames we know about to minimize requests
// for a new client.
err := m.clients.BroadcastUsernames()
if err == nil {
printInfo("Successfully broadcast usernames to the group")
} else {
printError("Tried to broadcast usernames but failed: %s", err)
}
}
case messageTypeChat:
// Received a chat message
sender := m.clients[senderAddr]
printChatMessage(msg.Body, sender.GetName())
}
}
// SendMessage takes a chat message to be sent and broadcasts it to the cluster
// and posts to the local chat view.
func SendMessage(text string) error {
text = strings.TrimSpace(text)
if text == "" {
return nil
}
// First let's make the message show up in our own chat history
printChatMessage(text, localUsername)
// Now we can send it on to others
msg := message{
Type: messageTypeChat,
Body: text,
}
return smudge.BroadcastBytes(msg.Encode())
}