mirror of
https://github.com/Gouryella/drip.git
synced 2026-02-24 05:10:43 +00:00
feat(client): Added the --short option to the version command to support plain text output.
Added the `--short` flag to the `version` command for printing version information without styles. In this mode, only the version, Git commit hash, and build time in plain text format will be output, facilitating script parsing. Optimized Windows process detection logic to improve runtime accuracy. Removed redundant comments and simplified signal checking methods, making the code clearer and easier to maintain. refactor(protocol): Replaced string matching of data frame types with enumeration types. Unified the representation of data frame types in the protocol, using the `DataType` enumeration to improve performance and readability. Introduced a pooled buffer mechanism to improve memory efficiency in high-load scenarios. refactor(ui): Adjusted style definitions, removing hard-coded color values. Removed fixed color settings from some lipgloss styles, providing flexibility for future theme customization. ``` docs(install): Improved the version extraction function in the installation script. Added the `get_version_from_binary` function to enhance version identification capabilities, prioritizing plain mode output, ensuring accurate version number acquisition for the drip client or server across different terminal environments. perf(tcp): Improved TCP processing performance and connection management capabilities. Adjusted HTTP client transmission parameter configuration, increasing the maximum number of idle connections to accommodate higher concurrent requests. Improved error handling logic, adding special checks for common cases such as closing network connections to avoid log pollution. chore(writer): Expanded the FrameWriter queue length to improve batch write stability. Increased the FrameWriter queue size from 1024 to 2048, and released pooled resources after flushing, better handling sudden traffic spikes and reducing memory usage fluctuations.
This commit is contained in:
@@ -17,14 +17,8 @@ func getSysProcAttr() *syscall.SysProcAttr {
|
||||
|
||||
// isProcessRunningOS checks if a process is running using OS-specific method
|
||||
func isProcessRunningOS(process *os.Process) bool {
|
||||
// On Windows, we try to open the process to check if it exists
|
||||
// FindProcess doesn't actually check if process exists on Windows
|
||||
// We can try to send signal, but Windows doesn't support signal 0
|
||||
// Instead, we'll try to kill with signal 0 which returns an error if process doesn't exist
|
||||
err := process.Signal(os.Signal(syscall.Signal(0)))
|
||||
if err != nil {
|
||||
// Try alternative: check if we can get process info
|
||||
// If the process doesn't exist, Signal will fail
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
var (
|
||||
// Version information
|
||||
Version = "dev"
|
||||
GitCommit = "unknown"
|
||||
BuildTime = "unknown"
|
||||
Version = "dev"
|
||||
GitCommit = "unknown"
|
||||
BuildTime = "unknown"
|
||||
versionPlain bool
|
||||
|
||||
// Global flags
|
||||
serverURL string
|
||||
@@ -51,6 +52,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
|
||||
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "k", false, "Skip TLS verification (testing only, NOT recommended)")
|
||||
|
||||
versionCmd.Flags().BoolVar(&versionPlain, "short", false, "Print version information without styling")
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
// http and tcp commands are added in their respective init() functions
|
||||
// config command is added in config.go init() function
|
||||
@@ -60,6 +63,11 @@ var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if versionPlain {
|
||||
fmt.Printf("Version: %s\nGit Commit: %s\nBuild Time: %s\n", Version, GitCommit, BuildTime)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(ui.Info(
|
||||
"Drip Client",
|
||||
"",
|
||||
|
||||
@@ -23,7 +23,6 @@ var (
|
||||
// Box styles - Vercel-like clean box
|
||||
boxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#333")).
|
||||
Padding(1, 2).
|
||||
MarginTop(1).
|
||||
MarginBottom(1)
|
||||
@@ -39,8 +38,7 @@ var (
|
||||
|
||||
// Text styles
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FFF"))
|
||||
Bold(true)
|
||||
|
||||
subtitleStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
|
||||
@@ -60,7 +60,7 @@ func RenderTunnelConnected(status *TunnelStatus) string {
|
||||
urlLine := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
urlStyle.Copy().Foreground(accent).Render(status.URL),
|
||||
lipgloss.NewStyle().MarginLeft(1).Foreground(mutedColor).Render("(forwarded link)"),
|
||||
lipgloss.NewStyle().MarginLeft(1).Foreground(mutedColor).Render("(forwarded address)"),
|
||||
)
|
||||
|
||||
forwardLine := lipgloss.NewStyle().
|
||||
|
||||
@@ -64,9 +64,9 @@ func NewFrameHandler(conn net.Conn, frameWriter *protocol.FrameWriter, localHost
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 500,
|
||||
MaxIdleConnsPerHost: 200,
|
||||
MaxConnsPerHost: 0,
|
||||
MaxIdleConns: 1000, // Optimized for both mid and high load scenarios
|
||||
MaxIdleConnsPerHost: 500, // Sufficient for 400+ concurrent connections
|
||||
MaxConnsPerHost: 0, // Unlimited
|
||||
IdleConnTimeout: 180 * time.Second,
|
||||
DisableCompression: true,
|
||||
DisableKeepAlives: false,
|
||||
@@ -95,11 +95,11 @@ func (h *FrameHandler) HandleDataFrame(frame *protocol.Frame) error {
|
||||
return fmt.Errorf("failed to decode data payload: %w", err)
|
||||
}
|
||||
|
||||
if header.Type == "http_request" {
|
||||
if header.Type == protocol.DataTypeHTTPRequest {
|
||||
return h.handleHTTPFrame(header, data)
|
||||
}
|
||||
|
||||
if header.Type == "close" {
|
||||
if header.Type == protocol.DataTypeClose {
|
||||
h.closeStream(header.StreamID)
|
||||
return nil
|
||||
}
|
||||
@@ -172,17 +172,17 @@ func (h *FrameHandler) handleLocalResponse(stream *Stream) {
|
||||
|
||||
header := protocol.DataHeader{
|
||||
StreamID: stream.ID,
|
||||
Type: "response",
|
||||
Type: protocol.DataTypeResponse,
|
||||
IsLast: false,
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, buf[:n])
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, buf[:n])
|
||||
if err != nil {
|
||||
h.logger.Error("Encode payload failed", zap.Error(err))
|
||||
break
|
||||
}
|
||||
|
||||
dataFrame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
dataFrame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
err = h.frameWriter.WriteFrame(dataFrame)
|
||||
if err != nil {
|
||||
h.logger.Error("Send frame failed", zap.Error(err))
|
||||
@@ -302,7 +302,7 @@ func (h *FrameHandler) sendHTTPResponse(streamID, requestID string, resp *protoc
|
||||
header := protocol.DataHeader{
|
||||
StreamID: streamID,
|
||||
RequestID: requestID,
|
||||
Type: "http_response",
|
||||
Type: protocol.DataTypeHTTPResponse,
|
||||
IsLast: true,
|
||||
}
|
||||
|
||||
@@ -311,12 +311,12 @@ func (h *FrameHandler) sendHTTPResponse(streamID, requestID string, resp *protoc
|
||||
return fmt.Errorf("encode http response: %w", err)
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, respBytes)
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, respBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode payload: %w", err)
|
||||
}
|
||||
|
||||
dataFrame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
dataFrame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
|
||||
h.stats.AddBytesOut(int64(len(payload)))
|
||||
|
||||
@@ -347,16 +347,16 @@ func (h *FrameHandler) closeStream(streamID string) {
|
||||
header := protocol.DataHeader{
|
||||
StreamID: streamID,
|
||||
RequestID: streamID,
|
||||
Type: "close",
|
||||
Type: protocol.DataTypeClose,
|
||||
IsLast: true,
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, nil)
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
closeFrame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
closeFrame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
|
||||
h.frameWriter.WriteFrame(closeFrame)
|
||||
}
|
||||
|
||||
@@ -105,18 +105,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
header := protocol.DataHeader{
|
||||
StreamID: requestID,
|
||||
RequestID: requestID,
|
||||
Type: "http_request",
|
||||
Type: protocol.DataTypeHTTPRequest,
|
||||
IsLast: true,
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, reqBytes)
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, reqBytes)
|
||||
if err != nil {
|
||||
h.logger.Error("Encode data payload failed", zap.Error(err))
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
frame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
frame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
|
||||
respChan := h.responses.CreateResponseChan(requestID)
|
||||
defer h.responses.CleanupResponseChan(requestID)
|
||||
|
||||
@@ -2,7 +2,7 @@ package tcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
json "github.com/goccy/go-json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -11,27 +11,30 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
|
||||
"drip/internal/server/tunnel"
|
||||
"drip/internal/shared/constants"
|
||||
"drip/internal/shared/protocol"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Connection represents a client TCP connection
|
||||
type Connection struct {
|
||||
conn net.Conn
|
||||
authToken string
|
||||
manager *tunnel.Manager
|
||||
logger *zap.Logger
|
||||
subdomain string
|
||||
port int
|
||||
domain string
|
||||
publicPort int
|
||||
portAlloc *PortAllocator
|
||||
tunnelConn *tunnel.Connection
|
||||
proxy *TunnelProxy
|
||||
stopCh chan struct{}
|
||||
once sync.Once
|
||||
conn net.Conn
|
||||
authToken string
|
||||
manager *tunnel.Manager
|
||||
logger *zap.Logger
|
||||
subdomain string
|
||||
port int
|
||||
domain string
|
||||
publicPort int
|
||||
portAlloc *PortAllocator
|
||||
tunnelConn *tunnel.Connection
|
||||
proxy *TunnelProxy
|
||||
stopCh chan struct{}
|
||||
once sync.Once
|
||||
lastHeartbeat time.Time
|
||||
mu sync.RWMutex
|
||||
frameWriter *protocol.FrameWriter
|
||||
@@ -67,6 +70,9 @@ func NewConnection(conn net.Conn, authToken string, manager *tunnel.Manager, log
|
||||
|
||||
// Handle handles the connection lifecycle
|
||||
func (c *Connection) Handle() error {
|
||||
// Register connection for adaptive load tracking
|
||||
protocol.RegisterConnection()
|
||||
|
||||
// Ensure cleanup of control connection, proxy, port, and registry on exit.
|
||||
defer c.Close()
|
||||
|
||||
@@ -257,6 +263,10 @@ func (c *Connection) handleHTTPRequest(reader *bufio.Reader) error {
|
||||
}
|
||||
// Connection reset by peer is normal - client closed connection abruptly
|
||||
errStr := err.Error()
|
||||
if errors.Is(err, net.ErrClosed) || strings.Contains(errStr, "use of closed network connection") {
|
||||
c.logger.Debug("HTTP connection closed during read", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(errStr, "connection reset by peer") ||
|
||||
strings.Contains(errStr, "broken pipe") ||
|
||||
strings.Contains(errStr, "connection refused") {
|
||||
@@ -389,12 +399,12 @@ func (c *Connection) handleDataFrame(frame *protocol.Frame) {
|
||||
|
||||
c.logger.Debug("Received data frame",
|
||||
zap.String("stream_id", header.StreamID),
|
||||
zap.String("type", header.Type),
|
||||
zap.String("type", header.Type.String()),
|
||||
zap.Int("data_size", len(data)),
|
||||
)
|
||||
|
||||
switch header.Type {
|
||||
case "response":
|
||||
case protocol.DataTypeResponse:
|
||||
// TCP tunnel response, forward to proxy
|
||||
if c.proxy != nil {
|
||||
if err := c.proxy.HandleResponse(header.StreamID, data); err != nil {
|
||||
@@ -404,7 +414,7 @@ func (c *Connection) handleDataFrame(frame *protocol.Frame) {
|
||||
)
|
||||
}
|
||||
}
|
||||
case "http_response":
|
||||
case protocol.DataTypeHTTPResponse:
|
||||
if c.responseChans == nil {
|
||||
c.logger.Warn("No response channel handler for HTTP response",
|
||||
zap.String("stream_id", header.StreamID),
|
||||
@@ -433,14 +443,14 @@ func (c *Connection) handleDataFrame(frame *protocol.Frame) {
|
||||
c.logger.Debug("Routed HTTP response to channel",
|
||||
zap.String("request_id", reqID),
|
||||
)
|
||||
case "close":
|
||||
case protocol.DataTypeClose:
|
||||
// Client is closing the stream
|
||||
if c.proxy != nil {
|
||||
c.proxy.CloseStream(header.StreamID)
|
||||
}
|
||||
default:
|
||||
c.logger.Warn("Unknown data frame type",
|
||||
zap.String("type", header.Type),
|
||||
zap.String("type", header.Type.String()),
|
||||
zap.String("stream_id", header.StreamID),
|
||||
)
|
||||
}
|
||||
@@ -500,6 +510,9 @@ func (c *Connection) sendError(code, message string) {
|
||||
// Close closes the connection
|
||||
func (c *Connection) Close() {
|
||||
c.once.Do(func() {
|
||||
// Unregister connection from adaptive load tracking
|
||||
protocol.UnregisterConnection()
|
||||
|
||||
close(c.stopCh)
|
||||
|
||||
if c.frameWriter != nil {
|
||||
|
||||
@@ -147,16 +147,16 @@ func (p *TunnelProxy) sendDataToTunnel(streamID string, data []byte) error {
|
||||
header := protocol.DataHeader{
|
||||
StreamID: streamID,
|
||||
RequestID: streamID,
|
||||
Type: "data",
|
||||
Type: protocol.DataTypeData,
|
||||
IsLast: false,
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, data)
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
|
||||
frame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
frame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
|
||||
err = p.frameWriter.WriteFrame(frame)
|
||||
if err != nil {
|
||||
@@ -170,16 +170,16 @@ func (p *TunnelProxy) sendCloseToTunnel(streamID string) {
|
||||
header := protocol.DataHeader{
|
||||
StreamID: streamID,
|
||||
RequestID: streamID,
|
||||
Type: "close",
|
||||
Type: protocol.DataTypeClose,
|
||||
IsLast: true,
|
||||
}
|
||||
|
||||
payload, err := protocol.EncodeDataPayload(header, nil)
|
||||
payload, poolBuffer, err := protocol.EncodeDataPayloadPooled(header, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame := protocol.NewFrame(protocol.FrameTypeData, payload)
|
||||
frame := protocol.NewFramePooled(protocol.FrameTypeData, payload, poolBuffer)
|
||||
p.frameWriter.WriteFrame(frame)
|
||||
}
|
||||
|
||||
|
||||
82
internal/shared/protocol/adaptive.go
Normal file
82
internal/shared/protocol/adaptive.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"drip/internal/shared/pool"
|
||||
)
|
||||
|
||||
// AdaptivePoolManager dynamically adjusts buffer pool usage based on load
|
||||
type AdaptivePoolManager struct {
|
||||
activeConnections atomic.Int64
|
||||
currentThreshold atomic.Int64
|
||||
highLoadConnectionThreshold int64
|
||||
midLoadConnectionThreshold int64
|
||||
midLoadThreshold int64
|
||||
highLoadThreshold int64
|
||||
}
|
||||
|
||||
var globalAdaptiveManager = NewAdaptivePoolManager()
|
||||
|
||||
func NewAdaptivePoolManager() *AdaptivePoolManager {
|
||||
m := &AdaptivePoolManager{
|
||||
highLoadConnectionThreshold: 300,
|
||||
midLoadConnectionThreshold: 150,
|
||||
midLoadThreshold: int64(pool.SizeLarge),
|
||||
highLoadThreshold: int64(pool.SizeMedium),
|
||||
}
|
||||
|
||||
m.currentThreshold.Store(m.midLoadThreshold)
|
||||
go m.monitor()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AdaptivePoolManager) monitor() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
connections := m.activeConnections.Load()
|
||||
|
||||
if connections >= m.highLoadConnectionThreshold {
|
||||
m.currentThreshold.Store(m.highLoadThreshold)
|
||||
} else if connections < m.midLoadConnectionThreshold {
|
||||
m.currentThreshold.Store(m.midLoadThreshold)
|
||||
}
|
||||
// Hysteresis zone (150-300): maintain current threshold
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AdaptivePoolManager) GetThreshold() int {
|
||||
return int(m.currentThreshold.Load())
|
||||
}
|
||||
|
||||
func (m *AdaptivePoolManager) RegisterConnection() {
|
||||
m.activeConnections.Add(1)
|
||||
}
|
||||
|
||||
func (m *AdaptivePoolManager) UnregisterConnection() {
|
||||
m.activeConnections.Add(-1)
|
||||
}
|
||||
|
||||
func (m *AdaptivePoolManager) GetActiveConnections() int64 {
|
||||
return m.activeConnections.Load()
|
||||
}
|
||||
|
||||
func GetAdaptiveThreshold() int {
|
||||
return globalAdaptiveManager.GetThreshold()
|
||||
}
|
||||
|
||||
func RegisterConnection() {
|
||||
globalAdaptiveManager.RegisterConnection()
|
||||
}
|
||||
|
||||
func UnregisterConnection() {
|
||||
globalAdaptiveManager.UnregisterConnection()
|
||||
}
|
||||
|
||||
func GetActiveConnections() int64 {
|
||||
return globalAdaptiveManager.GetActiveConnections()
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// DataHeaderV2 represents a binary-encoded data header (Protocol Version 2)
|
||||
// This replaces JSON encoding to improve performance
|
||||
type DataHeaderV2 struct {
|
||||
// DataHeader represents a binary-encoded data header for data plane
|
||||
// All data transmission uses pure binary encoding for performance
|
||||
type DataHeader struct {
|
||||
Type DataType
|
||||
IsLast bool
|
||||
StreamID string
|
||||
@@ -81,7 +81,7 @@ const (
|
||||
)
|
||||
|
||||
// MarshalBinary encodes the header to binary format
|
||||
func (h *DataHeaderV2) MarshalBinary() []byte {
|
||||
func (h *DataHeader) MarshalBinary() []byte {
|
||||
streamIDLen := len(h.StreamID)
|
||||
requestIDLen := len(h.RequestID)
|
||||
|
||||
@@ -111,7 +111,7 @@ func (h *DataHeaderV2) MarshalBinary() []byte {
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes the header from binary format
|
||||
func (h *DataHeaderV2) UnmarshalBinary(data []byte) error {
|
||||
func (h *DataHeader) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < binaryHeaderMinSize {
|
||||
return errors.New("invalid binary header: too short")
|
||||
}
|
||||
@@ -142,25 +142,7 @@ func (h *DataHeaderV2) UnmarshalBinary(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDataHeader converts binary header to JSON header (for compatibility)
|
||||
func (h *DataHeaderV2) ToDataHeader() DataHeader {
|
||||
return DataHeader{
|
||||
StreamID: h.StreamID,
|
||||
RequestID: h.RequestID,
|
||||
Type: h.Type.String(),
|
||||
IsLast: h.IsLast,
|
||||
}
|
||||
}
|
||||
|
||||
// FromDataHeader converts JSON header to binary header
|
||||
func (h *DataHeaderV2) FromDataHeader(dh DataHeader) {
|
||||
h.StreamID = dh.StreamID
|
||||
h.RequestID = dh.RequestID
|
||||
h.Type = DataTypeFromString(dh.Type)
|
||||
h.IsLast = dh.IsLast
|
||||
}
|
||||
|
||||
// Size returns the size of the binary-encoded header
|
||||
func (h *DataHeaderV2) Size() int {
|
||||
func (h *DataHeader) Size() int {
|
||||
return binaryHeaderMinSize + len(h.StreamID) + len(h.RequestID)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"drip/internal/shared/pool"
|
||||
)
|
||||
@@ -60,20 +61,21 @@ func WriteFrame(w io.Writer, frame *Frame) error {
|
||||
return fmt.Errorf("payload too large: %d bytes (max %d)", payloadLen, MaxFrameSize)
|
||||
}
|
||||
|
||||
lengthBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lengthBuf, uint32(payloadLen))
|
||||
if _, err := w.Write(lengthBuf); err != nil {
|
||||
return fmt.Errorf("failed to write length: %w", err)
|
||||
}
|
||||
var header [FrameHeaderSize]byte
|
||||
binary.BigEndian.PutUint32(header[0:4], uint32(payloadLen))
|
||||
header[4] = byte(frame.Type)
|
||||
|
||||
if _, err := w.Write([]byte{byte(frame.Type)}); err != nil {
|
||||
return fmt.Errorf("failed to write type: %w", err)
|
||||
}
|
||||
|
||||
if payloadLen > 0 {
|
||||
if _, err := w.Write(frame.Payload); err != nil {
|
||||
return fmt.Errorf("failed to write payload: %w", err)
|
||||
if payloadLen == 0 {
|
||||
if _, err := w.Write(header[:]); err != nil {
|
||||
return fmt.Errorf("failed to write frame header: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// net.Buffers will use writev for TCP connections and falls back to
|
||||
// sequential writes for other io.Writer implementations (e.g. TLS).
|
||||
if _, err := (&net.Buffers{header[:], frame.Payload}).WriteTo(w); err != nil {
|
||||
return fmt.Errorf("failed to write frame: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -134,3 +136,13 @@ func NewFrame(frameType FrameType, payload []byte) *Frame {
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFramePooled creates a new frame with a pooled buffer
|
||||
// The poolBuffer will be automatically released after the frame is written
|
||||
func NewFramePooled(frameType FrameType, payload []byte, poolBuffer *[]byte) *Frame {
|
||||
return &Frame{
|
||||
Type: frameType,
|
||||
Payload: payload,
|
||||
poolBuffer: poolBuffer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,22 +24,10 @@ type ErrorMessage struct {
|
||||
Message string `json:"message"` // Error message
|
||||
}
|
||||
|
||||
// DataHeader represents metadata for a data frame
|
||||
type DataHeader struct {
|
||||
StreamID string `json:"stream_id"` // Unique stream identifier
|
||||
RequestID string `json:"request_id"` // Request identifier (for HTTP)
|
||||
Type string `json:"type"` // "data", "response", "close", "http_request", "http_response"
|
||||
IsLast bool `json:"is_last"` // Is this the last frame for this stream
|
||||
}
|
||||
// Note: DataHeader is now defined in binary_header.go as a pure binary structure
|
||||
// TCPData has been removed - use DataHeader + raw bytes directly
|
||||
|
||||
// TCPData represents TCP tunnel data
|
||||
type TCPData struct {
|
||||
StreamID string `json:"stream_id"` // Stream identifier
|
||||
Data []byte `json:"data"` // Raw TCP data
|
||||
IsClose bool `json:"is_close"` // Close this stream
|
||||
}
|
||||
|
||||
// Marshal helpers
|
||||
// Marshal helpers for control plane messages (JSON encoding)
|
||||
func MarshalJSON(v interface{}) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
@@ -1,129 +1,106 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
json "github.com/goccy/go-json"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
|
||||
"drip/internal/shared/pool"
|
||||
)
|
||||
|
||||
// EncodeDataPayload encodes a data header and payload into a frame payload
|
||||
// Uses binary encoding (optimized format)
|
||||
// EncodeDataPayload encodes a data header and payload into a frame payload.
|
||||
// Deprecated: Use EncodeDataPayloadPooled for better performance.
|
||||
func EncodeDataPayload(header DataHeader, data []byte) ([]byte, error) {
|
||||
return EncodeDataPayloadV2(header, data)
|
||||
streamIDLen := len(header.StreamID)
|
||||
requestIDLen := len(header.RequestID)
|
||||
|
||||
totalLen := binaryHeaderMinSize + streamIDLen + requestIDLen + len(data)
|
||||
payload := make([]byte, totalLen)
|
||||
|
||||
flags := uint8(header.Type) & 0x07
|
||||
if header.IsLast {
|
||||
flags |= 0x08
|
||||
}
|
||||
payload[0] = flags
|
||||
|
||||
binary.BigEndian.PutUint16(payload[1:3], uint16(streamIDLen))
|
||||
binary.BigEndian.PutUint16(payload[3:5], uint16(requestIDLen))
|
||||
|
||||
offset := binaryHeaderMinSize
|
||||
copy(payload[offset:], header.StreamID)
|
||||
offset += streamIDLen
|
||||
copy(payload[offset:], header.RequestID)
|
||||
offset += requestIDLen
|
||||
copy(payload[offset:], data)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// EncodeDataPayloadV1 encodes using JSON (legacy)
|
||||
// Format: JSON_HEADER\nDATA
|
||||
func EncodeDataPayloadV1(header DataHeader, data []byte) ([]byte, error) {
|
||||
headerBytes, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// EncodeDataPayloadPooled encodes with adaptive allocation based on load.
|
||||
// Returns payload slice and pool buffer pointer (may be nil).
|
||||
//
|
||||
// Adaptive strategy:
|
||||
// - Mid-load (<150 conn): 256KB threshold, pool disabled → max QPS
|
||||
// - High-load (≥300 conn): 32KB threshold, pool enabled → stable latency
|
||||
// - Transition (150-300): Hysteresis to prevent flapping
|
||||
func EncodeDataPayloadPooled(header DataHeader, data []byte) (payload []byte, poolBuffer *[]byte, err error) {
|
||||
streamIDLen := len(header.StreamID)
|
||||
requestIDLen := len(header.RequestID)
|
||||
totalLen := binaryHeaderMinSize + streamIDLen + requestIDLen + len(data)
|
||||
|
||||
dynamicThreshold := GetAdaptiveThreshold()
|
||||
|
||||
if totalLen < dynamicThreshold {
|
||||
regularPayload, err := EncodeDataPayload(header, data)
|
||||
return regularPayload, nil, err
|
||||
}
|
||||
|
||||
// Combine: header + newline + data
|
||||
payload := make([]byte, 0, len(headerBytes)+1+len(data))
|
||||
payload = append(payload, headerBytes...)
|
||||
payload = append(payload, '\n')
|
||||
payload = append(payload, data...)
|
||||
if totalLen > pool.SizeLarge {
|
||||
regularPayload, err := EncodeDataPayload(header, data)
|
||||
return regularPayload, nil, err
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
poolBuffer = pool.GetBuffer(totalLen)
|
||||
payload = (*poolBuffer)[:totalLen]
|
||||
|
||||
flags := uint8(header.Type) & 0x07
|
||||
if header.IsLast {
|
||||
flags |= 0x08
|
||||
}
|
||||
payload[0] = flags
|
||||
|
||||
binary.BigEndian.PutUint16(payload[1:3], uint16(streamIDLen))
|
||||
binary.BigEndian.PutUint16(payload[3:5], uint16(requestIDLen))
|
||||
|
||||
offset := binaryHeaderMinSize
|
||||
copy(payload[offset:], header.StreamID)
|
||||
offset += streamIDLen
|
||||
copy(payload[offset:], header.RequestID)
|
||||
offset += requestIDLen
|
||||
copy(payload[offset:], data)
|
||||
|
||||
return payload, poolBuffer, nil
|
||||
}
|
||||
|
||||
// EncodeDataPayloadV2 encodes using binary format (optimized)
|
||||
// Format: BINARY_HEADER + DATA
|
||||
func EncodeDataPayloadV2(header DataHeader, data []byte) ([]byte, error) {
|
||||
// Convert to binary header
|
||||
var h2 DataHeaderV2
|
||||
h2.FromDataHeader(header)
|
||||
|
||||
// Encode header to binary
|
||||
headerBytes := h2.MarshalBinary()
|
||||
|
||||
// Combine: binary header + data
|
||||
payload := make([]byte, 0, len(headerBytes)+len(data))
|
||||
payload = append(payload, headerBytes...)
|
||||
payload = append(payload, data...)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// DecodeDataPayload decodes a frame payload into header and data
|
||||
// Auto-detects protocol version
|
||||
// DecodeDataPayload decodes a frame payload into header and data.
|
||||
func DecodeDataPayload(payload []byte) (DataHeader, []byte, error) {
|
||||
if len(payload) == 0 {
|
||||
return DataHeader{}, nil, errors.New("empty payload")
|
||||
}
|
||||
|
||||
// Try to detect version:
|
||||
// - V1 (JSON): starts with '{'
|
||||
// - V2 (Binary): first byte is flags (0x00-0x1F typically)
|
||||
if payload[0] == '{' {
|
||||
// V1: JSON format
|
||||
return DecodeDataPayloadV1(payload)
|
||||
}
|
||||
|
||||
// V2: Binary format
|
||||
return DecodeDataPayloadV2(payload)
|
||||
}
|
||||
|
||||
// DecodeDataPayloadV1 decodes JSON format (legacy)
|
||||
// Format: JSON_HEADER\nDATA
|
||||
func DecodeDataPayloadV1(payload []byte) (DataHeader, []byte, error) {
|
||||
// Find newline separator
|
||||
sepIdx := -1
|
||||
for i, b := range payload {
|
||||
if b == '\n' {
|
||||
sepIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sepIdx == -1 {
|
||||
return DataHeader{}, nil, errors.New("invalid v1 payload: no newline separator")
|
||||
}
|
||||
|
||||
// Parse JSON header
|
||||
var header DataHeader
|
||||
if err := json.Unmarshal(payload[:sepIdx], &header); err != nil {
|
||||
return DataHeader{}, nil, err
|
||||
}
|
||||
|
||||
// Extract data (after newline)
|
||||
data := payload[sepIdx+1:]
|
||||
|
||||
return header, data, nil
|
||||
}
|
||||
|
||||
// DecodeDataPayloadV2 decodes binary format (optimized)
|
||||
// Format: BINARY_HEADER + DATA
|
||||
func DecodeDataPayloadV2(payload []byte) (DataHeader, []byte, error) {
|
||||
if len(payload) < binaryHeaderMinSize {
|
||||
return DataHeader{}, nil, errors.New("invalid v2 payload: too short")
|
||||
return DataHeader{}, nil, errors.New("invalid payload: too short")
|
||||
}
|
||||
|
||||
// Decode binary header
|
||||
var h2 DataHeaderV2
|
||||
if err := h2.UnmarshalBinary(payload); err != nil {
|
||||
var header DataHeader
|
||||
if err := header.UnmarshalBinary(payload); err != nil {
|
||||
return DataHeader{}, nil, err
|
||||
}
|
||||
|
||||
// Extract data (after header)
|
||||
headerSize := h2.Size()
|
||||
headerSize := header.Size()
|
||||
if len(payload) < headerSize {
|
||||
return DataHeader{}, nil, errors.New("invalid v2 payload: data missing")
|
||||
return DataHeader{}, nil, errors.New("invalid payload: data missing")
|
||||
}
|
||||
|
||||
data := payload[headerSize:]
|
||||
|
||||
// Convert to DataHeader
|
||||
header := h2.ToDataHeader()
|
||||
|
||||
return header, data, nil
|
||||
}
|
||||
|
||||
// GetPayloadHeaderSize returns the size of the header in the payload
|
||||
// This is useful for pre-allocating buffers
|
||||
func GetPayloadHeaderSize(header DataHeader) int {
|
||||
var h2 DataHeaderV2
|
||||
h2.FromDataHeader(header)
|
||||
return h2.Size()
|
||||
return header.Size()
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ type FrameWriter struct {
|
||||
}
|
||||
|
||||
func NewFrameWriter(conn io.Writer) *FrameWriter {
|
||||
return NewFrameWriterWithConfig(conn, 128, 2*time.Millisecond, 1024)
|
||||
// Larger queue size for better burst handling across all load scenarios
|
||||
// With adaptive buffer pool, memory pressure is well controlled
|
||||
return NewFrameWriterWithConfig(conn, 128, 2*time.Millisecond, 2048)
|
||||
}
|
||||
|
||||
func NewFrameWriterWithConfig(conn io.Writer, maxBatch int, maxBatchWait time.Duration, queueSize int) *FrameWriter {
|
||||
@@ -126,6 +128,8 @@ func (w *FrameWriter) flushBatchLocked() {
|
||||
|
||||
for _, frame := range w.batch {
|
||||
_ = WriteFrame(w.conn, frame)
|
||||
// Release pooled buffer after writing
|
||||
frame.Release()
|
||||
}
|
||||
|
||||
w.batch = w.batch[:0]
|
||||
|
||||
@@ -246,6 +246,28 @@ EOF
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Extract version from a drip binary, preferring the plain output when available
|
||||
get_version_from_binary() {
|
||||
local binary="$1"
|
||||
local output=""
|
||||
local version=""
|
||||
|
||||
output=$("$binary" version --short 2>/dev/null || true)
|
||||
if [[ -n "$output" ]]; then
|
||||
version=$(printf '%s\n' "$output" | awk -F': ' '/Version/ {print $2; exit}')
|
||||
fi
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
output=$("$binary" version 2>/dev/null || true)
|
||||
if [[ -n "$output" ]]; then
|
||||
output=$(printf '%s\n' "$output" | sed -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g')
|
||||
version=$(printf '%s\n' "$output" | sed -nE 's/.*Version:[[:space:]]*([vV]?[0-9][^[:space:]]*).*/\1/p' | head -n1)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${version:-unknown}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Language selection
|
||||
# ============================================================================
|
||||
@@ -372,7 +394,7 @@ check_existing_install() {
|
||||
local server_path="$INSTALL_DIR/drip"
|
||||
|
||||
if [[ -f "$server_path" ]]; then
|
||||
local current_version=$("$server_path" version 2>/dev/null | awk '/Version:/ {print $2}' || echo "unknown")
|
||||
local current_version=$(get_version_from_binary "$server_path")
|
||||
|
||||
print_warning "$(msg already_installed): $server_path"
|
||||
print_info "$(msg current_version): $current_version"
|
||||
@@ -974,7 +996,7 @@ main() {
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local new_version=$("$INSTALL_DIR/drip" version 2>/dev/null | awk '/Version:/ {print $2}' || echo "unknown")
|
||||
local new_version=$(get_version_from_binary "$INSTALL_DIR/drip")
|
||||
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ $(msg update_ok) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
@@ -78,6 +78,7 @@ MSG_EN=(
|
||||
["press_enter"]="Press Enter to continue..."
|
||||
["windows_note"]="For Windows, please download the .exe file from GitHub Releases"
|
||||
["already_installed"]="Drip is already installed"
|
||||
["current_version"]="Current version"
|
||||
["update_now"]="Update to the latest version?"
|
||||
["updating"]="Updating..."
|
||||
["update_ok"]="Update completed"
|
||||
@@ -138,6 +139,7 @@ MSG_ZH=(
|
||||
["press_enter"]="按 Enter 继续..."
|
||||
["windows_note"]="Windows 用户请从 GitHub Releases 下载 .exe 文件"
|
||||
["already_installed"]="Drip 已安装"
|
||||
["current_version"]="当前版本"
|
||||
["update_now"]="是否更新到最新版本?"
|
||||
["updating"]="正在更新..."
|
||||
["update_ok"]="更新完成"
|
||||
@@ -181,6 +183,28 @@ EOF
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Extract version from a drip binary, preferring the plain output when available
|
||||
get_version_from_binary() {
|
||||
local binary="$1"
|
||||
local output=""
|
||||
local version=""
|
||||
|
||||
output=$("$binary" version --short 2>/dev/null || true)
|
||||
if [[ -n "$output" ]]; then
|
||||
version=$(printf '%s\n' "$output" | awk -F': ' '/Version/ {print $2; exit}')
|
||||
fi
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
output=$("$binary" version 2>/dev/null || true)
|
||||
if [[ -n "$output" ]]; then
|
||||
output=$(printf '%s\n' "$output" | sed -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g')
|
||||
version=$(printf '%s\n' "$output" | sed -nE 's/.*Version:[[:space:]]*([vV]?[0-9][^[:space:]]*).*/\1/p' | head -n1)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${version:-unknown}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Language selection
|
||||
# ============================================================================
|
||||
@@ -292,7 +316,7 @@ get_latest_version() {
|
||||
check_existing_install() {
|
||||
if command -v drip &> /dev/null; then
|
||||
local current_path=$(command -v drip)
|
||||
local current_version=$(drip version 2>/dev/null | awk '/Version:/ {print $2}' || echo "unknown")
|
||||
local current_version=$(get_version_from_binary "drip")
|
||||
|
||||
print_warning "$(msg already_installed): $current_path"
|
||||
print_info "$(msg current_version): $current_version"
|
||||
@@ -484,7 +508,7 @@ verify_installation() {
|
||||
fi
|
||||
|
||||
if [[ -x "$binary_path" ]]; then
|
||||
local version=$("$binary_path" version 2>/dev/null | awk '/Version:/ {print $2}' || echo "installed")
|
||||
local version=$(get_version_from_binary "$binary_path")
|
||||
print_success "$(msg verify_ok): $version"
|
||||
else
|
||||
print_error "$(msg verify_failed)"
|
||||
@@ -624,7 +648,7 @@ main() {
|
||||
test_connection
|
||||
else
|
||||
echo ""
|
||||
local new_version=$("$INSTALL_DIR/$BINARY_NAME" version 2>/dev/null | awk '/Version:/ {print $2}' || echo "installed")
|
||||
local new_version=$(get_version_from_binary "$INSTALL_DIR/$BINARY_NAME")
|
||||
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ $(msg update_ok) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
Reference in New Issue
Block a user