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或者你系统的包管理器来安装。 - 代码分成了两个核心部分:
- 握手处理:读取浏览器的HTTP握手请求,验证合法性,生成并发送符合规范的握手响应,完成协议切换。
- 帧处理:握手完成后,处理WebSocket的二进制帧,包括解掩码、解码收到的消息,以及构造响应帧发送回去。
- 你原来的浏览器端代码完全不用改,直接用就行。
现在运行修改后的Tcl服务器,再打开浏览器客户端,应该就能正常建立连接、发送和接收消息了!
内容的提问来源于stack exchange,提问作者Dinesh
相关产品推荐
相关产品推荐

