数据流式传输
介绍 (Introduction)
In my initial blog post around building an Android companion app for my NAD home cinema receiver, I explained how I used Android’s Netwerk Service Discovery to discover the device on the local area network.
在有关为NAD家庭影院接收器构建Android伴侣应用程序的最初博客文章中 ,我解释了如何使用Android的Netwerk Service Discovery在局域网上发现该设备。
In this post, I’ll explain how I used RxJava to connect to the home cinema receiver in a reactive, streaming fashion. It was, at the same time, the most interesting and fun part, and on the other hand, the most challenging.
在本文中,我将说明如何使用RxJava以React性,流式传输方式连接到家庭影院接收器。 同时,它是最有趣和有趣的部分,而另一方面却是最具挑战性的部分。
If you’re not interested in the reasoning behind using RxJava for the app, please skip the following section and go straight to the implementation.
如果您对在应用程序中使用RxJava背后的原因不感兴趣,请跳过以下部分,直接进行实现。
为什么要使用RxJava和React? (Why reactive and RxJava?)
As a techie, when investing time in hobby projects, I love to combine the practical with the technical. What I mean by this is that I love to solve a problem, but I also like to technically challenge myself to do it educationally.
作为技术人员,在业余爱好项目上投入时间时,我喜欢将实践与技术相结合。 我的意思是,我喜欢解决问题,但是我也想从技术上挑战自己,以做教育。
Being someone that hasn’t touched Android’s ecosystem for at least 8 years, I was blissfully unaware of what the ecosystem became. I was very excited to see that reactive programming also gained a lot of traction in the Android ecosystem. The days of creating AsyncTasks for executing asynchronous code were still giving me shivers and were luckily no more.
作为一个至少有8年没有接触过Android生态系统的人,我很高兴没有意识到生态系统的发展。 我很高兴看到React式编程在Android生态系统中也获得了很大的关注。 创建用于执行异步代码的AsyncTasks的日子仍然让我不寒而栗,幸运的是,此后再也没有。
I’ve been mostly working with Scala for the past 5 years and a lot of the software I write is built with the reactive manifesto in mind. I’ve been using Akka Streams for several years and have grown accustomed to the Reactive Streams initiative. The initiative focuses on providing a standard for asynchronous stream processing with non-blocking backpressure.
在过去的5年中,我主要从事Scala的工作,我编写的许多软件都是在考虑React式宣言的基础上构建的。 我使用Akka Streams已经有好几年了,并且已经习惯了Reactive Streams计划 。 该计划专注于为非阻塞背压提供异步流处理的标准。
Connecting to an external device, like my home cinema receiver, and sending and receiving data whose volume is not predetermined fits the use-case for a reactive application very well.
连接到外部设备(例如我的家庭影院接收器)并发送和接收未预先确定容量的数据非常适合被动应用程序的用例。
选择Android的实现 (Choosing an implementation for Android)
For Java, I was aware of Project Reactor which is a Reactive Streams implementation. However, to cater to as many potential users as possible, I wanted to set the minimum SDK version to Android 6.0(API version 23). This version of Android, unfortunately, doesn’t include the required JDK version to run Reactor.
对于Java,我知道Project Reactor是一种Reactive Streams实现。 但是,为了迎合尽可能多的潜在用户,我想将最低SDK版本设置为Android 6.0(API版本23)。 不幸的是,此版本的Android不包含运行Reactor所需的JDK版本。
I learned that RxJava was the most popular choice for anything reactive on Android. Luckily enough, RxJava 2 also added support for the Reactive Streams API specification.
我了解到RxJava是Android上任何React式的最受欢迎的选择。 幸运的是,RxJava 2还添加了对Reactive Streams API规范的支持。
深入实施 (Diving into the implementation)
The entire setup starts in the ViewModel. The ViewModel
is the class that’s designed to manage UI-related data in a lifecycle conscious way.
整个设置从ViewModel开始。 ViewModel
是用于以生命周期感知的方式管理与UI相关的数据的类。
When the ViewModel
initialises I start 2 Observable
streams. One for monitoring the WiFi state, and one for discovering the home cinema receiver on the network.
当ViewModel
初始化时,我启动了2个Observable
流。 一种用于监视WiFi状态,另一种用于发现网络上的家庭影院接收器。
The service discovery Observable is where it all starts. When this stream emits, I bootstrap the Observable which manages the connection to the device.
服务发现Observable是一切的起点。 当此流发出时,我引导了Observable,该Observable管理与设备的连接。
监控WiFi状态 (Monitoring WiFi state)
For our first stream, I’ll keep it simple. I want to notify our users whenever they’re not connected to a WiFi network, essentially rendering the application useless.
对于我们的第一个流,我将使其保持简单。 我想在用户未连接到WiFi网络时通知他们,这实际上使该应用程序无用。
For monitoring the WiFi state I used a BehaviorSubject
which hooks up to the Android ConnectivityManager.NetworkCallback
. A BehaviorSubject
emits the most recent item it has observed and all subsequent observed items to each subscribed Observer.
为了监视WiFi状态,我使用了一个BehaviorSubject
,它ConnectivityManager.NetworkCallback
到Android ConnectivityManager.NetworkCallback
。 BehaviorSubject
将其观察到的最新项目以及所有后续观察到的项目发送给每个订阅的Observer。
As you can see in the above snippet, on the instantiation of the class, I create a private BehaviorSubject
and I expose the BehaviorSubject
externally as the state
property. By implementing the two methods of the ConnectivityManager.NetworkCallback
I emit the right state to our downstream subscription in the ViewModel
.
如您在上面的代码段中看到的那样,在类的实例上,我创建了一个私有的BehaviorSubject
,并将BehaviorSubject
作为state
属性对外公开。 通过实现ConnectivityManager.NetworkCallback
的两种方法,我向ViewModel
下游订阅发出正确的状态。
In the ViewModel
, I’m able to adjust UI-related properties which update my view to display an informative message to the user.
在ViewModel
,我可以调整与UI相关的属性,这些属性会更新我的视图以向用户显示信息。
服务发现Observables (The service discovery Observables)
Since my blog post about discovering devices on the network, I’ve found out that quite a few users have network routing devices that make DNS-SD unusable which led to quite a few frustrated users.
自从我的博客文章关于发现网络上的设备以来,我发现相当多的用户拥有使DNS-SD无法使用的网络路由设备,这导致很多沮丧的用户。
I recently discovered that the NAD emits a secondary, proprietary service discovery protocol over UDP port 11430. I’ve updated the application to scan for both implementations. This means that there are 2 Observable streams which possibly both emit an item when they discover a device. I don’t care about which protocol finds the device. All I care about is that a device is found and that I can start a connection attempt.
我最近发现NAD在UDP端口11430上发出辅助专有服务发现协议。我已经更新了应用程序以扫描这两种实现。 这意味着有2个可观察流,它们在发现设备时可能都发出一个项目。 我不在乎哪个协议可以找到设备。 我所关心的只是找到了一个设备,并且可以开始尝试连接。
To solve this part I’ve created a ServiceDiscoveryProtocol
interface which I’ll use to implement both protocols. Besides that, a third implementation will take care of starting and stopping both protocol implementations and most importantly, it will merge the 2 Observables into 1 Observable to be consumed downstream.
为了解决这一部分,我创建了一个ServiceDiscoveryProtocol
接口,我将使用它来实现这两种协议。 除此之外,第三个实现将负责启动和停止这两个协议实现,最重要的是,它将把2个Observable合并为1个Observable,以在下游使用。
All implementations expose an Observable
which is a data structure that indicates the state of discovery.
所有实现都公开一个Observable
,它是一种指示发现状态的数据结构。
Each implementation uses the same pattern as the WifiManager
showed: a private property exposed by hiding the identity of the true Observable. The difference with the WifiManager
is that the implementations for the 2 discovery protocols use a PublishSubject
instead of a BehaviorSubject
. The BehaviorSubject
remembers the last item emitted even if a consumer hasn’t subscribed to it yet. The PublishSubject
only emits values emitted after a subscription happens.
每个实现都使用WifiManager
所示的相同模式:通过隐藏真正的Observable的身份公开的私有属性。 与WifiManager
的区别在于,这两种发现协议的实现使用PublishSubject
而不是BehaviorSubject
。 即使用户尚未订阅, BehaviorSubject
也会记住最后发出的项目。 PublishSubject
仅在订阅发生后才发出值。
An overly simplified implementation of the Network Service Discovery shown in my initial post is shown below. Just like in the WifiManager
I emit an item downstream whenever the right listener callback is triggered.
下面显示了我的第一篇文章中所示的网络服务发现的过度简化的实现。 就像在WifiManager
一样,每当触发正确的侦听器回调时,我都会向下游发出一个项目。
Last but not least, the implementation of ServiceDiscoveryProtocol
that hooks everything together uses Observable.merge
to merge the 2 Observables.
最后但并非最不重要的一点是,将所有内容挂钩在一起的ServiceDiscoveryProtocol
的实现使用Observable.merge
来合并2个Observable。
Now that I have my ServiceDiscoveryProtocol
implementations that merges the two possible service discovery protocols in place, I can create an instance of it in my ViewModel
. Using that instance, I can subscribe to the exposed Observable
and handle discovered devices.
现在,我已经实现了ServiceDiscoveryProtocol
实现,将两个可能的服务发现协议合并在一起,现在可以在ViewModel
创建它的一个实例。 使用该实例,我可以订阅公开的Observable
并处理发现的设备。
连接到设备 (Connecting to the device)
创建套接字 (Creating the Socket)
As shown in the last code snippet from the ViewModel
implementation, when a device is found, a connect
method is called. The most important part of the connect method is setting up the socket connection to send and receive data from and to the NAD.
如ViewModel
实现中的最后一个代码片段所示,找到设备后, connect
调用connect
方法。 connect方法最重要的部分是设置套接字连接,以从NAD发送数据和从NAD接收数据。
For this, I created a class which will expose a Observable
for further use. The Observable
will try to create a Socket
based on the device’s IP-address and hostname. The start of the Observable chain is an PublishSubject
. Establishing the Socket connection will happen in a retryable fashion. This is handled in the retryWhen
block that will retry for a maximum of 10 times, with a delay of 500 milliseconds. If it exceeds the 10 tries, it will return an Observable.error
.
为此,我创建了一个将公开Observable
以供进一步使用。 Observable
将尝试根据设备的IP地址和主机名创建一个Socket
。 Observable链的PublishSubject
是PublishSubject
。 建立Socket连接将以可重试的方式进行。 这在retryWhen
块中处理,该块最多可重试10次,延迟为500毫秒。 如果超过10次尝试,它将返回Observable.error
。
I, however, do not want to fail the stream, and that’s why the Observable
returns an Either
type. This Either
type is from the arrow Kotlin library, which adds a lot of functional types on top of the Kotlin standard library. By using this Either
type, I can differentiate between a ‘soft failure’ (an Either.left
) and a success case (an Either.right
) when I use the Observable
downstream. To transform the Observable.error
that occurs when the retry attempts reaches the maximum tries, I use the onErrorReturn
function to transform the failed Observable
into the Either.left
.
但是,我不想使流失败,这就是为什么Observable
返回Either
类型的原因。 这Either
类型均来自箭头 Kotlin库,该库在Kotlin标准库的顶部添加了许多功能类型。 通过使用这种Either
类型,当我在下游使用Observable
时,可以区分“软失败”( Either.left
)和成功案例( Either.right
)。 为了转换在重试次数达到最大尝试次数时发生的Observable.error
,我使用onErrorReturn
函数将失败的Observable
转换为Either.left
。
The major advantage of this approach is that I can have an immutable Observable
declared as a class property val. If I wouldn’t use the Either
type, and the stream would fail, I would have to re-instantiate the class containing the Observable
property when reconnecting. Reconnecting, as the next code snippet shows, is now as simple as emitting a Unit
using the PublishSubject
.
这种方法的主要优点是我可以将不可变的Observable
声明为类属性val。 如果我不使用Either
类型,并且流将失败,则在重新连接时我将不得不重新实例化包含Observable
属性的类。 正如下一个代码片段所示,重新连接现在就像使用PublishSubject
发出Unit
一样简单。
Last but not least, the stream uses the cache
function to make sure that the latest value is cached. This will prevent a new Socket from being created every time that I subscribe and use an emitted value. It is crucial that both the downstream Source
and Sink
that I’ll define in the next section use the same instance of the Socket
. If they do not use the same Socket, commands sent to the device using the Sink
will not receive an answer on the Source
. You may be thinking, wait, did he mix up the Sink
and Source
? Keep in mind that I’m referring to these concepts from the application point-of-view. This means that data that I send to the device will use the Sink
. Data received by our app from the device will be received through the Source
.
最后但并非最不重要的一点是,该流使用cache
功能来确保已缓存最新值。 这将防止每次我订阅并使用发射的值时都创建一个新的Socket。 在下一节中定义的下游Source
和Sink
都必须使用Socket
的同一实例,这一点至关重要。 如果它们不使用相同的套接字,则使用Sink
器发送到设备的命令将不会在Source
上收到答复。 您可能在想,等等,他是否将Sink
和Source
混合在一起? 请记住,我是从应用程序的角度指这些概念。 这意味着我发送到设备的数据将使用Sink
。 我们的应用从设备接收到的数据将通过Source
接收。
构造源和接收器 (Constructing the Source and Sink)
With the SocketProvider
in place, I now want to build upon this to create an Okio Source
and Sink
. Okio is a Kotlin library that makes it easier to perform IO operations. Among others, it provides its stream types Source
and Sink
that essentially are wrappers around InputStream
and OutputStream
with several benefits.
有了SocketProvider
之后,我现在想在此基础上创建Okio Source
和Sink
。 Okio是Kotlin库,可以更轻松地执行IO操作。 其中,它提供了其流类型Source
和Sink
,它们实际上是InputStream
和OutputStream
包装,具有许多优点。
The implementations of the SourceProvider
and SinkProvider
are quite the same, they both map the Observable
to Observable
where A
is either a BufferedSource
or a BufferedSink
. This new Observable
is again exposed as a public class property as I’ve done before.
SourceProvider
和SinkProvider
的实现完全相同,它们都将Observable
映射到Observable
,其中A
是BufferedSource
或BufferedSink
。 正如我之前所做的那样,此新的Observable
再次公开为公共类属性。
Last but not least, these 2 classes again use the cache
function to prevent the creation of too many redundant instances.
最后但并非最不重要的一点是,这两个类再次使用cache
功能来防止创建太多冗余实例。
The entire implementation of the SourceProvider
as just described:
刚刚描述的SourceProvider
的整个实现:
从NAD读取数据 (Reading data from the NAD)
The logic to control both sending commands and receiving responses lies in a ReceiverRepository
class.
控制发送命令和接收响应的逻辑都在ReceiverRepository
类中。
For reading responses sent by the device, I declared a class property called responses
which maps the Observable
that our SourceProvider
returns into a Observable
. The SocketResponse
is a wrapper around the protocol that the device supports, but it encapsulates a single response line sent by the device.
为了读取设备发送的响应,我声明了一个名为responses
的类属性,该属性将我们的SourceProvider
返回的Observable
映射到Observable
。 SocketResponse
是设备支持的协议的包装,但是它封装了设备发送的单个响应行。
The core of reading responses happens in the createSocketResponseStream
method. In here, I fold
on the Either
. If it is an Either.left
, thus a Throwable
, I simply pass it on to downstream. If it is an Either.right
, thus a valid Source
, I create a Observable.interval
which will try to read a line from the Source
every 10 milliseconds.
读取响应的核心发生在createSocketResponseStream
方法中。 在这里,我在Either
上fold
。 如果它是Either.left
,那么是Throwable
,我只是将其传递给下游。 如果它是Either.right
,那么它是一个有效的Source
,我创建一个Observable.interval
,它将尝试每隔10毫秒从Source
读取一行。
The reason for doing this is that I need a way to keep reading lines from the Source
. If the Source
does not have any new lines to read, it will block the thread that is used to run the Observable.interval
. If no data is received, it thus doesn’t constantly trigger the 10-millisecond interval but it will patiently wait for new data.
这样做的原因是我需要一种方法来保持从Source
读取行。 如果Source
没有要读取的新行,它将阻塞用于运行Observable.interval
的线程。 如果没有收到数据,它就不会持续触发10毫秒的间隔,但会耐心等待新数据。
Upon receiving a new line, I try to parse that line. If it succeeds, I return an Either.right
. If it fails, I again, gracefully prevent the stream from failing by using the Either.left
construct.
收到新行后,我尝试解析该行。 如果成功,则返回Either.right
。 如果失败,我再次使用Either.left
构造来优雅地防止流失败。
发送命令到NAD (Sending commands to the NAD)
The logic for sending commands is a lot easier than reading responses. However, there is a bit of added complexity in the form of Kotlin Coroutines.
发送命令的逻辑比读取响应要容易得多。 但是,以Kotlin Coroutines的形式增加了一些复杂性。
The use of Kotlin Coroutines is to prevent that I’m running the IO action on the UI thread. To send a command to the home cinema receiver, I take the last emitted value of the Observable
in a blocking fashion using .take(1).blockingLast()
. I then map the result and in the map
I write and flush the command to the device.
使用Kotlin Coroutines是为了防止我在UI线程上运行IO操作。 为了向家庭影院接收器发送命令,我使用.take(1).blockingLast()
以阻塞方式.take(1).blockingLast()
Observable
的最后发射值。 然后,我映射结果,并在map
编写并刷新命令至设备。
钩在一起 (Hooking it all together)
The last part of the puzzle is to subscribe to our responses
stream from the ReceiverRepository
. I subscribe to the stream in my MainViewModel
. By subscribing to the stream, I’m able to do several things based on the connection status. I can control the user interface presented to the user, send commands to the NAD upon first connecting, and last but not least, save received responses in the ViewModel
state.
难题的最后一部分是从ReceiverRepository
订阅我们的responses
流。 我在MainViewModel
订阅了流。 通过订阅流,我可以根据连接状态执行几项操作。 我可以控制呈现给用户的用户界面,在第一次连接时将命令发送到NAD,最后但并非最不重要的一点是,将收到的响应保存为ViewModel
状态。
结论 (Conclusion)
As simple as it sounds, connecting to, and sending and receiving messages from the home cinema is a lot more complex than expected. Throughout my journey in building the Android app, this was most definitely the most complicated part. I’ve used quite a few different RxJava techniques to end up with a solution that’s performant, fault-tolerant, maintainable, loosely coupled and easy to test. All by all, there are improvement points left to refactor, but overall, I’m quite satisfied with this implementation.
听起来很简单,但是连接到家庭影院以及从家庭影院发送和接收消息比预期的要复杂得多。 在我构建Android应用程序的整个过程中,这绝对是最复杂的部分。 我使用了许多不同的RxJava技术,最终得出了一种高性能,容错,可维护,松散耦合且易于测试的解决方案。 总体而言,还有一些要重构的改进点,但是总的来说,我对这种实现非常满意。
I do recognise that there’s a lot of complexity in the solution as it is and I’m very curious to try alternative methods, like Kotlin Flow, to see if I can reduce the code complexity.
我确实知道该解决方案确实存在很多复杂性,并且我很好奇尝试使用其他方法(例如Kotlin Flow)来查看我是否可以降低代码复杂性。
翻译自: https://levelup.gitconnected.com/streaming-data-from-a-home-cinema-device-using-rxjava-on-android-44de8f5a29a0
数据流式传输