Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ClientNetworkMonitor for tracking network changes #387

Merged
merged 7 commits into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ By convention the `--swift_out` option invokes the `protoc-gen-swift`
plugin and `--swiftgrpc_out` invokes `protoc-gen-swiftgrpc`.

#### Parameters

To pass extra parameters to the plugin, use a comma-separated parameter list
separated from the output directory by a colon.

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

### Known issues

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.

To aid in this problem, there is 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.

Setting the [`keepAliveTimeout` argument](https://github.com/grpc/grpc-swift/blob/c401b44ea81b246b8e7fea191ea1ee11a834ee60/Sources/SwiftGRPC/Core/ChannelArgument.swift#L46)
on channels is also encouraged.

Details:
- **Switching between wifi <> cellular:** 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: https://github.com/grpc/grpc-swift/issues/337.

## Having build problems?

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

`$ pod spec lint SwiftGRPC.podspec`

1. Update the Carthage Xcode project (diff will need to be checked in with the version bump):

`$ make project-carthage`

1. Bump the version in the `SwiftGRPC.podspec` file

1. Merge these changes, then create a new `Release` with corresponding `Tag`. Be sure to include a list of changes in the message
Expand Down
162 changes: 162 additions & 0 deletions Sources/SwiftGRPC/Core/ClientNetworkMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2019, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if os(iOS)
import CoreTelephony
import Dispatch
import SystemConfiguration

/// This class may be used to monitor changes on the device that can cause gRPC to silently disconnect (making
/// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as
/// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations
/// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular,
/// enabling/disabling airplane mode, etc.
/// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues
MrMage marked this conversation as resolved.
Show resolved Hide resolved
/// Original issue: https://github.com/grpc/grpc-swift/issues/337
open class ClientNetworkMonitor {
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
private let queue: DispatchQueue
private let callback: (State) -> Void
private var reachability: SCNetworkReachability?
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
private var appWasInForeground: Bool?
rebello95 marked this conversation as resolved.
Show resolved Hide resolved

/// Instance of network info being used for obtaining cellular technology names.
public private(set) lazy var cellularInfo = CTTelephonyNetworkInfo()
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
/// Whether the network is currently reachable. Backed by `SCNetworkReachability`.
public private(set) var isReachable: Bool?
MrMage marked this conversation as resolved.
Show resolved Hide resolved
/// Whether the device is currently using wifi (versus cellular).
public private(set) var isUsingWifi: Bool?
/// Name of the cellular technology being used (i.e., `CTRadioAccessTechnologyLTE`).
public private(set) var cellularName: String?

/// Represents a state of connectivity.
public struct State: Equatable {
/// The most recent change that was made to the state.
public let lastChange: Change
/// Whether this state is currently reachable/online.
public let isReachable: Bool
}

/// A change in network condition.
public enum Change: Equatable {
/// Reachability changed (online <> offline).
case reachability(isReachable: Bool)
/// The device switched from cellular to wifi.
case cellularToWifi
/// The device switched from wifi to cellular.
case wifiToCellular
/// The cellular technology changed (i.e., 3G <> LTE).
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
case cellularTechnology(technology: String)
}

/// Designated initializer for the network monitor.
///
/// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`.
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
/// Should always be used when accessing properties of this class.
/// - Parameter host: Host to use for monitoring reachability.
/// - Parameter callback: Closure to call whenever state changes.
public init(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) {
self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue")
self.callback = callback
self.startMonitoringReachability(host: host)
self.startMonitoringCellular()
}

deinit {
if let reachability = self.reachability {
SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetMain(),
CFRunLoopMode.commonModes.rawValue)
}
NotificationCenter.default.removeObserver(self)
}

// MARK: - Cellular

private func startMonitoringCellular() {
let notificationName: Notification.Name
if #available(iOS 13, *) {
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
notificationName = .CTServiceRadioAccessTechnologyDidChange
} else {
notificationName = .CTRadioAccessTechnologyDidChange
}

NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)),
name: notificationName, object: nil)
}

@objc
private func cellularDidChange(_ notification: NSNotification) {
self.queue.async {
let newCellularName: String?
if #available(iOS 13, *) {
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
let cellularKey = notification.object as? String
newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] }
} else {
newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology
}

if let newCellularName = newCellularName, self.cellularName != newCellularName {
self.cellularName = newCellularName
self.callback(State(lastChange: .cellularTechnology(technology: newCellularName),
isReachable: self.isReachable ?? false))
}
}
}

// MARK: - Reachability

private func startMonitoringReachability(host: String) {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else {
return
}

let info = Unmanaged.passUnretained(self).toOpaque()
var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil,
release: nil, copyDescription: nil)
let callback: SCNetworkReachabilityCallBack = { _, flags, info in
let observer = info.map { Unmanaged<ClientNetworkMonitor>.fromOpaque($0).takeUnretainedValue() }
observer?.reachabilityDidChange(with: flags)
}

SCNetworkReachabilitySetCallback(reachability, callback, &context)
SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(),
CFRunLoopMode.commonModes.rawValue)
self.queue.async { [weak self] in
self?.reachability = reachability

var flags = SCNetworkReachabilityFlags()
SCNetworkReachabilityGetFlags(reachability, &flags)
self?.reachabilityDidChange(with: flags)
}
}

private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) {
self.queue.async {
let isReachable = flags.contains(.reachable)
if let wasReachable = self.isReachable, wasReachable != isReachable {
self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable))
}
self.isReachable = isReachable

let isUsingWifi = !flags.contains(.isWWAN)
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
if let wasUsingWifi = self.isUsingWifi, wasUsingWifi != isUsingWifi {
self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular,
isReachable: self.isReachable ?? false))
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
}
self.isUsingWifi = isUsingWifi
}
}
}
#endif