rs2322tcp/internal/client/control.go
2026-08-10 17:59:35 +02:00

214 lines
4.6 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/control.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Client-seitige Control-Verbindung zum rs2322tcp-Server.
* Baut die TCP-Control-Verbindung auf, führt den Protokoll-Handshake durch
* und liest die vom Server bereitgestellten Geräteinformationen.
* ============================================================================
*/
package client
import (
"bufio"
"fmt"
"net"
"sync"
"git.lang-dieter.de/rs2322tcp/internal/transport"
)
///////////////////////////////////////////////////////////////////////////////
// Client
///////////////////////////////////////////////////////////////////////////////
// Client represents a connection to an rs2322tcp server.
type Client struct {
mu sync.Mutex
conn net.Conn
reader *bufio.Reader
}
///////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////
// New creates a new client and connects to the specified server address.
//
// The address must be in the form "host:port".
func New(address string) (*Client, error) {
if address == "" {
return nil, fmt.Errorf("server address is empty")
}
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, fmt.Errorf(
"connect to server %q: %w",
address,
err,
)
}
client := &Client{
conn: conn,
reader: bufio.NewReader(conn),
}
if err := client.hello(); err != nil {
_ = conn.Close()
return nil, err
}
return client, nil
}
///////////////////////////////////////////////////////////////////////////////
// Properties
///////////////////////////////////////////////////////////////////////////////
// Conn returns the underlying control connection.
//
// It returns nil if the client is nil or has already been closed.
func (c *Client) Conn() net.Conn {
if c == nil {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
return c.conn
}
///////////////////////////////////////////////////////////////////////////////
// Handshake
///////////////////////////////////////////////////////////////////////////////
func (c *Client) hello() error {
if c == nil {
return fmt.Errorf("client is not connected")
}
c.mu.Lock()
conn := c.conn
reader := c.reader
c.mu.Unlock()
if conn == nil {
return fmt.Errorf("client is not connected")
}
if err := transport.WriteMessage(
conn,
transport.NewHello(),
); err != nil {
return fmt.Errorf("send hello: %w", err)
}
var response transport.HelloResponseMessage
if err := transport.ReadMessage(
reader,
&response,
); err != nil {
return fmt.Errorf("read hello response: %w", err)
}
if response.Type != transport.MessageHelloResponse {
return fmt.Errorf(
"unexpected hello response: %q",
response.Type,
)
}
return nil
}
///////////////////////////////////////////////////////////////////////////////
// Devices
///////////////////////////////////////////////////////////////////////////////
// GetDevices requests the configured remote devices from the server.
//
// The returned list contains the session-specific dynamic TCP data ports.
func (c *Client) GetDevices() ([]transport.RemoteDeviceInfo, error) {
if c == nil {
return nil, fmt.Errorf("client is not connected")
}
c.mu.Lock()
conn := c.conn
reader := c.reader
c.mu.Unlock()
if conn == nil {
return nil, fmt.Errorf("client is not connected")
}
if err := transport.WriteMessage(
conn,
transport.NewGetDevices(),
); err != nil {
return nil, fmt.Errorf(
"send get-devices request: %w",
err,
)
}
var response transport.DeviceListMessage
if err := transport.ReadMessage(
reader,
&response,
); err != nil {
return nil, fmt.Errorf(
"read device list: %w",
err,
)
}
if response.Type != transport.MessageDeviceList {
return nil, fmt.Errorf(
"unexpected device list response: %q",
response.Type,
)
}
return response.Devices, nil
}
///////////////////////////////////////////////////////////////////////////////
// Close
///////////////////////////////////////////////////////////////////////////////
// Close closes the client control connection.
//
// The connection is removed from the Client while holding the mutex.
// The actual network close is performed afterwards so that another caller
// cannot race with the state change.
func (c *Client) Close() error {
if c == nil {
return nil
}
c.mu.Lock()
conn := c.conn
c.conn = nil
c.mu.Unlock()
if conn == nil {
return nil
}
return conn.Close()
}