/* * ============================================================================ * 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() }