466 lines
14 KiB
Go
466 lines
14 KiB
Go
package network
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"git.hpds.cc/Component/network/hpds_err"
|
||
"git.hpds.cc/Component/network/id"
|
||
pkgtls "git.hpds.cc/Component/network/tls"
|
||
"net"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/lucas-clemente/quic-go"
|
||
|
||
"git.hpds.cc/Component/network/auth"
|
||
"git.hpds.cc/Component/network/frame"
|
||
"git.hpds.cc/Component/network/log"
|
||
)
|
||
|
||
// ClientOption client options
|
||
type ClientOption func(*ClientOptions)
|
||
|
||
// ConnState describes the state of the connection.
|
||
type ConnState = string
|
||
|
||
// Client is the abstraction of a HPDS-Client. a HPDS-Client can be
|
||
// Protocol Gateway, Message Queue or StreamFunction.
|
||
type Client struct {
|
||
name string // name of the client
|
||
clientId string // id of the client
|
||
clientType ClientType // type of the connection
|
||
conn quic.Connection // quic connection
|
||
stream quic.Stream // quic stream
|
||
state ConnState // state of the connection
|
||
processor func(*frame.DataFrame) // functions to invoke when data arrived
|
||
receiver func(*frame.BackFlowFrame) // functions to invoke when data is processed
|
||
addr string // the address of server connected to
|
||
mu sync.Mutex
|
||
opts ClientOptions
|
||
localAddr string // client local addr, it will be changed on reconnect
|
||
logger log.Logger
|
||
errChan chan error
|
||
closeChan chan bool
|
||
closed bool
|
||
}
|
||
|
||
// NewClient creates a new HPDS-Client.
|
||
func NewClient(appName string, connType ClientType, opts ...ClientOption) *Client {
|
||
c := &Client{
|
||
name: appName,
|
||
clientId: id.New(),
|
||
clientType: connType,
|
||
state: ConnStateReady,
|
||
opts: ClientOptions{},
|
||
errChan: make(chan error),
|
||
closeChan: make(chan bool),
|
||
}
|
||
c.Init(opts...)
|
||
once.Do(func() {
|
||
c.init()
|
||
})
|
||
|
||
return c
|
||
}
|
||
|
||
// Init the options.
|
||
func (c *Client) Init(opts ...ClientOption) error {
|
||
for _, o := range opts {
|
||
o(&c.opts)
|
||
}
|
||
return c.initOptions()
|
||
}
|
||
|
||
// Connect connects to HPDS-MessageQueue.
|
||
func (c *Client) Connect(ctx context.Context, addr string) error {
|
||
|
||
// TODO: refactor this later as a Connection Manager
|
||
// reconnect
|
||
// for download mq
|
||
// If you do not check for errors, the connection will be automatically reconnected
|
||
go c.reconnect(ctx, addr)
|
||
|
||
// connect
|
||
if err := c.connect(ctx, addr); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (c *Client) connect(ctx context.Context, addr string) error {
|
||
c.addr = addr
|
||
c.state = ConnStateConnecting
|
||
|
||
// create quic connection
|
||
conn, err := quic.DialAddrContext(ctx, addr, c.opts.TLSConfig, c.opts.QuicConfig)
|
||
if err != nil {
|
||
c.state = ConnStateDisconnected
|
||
return err
|
||
}
|
||
|
||
// quic stream
|
||
stream, err := conn.OpenStreamSync(ctx)
|
||
if err != nil {
|
||
c.state = ConnStateDisconnected
|
||
return err
|
||
}
|
||
|
||
c.stream = stream
|
||
c.conn = conn
|
||
|
||
c.state = ConnStateAuthenticating
|
||
// send handshake
|
||
handshake := frame.NewHandshakeFrame(
|
||
c.name,
|
||
c.clientId,
|
||
byte(c.clientType),
|
||
c.opts.ObserveDataTags,
|
||
c.opts.Credential.Name(),
|
||
c.opts.Credential.Payload(),
|
||
)
|
||
err = c.WriteFrame(handshake)
|
||
if err != nil {
|
||
c.state = ConnStateRejected
|
||
return err
|
||
}
|
||
c.state = ConnStateConnected
|
||
c.localAddr = c.conn.LocalAddr().String()
|
||
|
||
c.logger.Printf("%s [%s][%s](%s) is connected to HPDS-MQ %s", ClientLogPrefix, c.name, c.clientId, c.localAddr, addr)
|
||
|
||
// receiving frames
|
||
go c.handleFrame()
|
||
|
||
return nil
|
||
}
|
||
|
||
// handleFrame handles the logic when receiving frame from server.
|
||
func (c *Client) handleFrame() {
|
||
// transform raw QUIC stream to wire format
|
||
fs := NewFrameStream(c.stream)
|
||
for {
|
||
c.logger.Debugf("%shandleFrame connection state=%v", ClientLogPrefix, c.state)
|
||
// this will block until a frame is received
|
||
f, err := fs.ReadFrame()
|
||
if err != nil {
|
||
defer c.stream.Close()
|
||
// defer c.conn.CloseWithError(0xD0, err.Error())
|
||
|
||
c.logger.Debugf("%shandleFrame(): %T | %v", ClientLogPrefix, err, err)
|
||
if e, ok := err.(*quic.IdleTimeoutError); ok {
|
||
c.logger.Errorf("%sconnection timeout, err=%v, mq addr=%s", ClientLogPrefix, e, c.addr)
|
||
c.setState(ConnStateDisconnected)
|
||
} else if e, ok := err.(*quic.ApplicationError); ok {
|
||
c.logger.Infof("%sapplication error, err=%v, errcode=%v", ClientLogPrefix, e, e.ErrorCode)
|
||
if hpds_err.Is(e.ErrorCode, hpds_err.ErrorCodeRejected) {
|
||
// if connection is rejected(eg: authenticate fails) from server
|
||
c.logger.Errorf("%sIllegal client, server rejected.", ClientLogPrefix)
|
||
c.setState(ConnStateRejected)
|
||
break
|
||
} else if hpds_err.Is(e.ErrorCode, hpds_err.ErrorCodeClientAbort) {
|
||
// client abort
|
||
c.logger.Infof("%sclient close the connection", ClientLogPrefix)
|
||
c.setState(ConnStateAborted)
|
||
break
|
||
} else if hpds_err.Is(e.ErrorCode, hpds_err.ErrorCodeGoaway) {
|
||
// server goaway
|
||
c.logger.Infof("%sserver goaway the connection", ClientLogPrefix)
|
||
c.setState(ConnStateGoaway)
|
||
break
|
||
} else if hpds_err.Is(e.ErrorCode, hpds_err.ErrorCodeHandshake) {
|
||
// handshake
|
||
c.logger.Errorf("%shandshake fails", ClientLogPrefix)
|
||
c.setState(ConnStateRejected)
|
||
break
|
||
}
|
||
} else if errors.Is(err, net.ErrClosed) {
|
||
// if client close the connection, net.ErrClosed will be raised
|
||
c.logger.Errorf("%sconnection is closed, err=%v", ClientLogPrefix, err)
|
||
c.setState(ConnStateDisconnected)
|
||
// by quic-go IdleTimeoutError after connection's KeepAlive config.
|
||
break
|
||
} else {
|
||
// any error occurred, we should close the stream
|
||
// after this, conn.AcceptStream() will raise the error
|
||
c.setState(ConnStateClosed)
|
||
c.conn.CloseWithError(hpds_err.To(hpds_err.ErrorCodeUnknown), err.Error())
|
||
c.logger.Errorf("%sunknown error occurred, err=%v, state=%v", ClientLogPrefix, err, c.getState())
|
||
break
|
||
}
|
||
}
|
||
if f == nil {
|
||
break
|
||
}
|
||
// read frame
|
||
// first, get frame type
|
||
frameType := f.Type()
|
||
c.logger.Debugf("%stype=%s, frame=%# x", ClientLogPrefix, frameType, frame.Shortly(f.Encode()))
|
||
switch frameType {
|
||
case frame.TagOfHandshakeFrame:
|
||
if v, ok := f.(*frame.HandshakeFrame); ok {
|
||
c.logger.Debugf("%sreceive HandshakeFrame, name=%v", ClientLogPrefix, v.Name)
|
||
}
|
||
case frame.TagOfPongFrame:
|
||
c.setState(ConnStatePong)
|
||
case frame.TagOfAcceptedFrame:
|
||
c.setState(ConnStateAccepted)
|
||
case frame.TagOfRejectedFrame:
|
||
c.setState(ConnStateRejected)
|
||
if v, ok := f.(*frame.RejectedFrame); ok {
|
||
c.logger.Errorf("%s receive RejectedFrame, message=%s", ClientLogPrefix, v.Message())
|
||
c.conn.CloseWithError(hpds_err.To(hpds_err.ErrorCodeRejected), v.Message())
|
||
c.errChan <- errors.New(v.Message())
|
||
break
|
||
}
|
||
case frame.TagOfGoawayFrame:
|
||
c.setState(ConnStateGoaway)
|
||
if v, ok := f.(*frame.GoawayFrame); ok {
|
||
c.logger.Errorf("%s️ receive GoawayFrame, message=%s", ClientLogPrefix, v.Message())
|
||
c.conn.CloseWithError(hpds_err.To(hpds_err.ErrorCodeGoaway), v.Message())
|
||
c.errChan <- errors.New(v.Message())
|
||
break
|
||
}
|
||
case frame.TagOfDataFrame: // DataFrame carries user's data
|
||
if v, ok := f.(*frame.DataFrame); ok {
|
||
c.setState(ConnStateTransportData)
|
||
c.logger.Debugf("%sreceive DataFrame, tag=%#x, tid=%s, carry=%# x", ClientLogPrefix, v.GetDataTag(), v.TransactionId(), v.GetCarriage())
|
||
if c.processor == nil {
|
||
c.logger.Warnf("%sprocessor is nil", ClientLogPrefix)
|
||
} else {
|
||
// TODO: should c.processor accept a DataFrame as parameter?
|
||
// c.processor(v.GetDataTagID(), v.GetCarriage(), v.GetMetaFrame())
|
||
c.processor(v)
|
||
}
|
||
}
|
||
case frame.TagOfBackFlowFrame:
|
||
if v, ok := f.(*frame.BackFlowFrame); ok {
|
||
c.logger.Debugf("%sreceive BackFlowFrame, tag=%#x, carry=%# x", ClientLogPrefix, v.GetDataTag(), v.GetCarriage())
|
||
if c.receiver == nil {
|
||
c.logger.Warnf("%sreceiver is nil", ClientLogPrefix)
|
||
} else {
|
||
c.setState(ConnStateBackFlow)
|
||
c.receiver(v)
|
||
}
|
||
}
|
||
default:
|
||
c.logger.Errorf("%sunknown signal", ClientLogPrefix)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Close the client.
|
||
func (c *Client) Close() (err error) {
|
||
if c.conn != nil {
|
||
c.logger.Printf("%sclose the connection, name:%s, id:%s, addr:%s", ClientLogPrefix, c.name, c.clientId, c.conn.RemoteAddr().String())
|
||
}
|
||
if c.stream != nil {
|
||
err = c.stream.Close()
|
||
if err != nil {
|
||
c.logger.Errorf("%s stream.Close(): %v", ClientLogPrefix, err)
|
||
}
|
||
}
|
||
if c.conn != nil {
|
||
err = c.conn.CloseWithError(0, "client-ask-to-close-this-connection")
|
||
if err != nil {
|
||
c.logger.Errorf("%s connection.Close(): %v", ClientLogPrefix, err)
|
||
}
|
||
}
|
||
// close channel
|
||
c.mu.Lock()
|
||
if !c.closed {
|
||
close(c.errChan)
|
||
close(c.closeChan)
|
||
c.closed = true
|
||
}
|
||
c.mu.Unlock()
|
||
|
||
return err
|
||
}
|
||
|
||
// WriteFrame writes a frame to the connection, gurantee threadsafe.
|
||
func (c *Client) WriteFrame(frm frame.Frame) error {
|
||
// write on QUIC stream
|
||
if c.stream == nil {
|
||
return errors.New("stream is nil")
|
||
}
|
||
if c.state == ConnStateDisconnected || c.state == ConnStateRejected {
|
||
return fmt.Errorf("client connection state is %s", c.state)
|
||
}
|
||
c.logger.Debugf("%s[%s](%s)@%s WriteFrame() will write frame: %s", ClientLogPrefix, c.name, c.localAddr, c.state, frm.Type())
|
||
|
||
data := frm.Encode()
|
||
// emit raw bytes of Frame
|
||
c.mu.Lock()
|
||
n, err := c.stream.Write(data)
|
||
c.mu.Unlock()
|
||
c.logger.Debugf("%sWriteFrame() wrote n=%d, data=%# x", ClientLogPrefix, n, frame.Shortly(data))
|
||
if err != nil {
|
||
c.setState(ConnStateDisconnected)
|
||
// c.state = ConnStateDisconnected
|
||
if e, ok := err.(*quic.IdleTimeoutError); ok {
|
||
c.logger.Errorf("%sWriteFrame() connection timeout, err=%v", ClientLogPrefix, e)
|
||
} else {
|
||
c.logger.Errorf("%sWriteFrame() wrote error=%v", ClientLogPrefix, err)
|
||
return err
|
||
}
|
||
}
|
||
if n != len(data) {
|
||
err := errors.New("[client] hpds Client .Write() wrote error")
|
||
c.logger.Errorf("%s error:%v", ClientLogPrefix, err)
|
||
return err
|
||
}
|
||
return err
|
||
}
|
||
|
||
// update connection state
|
||
func (c *Client) setState(state ConnState) {
|
||
c.logger.Debugf("setState to:%s", state)
|
||
c.mu.Lock()
|
||
c.state = state
|
||
c.mu.Unlock()
|
||
}
|
||
|
||
// getState get connection state
|
||
func (c *Client) getState() ConnState {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
return c.state
|
||
}
|
||
|
||
// update connection local addr
|
||
func (c *Client) setLocalAddr(addr string) {
|
||
c.mu.Lock()
|
||
c.localAddr = addr
|
||
c.mu.Unlock()
|
||
}
|
||
|
||
// SetDataFrameObserver sets the data frame handler.
|
||
func (c *Client) SetDataFrameObserver(fn func(*frame.DataFrame)) {
|
||
c.processor = fn
|
||
c.logger.Debugf("%sSetDataFrameObserver(%v)", ClientLogPrefix, c.processor)
|
||
}
|
||
|
||
// SetBackFlowFrameObserver sets the backflow frame handler.
|
||
func (c *Client) SetBackFlowFrameObserver(fn func(*frame.BackFlowFrame)) {
|
||
c.receiver = fn
|
||
c.logger.Debugf("%sSetBackFlowFrameObserver(%v)", ClientLogPrefix, c.receiver)
|
||
}
|
||
|
||
// reconnect the connection between client and server.
|
||
func (c *Client) reconnect(ctx context.Context, addr string) {
|
||
t := time.NewTicker(1 * time.Second)
|
||
defer t.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
c.logger.Debugf("%s[%s](%s) context.Done()", ClientLogPrefix, c.name, c.localAddr)
|
||
return
|
||
case <-c.closeChan:
|
||
c.logger.Debugf("%s[%s](%s) close channel", ClientLogPrefix, c.name, c.localAddr)
|
||
return
|
||
case <-t.C:
|
||
if c.getState() == ConnStateDisconnected {
|
||
c.logger.Printf("%s[%s][%s](%s) is reconnecting to HPDS-MQ %s...", ClientLogPrefix, c.name, c.clientId, c.localAddr, addr)
|
||
err := c.connect(ctx, addr)
|
||
if err != nil {
|
||
c.logger.Errorf("%s[%s][%s](%s) reconnect error:%v", ClientLogPrefix, c.name, c.clientId, c.localAddr, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (c *Client) init() {
|
||
// // tracing
|
||
// _, _, err := tracing.NewTracerProvider(c.name)
|
||
// if err != nil {
|
||
// logger.Errorf("tracing: %v", err)
|
||
// }
|
||
}
|
||
|
||
// ServerAddr returns the address of the server.
|
||
func (c *Client) ServerAddr() string {
|
||
return c.addr
|
||
}
|
||
|
||
// initOptions init options defaults
|
||
func (c *Client) initOptions() error {
|
||
// logger
|
||
if c.logger == nil {
|
||
if c.opts.Logger != nil {
|
||
c.logger = c.opts.Logger
|
||
} else {
|
||
c.logger = log.Default()
|
||
}
|
||
}
|
||
// observe tag list
|
||
if c.opts.ObserveDataTags == nil {
|
||
c.opts.ObserveDataTags = make([]byte, 0)
|
||
}
|
||
// credential
|
||
if c.opts.Credential == nil {
|
||
c.opts.Credential = auth.NewCredential("")
|
||
}
|
||
// tls config
|
||
if c.opts.TLSConfig == nil {
|
||
tc, err := pkgtls.CreateClientTLSConfig()
|
||
if err != nil {
|
||
c.logger.Errorf("%sCreateClientTLSConfig: %v", ClientLogPrefix, err)
|
||
return err
|
||
}
|
||
c.opts.TLSConfig = tc
|
||
}
|
||
// quic config
|
||
if c.opts.QuicConfig == nil {
|
||
c.opts.QuicConfig = &quic.Config{
|
||
Versions: []quic.VersionNumber{quic.Version1, quic.VersionDraft29},
|
||
MaxIdleTimeout: time.Second * 40,
|
||
KeepAlivePeriod: time.Second * 20,
|
||
MaxIncomingStreams: 1000,
|
||
MaxIncomingUniStreams: 1000,
|
||
HandshakeIdleTimeout: time.Second * 3,
|
||
InitialStreamReceiveWindow: 1024 * 1024 * 2,
|
||
InitialConnectionReceiveWindow: 1024 * 1024 * 2,
|
||
TokenStore: quic.NewLRUTokenStore(10, 5),
|
||
DisablePathMTUDiscovery: true,
|
||
}
|
||
}
|
||
// credential
|
||
if c.opts.Credential != nil {
|
||
c.logger.Printf("%suse credential: [%s]", ClientLogPrefix, c.opts.Credential.Name())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// SetObserveDataTags set the data tag list that will be observed.
|
||
// Deprecated: use hpds.WithObserveDataTags instead
|
||
func (c *Client) SetObserveDataTags(tag ...byte) {
|
||
c.opts.ObserveDataTags = append(c.opts.ObserveDataTags, tag...)
|
||
}
|
||
|
||
// Logger get client's logger instance, you can customize this using `hpds.WithLogger`
|
||
func (c *Client) Logger() log.Logger {
|
||
return c.logger
|
||
}
|
||
|
||
// SetErrorHandler set error handler
|
||
func (c *Client) SetErrorHandler(fn func(err error)) {
|
||
if fn != nil {
|
||
go func() {
|
||
err := <-c.errChan
|
||
if err != nil {
|
||
fn(err)
|
||
}
|
||
}()
|
||
}
|
||
}
|
||
|
||
// ClientId return the client ID
|
||
func (c *Client) ClientId() string {
|
||
return c.clientId
|
||
}
|