我正在编写一个纯websocket Web应用程序,这意味着在websocket升级之前没有用户/客户端步骤,更具体地说: 与其他通信
一样,身份验证请求也会通过websockets进行有/是:
现在,并非每个客户端都可以访问每个项目 - 它的访问控制是在服务器端(ofc)实现的,并且与websockets本身无关。
我的问题是,我希望允许协作,这意味着N个客户可以一起处理1个项目。
现在,如果其中一个客户端修改了某些内容,我想通知正在处理该项目的所有其他客户端。
这一点尤其重要,因为atm我是唯一一个正在研究它并进行测试的人,这是我身上的重大疏忽,因为现在:
如果客户A连接到Project X而客户B连接到Proejct Y,如果其中任何一个更新了他们各自项目中的内容,则另一个会收到有关这些更改的通知。
现在我的WebsocketController非常简单,基本上我有这个:
private val fanIn = MergeHub.source[AllowedWSMessage].to(sink).run()
private val fanOut = source.toMat(BroadcastHub.sink[AllowedWSMessage])(Keep.right).run()
def handle: WebSocket = WebSocket.accept[AllowedWSMessage, AllowedWSMessage]
{
_ => Flow.fromSinkAndSource(fanIn, fanOut)
}
现在根据我的理解,我需要的是
1)每个项目有多个websocket端点,例如/ api / {project_identifier} / ws
(X)OR
2)根据他们正在运行的项目分割WebSocket连接/连接客户端的一些方法。
因为我不想去路线1)我会在2)分享我的想法:
我现在没有看到解决方法的问题是,我可以轻松地在服务器端创建一些集合,在那里我存储哪个用户正在处理哪个项目在任何给定时刻(如果他们选择/切换一个项目,客户端将其发送到服务器并存储此信息)
但我仍然有那个fanOut
,所以这不会解决我在WebSocket / AkkaStreams方面的问题。
是否有一些魔法(过滤)可以在BroadcastHub
调用,以实现我想要的效果?
编辑:现在在这里分享我的整个websocket逻辑,尝试但未能应用@James Roper的好提示:
class WebSocketController @Inject()(implicit cc: ControllerComponents, ec: ExecutionContext, system: ActorSystem, mat: Materializer) extends AbstractController(cc)
{ val logger:Logger = Logger(this.getClass())
type WebSocketMessage = Array[Byte]
import scala.concurrent.duration._
val tickingSource: Source[WebSocketMessage, Cancellable] =
Source.tick(initialDelay = 1 second, interval = 10 seconds, tick = NotUsed)
.map(_ => Wrapper().withKeepAlive(KeepAlive()).toByteArray)
private val generalActor = system.actorOf(Props
{
new myActor(system, "generalActor")
}, "generalActor")
private val serverMessageSource = Source
.queue[WebSocketMessage](10, OverflowStrategy.backpressure)
.mapMaterializedValue
{ queue => generalActor ! InitTunnel(queue) }
private val sink: Sink[WebSocketMessage, NotUsed] = Sink.actorRefWithAck(generalActor, InternalMessages.Init(), InternalMessages.Acknowledged(), InternalMessages.Completed())
private val source: Source[WebSocketMessage, Cancellable] = tickingSource.merge(serverMessageSource)
private val fanIn = MergeHub.source[WebSocketMessage].to(sink).run()
private val fanOut = source.toMat(BroadcastHub.sink[WebSocketMessage])(Keep.right).run()
// TODO switch to WebSocket.acceptOrResult
def handle: WebSocket = WebSocket.accept[WebSocketMessage, WebSocketMessage]
{
//_ => createFlow()
_ => Flow.fromSinkAndSource(fanIn, fanOut)
}
private val projectHubs = TrieMap.empty[String, (Sink[WebSocketMessage, NotUsed], Source[WebSocketMessage, NotUsed])]
private def buildProjectHub(projectName: String) =
{
logger.info(s"building projectHub for $projectName")
val projectActor = system.actorOf(Props
{
new myActor(system, s"${projectName}Actor")
}, s"${projectName}Actor")
val projectServerMessageSource = Source
.queue[WebSocketMessage](10, OverflowStrategy.backpressure)
.mapMaterializedValue
{ queue => projectActor ! InitTunnel(queue) }
val projectSink: Sink[WebSocketMessage, NotUsed] = Sink.actorRefWithAck(projectActor, InternalMessages.Init(), InternalMessages.Acknowledged(), InternalMessages.Completed())
val projectSource: Source[WebSocketMessage, Cancellable] = tickingSource.merge(projectServerMessageSource)
val projectFanIn = MergeHub.source[WebSocketMessage].to(projectSink).run()
val projectFanOut = projectSource.toMat(BroadcastHub.sink[WebSocketMessage])(Keep.right).run()
(projectFanIn, projectFanOut)
}
private def getProjectHub(userName: String, projectName: String): Flow[WebSocketMessage, WebSocketMessage, NotUsed] =
{
logger.info(s"trying to get projectHub for $projectName")
val (sink, source) = projectHubs.getOrElseUpdate(projectName, {
buildProjectHub(projectName)
})
Flow.fromSinkAndSourceCoupled(sink, source)
}
private def extractUserAndProject(msg: WebSocketMessage): (String, String) =
{
Wrapper.parseFrom(msg).`type` match
{
case m: MessageType =>
val message = m.value
(message.userName, message.projectName)
case _ => ("", "")
}
}
private def createFlow(): Flow[WebSocketMessage, WebSocketMessage, NotUsed] =
{
// broadcast source and sink for demux/muxing multiple chat rooms in this one flow
// They'll be provided later when we materialize the flow
var broadcastSource: Source[WebSocketMessage, NotUsed] = null
var mergeSink: Sink[WebSocketMessage, NotUsed] = null
Flow[WebSocketMessage].map
{
m: WebSocketMessage =>
val msg = Wrapper.parseFrom(m)
logger.warn(s"client sent project related message: ${msg.toString}");
m
}.map
{
case isProjectRelated if !extractUserAndProject(isProjectRelated)._2.isEmpty =>
val (userName, projectName) = extractUserAndProject(isProjectRelated)
logger.info(s"userName: $userName, projectName: $projectName")
val projectFlow = getProjectHub(userName, projectName)
broadcastSource.filter
{
msg =>
val (_, project) = extractUserAndProject(msg)
logger.info(s"$project == $projectName")
(project == projectName)
}
.via(projectFlow)
.runWith(mergeSink)
isProjectRelated
case other =>
{
logger.info("other")
other
}
} via {
Flow.fromSinkAndSourceCoupledMat(BroadcastHub.sink[WebSocketMessage], MergeHub.source[WebSocketMessage])
{
(source, sink) =>
broadcastSource = source
mergeSink = sink
source.filter(extractUserAndProject(_)._2.isEmpty)
.map
{ x => logger.info("Non project related stuff"); x }
.via(Flow.fromSinkAndSource(fanIn, fanOut))
.runWith(sink)
NotUsed
}
}
}
}
解决方案/了解我的理解方式:
1)我们有一个“包装器流”,我们有一个为空的broadcastSource和mergeSink,直到我们在外部} via {
块中实现它们
2)在“包装流”中,我们映射每个元素以检查它。
I)如果项目相关,我们
a)获取/创建项目的自己的子流程 b)根据项目名称过滤元素 c)让那些通过过滤器的人被子/项目流使用,这样连接到项目的每个人都可以获得该元素
II)如果它与项目无关,我们只需将其传递给
3)我们的包装流程是通过“按需”物化流程进行的,并且在实现它的via
中,我们将与项目无关的元素分发给所有连接的Web套接字客户端。 / p>
总结一下:我们有一个websocket连接的“包装流”,它可以通过projectFlow或generalFlow进行,具体取决于它正在使用的消息/元素。
我现在的问题是(而且似乎是微不足道的,但我正在以某种方式挣扎)每条消息都应该进入myActor
(atm),并且应该也会有来自那里的消息(请参阅{ {1}}和serverMesssageSource
)
但上述代码正在创建非确定性结果,例如一个客户端发送2条消息,但有4条正在处理(根据日志和服务器发回的结果),有时消息在从控制器到演员的路上突然丢失。
我无法解释这一点,但是如果我将它留给source
,每个人都会得到所有东西,但至少如果只有一个客户端它完全符合预期(显然:))
答案 0 :(得分:3)
我实际上建议使用Play的socket.io support。这提供了命名空间,从我的描述中可以看出,它可以直接实现您想要的 - 每个命名空间都是它自己独立管理的流,但所有命名空间都在同一个WebSocket上。我wrote a blog post了解您今天可能选择使用socket.io的原因。
如果您不想使用socket.io,我在这里有一个示例(这使用socket.io,但不使用socket.io名称空间,因此可以很容易地适应在直接WebSockets上运行)一个多聊天室协议 - 它将消息提供给BroadcastHub,然后用户当前所在的每个聊天室都有一个集线器订阅(对于您来说,这将是每个项目的一个订阅)。这些订阅中的每一个都过滤来自集线器的消息,仅包括该订阅聊天室的消息,然后将消息提供给该聊天室MergeHub。
此处突出显示的代码根本不是特定于socket.io,如果您可以将WebSocket连接调整为ChatEvent
的流,则可以按原样使用:
要满足您通过每个人连接的广播频道引导非项目特定消息的要求,首先要创建该频道:
val generalFlow = {
val (sink, source) = MergeHub.source[NonProjectSpecificEvent]
.toMat(BroadcastHub.sink[NonProjectSpecificEvent])(Keep.both).run
Flow.fromSinkAndSourceCoupled(sink, source)
}
然后,当每个连接的WebSocket的广播接收器/源连接时,附加它(这是来自聊天示例:
} via {
Flow.fromSinkAndSourceCoupledMat(BroadcastHub.sink[YourEvent], MergeHub.source[YourEvent]) { (source, sink) =>
broadcastSource = source
mergeSink = sink
source.filter(_.isInstanceOf[NonProjectSpecificEvent])
.via(generalFlow)
.runWith(sink)
NotUsed
}
}