我正在尝试创建一个简单的匹配服务器来将两个客户端相互匹配。这是结构
.
├── bootstrap-server
├── cert.pem.local
├── client.go
├── config.go
├── go.mod
├── go.sum
├── key.pem.local
└── main.go
1 directory, 8 files
main.go
package main
import (
"context"
"log"
"net/http"
"time"
_ "github.com/joho/godotenv/autoload"
)
var queue = NewClientQueue()
func main() {
cfg := newConfigFromEnv()
mux := http.NewServeMux()
mux.HandleFunc("GET /", handleFindPeer)
log.Println("Bootstrap server running on", cfg.serverAddr)
log.Fatal(http.ListenAndServeTLS(cfg.serverAddr, cfg.tlsCert, cfg.tlsKey, mux))
}
func handleFindPeer(w http.ResponseWriter, r *http.Request) {
const pairTimeout = time.Minute
client := Client{
addr: r.RemoteAddr,
respWriter: w,
peerMatchedCh: make(chan struct{}),
}
// Use 30 seconds timeout for finding a connection
ctx, cancel := context.WithTimeout(r.Context(), pairTimeout)
defer cancel()
go func() {
if queue.Len() > 0 {
peer := queue.Dequeue()
sendPairingResponse(client, peer)
close(client.peerMatchedCh)
close(peer.peerMatchedCh)
log.Printf("Paired %v and %v\n", client.addr, peer.addr)
} else {
queue.Enqueue(client)
log.Printf("Added %v to waiting queue\n", client.addr)
}
}()
select {
case <-client.peerMatchedCh:
return
case <-ctx.Done():
http.Error(w, "timeout waiting for peer", http.StatusRequestTimeout)
}
}
func sendPairingResponse(c1, c2 Client) {
c1.respWriter.WriteHeader(http.StatusOK)
c2.respWriter.WriteHeader(http.StatusOK)
c1.respWriter.Write([]byte(c2.addr))
c2.respWriter.Write([]byte(c1.addr))
}
客户端.go
package main
import (
"net/http"
"sync"
)
type Client struct {
addr string
respWriter http.ResponseWriter
peerMatchedCh chan struct{}
}
type ClientQueue struct {
clients []Client
mu sync.Mutex
}
func NewClientQueue() *ClientQueue {
return &ClientQueue{clients: []Client{}, mu: sync.Mutex{}}
}
func (q *ClientQueue) Enqueue(c Client) {
q.mu.Lock()
defer q.mu.Unlock()
q.clients = append(q.clients, c)
}
func (q *ClientQueue) Dequeue() Client {
q.mu.Lock()
defer q.mu.Unlock()
client := q.clients[0] // <-- panicking here
q.clients = q.clients[1:]
return client
}
func (q *ClientQueue) Remove(client Client) {
q.mu.Lock()
defer q.mu.Unlock()
for i, c := range q.clients {
if c.addr == client.addr {
q.clients = append(q.clients[:i], q.clients[i+1:]...)
return
}
}
}
func (q *ClientQueue) Len() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.clients)
}
我正在使用curl打开很多请求
while true; do curl --cacert cert.pem.local https://localhost:3030 & ; done
我正在使用
go run .
运行服务器,几秒钟后,我出现索引不足的恐慌
....
2024/10/24 13:54:41 Paired [::1]:57753 and [::1]:57755
2024/10/24 13:54:41 Added 127.0.0.1:57759 to waiting queue
2024/10/24 13:54:41 Paired 127.0.0.1:57760 and 127.0.0.1:57759
2024/10/24 13:54:41 Paired [::1]:57820 and 127.0.0.1:57756
2024/10/24 13:54:41 Added [::1]:57792 to waiting queue
panic: runtime error: index out of range [0] with length 0
goroutine 8864 [running]:
main.(*ClientQueue).Dequeue(0x104833f00?)
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/client.go:32 +0x1a0
main.handleFindPeer.func1()
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/main.go:38 +0x44
created by main.handleFindPeer in goroutine 9330
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/main.go:36 +0x118
exit status 2
我也有这种恐慌
....
2024/10/24 14:07:47 Added [::1]:60624 to waiting queue
2024/10/24 14:07:47 http: TLS handshake error from 127.0.0.1:60680: EOF
panic: WriteHeader called after Handler finished
goroutine 4934 [running]:
net/http.(*http2responseWriter).WriteHeader(0x140000fe380?, 0x104a51620?)
/usr/local/go/src/net/http/h2_bundle.go:6773 +0x44
main.sendPairingResponse({{0x1400032eeb5, 0xb}, {0x104b55bd8, 0x14000502010}, 0x14000100540}, {{0x140003c8585, 0xb}, {0x104b55bd8, 0x140000a8040}, 0x140000fe380})
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/main.go:58 +0x54
main.handleFindPeer.func1()
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/main.go:39 +0xb0
created by main.handleFindPeer in goroutine 4933
/Users/lufy/Developer/pg/go/p2p/bootstrap-server/main.go:36 +0x118
exit status 2
我不知道这些恐慌是如何发生的。请帮忙
对
Len()
、Dequeue()
和 Enqueue()
的调用之间存在简单明了的逻辑竞争:一旦 Len()
中的互斥锁被解锁,并且在 Enqueue()
或 Dequeue()
中再次锁定之前——基于切片的长度是在互斥体被锁定期间,可能会发生任意数量的对 Enqueue()
和 Dequeue()
的其他调用,这些调用在并行运行的 goroutine 中执行。
解决方法是使用诸如 DequeueOrEnqueue()
之类的调用,其中 1)锁定互斥锁; 2)测试队列长度; 3) 根据该测试执行排队或出队,然后 4) 解锁互斥锁。