2023-08-21 03:50:38 +00:00
|
|
|
# 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:
|
|
|
|
|
|
|
|
![An animation showing three terminal windows, each running the example application.](./chat-example.gif)
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
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:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
go run .
|
|
|
|
|
|
|
|
# or, build and run separately
|
|
|
|
go build .
|
|
|
|
./chat
|
|
|
|
```
|
|
|
|
|
|
|
|
To set a nickname, use the `-nick` flag:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
go run . -nick=zoidberg
|
|
|
|
```
|
|
|
|
|
|
|
|
You can join a specific chat room with the `-room` flag:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
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`](./main.go), we create a new libp2p `Host` and then create a new `PubSub` service
|
|
|
|
using the GossipSub router:
|
|
|
|
|
|
|
|
```go
|
|
|
|
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`:
|
|
|
|
|
|
|
|
```go
|
|
|
|
// 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.go):
|
|
|
|
|
|
|
|
```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:
|
|
|
|
|
|
|
|
```go
|
|
|
|
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`:
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (cr *ChatRoom) Publish(message string) error {
|
|
|
|
m := ChatMessage{
|
|
|
|
Message: message,
|
2024-06-07 06:25:43 +00:00
|
|
|
SenderID: cr.self.String(),
|
2023-08-21 03:50:38 +00:00
|
|
|
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:
|
|
|
|
|
|
|
|
```go
|
|
|
|
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:
|
|
|
|
|
|
|
|
```go
|
|
|
|
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`](./ui.go), and the interesting bit is in the `handleEvents` event loop
|
|
|
|
method:
|
|
|
|
|
|
|
|
```go
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|