examples: Add chat example that also doesn't work properly yet

This commit is contained in:
Sasha Koshka 2025-01-27 22:36:52 -05:00
parent 72df1e8d11
commit 54022e5541
6 changed files with 722 additions and 0 deletions

View File

@ -0,0 +1,102 @@
package main
import "os"
import "fmt"
import "time"
import "bufio"
import "context"
import "crypto/tls"
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/examples/chat"
func main() {
name := os.Args[0]
if len(os.Args) != 3 && len(os.Args) != 4 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT ROOM [NICKNAME]\n", name)
os.Exit(2)
}
address := os.Args[1]
room := os.Args[2]
var nickname hopp.Option[string]; if len(os.Args) >= 4 {
nickname = hopp.O(os.Args[3])
}
trans, err := join(address, room, nickname)
handleErr(1, err)
go func() {
reader := bufio.NewReader(os.Stdin)
for {
line, err := reader.ReadString('\n')
if err != nil { break }
send(trans, line)
}
}()
for {
message, err := chat.Receive(trans)
handleErr(1, err)
switch message := message.(type) {
case *chat.MessageChat:
nickname := "Anonymous"
if value, ok := message.Nickname.Get(); ok {
nickname = value
}
fmt.Fprintf(os.Stdout, "%s: %s\n", nickname, message.Content)
case *chat.MessageJoinNotify:
fmt.Fprintf(os.Stdout, "(i) %s joined the room\n", message.Nickname)
case *chat.MessageLeaveNotify:
fmt.Fprintf(os.Stdout, "(i) %s left the room\n", message.Nickname)
}
}
}
func join(address string, room string, nickname hopp.Option[string]) (hopp.Trans, error) {
ctx, done := context.WithTimeout(context.Background(), 16 * time.Second)
defer done()
dialer := hopp.Dialer {
TLSConfig: &tls.Config {
// don't actually do this in real life
InsecureSkipVerify: true,
},
}
conn, err := dialer.Dial(ctx, "quic", address)
if err != nil { return nil, err }
err = updateProfile(conn, nickname)
if err != nil { return nil, err }
transRoom, err := conn.OpenTrans()
if err != nil { return nil, err }
err = chat.Send(transRoom, &chat.MessageJoin {
Room: room,
})
if err != nil { return nil, err }
return transRoom, nil
}
func send(trans hopp.Trans, content string) error {
return chat.Send(trans, &chat.MessageChat {
Content: content,
})
}
func updateProfile(conn hopp.Conn, nickname hopp.Option[string]) error {
trans, err := conn.OpenTrans()
if err != nil { return err }
defer trans.Close()
err = chat.Send(trans, &chat.MessageUpdateProfile {
Nickname: nickname,
})
if err != nil { return err }
message, err := chat.Receive(trans)
if err != nil { return err }
switch message := message.(type) {
case *chat.MessageError: return message
default: return nil
}
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}

6
examples/chat/doc.go Normal file
View File

@ -0,0 +1,6 @@
// Package chat implements a simple chat protocol over HOPP. To re-generate the
// source files, run this command from within the root directory of the
// repository:
//
// go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol
package chat

11
examples/chat/error.go Normal file
View File

@ -0,0 +1,11 @@
package chat
import "fmt"
func (msg *MessageError) Error() string {
if description, ok := msg.Description.Get(); ok {
return fmt.Sprintf("other party sent error: %d %s", msg.Error, description)
} else {
return fmt.Sprintf("other party sent error: %d", msg.Code)
}
}

369
examples/chat/generated.go Normal file
View File

@ -0,0 +1,369 @@
package chat
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/tape"
// Send sends one message along a transaction.
func Send(trans hopp.Trans, message hopp.Message) error {
buffer, err := message.MarshalBinary()
if err != nil { return err }
return trans.Send(message.Method(), buffer)
}
// Receive receives one message from a transaction.
func Receive(trans hopp.Trans) (hopp.Message, error) {
method, data, err := trans.Receive()
if err != nil { return nil, err }
switch method {
case 0x0000:
message := &MessageError { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0001:
message := &MessageSuccess { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0100:
message := &MessageUpdateProfile { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0200:
message := &MessageJoin { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0201:
message := &MessageChat { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0300:
message := &MessageJoinNotify { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0301:
message := &MessageLeaveNotify { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
default: return nil, hopp.ErrUnknownMethod
}
}
// (0) Error is sent by a party when the other party has done something erroneous. The valid error codes are:
//
// 0: General, unspecified error
//
// The description field, if specified, determines a human-readable error to be shown to the user. The sending party must immediately close the transaction after this message is sent.
type MessageError struct {
/* 0 */ Code uint16
/* 1 */ Description hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageError) Method() uint16 {
return 0
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageError) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetCode := size
{ value := msg.Code
size += 2; _ = value }
offsetDescription := size
if value, ok := msg.Description.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
{ value := msg.Code
tape.EncodeI16(buffer[offsetCode:], value)}
if value, ok := msg.Description.Get(); ok {
tape.EncodeString(buffer[offsetDescription:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageError) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeI16[uint16](data)
if err != nil { return err }
msg.Code = value
foundRequired ++
case 1:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Description = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (1) Success is sent by a party when it has successfully completed a task given to it by the other party. The sending party must immediately close the transaction after this message is sent.
type MessageSuccess struct {
}
// Method returns the method number of the message.
func (msg MessageSuccess) Method() uint16 {
return 1
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageSuccess) MarshalBinary() ([]byte, error) {
size := 0
count := 0
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageSuccess) UnmarshalBinary(buffer []byte) error {
// no fields
return nil
}
// (256) UpdateProfile is sent by the client in a new transaction to update the profile details that will be shown to other connected clients.
type MessageUpdateProfile struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageUpdateProfile) Method() uint16 {
return 256
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageUpdateProfile) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageUpdateProfile) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (512) Join is sent by the client when it wishes to join a room. It must begin a new transaction, and that transaction will persist while the user is in that room. Messages having to do with the room will be sent along this transaction. To leave the room, the client must close the transaction.
type MessageJoin struct {
/* 0 */ Room string
}
// Method returns the method number of the message.
func (msg MessageJoin) Method() uint16 {
return 512
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageJoin) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetRoom := size
{ value := msg.Room
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
{ value := msg.Room
tape.EncodeString(buffer[offsetRoom:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageJoin) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Room = value
}
}
return nil
}
// (513) Chat is sent by the client when it wishes to post a message to the room. It is also relayed by the server to other clients to notify them of the message. It must be sent within a room transaction.
type MessageChat struct {
/* 0 */ Nickname hopp.Option[string]
/* 1 */ Content string
}
// Method returns the method number of the message.
func (msg MessageChat) Method() uint16 {
return 513
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageChat) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
offsetContent := size
{ value := msg.Content
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
{ value := msg.Content
tape.EncodeString(buffer[offsetContent:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageChat) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
case 1:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Content = value
foundRequired ++
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (768) JoinNotify is sent by the server when another client joins the room. It must be sent within a room transaction.
type MessageJoinNotify struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageJoinNotify) Method() uint16 {
return 768
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageJoinNotify) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageJoinNotify) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (769) LeaveNotify is sent by the server when another client leaves the room. It must be sent within a room transaction.
type MessageLeaveNotify struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageLeaveNotify) Method() uint16 {
return 769
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageLeaveNotify) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageLeaveNotify) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}

70
examples/chat/protocol.md Normal file
View File

@ -0,0 +1,70 @@
# Chat Protocol
This document describes a simple chat protocol. To re-generate the source files,
run `go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol`
## Messages
### 0000 Error
| Tag | Name | Type | Required |
| --: | ----------- | ------ | -------- |
| 0 | Code | U16 | Yes |
| 1 | Description | String | No |
Error is sent by a party when the other party has done something erroneous. The
valid error codes are:
- 0: General, unspecified error
The description field, if specified, determines a human-readable error to be
shown to the user. The sending party must immediately close the transaction
after this message is sent.
### 0001 Success
Success is sent by a party when it has successfully completed a task given to it
by the other party. The sending party must immediately close the transaction
after this message is sent.
### 0100 UpdateProfile
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
UpdateProfile is sent by the client in a new transaction to update the profile
details that will be shown to other connected clients.
### 0200 Join
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Room | String | Yes |
Join is sent by the client when it wishes to join a room. It must begin a new
transaction, and that transaction will persist while the user is in that room.
Messages having to do with the room will be sent along this transaction. To
leave the room, the client must close the transaction.
### 0201 Chat
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
| 1 | Content | String | Yes |
Chat is sent by the client when it wishes to post a message to the room. It is
also relayed by the server to other clients to notify them of the message. It
must be sent within a room transaction.
### 0300 JoinNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
JoinNotify is sent by the server when another client joins the room. It must be
sent within a room transaction.
### 0301 LeaveNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
LeaveNotify is sent by the server when another client leaves the room. It must
be sent within a room transaction.

View File

@ -0,0 +1,164 @@
package main
import "os"
import "fmt"
import "log"
import "errors"
import "crypto/tls"
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/go-util/sync"
import "git.tebibyte.media/sashakoshka/go-util/container"
import "git.tebibyte.media/sashakoshka/hopp/examples/chat"
var clients usync.RWMonitor[ucontainer.Set[*client]]
func main() {
name := os.Args[0]
if len(os.Args) != 4 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT CERT KEY\n", name)
os.Exit(2)
}
address := os.Args[1]
certPath := os.Args[2]
keyPath := os.Args[3]
err := host(address, certPath, keyPath)
handleErr(1, err)
}
func host(address string, certPath, keyPath string) error {
keyPair, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil { return err }
listener, err := hopp.ListenQUIC("quic", address, &tls.Config {
InsecureSkipVerify: true,
Certificates: []tls.Certificate { keyPair },
})
clients.Set(ucontainer.NewSet[*client]())
if err != nil { return err }
log.Printf("(i) hosting on %s", address)
for {
conn, err := listener.Accept()
if err != nil { return err }
client := &client {
conn: conn,
rooms: usync.NewRWMonitor(make(map[string] hopp.Trans)),
}
go client.run()
}
}
type client struct {
conn hopp.Conn
nickname hopp.Option[string]
rooms usync.RWMonitor[map[string] hopp.Trans]
}
func (this *client) run() {
log.Printf("-=E %v connected", this.conn.RemoteAddr())
defer log.Printf("X=- %v disconnected", this.conn.RemoteAddr())
defer this.conn.Close()
for {
log.Println("accepting transaction")
trans, err := this.conn.AcceptTrans()
log.Println("accepted transaction")
if err != nil {
log.Printf("XXX %v failed: %v", this.conn.RemoteAddr(), err)
}
go this.runTrans(trans)
}
}
func (this *client) runTrans(trans hopp.Trans) {
defer trans.Close()
message, err := chat.Receive(trans)
if err != nil {
log.Printf(
"XXX %v transaction failed: %v",
this.conn.RemoteAddr(), err)
}
switch message := message.(type) {
case *chat.MessageJoin:
err = this.transTalk(trans, message)
}
if err != nil {
var actual *chat.MessageError
if !errors.As(err, &actual) {
chat.Send(trans, &chat.MessageError {
Description: hopp.O(fmt.Sprint(err)),
})
}
log.Printf("XXX %v transaction failed: %v", this.conn.RemoteAddr(), err)
}
}
func (this *client) transTalk(trans hopp.Trans, initial *chat.MessageJoin) error {
room := initial.Room
err := this.joinRoom(trans, room)
if err != nil { return err }
defer this.leaveRoom(trans, room)
for {
message, err := chat.Receive(trans)
if err != nil { return err }
switch message := message.(type) {
case *chat.MessageChat:
err := this.handleMessageChat(trans, room, message)
if err != nil { return err }
case *chat.MessageError:
return message
}
}
}
func (this *client) handleMessageChat(trans hopp.Trans, room string, message *chat.MessageChat) error {
log.Println("(). %s #%s: %s", this.nickname.Default("Anonymous"), room, message.Content)
clients, done := clients.RBorrow()
defer done()
for client := range clients {
err := client.relayMessage(room, message)
if err != nil {
log.Printf("!!! %v", err)
}
}
return nil
}
func (this *client) relayMessage(room string, message *chat.MessageChat) error {
rooms, done := this.rooms.RBorrow()
defer done()
if trans, ok := rooms[room]; ok {
err := chat.Send(trans, message)
if err != nil {
return fmt.Errorf("could not relay message: %w", err)
}
}
return nil
}
func (this *client) joinRoom(trans hopp.Trans, room string) error {
rooms, done := this.rooms.Borrow()
defer done()
if _, exists := rooms[room]; exists {
return fmt.Errorf("already joined %s", room)
}
rooms[room] = trans
log.Printf("--> user %s joined #%s", this.nickname.Default("Anonymous"), room)
return nil
}
func (this *client) leaveRoom(trans hopp.Trans, room string) error {
rooms, done := this.rooms.Borrow()
defer done()
if _, exists := rooms[room]; !exists {
return fmt.Errorf("not in %s", room)
}
delete(rooms, room)
log.Printf("<-- user %s left #%s", this.nickname.Default("Anonymous"), room)
return nil
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}