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:
Gouryella
2025-12-03 18:11:37 +08:00
parent bb5ed1739e
commit 35e6c86e1f
16 changed files with 315 additions and 211 deletions

View File

@@ -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

View File

@@ -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",
"",

View File

@@ -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)

View File

@@ -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().

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}

View 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()
}

View File

@@ -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)
}

View File

@@ -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,
}
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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]

View File

@@ -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}"

View File

@@ -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}"