You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Guile连接WebSocket?零基础Socket用户技术求助

别担心,从零开始搞WebSocket确实容易摸不着头脑,尤其是Guile本身没自带WebSocket支持的情况下。我来一步步帮你拆解怎么用Guile实现和wss://node2.wsninja.io的连接~

先搞懂核心前提:WebSocket≠普通HTTP请求

WebSocket是先通过HTTP协议升级握手建立连接,之后切换成双向的帧通信模式,所以直接用普通HTTP请求模块完全行不通,得手动处理握手流程+后续的WebSocket帧编码/解码。

步骤1:建立加密TCP连接(wss协议必备)

因为你用的是wss://(WebSocket over TLS),所以必须先创建TCP套接字,再封装TLS加密层,否则连不上服务器。

(use-modules (ice-9 rdelim)
             (rnrs bytevectors)
             (gnutls)
             (srfi srfi-1)
             (srfi srfi-27))

;; 初始化随机数生成器(后续生成掩码要用)
(random-source-randomize! default-random-source)

;; 1. 获取服务器地址信息
(define addrinfo (getaddrinfo "node2.wsninja.io" "443" AI_NUMERICSERV))
(define addr (addrinfo:addr (car addrinfo)))
;; 2. 创建TCP套接字
(define sock (socket PF_INET SOCK_STREAM 0))

;; 3. 连接服务器并封装TLS层
(connect sock addr)
(define session (make-session connection-end/client))
(set-session-default-priority! session)
(set-session-transport-fd! session (fileno sock))
(handshake session)
步骤2:发送WebSocket握手请求

握手请求是特殊的HTTP GET请求,必须包含Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key等头部,服务器验证后会返回101 Switching Protocols响应。

;; 生成随机的Sec-WebSocket-Key(Base64编码的16字节随机数)
(define (make-websocket-key)
  (base64-encode (random-bytevector 16)))

;; 构造握手请求
(define key (make-websocket-key))
(define handshake-request
  (string-append
   "GET / HTTP/1.1\r\n"
   "Host: node2.wsninja.io\r\n"
   "Upgrade: websocket\r\n"
   "Connection: Upgrade\r\n"
   "Sec-WebSocket-Key: " key "\r\n"
   "Sec-WebSocket-Version: 13\r\n\r\n"))

;; 发送请求并检查响应
(send-session-data session (string->utf8 handshake-request))
(define status-line (read-line session))
(if (string-prefix? "HTTP/1.1 101" status-line)
    (display "WebSocket握手成功!\n")
    (error "握手失败,服务器响应:" status-line))
步骤3:编码/解码WebSocket帧

握手成功后,必须按照WebSocket帧格式收发数据——客户端发送的帧必须加掩码,服务器返回的帧不需要。

编码文本帧(客户端发送消息用)

;; 辅助函数:用掩码处理消息字节
(define (mask-bytevector bv mask)
  (let ([len (bytevector-length bv)])
    (do ([i 0 (1+ i)])
        ((= i len) bv)
      (bytevector-u8-set! bv i (bitwise-xor (bytevector-u8-ref bv i)
                                             (bytevector-u8-ref mask (modulo i 4)))))))

;; 编码文本帧
(define (encode-websocket-text-frame msg)
  (let* ([msg-bytes (string->utf8 msg)]
         [len (bytevector-length msg-bytes)]
         [mask (random-bytevector 4)]
         [header (make-bytevector (if (< len 126) 2 6))])
    ;; 设置FIN=1(结束帧)、Opcode=1(文本帧)
    (bytevector-u8-set! header 0 #x81)
    ;; 设置掩码位=1 + 消息长度
    (if (< len 126)
        (bytevector-u8-set! header 1 (bitwise-ior #x80 len))
        (begin
          (bytevector-u8-set! header 1 #xFE)
          (bytevector-u16-set! header 2 len (endianness big))))
    (bytevector-append header mask (mask-bytevector msg-bytes mask))))

解码文本帧(接收服务器响应用)

(define (decode-websocket-frame session)
  (let* ([byte1 (get-u8 session)]
         [opcode (bitwise-and byte1 #x0F)] ; 只处理文本帧(Opcode=1)
         [byte2 (get-u8 session)]
         [masked? (bitwise-bit-set? byte2 7)]
         [len (bitwise-and byte2 #x7F)]
         ;; 读取实际消息长度
         [actual-len
          (cond
           [(= len 126) (get-u16 session (endianness big))]
           [(= len 127) (get-u64 session (endianness big))]
           [else len])]
         [mask (if masked? (get-bytevector-n session 4) #f)]
         [payload (get-bytevector-n session actual-len)])
    ;; 解掩码(服务器返回的帧可能不带掩码)
    (when masked?
      (mask-bytevector payload mask))
    (utf8->string payload)))
完整测试代码

把上面的逻辑整合起来,加上资源清理:

(use-modules (ice-9 rdelim)
             (rnrs bytevectors)
             (gnutls)
             (srfi srfi-1)
             (srfi srfi-27))

(random-source-randomize! default-random-source)

(define (make-websocket-key)
  (base64-encode (random-bytevector 16)))

(define (mask-bytevector bv mask)
  (let ([len (bytevector-length bv)])
    (do ([i 0 (1+ i)])
        ((= i len) bv)
      (bytevector-u8-set! bv i (bitwise-xor (bytevector-u8-ref bv i)
                                             (bytevector-u8-ref mask (modulo i 4)))))))

(define (encode-websocket-text-frame msg)
  (let* ([msg-bytes (string->utf8 msg)]
         [len (bytevector-length msg-bytes)]
         [mask (random-bytevector 4)]
         [header (make-bytevector (if (< len 126) 2 6))])
    (bytevector-u8-set! header 0 #x81)
    (if (< len 126)
        (bytevector-u8-set! header 1 (bitwise-ior #x80 len))
        (begin
          (bytevector-u8-set! header 1 #xFE)
          (bytevector-u16-set! header 2 len (endianness big))))
    (bytevector-append header mask (mask-bytevector msg-bytes mask))))

(define (decode-websocket-frame session)
  (let* ([byte1 (get-u8 session)]
         [opcode (bitwise-and byte1 #x0F)]
         [byte2 (get-u8 session)]
         [masked? (bitwise-bit-set? byte2 7)]
         [len (bitwise-and byte2 #x7F)]
         [actual-len
          (cond
           [(= len 126) (get-u16 session (endianness big))]
           [(= len 127) (get-u64 session (endianness big))]
           [else len])]
         [mask (if masked? (get-bytevector-n session 4) #f)]
         [payload (get-bytevector-n session actual-len)])
    (when masked?
      (mask-bytevector payload mask))
    (utf8->string payload)))

(define (main)
  (let* ([addrinfo (getaddrinfo "node2.wsninja.io" "443" AI_NUMERICSERV)]
         [addr (addrinfo:addr (car addrinfo))]
         [sock (socket PF_INET SOCK_STREAM 0)])
    (dynamic-wind
      (lambda () #t)
      (lambda ()
        (connect sock addr)
        (let ([session (make-session connection-end/client)])
          (set-session-default-priority! session)
          (set-session-transport-fd! session (fileno sock))
          (handshake session)
          
          ;; 发送握手请求
          (let* ([key (make-websocket-key)]
                 [handshake-req
                  (string-append
                   "GET / HTTP/1.1\r\n"
                   "Host: node2.wsninja.io\r\n"
                   "Upgrade: websocket\r\n"
                   "Connection: Upgrade\r\n"
                   "Sec-WebSocket-Key: " key "\r\n"
                   "Sec-WebSocket-Version: 13\r\n\r\n")])
            (send-session-data session (string->utf8 handshake-req))
            (let ([status-line (read-line session)])
              (if (string-prefix? "HTTP/1.1 101" status-line)
                  (begin
                    (display "WebSocket握手成功!\n")
                    ;; 发送测试消息
                    (let ([msg "Hello from Guile!"])
                      (send-session-data session (encode-websocket-text-frame msg))
                      (display "已发送消息: ") (display msg) (newline)
                      ;; 接收响应
                      (let ([resp (decode-websocket-frame session)])
                        (display "收到响应: ") (display resp) (newline))))
                  (error "握手失败,状态行:" status-line)))))
      (lambda ()
        (close sock)))))

(main)
关键注意事项
  • TLS层不能少:wss://协议必须用TLS加密,直接连TCP套接字会失败。
  • 帧格式要严格:客户端发送的帧必须加掩码,否则服务器会拒绝接收。
  • 错误处理要补充:上面的代码是简化版,实际使用要处理连接失败、帧解码错误等情况。

内容的提问来源于stack exchange,提问作者Jaft

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:21:33