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

WebSocket新手求助:Tcl服务器与浏览器客户端连接失败排查

问题分析与解决方案

嘿,我一眼就看出问题所在了——你的Tcl服务器只是个普通的TCP行缓冲服务器,完全没实现WebSocket协议要求的握手流程,这才导致浏览器发起连接后直接触发onclose,还报了ERR_INVALID_HTTP_RESPONSE错误。

为啥会失败?

当浏览器用ws://发起连接时,它先会发一个HTTP格式的握手请求(就是你在服务器输出里看到的那些GET / HTTP/1.1、Sec-WebSocket-Key这类内容)。服务器必须按照WebSocket的规范返回一个特定的HTTP响应,完成握手后,才能切换到WebSocket的帧传输模式。

但你的服务器现在只是把这些握手请求当成普通文本接收,然后原样回传,这完全不符合WebSocket的要求。浏览器收到错误的响应后,自然就直接关闭连接了。

修改后的Tcl服务器代码

下面是实现了完整WebSocket握手和帧处理的Tcl服务器,能和你的浏览器客户端正常通信:

#!/usr/bin/tclsh
package require sha1

proc Echo_Server {} {
    global echo
    set echo(main) [socket -server EchoAccept 1617]
    puts "I'm waiting... on the port [lindex [fconfigure $echo(main) -sockname] end]"
    vwait events
}

proc EchoAccept {sock addr port} {
    global echo
    puts "Accept $sock from $addr port $port"
    set echo(addr,$sock) [list $addr $port]
    # 握手阶段用行缓冲,方便读取HTTP头部
    fconfigure $sock -buffering line
    fileevent $sock readable [list HandleWebSocketHandshake $sock]
}

proc HandleWebSocketHandshake {sock} {
    global echo
    array set headers {}
    # 读取握手请求的所有头部行,直到空行
    while {[gets $sock line] >= 0} {
        if {$line eq ""} {
            break
        }
        if {[string match "GET *" $line]} {
            set headers(request) $line
        } else {
            set idx [string first ": " $line]
            if {$idx != -1} {
                set key [string tolower [string range $line 0 [expr {$idx-1}]]]
                set value [string range $line [expr {$idx+2}] end]
                set headers($key) $value
            }
        }
    }

    # 校验是否是合法的WebSocket握手请求
    if {![info exists headers(upgrade)] || [string tolower $headers(upgrade)] ne "websocket"} {
        close $sock
        puts "Not a WebSocket handshake, closing $echo(addr,$sock)"
        unset echo(addr,$sock)
        return
    }

    # 生成握手响应需要的Sec-WebSocket-Accept值
    set acceptKey [::sha1::sha1 -bin $headers(sec-websocket-key)258EAFA5-E914-47DA-95CA-C5AB0DC85B11]
    set acceptKey [binary encode base64 $acceptKey]

    # 发送标准的握手响应
    puts $sock "HTTP/1.1 101 Switching Protocols"
    puts $sock "Upgrade: websocket"
    puts $sock "Connection: Upgrade"
    puts $sock "Sec-WebSocket-Accept: $acceptKey"
    puts $sock ""
    flush $sock

    # 切换到二进制无缓冲模式,准备处理WebSocket帧
    fconfigure $sock -buffering none -translation binary
    fileevent $sock readable [list HandleWebSocketFrame $sock]
    puts "WebSocket handshake completed with $echo(addr,$sock)"
}

proc HandleWebSocketFrame {sock} {
    global echo
    if {[eof $sock] || [catch {
        # 读取WebSocket帧的前2字节头部
        binary scan [read $sock 2] cu opcode payloadLen
        set fin [expr {($opcode >> 7) & 1}]
        set opcode [expr {$opcode & 0x0F}]

        # 处理不同长度的payload
        if {$payloadLen == 126} {
            binary scan [read $sock 2] S payloadLen
        } elseif {$payloadLen == 127} {
            binary scan [read $sock 8] Q payloadLen
        }

        # 读取客户端发送帧的掩码(必须)
        set mask [read $sock 4]

        # 读取payload内容并解掩码
        set payload [read $sock $payloadLen]
        set decoded ""
        for {set i 0} {$i < $payloadLen} {incr i} {
            append decoded [binary format c [expr {[binary scan [string index $payload $i] c val] ? ($val ^ [binary scan [string index $mask [expr {$i%4}]] c mval] ? $mval : 0) : 0}]]
        }

        puts "Received WebSocket message: $decoded"

        # 构造响应帧(服务器发送的帧不需要掩码)
        set responseOpcode 1 ;# 文本类型帧
        set responseFin 1
        set responseHeader [binary format c [expr {($responseFin << 7) | $responseOpcode}]]

        set responsePayloadLen [string length $decoded]
        if {$responsePayloadLen < 126} {
            append responseHeader [binary format c $responsePayloadLen]
        } elseif {$responsePayloadLen < 65536} {
            append responseHeader [binary format c 126]
            append responseHeader [binary format S $responsePayloadLen]
        } else {
            append responseHeader [binary format c 127]
            append responseHeader [binary format Q $responsePayloadLen]
        }

        # 发送响应
        puts -nonewline $sock $responseHeader$decoded
        flush $sock

        # 处理quit命令,停止接受新连接
        if {$decoded eq "quit"} {
            close $echo(main)
            puts "Stopping new connections"
        }
    } err]} {
        close $sock
        puts "Close $echo(addr,$sock) (error: $err)"
        unset echo(addr,$sock)
    }
}

Echo_Server

注意事项

  • 首先得安装sha1包,Tcl默认可能没有这个包,你可以用teacup install sha1或者你系统的包管理器来安装。
  • 代码分成了两个核心部分:
    1. 握手处理:读取浏览器的HTTP握手请求,验证合法性,生成并发送符合规范的握手响应,完成协议切换。
    2. 帧处理:握手完成后,处理WebSocket的二进制帧,包括解掩码、解码收到的消息,以及构造响应帧发送回去。
  • 你原来的浏览器端代码完全不用改,直接用就行。

现在运行修改后的Tcl服务器,再打开浏览器客户端,应该就能正常建立连接、发送和接收消息了!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:29:37