Skip to content

Commit

Permalink
refactor: prepartion for WebSocket client addition
Browse files Browse the repository at this point in the history
This renames ClientSession => Session and places these common handlers
and struct into its own file common.go
  • Loading branch information
mikefero committed Jun 3, 2024
1 parent cfa5c31 commit de5513e
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 66 deletions.
47 changes: 24 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Features:

- **Efficient Connection Management**: Ankh streamlines upgrading HTTP to
WebSocket connections and their ongoing management.
- **Thread-safe Client Interaction**: Provides thread-safe mechanisms to send
- **Thread-safe Session Interaction**: Provides thread-safe mechanisms to send
messages and close connections, ensuring safe concurrent access to interact
directly with connected clients.
- **Customizable Event Handlers and Lifecycle Management**: Customize handlers
Expand All @@ -41,34 +41,34 @@ events:

```go
type MyWebSocketServerHandler struct{
clientSessions map[any]ankh.ClientSession
mutex sync.Mutex
sessions map[any]ankh.Session
mutex sync.Mutex
}

func (h *MyWebSocketServerHandler) OnConnectionHandler(w http.ResponseWriter, r *http.Request) (any, error) {
// Authenticate the client and return a client key
return "clientKey", nil
}

func (h *MyWebSocketServerHandler) OnConnectedHandler(clientKey any, clientSession ClientSession) error {
func (h *MyWebSocketServerHandler) OnConnectedHandler(clientKey any, session Session) error {
// Handle post-connection setup
log.Printf("client connected: %v", clientKey)

// Store the client session in a thread-safe manner
// Store the session in a thread-safe manner
h.mutex.Lock()
defer h.mutex.Unlock()
h.clientSessions[clientKey] = clientSession
h.sessions[clientKey] = session
return nil
}

func (h *MyWebSocketServerHandler) OnDisconnectionHandler(clientKey any) {
// Handle disconnection cleanup
log.Printf("client disconnected: %v", clientKey)

// Remove the client session in a thread-safe manner
// Remove the session in a thread-safe manner
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.clientSessions, clientKey)
delete(h.sessions, clientKey)
}

func (h *MyWebSocketServerHandler) OnDisconnectionErrorHandler(clientKey any, err error) {
Expand Down Expand Up @@ -155,38 +155,38 @@ import (
)

type MyWebSocketServerHandler struct{
clientSessions map[any]ankh.ClientSession
mutex sync.Mutex
sessions map[any]ankh.Session
mutex sync.Mutex
}

func (h *MyWebSocketServerHandler) OnConnectionHandler(w http.ResponseWriter, r *http.Request) (any, error) {
return "clientKey", nil
}

func (h *MyWebSocketServerHandler) OnConnectedHandler(clientKey any, clientSession ankh.ClientSession) error {
func (h *MyWebSocketServerHandler) OnConnectedHandler(clientKey any, session ankh.Session) error {
log.Printf("client connected: %v", clientKey)

// Send a welcome message
err := clientSession.Send([]byte("Welcome to the Ankh WebSocket server!"))
err := session.Send([]byte("Welcome to the Ankh WebSocket server!"))
if err != nil {
log.Printf("failed to send welcome message: %v", err)
return err
}

// Store the client session in a thread-safe manner
// Store the session in a thread-safe manner
h.mutex.Lock()
defer h.mutex.Unlock()
h.clientSessions[clientKey] = clientSession
h.sessions[clientKey] = session
return nil
}

func (h *MyWebSocketServerHandler) OnDisconnectionHandler(clientKey any) {
log.Printf("client disconnected: %v", clientKey)

// Remove the client session in a thread-safe manner
// Remove the session in a thread-safe manner
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.clientSessions, clientKey)
delete(h.sessions, clientKey)
}

func (h *MyWebSocketServerHandler) OnDisconnectionErrorHandler(clientKey any, err error) {
Expand Down Expand Up @@ -240,15 +240,16 @@ func main() {
}
```

#### Handling Client Sessions
#### Handling Sessions

The ClientSession type provides thread-safe methods to interact with a connected
WebSocket client. You can use it to send messages or close the connection.
The Session type provides thread-safe methods to interact with a connected
WebSocket client/server. You can use it to send messages or close the
connection.

- **Send a Binary Message**: To send a binary message to the client, use the
`Send` method.
- **Close the Connection**: To close the client connection, use the `Close`
method.
- **Send a Binary Message**: To send a binary message to the client/server, use
the `Send` method.
- **Close the Connection**: To close the client/server connection, use the
`Close` method.

## License

Expand Down
32 changes: 32 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright © 2024 Michael Fero
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ankh

// CloseConnection will terminate the connection with the client/server
// application. This handle will be given during the OnConnectedHandler callback
// and is guaranteed to be thread safe.
type CloseConnection func()

// SendMessage will send a binary message to the client/server application. This
// handle will be given during the OnConnectedHandler callback and is guaranteed
// to be thread safe.
type SendMessage func(data []byte) error

// Session represents the client/server session for the WebSocket.
type Session struct {
// Close will close the connection with the client/server application.
Close CloseConnection
// Send will send a binary message to the client/server application.
Send SendMessage
}
26 changes: 4 additions & 22 deletions websocket_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,6 @@ import (
"github.com/gorilla/websocket"
)

// CloseConnection will terminate the connection with the client application.
// This handle will be given during the OnConnectedHandler callback and is
// guaranteed to be thread safe.
type CloseConnection func()

// SendMessage will send a binary message to the client application. This handle
// will be given during the OnConnectedHandler callback and is guaranteed to be
// thread safe.
type SendMessage func(data []byte) error

// ClientSession represents the client session for the WebSocket.
type ClientSession struct {
// Close will close the connection with the client application.
Close CloseConnection
// Send will send a binary message to the client application.
Send SendMessage
}

// WebsocketServerEventHandler represents the callback handlers for the
// WebSocket server.
type WebSocketServerEventHandler interface {
Expand All @@ -59,7 +41,7 @@ type WebSocketServerEventHandler interface {
// OnConnectedHandler is a function callback for indicating that the client
// connection is completed while creating handles for closing connections and
// sending messages on the WebSocket.
OnConnectedHandler(clientKey any, clientSession ClientSession) error
OnConnectedHandler(clientKey any, session Session) error

// OnDisconnectionHandler is a function callback for WebSocket connections
// that is executed upon disconnection of a client.
Expand Down Expand Up @@ -263,12 +245,12 @@ func (s *WebSocketServer) handleConnection(ctx context.Context, w http.ResponseW
return writeMessage(conn, &mutex, websocket.PongMessage, data)
})

if err := handler.OnConnectedHandler(clientKey, ClientSession{
// Generate a close function for the client session
if err := handler.OnConnectedHandler(clientKey, Session{
// Generate a close function for the session
Close: func() {
closeConnection(conn, &mutex, clientKey, handler)
},
// Generate a send message function for the client session
// Generate a send message function for the session
Send: func(data []byte) error {
return writeMessage(conn, &mutex, websocket.BinaryMessage, data)
},
Expand Down
42 changes: 21 additions & 21 deletions websocket_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,14 +397,14 @@ func TestWebSocketServer(t *testing.T) {
When(handler2.OnConnectionHandler(Any[http.ResponseWriter](), Any[*http.Request]())).
ThenReturn("client-key-3", nil).
ThenReturn("client-key-4", nil)
captor1ClientSession := Captor[ankh.ClientSession]()
captor2ClientSession := Captor[ankh.ClientSession]()
captor3ClientSession := Captor[ankh.ClientSession]()
captor4ClientSession := Captor[ankh.ClientSession]()
WhenSingle(handler1.OnConnectedHandler(Exact("client-key-1"), captor1ClientSession.Capture())).ThenReturn(nil)
WhenSingle(handler1.OnConnectedHandler(Exact("client-key-2"), captor2ClientSession.Capture())).ThenReturn(nil)
WhenSingle(handler2.OnConnectedHandler(Exact("client-key-3"), captor3ClientSession.Capture())).ThenReturn(nil)
WhenSingle(handler2.OnConnectedHandler(Exact("client-key-4"), captor4ClientSession.Capture())).ThenReturn(nil)
captor1Session := Captor[ankh.Session]()
captor2Session := Captor[ankh.Session]()
captor3Session := Captor[ankh.Session]()
captor4Session := Captor[ankh.Session]()
WhenSingle(handler1.OnConnectedHandler(Exact("client-key-1"), captor1Session.Capture())).ThenReturn(nil)
WhenSingle(handler1.OnConnectedHandler(Exact("client-key-2"), captor2Session.Capture())).ThenReturn(nil)
WhenSingle(handler2.OnConnectedHandler(Exact("client-key-3"), captor3Session.Capture())).ThenReturn(nil)
WhenSingle(handler2.OnConnectedHandler(Exact("client-key-4"), captor4Session.Capture())).ThenReturn(nil)
client1, err := createWebSocketClient(t, webSocketServer.address, "/path1", tt.withTLS)
require.NoError(t, err)
client2, err := createWebSocketClient(t, webSocketServer.address, "/path1", tt.withTLS)
Expand All @@ -421,22 +421,22 @@ func TestWebSocketServer(t *testing.T) {
}()

t.Log("verify WebSocket clients connected")
waitForCapture(captor1ClientSession)
waitForCapture(captor2ClientSession)
waitForCapture(captor3ClientSession)
waitForCapture(captor4ClientSession)
waitForCapture(captor1Session)
waitForCapture(captor2Session)
waitForCapture(captor3Session)
waitForCapture(captor4Session)
Verify(handler1, Times(2)).OnConnectionHandler(Any[http.ResponseWriter](), Any[*http.Request]())
Verify(handler2, Times(2)).OnConnectionHandler(Any[http.ResponseWriter](), Any[*http.Request]())
Verify(handler1, Once()).OnConnectedHandler(Exact("client-key-1"), Any[ankh.ClientSession]())
Verify(handler1, Once()).OnConnectedHandler(Exact("client-key-2"), Any[ankh.ClientSession]())
Verify(handler2, Once()).OnConnectedHandler(Exact("client-key-3"), Any[ankh.ClientSession]())
Verify(handler2, Once()).OnConnectedHandler(Exact("client-key-4"), Any[ankh.ClientSession]())
Verify(handler1, Once()).OnConnectedHandler(Exact("client-key-1"), Any[ankh.Session]())
Verify(handler1, Once()).OnConnectedHandler(Exact("client-key-2"), Any[ankh.Session]())
Verify(handler2, Once()).OnConnectedHandler(Exact("client-key-3"), Any[ankh.Session]())
Verify(handler2, Once()).OnConnectedHandler(Exact("client-key-4"), Any[ankh.Session]())
VerifyNoMoreInteractions(handler1)
VerifyNoMoreInteractions(handler2)

t.Log("verify closing the connection of the WebSocket client will close the connection with the server")
// Close the WebSocket server connection and wait for client to receive the close message
captor2ClientSession.Last().Close()
captor2Session.Last().Close()
_, _, err = client2.readMessage()
var closeErr *websocket.CloseError
if !errors.As(err, &closeErr) {
Expand All @@ -462,17 +462,17 @@ func TestWebSocketServer(t *testing.T) {
VerifyNoMoreInteractions(handler2)

t.Log("verify message from WebSocket server to the client is sent")
captor1ClientSession.Last().Send([]byte("fero-1"))
captor1Session.Last().Send([]byte("fero-1"))
messageType, data, err := client1.readMessage()
require.NoError(t, err)
require.Equal(t, websocket.BinaryMessage, messageType)
require.Equal(t, []byte("fero-1"), data)
captor3ClientSession.Last().Send([]byte("fero-3"))
captor3Session.Last().Send([]byte("fero-3"))
messageType, data, err = client3.readMessage()
require.NoError(t, err)
require.Equal(t, websocket.BinaryMessage, messageType)
require.Equal(t, []byte("fero-3"), data)
captor4ClientSession.Last().Send([]byte("fero-4"))
captor4Session.Last().Send([]byte("fero-4"))
messageType, data, err = client4.readMessage()
require.NoError(t, err)
require.Equal(t, websocket.BinaryMessage, messageType)
Expand Down Expand Up @@ -543,7 +543,7 @@ func TestWebSocketServer(t *testing.T) {
waitFor(t) // wait for handlers to be called

Verify(handler, Once()).OnConnectionHandler(Any[http.ResponseWriter](), Any[*http.Request]())
Verify(handler, Once()).OnConnectedHandler(Exact("client-key"), Any[ankh.ClientSession]())
Verify(handler, Once()).OnConnectedHandler(Exact("client-key"), Any[ankh.Session]())
Verify(handler, Once()).OnDisconnectionHandler(Exact("client-key"))
Verify(handler, Once()).OnReadMessageErrorHandler(Exact("client-key"), Any[error]()) // connection closed improperly
VerifyNoMoreInteractions(handler)
Expand Down

0 comments on commit de5513e

Please sign in to comment.