在 owlkettle (https://github.com/can-lehmann/owlkettle) 中对“客户端-服务器”架构进行了一些研究后(参见here),我试图弄清楚如何具体编写一个对于 owlkettle 应用程序。
这个想法是有 2 个线程:
为了让他们沟通,您需要 2 个渠道:
现在的问题是,来自服务器的消息不会并且不能触发owlkettle中的更新它会卡在通道中,直到owlkettle本身决定触发更新(例如,当单击按钮时),在此期间它会读取消息。没有方便的方法或挂钩来表示“新服务器消息到达,用新数据更新 UI”。
这在下面的例子中很明显。单击按钮时,它会向服务器发送一条消息(通过通道 1),服务器发送响应(通过通道 2)。
您不会立即在用户界面中看到该更新。仅当您单击按钮时,因为按钮单击会自行触发常规 UI 更新。
import owlkettle, owlkettle/[playground, adw]
import std/[options, os]
var counter: int = 1
type ChannelHub = ref object
serverChannel: Channel[string]
clientChannel: Channel[string]
proc sendToServer(hub: ChannelHub, msg: string): bool =
echo "send client => server: ", msg
hub.clientChannel.trySend(msg)
proc sendToClient(hub: ChannelHub, msg: string): bool =
echo "send client <= server: ", msg
hub.serverChannel.trySend(msg)
proc readClientMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
return if response.dataAvailable:
echo "read client => server: ", response.repr
some(response.msg)
else:
none(string)
proc readServerMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()
return if response.dataAvailable:
echo "read client <= server: ", response.repr
some(response.msg)
else:
none(string)
proc setupServer(channels: ChannelHub): Thread[ChannelHub] =
proc serverLoop(hub: ChannelHub) =
while true:
let msg = hub.readClientMsg()
if msg.isSome():
discard hub.sendToClient("Received Message " & $counter)
counter.inc
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
createThread(result, serverLoop, channels)
viewable App:
hub: ChannelHub
backendMsg: string = ""
method view(app: AppState): Widget =
let msg: Option[string] = app.hub.readServerMsg()
if msg.isSome():
app.backendMsg = msg.get()
result = gui:
Window:
defaultSize = (500, 150)
title = "Client Server Example"
Box:
orient = OrientY
margin = 12
spacing = 6
Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
Label(text = "Click me")
proc clicked() =
discard app.hub.sendToServer("Frontend message!")
Label(text = "Message sent by Backend: ")
Label(text = app.backendMsg)
proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
proc startOwlkettle(hub: ChannelHub) =
adw.brew(gui(App(hub = hub)))
createThread(result, startOwlkettle, channels)
proc main() =
var serverToClientChannel: Channel[string]
var clientToServerChannel: Channel[string]
serverToClientChannel.open()
clientToServerChannel.open()
let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel)
let client = setupClient(hub)
let server = setupServer(hub)
joinThreads(server, client)
main()
当服务器发送消息时,如何让它触发前端更新?
g_idle_add_full
proc(或其他类似这样的proc)。它们的作用是向 GTK 注册一个函数,只要 GTK 主线程空闲,该函数就会被调用。
用它来检查通道中是否有从服务器接收消息的消息。如果有,则触发 owlkettle 中的更新。
您可以在启动时在 afterBuild
小组件的
App
钩子中注册该进程。
看起来像这样:
type ListenerData = object
hub: ChannelHub[string, string]
app: Viewable
proc addServerListener(app: Viewable, hub: ChannelHub[string, string], priority: int = 200) =
proc listener(cell: pointer): cbool {.cdecl.} =
let data = cast[ptr ListenerData](cell)[]
if data.hub.hasServerMsg():
discard data.app.redraw()
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
const KEEP_LISTENER_ACTIVE = true
return KEEP_LISTENER_ACTIVE.cbool
let data = allocSharedCell(ListenerData(hub: hub, app: app))
discard g_idle_add_full(priority.cint, listener, data, nil)
viewable App:
hub: ChannelHub[string, string]
backendMsg: string = ""
hooks:
afterBuild:
addServerListener(state, state.hub)
这是完整的示例(还有一些清理):
import owlkettle, owlkettle/[widgetutils, adw]
import owlkettle/bindings/gtk
import std/[options, os]
var counter: int = 0
## Communication
type ChannelHub[SMsg, CMsg] = ref object
serverChannel: Channel[SMsg]
clientChannel: Channel[CMsg]
proc new[SMsg, CMsg](t: typedesc[ChannelHub[SMsg, CMsg]]): ChannelHub[SMsg, CMsg] =
result = ChannelHub[SMsg, CMsg]()
result.serverChannel.open()
result.clientChannel.open()
proc destroy[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]) =
hub.serverChannel.close()
hub.clientChannel.close()
proc sendToServer[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg], msg: string): bool =
echo "send client => server: ", msg
hub.clientChannel.trySend(msg)
proc sendToClient[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg], msg: string): bool =
echo "send client <= server: ", msg
hub.serverChannel.trySend(msg)
proc readClientMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
result = if response.dataAvailable:
echo "read client => server: ", response.repr
some(response.msg)
else:
none(string)
proc readServerMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()
result = if response.dataAvailable:
echo "read client <= server: ", response.repr
some(response.msg)
else:
none(string)
proc hasServerMsg[SMsg, CMsg](hub: ChannelHub[SMsg, CMsg]): bool =
hub.serverChannel.peek() > 0
## Server
proc setupServer(channels: ChannelHub[string, string]): Thread[ChannelHub[string, string]] =
proc serverLoop(hub: ChannelHub[string, string]) =
while true:
let msg = hub.readClientMsg()
if msg.isSome():
discard hub.sendToClient("Received Message " & $counter)
counter.inc
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
createThread(result, serverLoop, channels)
## Client
type ListenerData = object
hub: ChannelHub[string, string]
app: Viewable
proc addServerListener(app: Viewable, hub: ChannelHub[string, string], priority: int = 200) =
proc listener(cell: pointer): cbool {.cdecl.} =
let data = cast[ptr ListenerData](cell)[]
if data.hub.hasServerMsg():
discard data.app.redraw()
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
const KEEP_LISTENER_ACTIVE = true
return KEEP_LISTENER_ACTIVE.cbool
let data = allocSharedCell(ListenerData(hub: hub, app: app))
discard g_idle_add_full(priority.cint, listener, data, nil)
viewable App:
hub: ChannelHub[string, string]
backendMsg: string = ""
hooks:
afterBuild:
addServerListener(state, state.hub)
method view(app: AppState): Widget =
let msg: Option[string] = app.hub.readServerMsg()
if msg.isSome():
app.backendMsg = msg.get()
result = gui:
Window:
defaultSize = (500, 150)
title = "Client Server Example"
Box:
orient = OrientY
margin = 12
spacing = 6
Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
Label(text = "Click me")
proc clicked() =
discard app.hub.sendToServer("Frontend message!")
Label(text = "Message sent by Backend: ")
Label(text = app.backendMsg)
proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
proc clientLoop(hub: ChannelHub) =
adw.brew(gui(App(hub = hub)))
createThread(result, clientLoop, channels)
## Main
proc main() =
let hub = new(ChannelHub[string, string])
let client = setupClient(hub)
let server = setupServer(hub)
joinThreads(server, client)
hub.destroy()
main()