From 484c2bf5226ecb56007ce0cc6ee303300ddfeed0 Mon Sep 17 00:00:00 2001 From: shinosaki Date: Thu, 27 Feb 2025 11:04:15 +0000 Subject: [PATCH] init --- LICENSE | 21 ++++++++ README.md | 83 ++++++++++++++++++++++++++++++++ example.go | 62 ++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + websocket/client.go | 115 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 288 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 websocket/client.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7fde2f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 shinosaki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c67485e --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# websocket-client-go + +`websocket-client-go` is a simple wrapper library for Go's `x/net/websocket`. + +## Installation + +```sh +go get github.com/shinosaki/websocket-client-go +``` + +## Usage + +[example.go](./example.go) +```go +package main + +import ( + "log" + "time" + + "github.com/shinosaki/websocket-client-go/websocket" +) + +const ( + WEBSOCKET_URL = "wss://echo.websocket.org" + ATTEMPTS = 3 + INTERVAL = 2 +) + +func main() { + ws := websocket.NewWebSocketClient( + // onOpen + func(ws *websocket.WebSocketClient) { + log.Println("Connected") + }, + + // onClose + func(ws *websocket.WebSocketClient, isReconnecting bool) { + if isReconnecting { + log.Println("Reconnecting...") + } else { + log.Println("Disconnected") + } + }, + + // onMessage + func(ws *websocket.WebSocketClient, payload []byte) { + log.Println("Received message:", string(payload)) + }, + ) + + // Connect to server + if err := ws.Connect(WEBSOCKET_URL, ATTEMPTS, INTERVAL); err != nil { + log.Println("Failed to connect:", err) + } + + sendMessage := func() { + data := map[string]string{"message": "Hello WebSocket"} + if err := ws.SendJSON(data); err != nil { + log.Println("Failed to send message:", err) + } + } + + // Send a message + sendMessage() + time.Sleep(2 * time.Second) + + // Reconnecting + ws.Reconnect(WEBSOCKET_URL, ATTEMPTS, INTERVAL) + sendMessage() + time.Sleep(2 * time.Second) + + ws.Disconnect(false) + + log.Println("Done") +} +``` + +## License +[MIT](./LICENSE) + +## Author +[shinosaki](https://shinosaki.com) diff --git a/example.go b/example.go new file mode 100644 index 0000000..25c2ff5 --- /dev/null +++ b/example.go @@ -0,0 +1,62 @@ +package main + +import ( + "log" + "time" + + "github.com/shinosaki/websocket-client-go/websocket" +) + +const ( + WEBSOCKET_URL = "wss://echo.websocket.org" + ATTEMPTS = 3 + INTERVAL = 2 +) + +func main() { + ws := websocket.NewWebSocketClient( + // onOpen + func(ws *websocket.WebSocketClient) { + log.Println("Connected") + }, + + // onClose + func(ws *websocket.WebSocketClient, isReconnecting bool) { + if isReconnecting { + log.Println("Reconnecting...") + } else { + log.Println("Disconnected") + } + }, + + // onMessage + func(ws *websocket.WebSocketClient, payload []byte) { + log.Println("Received message:", string(payload)) + }, + ) + + // Connect to server + if err := ws.Connect(WEBSOCKET_URL, ATTEMPTS, INTERVAL); err != nil { + log.Println("Failed to connect:", err) + } + + sendMessage := func() { + data := map[string]string{"message": "Hello WebSocket"} + if err := ws.SendJSON(data); err != nil { + log.Println("Failed to send message:", err) + } + } + + // Send a message + sendMessage() + time.Sleep(2 * time.Second) + + // Reconnecting + ws.Reconnect(WEBSOCKET_URL, ATTEMPTS, INTERVAL) + sendMessage() + time.Sleep(2 * time.Second) + + ws.Disconnect(false) + + log.Println("Done") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83eda93 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/shinosaki/websocket-client-go + +go 1.23.6 + +require golang.org/x/net v0.35.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f4761f9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= diff --git a/websocket/client.go b/websocket/client.go new file mode 100644 index 0000000..d1044be --- /dev/null +++ b/websocket/client.go @@ -0,0 +1,115 @@ +package websocket + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "sync" + "time" + + "golang.org/x/net/websocket" +) + +type WebSocketClient struct { + URL *url.URL + conn *websocket.Conn + wg sync.WaitGroup + ctx context.Context + cancel context.CancelFunc + onOpen func(ws *WebSocketClient) + onClose func(ws *WebSocketClient, isReconnecting bool) + onMessage func(ws *WebSocketClient, payload []byte) +} + +func NewWebSocketClient( + onOpen func(ws *WebSocketClient), + onClose func(ws *WebSocketClient, isReconnecting bool), + onMessage func(ws *WebSocketClient, payload []byte), +) *WebSocketClient { + ctx, cancel := context.WithCancel(context.Background()) + return &WebSocketClient{ + ctx: ctx, + cancel: cancel, + onOpen: onOpen, + onClose: onClose, + onMessage: onMessage, + } +} + +func (ws *WebSocketClient) Connect( + webSocketUrl string, + attempts int, + interval time.Duration, +) (err error) { + ws.URL, err = url.Parse(webSocketUrl) + if err != nil { + return err + } + + // attempt retry + for range attempts { + origin := ws.URL.Scheme + "://" + ws.URL.Host + ws.conn, err = websocket.Dial(ws.URL.String(), "", origin) + if err == nil { + break + } + time.Sleep(interval * time.Second) + } + if err != nil { + return fmt.Errorf("websocket dial falied: %v", err) + } + + if ws.onOpen != nil { + ws.onOpen(ws) + } + + // Message Handler + ws.wg.Add(1) + go func() { + defer ws.wg.Done() + for { + select { + case <-ws.ctx.Done(): + // log.Println("websocket receive cancel") + return + default: + var payload []byte + if err := websocket.Message.Receive(ws.conn, &payload); err != nil { + // log.Println("receive error", err) + return + } + ws.onMessage(ws, payload) + } + } + }() + + return nil +} + +func (ws *WebSocketClient) Disconnect(isReconnecting bool) { + if ws.conn != nil { + ws.conn.Close() + } + + ws.cancel() + ws.wg.Wait() + + if ws.onClose != nil { + ws.onClose(ws, isReconnecting) + } +} + +func (ws *WebSocketClient) Reconnect(url string, attempts int, interval time.Duration) error { + ws.Disconnect(true) + ws.ctx, ws.cancel = context.WithCancel(context.Background()) + return ws.Connect(url, attempts, interval) +} + +func (ws *WebSocketClient) SendJSON(v any) error { + bytes, err := json.Marshal(v) + if err != nil { + return fmt.Errorf("failed to marshal json: %v", err) + } + return websocket.Message.Send(ws.conn, string(bytes)) +}