.. | ||
.gitignore | ||
chat-example.gif | ||
chatroom.go | ||
go.mod | ||
go.sum | ||
main.go | ||
README.md | ||
ui.go |
go-libp2p-pubsub chat example
This example project builds a chat room application using go-libp2p-pubsub. The app runs in the terminal, and uses a text UI to show messages from other peers:
The goal of this example is to demonstrate the basic usage of the PubSub
API, without getting into
the details of configuration.
Running
Clone this repo, then cd
into the examples/pubsub/chat
directory:
git clone https://github.com/libp2p/go-libp2p
cd go-libp2p/examples/pubsub/chat
Now you can either run with go run
, or build and run the binary:
go run .
# or, build and run separately
go build .
./chat
To set a nickname, use the -nick
flag:
go run . -nick=zoidberg
You can join a specific chat room with the -room
flag:
go run . -room=planet-express
It's usually more fun to chat with others, so open a new terminal and run the app again.
If you set a custom chat room name with the -room
flag, make sure you use the same one
for both apps. Once the new instance starts, the two chat apps should discover each other
automatically using mDNS, and typing a message into one app will send it to any others that are open.
To quit, hit Ctrl-C
, or type /quit
into the input field.
Code Overview
In main.go
, we create a new libp2p Host
and then create a new PubSub
service
using the GossipSub router:
func main() {
// (omitted) parse flags, etc...
// create a new libp2p Host that listens on a random TCP port
h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"))
if err != nil {
panic(err)
}
// create a new PubSub service using the GossipSub router
ps, err := pubsub.NewGossipSub(ctx, h)
if err != nil {
panic(err)
}
// (omitted) setup mDNS discovery...
}
We configure the host to use local mDNS discovery, so that we can find other peers to chat with on the local network. We also parse a few command line flags, so we can set a friendly nickname, or choose a chat room by name.
Once we have a Host
with an attached PubSub
service, we join a ChatRoom
:
// still in the main func
cr, err := JoinChatRoom(ctx, ps, h.ID(), nick, room)
if err != nil {
panic(err)
}
ChatRoom
is a custom struct defined in chatroom.go
:
// ChatRoom represents a subscription to a single PubSub topic. Messages
// can be published to the topic with ChatRoom.Publish, and received
// messages are pushed to the Messages channel.
type ChatRoom struct {
// Messages is a channel of messages received from other peers in the chat room
Messages chan *ChatMessage
ctx context.Context
ps *pubsub.PubSub
topic *pubsub.Topic
sub *pubsub.Subscription
roomName string
self peer.ID
nick string
}
A ChatRoom
subscribes to a PubSub Topic
, and reads messages from the Subscription
. We're sending our messages
wrapped inside of a ChatMessage
struct:
type ChatMessage struct {
Message string
SenderID string
SenderNick string
}
This lets us attach friendly nicknames to the messages for display. A real app might want to make sure that nicks are unique, but we just let anyone claim whatever nick they want and send it along with their messages.
The ChatMessage
s are encoded to JSON and published to the PubSub topic, in the Data
field of a pubsub.Message
.
We could have used any encoding, as long as everyone in the topic agrees on the format, but JSON is simple and good
enough for our purposes.
To send messages, we have a Publish
method, which wraps messages in ChatMessage
structs, encodes them, and publishes
to the pubsub.Topic
:
func (cr *ChatRoom) Publish(message string) error {
m := ChatMessage{
Message: message,
SenderID: cr.self.String(),
SenderNick: cr.nick,
}
msgBytes, err := json.Marshal(m)
if err != nil {
return err
}
return cr.topic.Publish(cr.ctx, msgBytes)
}
In the background, the ChatRoom
runs a readLoop
goroutine, which reads messages from the pubsub.Subscription
,
decodes the ChatMessage
JSON, and sends the ChatMessage
s on a channel:
func (cr *ChatRoom) readLoop() {
for {
msg, err := cr.sub.Next(cr.ctx)
if err != nil {
close(cr.Messages)
return
}
// only forward messages delivered by others
if msg.ReceivedFrom == cr.self {
continue
}
cm := new(ChatMessage)
err = json.Unmarshal(msg.Data, cm)
if err != nil {
continue
}
// send valid messages onto the Messages channel
cr.Messages <- cm
}
}
There's also a ListPeers
method, which just wraps the method of the same name in the PubSub
service:
func (cr *ChatRoom) ListPeers() []peer.ID {
return cr.ps.ListPeers(topicName(cr.roomName))
}
That's pretty much it for the ChatRoom
!
Back in main.go
, once we've created our ChatRoom
, we pass it
to NewChatUI
, which constructs a three panel text UI for entering and viewing chat messages, because UIs
are fun.
The ChatUI
is defined in ui.go
, and the interesting bit is in the handleEvents
event loop
method:
func (ui *ChatUI) handleEvents() {
peerRefreshTicker := time.NewTicker(time.Second)
defer peerRefreshTicker.Stop()
for {
select {
case input := <-ui.inputCh:
// when the user types in a line, publish it to the chat room and print to the message window
err := ui.cr.Publish(input)
if err != nil {
printErr("publish error: %s", err)
}
ui.displaySelfMessage(input)
case m := <-ui.cr.Messages:
// when we receive a message from the chat room, print it to the message window
ui.displayChatMessage(m)
case <-peerRefreshTicker.C:
// refresh the list of peers in the chat room periodically
ui.refreshPeers()
case <-ui.cr.ctx.Done():
return
case <-ui.doneCh:
return
}
}
}