mirror of
				https://source.quilibrium.com/quilibrium/ceremonyclient.git
				synced 2025-11-04 10:37:27 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			221 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 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:
 | 
						|
 | 
						|
```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,
 | 
						|
		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:
 | 
						|
 | 
						|
```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
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
```
 |