Add timeserve example to demonstrate writing to multiple live peers #3

Merged
decentral1se merged 2 commits from cblgh/iroh-go:main into main 2026-06-25 14:05:07 +00:00
2 changed files with 182 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# timeserve
This example demonstrates that an endpoint can support multiple live connections at once. The
endpoint aka listener writes the time, once every 2 seconds, to every connected peer.
Try connecting to a running listener with 3 (or more!) receiver peers at once.
## Build a binary
```
go build --ldflags '-linkmode external -extldflags=-static -s -w' ./timeserve.go
```
If you run into `glibc` incompatibility, you can compile with `zig`/`musl`.
```
CC="zig cc -target x86_64-linux-musl -lunwind" \
CGO_ENABLED=1 \
CGO_LDFLAGS="-static" \
GOOS=linux GOARCH=amd64 \
go build -v -a -ldflags '-extldflags "-static"' ./timeserve.go
```
## Terminal 1 (listener)
```
./timeserve
```
## Terminal 2 (receiver)
```
./timeserve -endpoint <endpoint-id>
```
## Terminal 3 (receiver)
```
./timeserve -endpoint <endpoint-id>
```
You'll see a connection notice if a receiver peer manages to connect to the listener.
Receivers (Terminal 2 and Terminal 3) will display the time sent by the listener (Terminal 1).
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
)
var (
alpn = []byte("iroh-go-timeserve/0")
frameSize uint32 = 256
endpointPeer *string
)
func check(err error) {
if err != nil {
irohErr := err.(*iroh.IrohError)
panic(irohErr.Message())
}
}
func awaitInterrupt() {
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
<-done
os.Exit(0)
}
func parseFlags() {
endpointPeer = flag.String("endpoint", "", "endpoint id of peer")
flag.Parse()
}
func receiver(e *iroh.Endpoint, id string) {
remote, err := iroh.EndpointIdFromString(id)
check(err)
addr := iroh.NewEndpointAddr(remote, nil, nil)
conn, err := e.Connect(addr, alpn)
check(err)
// from docs:
// "The peer that calls open_bi must write to its SendStream before the peer Connection is able to accept the stream
// using accept_bi()"
// https://docs.rs/iroh/latest/iroh/endpoint/struct.Connection.html#method.accept_bi
stream, err := conn.OpenBi()
check(err)
// NOTE: for the other peer to realize the bidi stream is open, you have to send some data! the data sent cannot be
// the empty string ""; empty string == no data
err = stream.Send().WriteAll([]byte{1})
check(err)
handleReading(stream.Recv())
}
func handleReading(recv *iroh.RecvStream) {
for {
frame, err := recv.Read(frameSize)
if checkForTimeout(err) {
break
}
fmt.Println(string(frame))
check(err)
}
}
func checkForTimeout(err error) bool {
// handle timeout
if err != nil {
irohErr := err.(*iroh.IrohError)
errMsg := irohErr.Message()
if strings.HasPrefix(errMsg, "ConnectionLost") {
fmt.Println("lost connection")
return true
} else {
// panics and exits bc unhandled err
check(err)
}
}
return false
}
func listen(e *iroh.Endpoint) {
for {
incoming := *e.AcceptNext()
fmt.Println("new connection")
go handleIncoming(incoming)
}
}
func handleIncoming(incoming *iroh.Incoming) {
accepting, err := incoming.Accept()
check(err)
conn, err := accepting.Connect()
check(err)
stream, err := conn.AcceptBi()
check(err)
for {
err := stream.Send().WriteAll([]byte(time.Now().Format(time.DateTime)))
if checkForTimeout(err) {
break
}
// prints every two seconds (not for any technical reason, 1/sec just feels hectic!)
<-time.After(2 * time.Second)
}
}
func main() {
parseFlags()
preset := iroh.PresetN0()
opts := iroh.EndpointOptions{
Preset: &preset,
Alpns: &[][]byte{alpn},
}
endpoint, err := iroh.EndpointBind(opts)
check(err)
endpoint.Online()
if *endpointPeer == "" {
fmt.Println("this endpoint is online and listening! use this id to connect via a peer:")
fmt.Printf("timeserve\n -endpoint %s\n", endpoint.Id())
go func() { listen(endpoint) }()
} else {
go func() { receiver(endpoint, *endpointPeer) }()
}
awaitInterrupt()
}