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

Tcl非阻塞通道管道模拟延迟:WebSocket帧读取与并发咨询

Tcl非阻塞通道实现WebSocket通信问题

背景

我最近在摸索Tcl下用非阻塞通道做WebSocket通信的实现方法,目前已经打通了和浏览器的WebSocket连接:不管是阻塞还是非阻塞模式,用下面标注WebSocket简易版本的代码,都能解码XOR帧、读取简单的单帧消息。这段代码是参考MDN的Java版WebSocket服务端编写示例改的,我之前猜测非阻塞模式下这段代码能跑通,纯粹是因为测试用的消息短,或者浏览器会攒够数据才刷新输出缓冲区。

虽然我自认为搞懂了阻塞/非阻塞通道的区别,也清楚chan gets和chan read的用法差异,Ashok Nadkarni写的Tcl书里的示例我也能跑通,但对Tcl的通道机制整体还是没摸透。书里的事件驱动I/O示例我能复现、能改,但那些例子全是按行读数据,没有用chan read做事件驱动读的案例。可WebSocket的XOR帧里既没有换行符也没有EOF标记,只能靠载荷长度判断什么时候读完,还要判断同一个消息有没有后续分片。

要是不用浏览器发WebSocket消息测,而是照着Nadkarni书里的管道例子,用after命令模拟延迟往非阻塞通道里写数据,前面说的简易版本代码直接就跑崩了,但阻塞模式下还是能正常工作,问题1下面的代码就能复现这个现象。

待解决的两个问题

  • 非阻塞通道读数据,靠跟踪「读取状态」的方式实现是否可行?比如下面标注带读取状态的非阻塞管道的示例代码,把消息拆成多个分片延迟发送:每次可读事件触发时,代码根据当前的读取状态、该状态下已经读到的字节数,从输入缓冲区读对应长度的字节,一直读到缓冲区空或者读完一整个帧的载荷为止。这段代码跑出来结果是对的,但写法看着比较糙,想确认下这是不是通用的正确实现方式?
  • 如果上面说的状态跟踪方案是可行的,在「Tcl做桌面应用配套的轻量本地服务、Web浏览器当GUI」的场景下,用非阻塞通道能带来什么实际收益?如果单页面有多个WebSocket连接,或者多标签页每个标签页各持一个WebSocket连接,多个socket同时发消息会出现什么情况?用非阻塞通道的时候,Tcl是能并发处理消息,还是和JavaScript那种单线程异步的运行机制类似?

感谢各位解答。


相关代码

WebSocket简易版本

chan configure $sock -buffering line -blocking 1 -encoding iso8859-1 -translation crlf
chan event $sock readable [list ReadLine $sock]

# 处理完WebSocket升级的GET请求后,将读事件回调改为ReadXOR
chan event $sock readable [list ReadXOR $sock]

proc ReadXOR {sock} {
  chan configure $sock -translation binary
  if {[catch {chan read $sock 2} XORframe]} {
      chan puts stdout "Error in sock $sock"
      return
  }

  # 除非通道关闭,否则XOR帧不会触发EOF
  if {[chan eof $sock]} {
      CloseSock $sock
      return
  }

  binary scan $XORframe B8B8 frop mpl

  set f [string range $frop 0 0]
  set r1 [string range $frop 1 1]
  set r2 [string range $frop 2 2]
  set r3 [string range $frop 3 3]
  set op [string range $frop 4 7]
  set m [string range $mpl 0 0]
  set pl 0[string range $mpl 1 7]
  set g [binary format B8 $pl]

  binary scan $g cu n

  # 注:当前载荷长度判断逻辑并不完整,仅适配小消息测试场景
  # 已知测试场景下,长度字段后4字节为掩码密钥,后续跟随$n字节载荷
  if {[catch {chan read $sock [expr {$n+4}]} XORframe]} {
      chan puts stdout "Error in sock $sock"
      return
  }

  binary scan $XORframe cu4cu* mKey encMsg

  set l [llength $encMsg]
  set decMsg {}

  for { set i 0 } { $i < $l } { incr i } {
    lappend decMsg [binary format c [expr { [lindex $encMsg $i] ^ [lindex $mKey [expr {$i % 4}]] } ]]
  }

  chan puts stdout "Decoded message: [join $decMsg ""]"

}

带读取状态的非阻塞管道

# 管道测试代码
lassign [chan pipe] rchan wchan
chan configure $rchan -buffering full -blocking 0 -translation binary
chan configure $wchan -buffering none -blocking 0 -translation binary
chan event $rchan readable [list ReadXOR $rchan]

dict set comPorts $rchan { state 0 bytes 0 bytesH {} bytesM {} bytesPL {} pl {} encMsg {} mKey {}}

writeFrames

after 4000 writeFrames
after 8000 [list set forever 1]

set forever 0
vwait forever

close $rchan
close $wchan

# 构造分片二进制数据,模拟网络延迟下的数据分片到达场景
proc writeFrames {} {
  global wchan
  set binlist1 {}
  set binlist2 {}
  set binlist3 {}
  set binlist4 {}

  # 测试数据来自MDN Java版WebSocket服务端示例
  #set data {129 134 167 225 225 210 198 131 130 182 194 135}; # 解码结果为abcdef

  set data1 129
  set data2 {134 167 225}
  set data3 {225 210 198 131 130}
  set data4 {182 194 135}

  foreach i $data2 {
    lappend binlist2 [binary format cu $i]
  }
  foreach i $data3 {
    lappend binlist3 [binary format cu $i]
  }
  foreach i $data4 {
    lappend binlist4 [binary format cu $i]
  }


  # 注:变量名有误导性,实际是将单个XOR帧拆分为4个分片发送
  set XORframe1 [binary format cu $data1]
  set XORframe2 [join $binlist2 ""]
  set XORframe3 [join $binlist3 ""]
  set XORframe4 [join $binlist4 ""]

  after 500 [list chan puts -nonewline $wchan $XORframe1]
  after 700 [list chan puts -nonewline $wchan $XORframe2]
  after 800 [list chan puts -nonewline $wchan $XORframe3]
  after 1000 [list chan puts -nonewline $wchan $XORframe4]
}


# 带读取状态的ReadXOR实现
proc ReadXOR {sock} {
  global comPorts
  if { [dict get $comPorts $sock state] == 0 } {
    if {[catch {chan read $sock [expr { 2 - [dict get $comPorts $sock bytes] }] } head]} {
        chan puts stdout "Error in sock $sock"
        return
    }

    dict with comPorts $sock {
      set bytes [expr {$bytes + [string length $head]}]
      lappend bytesH $head
    }

    if { [dict get $comPorts $sock bytes] == 2 } {
      binary scan [join [dict get $comPorts $sock bytesH] ""] B8B8 frop mpl
      binary scan [binary format B8 0[string range $mpl 1 7]] cu nt
      dict with comPorts $sock {
        set state 1
        set f [string range $frop 0 0]
        set r1 [string range $frop 1 1]
        set r2 [string range $frop 2 2]
        set r3 [string range $frop 3 3]
        set op [string range $frop 4 7]
        set m [string range $mpl 0 0]
        set pl $nt
        set bytes 0
        set bytesH {}
        chan puts stdout "state: $state, f: $f, r1: $r1, r2: $r2, r3: $r3, op: $op, m: $m, pl: $pl"
        # 输出 => state: 1, f: 1, r1: 0, r2: 0, r3: 0, op: 0001, m: 1, pl: 6
      }
    }
  }

  if { [dict get $comPorts $sock state] == 1 } {
    set b [dict get $comPorts $sock bytes]
    if {[catch {chan read $sock [expr {4-$b}]} mask]} {
        chan puts stdout "Error in sock $sock"
        return
    }

    dict with comPorts $sock {
      set bytes [expr {$bytes + [string length $mask]}]
      lappend bytesM $mask
    }
    if { [dict get $comPorts $sock bytes] == 4 } {
      binary scan [join [dict get $comPorts $sock bytesM] ""] cu4 mCode
      dict with comPorts $sock {
        set bytes 0
        set state 2
        set mKey $mCode
        set bytesM {}
        chan puts stdout $mKey
        # 输出 => 167 225 225 210
      }
    }
  }

  if { [dict get $comPorts $sock state] == 2 } {
    set b [dict get $comPorts $sock bytes]
    set l [dict get $comPorts $sock pl]
    if {[catch {chan read $sock [expr {$l-$b}]} load]} {
        chan puts stdout "Error in sock $sock"
        return
    }

    dict with comPorts $sock {
      set bytes [expr {$bytes + [string length $load]}]
      lappend bytesPL $load
    }
    if { [dict get $comPorts $sock bytes] == $l } {
      binary scan [join [dict get $comPorts $sock bytesPL] ""] cu* encoded
      dict with comPorts $sock {
        set bytes 0
        set state 3
        set encMsg $encoded
        set bytesPL {}
        chan puts stdout "encMsg: $encMsg"
        # 输出 => encMsg: 198 131 130 182 194 135
      }
    }
  }

  if { [dict get $comPorts $sock state] == 3 } {
    set decMsg {}
    set mKey [dict get $comPorts $sock mKey]
    set encMsg [dict get $comPorts $sock encMsg]
    set l [llength $encMsg ]
    for { set i 0 } { $i < $l } { incr i } {
      lappend decMsg [binary format c [expr { [lindex $encMsg $i] ^ [lindex $mKey [expr {$i % 4}]] } ]]
    }
    dict with comPorts $sock {
      set state 0
      set mKey {}
      set encMsg {}
    }
    chan puts stdout "Decoded message: [join $decMsg ""]"
    # 输出 => Decoded message: abcdef
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 21:57:24