Skip to content

feat: Add UDP transport layer with KCP Protocol #80

@metaphorics

Description

@metaphorics

KCP 도입에 따른 아키텍처 변화

핵심 변경 사항은 **"전송 계층의 이원화(TCP Only → UDP 우선)"**와 선택적인 **"멀티플렉서 교체(Yamux → Smux)"**입니다.


1. AS-IS: 현재 아키텍처 (Yamux + TCP/WebSocket)

현재는 모든 트래픽이 TCP 기반(Raw TCP 또는 WebSocket) 위에서 돌아가며, 그 위에 Yamux가 올라가 있습니다.

  • 문제점: TCP의 특성상 패킷 하나만 유실되어도 전체 스트림이 멈추는 HOL(Head-of-Line) 블로킹에 취약합니다.
  • 구조: 단일 전송 경로에 의존적입니다.
graph TD
    subgraph Client_Side ["Client Side"]
        App[Application Logic]
        RDSEC[RDSEC Encryption]
        Yamux["<b>Yamux Session</b>"]
        TCP[TCP / WebSocket]
        
        App --> RDSEC
        RDSEC --> Yamux
        Yamux --> TCP
    end

    subgraph Network ["Network"]
        Net["Internet<br/>(TCP Packet Loss = HOL Blocking)"]
    end

    subgraph Server_Side ["Server Side"]
        S_TCP[TCP / WS Listener]
        S_Yamux["<b>Yamux Server</b>"]
        Handler[Relay Handler]

        S_TCP --> S_Yamux
        S_Yamux --> Handler
    end

    TCP <==> Net <==> S_TCP
    
    style Yamux fill:#f9f,stroke:#333,stroke-width:2px
    style S_Yamux fill:#f9f,stroke:#333,stroke-width:2px
Loading

2. TO-BE: 변경 후 아키텍처 (Smux + KCP/WS Unified)

Smux가 중심을 잡고, 하부 전송 계층을 **KCP(UDP)**와 WebSocket(TCP) 두 갈래로 나누어 관리합니다.

  • 개선점:
    1. 성능: 네이티브 앱은 **KCP(UDP)**를 통해 HOL 블로킹 없이 고속 통신합니다.
    2. 호환성: 브라우저(WASM)는 WebSocket을 그대로 사용하되, 상위에서 Smux/Yamux로 통일되어 서버 로직 분기가 발생하지 않습니다.
    3. 효율: Smux는 Yamux보다 헤더 오버헤드가 작고 메모리 사용량이 적습니다. (선택적)
graph TD
    subgraph Client_Side ["Client Side"]
        App[Application Logic]
        Noise[Noise Protocol Encryption]
        Smux["<b>Smux Session</b><br/>(Unified Interface)"]
        
        subgraph Transport_Selector ["Transport Selector"]
            KCP["<b>KCP</b><br/>(Reliable UDP)"]
            WS["WebSocket<br/>(TCP Fallback)"]
        end
        
        App --> Noise
        Noise --> Smux
        Smux --> KCP
        Smux -.-> WS
    end

    subgraph Network ["Network"]
        NetUDP["UDP Path<br/>(Fast, No HOL Blocking)"]
        NetTCP["TCP Path<br/>(Browser/Firewall Compat)"]
    end

    subgraph Server_Side ["Server Side"]
        KCP_L[UDP Listener :4017]
        WS_L[TCP Listener :4017]
        S_Smux["<b>Smux Server</b><br/>(Unified Handler)"]
        Handler[Relay Handler]

        KCP_L --> S_Smux
        WS_L --> S_Smux
        S_Smux --> Handler
    end

    KCP <==> NetUDP <==> KCP_L
    WS <==> NetTCP <==> WS_L

    style Smux fill:#4AB,stroke:#333,stroke-width:4px
    style S_Smux fill:#4AB,stroke:#333,stroke-width:4px
    style KCP fill:#F96,stroke:#333,stroke-width:2px
    style KCP_L fill:#F96,stroke:#333,stroke-width:2px
Loading

구현 전략 요약

  1. 의존성 교체: hashicorp/yamux 제거 → xtaci/smux, xtaci/kcp-go/v5 추가.
  2. 서버 수정 (main.go): UDP(KCP) 포트를 추가로 리스닝하고, TCP(WS)와 UDP 연결 모두 io.ReadWriteCloser 인터페이스로 HandleConnection에 전달.
  3. 핸들러 수정 (relay.go): yamux.Serversmux.Server로 교체 (코드 구조는 동일).
  4. 클라이언트 수정 (sdk.go): kcp:// 스키마 지원 추가 및 smux.Client 적용.

세부 구현 전략 (Breakdown)

Phase 1: 의존성 교체 (Dependencies) [Optional]

기존 yamux를 제거하고 성능이 검증된 KCP 및 Smux 라이브러리를 도입합니다. (선택적, 만약 WASM WebClient를 Rust로 포팅한다면 yamux 유지가 더 feasible함)

  • Remove: github.com/hashicorp/yamux
  • Add:
    • github.com/xtaci/kcp-go/v5: KCP 프로토콜 Go 구현체 (FEC, Encryption 포함).
    • github.com/xtaci/smux: 멀티플렉싱 라이브러리.

Phase 2: KCP 파라미터 튜닝 (Turbo Mode Configuration)

KCP의 성능을 극대화하기 위해 다음과 같이 설정합니다. 이는 게임이나 실시간 스트리밍 수준의 반응성을 제공합니다.

파라미터 설정값 설명
NoDelay 1 Nagle 알고리즘 비활성화 (데이터 즉시 전송)
Interval 10ms 내부 루프 주기 (기본 100ms → 10ms로 단축하여 반응성 10배 향상)
Resend 2 ACK 2회 누락 시 즉시 빠른 재전송 (Fast Retransmit)
NC 1 혼잡 제어(Flow Control은 유지하되 Congestion Control은 끔)
FEC 10:3 데이터 10개당 패리티 3개 전송 (패킷 손실 30%까지 재전송 없이 복구)
Window 1024 송수신 윈도우 크기 대폭 상향 (기본 32 → 1024)

Phase 3: 서버 사이드 구현 (cmd/relay-server)

서버는 UDP와 TCP 포트를 동시에 열고, 들어오는 연결을 추상화하여 처리합니다.

// 개념적 코드 (Pseudo-code)
func runServer() {
    // 1. WebSocket Listener (TCP)
    go func() {
        http.HandleFunc("/relay", func(w, r) {
            conn := upgradeToWS(w, r)
            handleUnifiedSession(conn) // WebSocket 연결 처리
        })
        http.ListenAndServe(":4017", nil)
    }()

    // 2. KCP Listener (UDP)
    go func() {
        listener, _ := kcp.ListenWithOptions(":4017", nil, 10, 3) // FEC 10+3
        listener.SetDSCP(46) // QoS 설정
        for {
            conn, _ := listener.AcceptKCP()
            // Turbo Mode 설정
            conn.SetNoDelay(1, 10, 2, 1)
            conn.SetWindowSize(1024, 1024)
            conn.SetACKNoDelay(true)

            go handleUnifiedSession(conn) // KCP 연결 처리
        }
    }()
}

// 3. 통합 핸들러 (Unified Handler)
func handleUnifiedSession(conn io.ReadWriteCloser) {
    // 하부 프로토콜이 무엇이든 Smux 세션으로 래핑
    smuxConfig := smux.DefaultConfig()
    session, _ := smux.Server(conn, smuxConfig)

    // 이후 로직은 기존과 동일하게 Stream Accept
    serv.HandleSession(session)
}

Phase 4: 클라이언트 구현 (sdk/ & cmd/webclient)

  • Native Client (Go SDK):
    • Race Dialing: KCP 연결을 우선 시도하고, 500ms 내 실패 시 WebSocket으로 즉시 전환합니다.
    • KCP 연결 성공 시, 서버와 동일한 Turbo Mode 파라미터를 적용합니다.
  • Web Client (WASM):
    • 브라우저에서는 Raw UDP를 사용할 수 없으므로 WebSocket을 사용합니다.
    • 중요: WebSocket 연결 후, smux.Client(wsConn)을 호출하여 WASM 내부에서 Smux 프로토콜을 구동해야 합니다. 그래야 서버가 이를 KCP 클라이언트와 동일한 구조로 인식합니다.

Phase 5: 능동형 흐름 제어 (Active BPS Manager)

기존의 time.Sleep 기반 bps_manager를 제거하고, Smux/KCP의 Window Size를 조절하는 방식으로 변경합니다.

  • Active Control: 사용자별 BPS 한도에 도달하면 session.SetStreamBuffer() 등을 통해 윈도우 크기를 줄입니다.
  • Backpressure: 수신 윈도우가 줄어들면 송신 측(Sender)의 KCP 엔진이 알아서 전송 속도를 늦추므로, CPU 점유율 없이 자연스럽게 속도 제어가 가능합니다.

Sub-issues

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions