Skip to content

Commit 3b3c5c5

Browse files
authored
Add ClientNetworkMonitor for tracking network changes (#387)
The SwiftGRPC implementation that is backed by [gRPC-Core(https://github.com/grpc/grpc) (and not SwiftNIO) is known to have some connectivity issues on iOS clients - namely, silently disconnecting (making it seem like active calls/connections are hanging) when switching between wifi <> cellular. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations made by iOS' networking stack when these types of changes occur, and isn't able to handle them itself. There is also documentation of this behavior in [this gRPC-Core readme](https://github.com/grpc/grpc/blob/v1.19.0/src/objective-c/NetworkTransitionBehavior.md). To aid in this problem, we're adding a [`ClientNetworkMonitor`](./Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift) that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable. Details: - **Switching between wifi <> cellular:** Channels silently disconnect - **Switching between 3G <> LTE (etc.):** Channels silently disconnect - **Network becoming unreachable:** Most times channels will time out after a few seconds, but `ClientNetworkMonitor` will notify of these changes much faster - **Switching between background <> foreground:** No known issues Original issue: #337.
1 parent 97ff923 commit 3b3c5c5

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ By convention the `--swift_out` option invokes the `protoc-gen-swift`
9191
plugin and `--swiftgrpc_out` invokes `protoc-gen-swiftgrpc`.
9292

9393
#### Parameters
94+
9495
To pass extra parameters to the plugin, use a comma-separated parameter list
9596
separated from the output directory by a colon.
9697

@@ -133,6 +134,32 @@ to directly build API clients and servers with no generated code.
133134
For an example of this in Swift, please see the
134135
[Simple](Examples/SimpleXcode) example.
135136

137+
### Known issues
138+
139+
The SwiftGRPC implementation that is backed by [gRPC-Core](https://github.com/grpc/grpc)
140+
(and not SwiftNIO) is known to have some connectivity issues on iOS clients - namely, silently
141+
disconnecting (making it seem like active calls/connections are hanging) when switching
142+
between wifi <> cellular or between cellular technologies (3G <> LTE). The root cause of these problems is that the
143+
backing gRPC-Core doesn't get the optimizations made by iOS' networking stack when these
144+
types of changes occur, and isn't able to handle them itself.
145+
146+
There is also documentation of this behavior in [this gRPC-Core readme](https://github.com/grpc/grpc/blob/v1.19.0/src/objective-c/NetworkTransitionBehavior.md).
147+
148+
To aid in this problem, there is a [`ClientNetworkMonitor`](./Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift)
149+
that monitors the device for events that can cause gRPC to disconnect silently. We recommend utilizing this component to
150+
call `shutdown()` (or destroy) any active `Channel` instances, and start new ones when the network is reachable.
151+
152+
Setting the [`keepAliveTimeout` argument](https://github.com/grpc/grpc-swift/blob/0.7.0/Sources/SwiftGRPC/Core/ChannelArgument.swift#L46)
153+
on channels is also encouraged.
154+
155+
Details:
156+
- **Switching between wifi <> cellular:** Channels silently disconnect
157+
- **Switching between 3G <> LTE (etc.):** Channels silently disconnect
158+
- **Network becoming unreachable:** Most times channels will time out after a few seconds, but `ClientNetworkMonitor` will notify of these changes much faster
159+
- **Switching between background <> foreground:** No known issues
160+
161+
Original SwiftGRPC issue: https://github.com/grpc/grpc-swift/issues/337.
162+
136163
## Having build problems?
137164

138165
grpc-swift depends on Swift, Xcode, and swift-protobuf. We are currently
@@ -175,11 +202,11 @@ When issuing a new release, the following steps should be followed:
175202
1. Run the CocoaPods linter to ensure that there are no new warnings/errors:
176203

177204
`$ pod spec lint SwiftGRPC.podspec`
178-
205+
179206
1. Update the Carthage Xcode project (diff will need to be checked in with the version bump):
180207

181208
`$ make project-carthage`
182-
209+
183210
1. Bump the version in the `SwiftGRPC.podspec` file
184211

185212
1. Merge these changes, then create a new `Release` with corresponding `Tag`. Be sure to include a list of changes in the message
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2019, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
#if os(iOS)
17+
import CoreTelephony
18+
import Dispatch
19+
import SystemConfiguration
20+
21+
/// This class may be used to monitor changes on the device that can cause gRPC to silently disconnect (making
22+
/// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as
23+
/// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations
24+
/// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular,
25+
/// switching between 3G and LTE, enabling/disabling airplane mode, etc.
26+
/// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues
27+
/// Original issue: https://github.com/grpc/grpc-swift/issues/337
28+
open class ClientNetworkMonitor {
29+
private let queue: DispatchQueue
30+
private let callback: (State) -> Void
31+
private let reachability: SCNetworkReachability
32+
33+
/// Instance of network info being used for obtaining cellular technology names.
34+
public let cellularInfo = CTTelephonyNetworkInfo()
35+
/// Whether the network is currently reachable. Backed by `SCNetworkReachability`.
36+
public private(set) var isReachable: Bool?
37+
/// Whether the device is currently using wifi (versus cellular).
38+
public private(set) var isUsingWifi: Bool?
39+
/// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`).
40+
public private(set) var cellularName: String?
41+
42+
/// Represents a state of connectivity.
43+
public struct State: Equatable {
44+
/// The most recent change that was made to the state.
45+
public let lastChange: Change
46+
/// Whether this state is currently reachable/online.
47+
public let isReachable: Bool
48+
}
49+
50+
/// A change in network condition.
51+
public enum Change: Equatable {
52+
/// Reachability changed (online <> offline).
53+
case reachability(isReachable: Bool)
54+
/// The device switched from cellular to wifi.
55+
case cellularToWifi
56+
/// The device switched from wifi to cellular.
57+
case wifiToCellular
58+
/// The cellular technology changed (e.g., 3G <> LTE).
59+
case cellularTechnology(technology: String)
60+
}
61+
62+
/// Designated initializer for the network monitor. Initializer fails if reachability is unavailable.
63+
///
64+
/// - Parameter host: Host to use for monitoring reachability.
65+
/// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`.
66+
/// Should always be used when accessing properties of this class.
67+
/// - Parameter callback: Closure to call whenever state changes.
68+
public init?(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) {
69+
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else {
70+
return nil
71+
}
72+
73+
self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue")
74+
self.callback = callback
75+
self.reachability = reachability
76+
self.startMonitoringReachability(reachability)
77+
self.startMonitoringCellular()
78+
}
79+
80+
deinit {
81+
SCNetworkReachabilitySetCallback(self.reachability, nil, nil)
82+
SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(),
83+
CFRunLoopMode.commonModes.rawValue)
84+
NotificationCenter.default.removeObserver(self)
85+
}
86+
87+
// MARK: - Cellular
88+
89+
private func startMonitoringCellular() {
90+
let notificationName: Notification.Name
91+
if #available(iOS 12.0, *) {
92+
notificationName = .CTServiceRadioAccessTechnologyDidChange
93+
} else {
94+
notificationName = .CTRadioAccessTechnologyDidChange
95+
}
96+
97+
NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)),
98+
name: notificationName, object: nil)
99+
}
100+
101+
@objc
102+
private func cellularDidChange(_ notification: NSNotification) {
103+
self.queue.async {
104+
let newCellularName: String?
105+
if #available(iOS 12.0, *) {
106+
let cellularKey = notification.object as? String
107+
newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] }
108+
} else {
109+
newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology
110+
}
111+
112+
if let newCellularName = newCellularName, self.cellularName != newCellularName {
113+
self.cellularName = newCellularName
114+
self.callback(State(lastChange: .cellularTechnology(technology: newCellularName),
115+
isReachable: self.isReachable ?? false))
116+
}
117+
}
118+
}
119+
120+
// MARK: - Reachability
121+
122+
private func startMonitoringReachability(_ reachability: SCNetworkReachability) {
123+
let info = Unmanaged.passUnretained(self).toOpaque()
124+
var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil,
125+
release: nil, copyDescription: nil)
126+
let callback: SCNetworkReachabilityCallBack = { _, flags, info in
127+
let observer = info.map { Unmanaged<ClientNetworkMonitor>.fromOpaque($0).takeUnretainedValue() }
128+
observer?.reachabilityDidChange(with: flags)
129+
}
130+
131+
SCNetworkReachabilitySetCallback(reachability, callback, &context)
132+
SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(),
133+
CFRunLoopMode.commonModes.rawValue)
134+
self.queue.async { [weak self] in
135+
var flags = SCNetworkReachabilityFlags()
136+
SCNetworkReachabilityGetFlags(reachability, &flags)
137+
self?.reachabilityDidChange(with: flags)
138+
}
139+
}
140+
141+
private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) {
142+
self.queue.async {
143+
let isUsingWifi = !flags.contains(.isWWAN)
144+
let isReachable = flags.contains(.reachable)
145+
146+
let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi
147+
let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable
148+
149+
self.isUsingWifi = isUsingWifi
150+
self.isReachable = isReachable
151+
152+
if notifyForWifi {
153+
self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable))
154+
}
155+
156+
if notifyForReachable {
157+
self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable))
158+
}
159+
}
160+
}
161+
}
162+
#endif

0 commit comments

Comments
 (0)