proto-socket/go/tcp_client.go
toki 14e4cd56d0 feat: add WebSocket/WSS support and Go implementation
- Add WebSocket binary frame support to protocol specification
- Update README and PROTOCOL.md to reflect dual TCP/WebSocket transport
- Add Go implementation with TCP, WebSocket, and heartbeat support
- Include .claude settings configuration
2026-04-11 08:33:46 +09:00

216 lines
4.7 KiB
Go

package toki_socket
import (
"context"
"crypto/tls"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"google.golang.org/protobuf/proto"
"toki-labs.com/toki_socket/go/packets"
)
const MaxPacketSize = 64 << 20
type TcpClient struct {
Communicator
conn net.Conn
writeMu sync.Mutex // Defensive if WritePacket is called outside the queued writer.
heartbeatInterval time.Duration
heartbeatWait time.Duration
waitingHBResponse bool
hbTimer *HeartbeatTimer
hbMu sync.Mutex
disconnectListeners []func(*TcpClient)
disconnectMu sync.Mutex
tcpCloseOnce sync.Once
}
func NewTcpClient(conn net.Conn, intervalSec, waitSec int, parserMap ParserMap) *TcpClient {
c := &TcpClient{
conn: conn,
heartbeatInterval: time.Duration(intervalSec) * time.Second,
heartbeatWait: time.Duration(waitSec) * time.Second,
}
c.Communicator.Initialize(c, parserMap)
c.SetWriteErrorHandler(func(error) {
c.onDisconnected()
})
c.AddListener(TypeNameOf(&packets.HeartBeat{}), func(m proto.Message) {
c.onHeartBeat()
})
go c.readLoop()
c.sendHeartBeat()
return c
}
func DialTcp(ctx context.Context, host string, port int, intervalSec, waitSec int, parserMap ParserMap) (*TcpClient, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, err
}
return NewTcpClient(conn, intervalSec, waitSec, parserMap), nil
}
func DialTcpTLS(ctx context.Context, host string, port int, tlsCfg *tls.Config, intervalSec, waitSec int, parserMap ParserMap) (*TcpClient, error) {
d := tls.Dialer{Config: tlsCfg}
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, err
}
return NewTcpClient(conn, intervalSec, waitSec, parserMap), nil
}
func (c *TcpClient) readLoop() {
header := make([]byte, 4)
for c.IsAlive() {
if _, err := io.ReadFull(c.conn, header); err != nil {
c.onDisconnected()
return
}
length := binary.BigEndian.Uint32(header)
if length == 0 {
continue
}
if length > MaxPacketSize {
c.onDisconnected()
return
}
packetBytes := make([]byte, int(length))
if _, err := io.ReadFull(c.conn, packetBytes); err != nil {
c.onDisconnected()
return
}
base := &packets.PacketBase{}
if err := proto.Unmarshal(packetBytes, base); err != nil {
c.onDisconnected()
return
}
c.OnReceivedData(base.GetTypeName(), base.GetData(), base.GetNonce(), base.GetResponseNonce())
c.sendHeartBeat()
}
}
func (c *TcpClient) WritePacket(base *packets.PacketBase) error {
b, err := proto.Marshal(base)
if err != nil {
return err
}
header := make([]byte, 4)
binary.BigEndian.PutUint32(header, uint32(len(b)))
c.writeMu.Lock()
defer c.writeMu.Unlock()
if err := writeFull(c.conn, header); err != nil {
return err
}
return writeFull(c.conn, b)
}
func (c *TcpClient) AddDisconnectListener(handler func(*TcpClient)) {
c.disconnectMu.Lock()
defer c.disconnectMu.Unlock()
c.disconnectListeners = append(c.disconnectListeners, handler)
}
func (c *TcpClient) RemoveDisconnectListeners() {
c.disconnectMu.Lock()
defer c.disconnectMu.Unlock()
c.disconnectListeners = nil
}
func (c *TcpClient) Close() error {
var err error
c.tcpCloseOnce.Do(func() {
c.Communicator.shutdown()
c.stopHeartbeat()
err = c.conn.Close()
c.notifyDisconnected()
})
return err
}
func (c *TcpClient) sendHeartBeat() {
if !c.IsAlive() || c.heartbeatInterval <= 0 {
return
}
c.hbMu.Lock()
if c.hbTimer != nil {
c.hbTimer.Stop()
}
c.hbTimer = NewHeartbeatTimer(c.heartbeatInterval, func() {
if !c.IsAlive() {
return
}
c.hbMu.Lock()
c.waitingHBResponse = true
c.hbMu.Unlock()
_ = c.Send(&packets.HeartBeat{})
c.hbMu.Lock()
if c.hbTimer != nil {
c.hbTimer.Stop()
}
c.hbTimer = NewHeartbeatTimer(c.heartbeatWait, func() {
if c.IsAlive() {
c.onDisconnected()
}
})
c.hbMu.Unlock()
})
c.hbMu.Unlock()
}
func (c *TcpClient) onHeartBeat() {
c.hbMu.Lock()
if c.waitingHBResponse {
c.waitingHBResponse = false
c.hbMu.Unlock()
return
}
c.hbMu.Unlock()
_ = c.Send(&packets.HeartBeat{})
}
func (c *TcpClient) stopHeartbeat() {
c.hbMu.Lock()
defer c.hbMu.Unlock()
if c.hbTimer != nil {
c.hbTimer.Stop()
c.hbTimer = nil
}
}
func (c *TcpClient) onDisconnected() {
_ = c.Close()
}
func (c *TcpClient) notifyDisconnected() {
c.disconnectMu.Lock()
listeners := append([]func(*TcpClient){}, c.disconnectListeners...)
c.disconnectListeners = nil
c.disconnectMu.Unlock()
for _, listener := range listeners {
listener(c)
}
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
b = b[n:]
}
return nil
}