Real-time communication
swift iOS socket networking CFSocket CFNetwork Network Stream Estimated reading time: 14 minutesNetworking, I guess, is one of the features that is used almost in every app. There are a lot of approaches to how we can use some remote resources using a network.
Various architecture styles like (REST or SOAP), secured/non-secured options in combination with different software structures and protocols provide for us a lot of options.
I already wrote a few articles about networking like this one
In most projects we use REST - old, good, cheap, and easy-to-use approach.
REST works almost for everything, except for things where its not work :].
I mean in something that needs a real-time component: games, chats, state observation between a few independent parts - cases when we wait for an event from server-side in other words, and other similar functionality.
the WebSocket Protocol
If u wondering what is a socket, then, we can refer to official doc:
The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code. The security model used for this is the origin-based security model commonly used by web browsers. The protocol consists of an opening handshake followed by basic message framing, layered over TCP. The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that do not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or <iframe>s and long polling).
There are a lot of interesting moments related to how this protocol works with details, so if u want to know them all - check that link.
As always, one image is better than 1000 words:
I grab this image from the perfect guide available here
On iOS we can work with sockets in a lot ways:
Let’s review them in a bit more detail.
URLSession
URLSession
well-known for years (starting from iOS 7). We use it wisely and it’s one of the most known types for iOS developers.
But only starting from iOS 13 do we get URLSessionWebSocketTask
. This is the simplest way to use sockets. Simplicity - is a key for this approach.
We can use URLSessionWebSocketTask
for communication using ws:
or wss:
protocol.
“the WebSocket protocol specification defines ws (WebSocket) and wss (WebSocket Secure) as two new uniform resource identifier (URI) schemes that are used for unencrypted and encrypted connections respectively.” - wiki
Another good explanation for ws and wss available on SO
The API is very simple and requires only to create a task, ask to send/receive, and use ping/pong to keep the connection active.
One strange moment in API - after each message u should call
receive
- to read frames again.
We can easily combine this in some modules. And with Combine
it can be even easier to use.
Here is the option how it can be done.
import Foundation
import Combine
final public class SocketConnection: NSObject {
public enum Failure: Error {
case invalidURL
}
public enum ConnectionState {
case opened
case closed
case failed(Error)
}
private let baseURL: String
private var session: URLSession!
private var webSocketTask: URLSessionWebSocketTask!
private var connectionState: PassthroughSubject<ConnectionState, Never> = .init()
private var listenerState: PassthroughSubject<Result<URLSessionWebSocketTask.Message, Error>, Never> = .init()
private var senderState: PassthroughSubject<Result<Void, Error>, Never> = .init()
private var pingToken: AnyCancellable?
public var connectionPipe: AnyPublisher<ConnectionState, Never> {
connectionState
.eraseToAnyPublisher()
}
public var listenPipe: AnyPublisher<Result<URLSessionWebSocketTask.Message, Error>, Never> {
listenerState
.eraseToAnyPublisher()
}
public var senderPipe: AnyPublisher<Result<Void, Error>, Never> {
senderState
.eraseToAnyPublisher()
}
// MARK: - Lifecycle
public init(baseURL: String) throws {
if let url = URL(string: baseURL) {
self.baseURL = baseURL
super.init()
session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: OperationQueue()
)
webSocketTask = session.webSocketTask(with: url)
} else {
throw Self.Failure.invalidURL
}
}
public func connect() {
webSocketTask.resume()
startPing()
listen()
}
public func disconnect() {
pingToken?.cancel()
pingToken = nil
webSocketTask.cancel(with: .goingAway, reason: nil)
}
public func send(text: String) {
send(message: URLSessionWebSocketTask.Message.string(text))
}
public func send(data: Data) {
send(message: URLSessionWebSocketTask.Message.data(data))
}
// MARK: - Private
private func startPing() {
pingToken = Timer.publish(every: 5, on: .main, in: .common)
.autoconnect()
.flatMap { _ in
self.ping()
}
.mapError { [weak self] error -> Error in
// side effect
self?.pingToken?.cancel()
self?.connectionState.send(.failed(error))
return error
}
.replaceError(with: ())
.sink { _ in }
}
private func ping() -> AnyPublisher<Void, Error> {
return Deferred {
Future { [weak self] promise in
self?.webSocketTask.sendPing { err in
if let error = err {
promise(.failure(error))
} else {
promise(.success(()))
}
}
}
}
.eraseToAnyPublisher()
}
private func send(message: URLSessionWebSocketTask.Message) {
webSocketTask.send(message) { [weak self] error in
if let error = error {
self?.senderState.send(.failure(error))
} else {
self?.senderState.send(.success(()))
}
}
}
private func listen() {
webSocketTask.receive { [weak self] result in
switch result {
case .success(let message):
self?.listenerState.send(.success(message))
case .failure(let error):
self?.listenerState.send(.failure(error))
}
// should re-ask to receive a message after receiving data
self?.listen()
}
}
}
extension SocketConnection: URLSessionWebSocketDelegate {
// MARK: - URLSessionWebSocketDelegate
public func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
connectionState.send(.opened)
}
public func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
connectionState.send(.closed)
}
}
socket = try? .init(baseURL: "wss://yourAddress")
socket?.listenPipe
.sink(receiveValue: { value in
switch value {
case .success(let message):
print(message)
case .failure(let error):
print(error)
}
})
.store(in: &tokens)
socket?.connectionPipe
.sink(receiveValue: { value in
print(value)
})
.store(in: &tokens)
// to start communication
socket?.connect()
// to end communication
socket?.disconnect()
Network
framework
URLSession
that I mentioned is built on top of this framework.
Use this framework when you need direct access to protocols like TLS, TCP, and UDP for your custom application protocols. Continue to use URLSession, which is built upon this framework, for loading HTTP- and URL-based resources. - as mentioned in official doc
So, we can use this framework to perform socket communication.
As usual, the most interesting stuff from Apple is without a piece of documentation.
With the Network
framework u get additional options such as:
- configure additional params for connections (for example auto-reply for ping or message size, additional headers, etc)
- use IP address or URL (and control additional params)
- monitor viability
- monitor better path for u’r connection (
WebSocketConnection
) - better error description with Posix err codes
The downside - is that u need to write a bit more code.
ping-pong dance is still needed to be done
Here is a minimal implementation of sockets using Network
import Foundation
import Network
final class SocketCommunicator {
public enum Failure: Error {
case invalidURL
}
private let queue = DispatchQueue(label: "com.socket-kh-example.tcp")
private let baseURL: String
private let connection: NWConnection
private var pingPongTimer: Timer?
// MARK: - Lifecycle
public init(baseURL: String) throws {
if let url = URL(string: baseURL) {
self.baseURL = baseURL
// ws - tcp; wss - tls
let parameters = NWParameters.tls
let options = NWProtocolWebSocket.Options()
options.autoReplyPing = true
parameters.defaultProtocolStack.applicationProtocols.insert(options, at: 0)
connection = NWConnection(to: .url(url), using: parameters)
} else {
throw Self.Failure.invalidURL
}
}
public func connect() {
connection.stateUpdateHandler = { state in
print(state)
if state == .ready {
self.listen()
self.ping(interval: 5)
}
}
connection.start(queue: queue)
}
public func send(string: String) {
if let data = string.data(using: .utf8) {
let metadata = NWProtocolWebSocket.Metadata(opcode: .text)
let context = NWConnection.ContentContext(
identifier: "youIDforText",
metadata: [metadata]
)
send(data: data, context: context)
}
}
public func send(data: Data) {
let metadata = NWProtocolWebSocket.Metadata(opcode: .binary)
let context = NWConnection.ContentContext(
identifier: "youIDforBinary",
metadata: [metadata]
)
send(data: data, context: context)
}
public func ping(interval: TimeInterval) {
DispatchQueue.main.async { [weak self] in
self?.pingPongTimer = .scheduledTimer(
withTimeInterval: interval,
repeats: true
) { [weak self] _ in
self?.ping()
}
}
}
func disconnect() {
connection.cancel()
// u can also use
// NWProtocolWebSocket.Metadata(opcode: .close) in message
pingPongTimer?.invalidate()
pingPongTimer = nil
}
// MARK: - Private
private func listen() {
connection.receiveMessage { (data, context, isComplete, error) in
if let error = error {
// handle error
} else {
if isComplete,
let data = data,
let metadata = context?.protocolMetadata.first as? NWProtocolWebSocket.Metadata {
switch metadata.opcode {
case .binary:
// handle this
break
case .cont:
break
case .text:
if let string = String(data: data, encoding: .utf8) {
print(string)
}
case .close:
break
case .ping:
print("ping")
case .pong:
print("pong")
@unknown default:
fatalError("unhandled")
}
}
self.listen()
}
}
}
private func ping() {
let metadata = NWProtocolWebSocket.Metadata(opcode: .ping)
metadata.setPongHandler(queue) { [weak self] error in
// handle
}
let context = NWConnection.ContentContext(
identifier: "pingID",
metadata: [metadata]
)
send(data: "ping".data(using: .utf8), context: context)
}
private func send(
data: Data?,
context: NWConnection.ContentContext
) {
connection.send(
content: data,
contentContext: context,
isComplete: true,
completion: .contentProcessed { [weak self] error in
if let socketMetadata = context.protocolMetadata.first as? NWProtocolWebSocket.Metadata,
socketMetadata.opcode == .close {
// handle case when u close connection
}
if let error = error {
// handle error
}
}
)
}
}
NSStream
Stream
is a class that represents streams in cocoa .] .
Stream objects provide an easy way to read and write data to and from a variety of media in a device-independent way. You can create stream objects for data located in memory, in a file, or on a network (using sockets), and you can use stream objects without loading all of the data into memory at once.
Stream - is just a potentially never-ending data flow.
The last time I used stream - it was, maybe, 3 or 4 years ago. What does this mean? There are a lot of wrappers for this because this is a low-level Unix feature. This means, that usage is not so obvious sometimes and it’s an error-prone process.
Anyway, we still can use it for socket-base communication.
Here is a Stream example
extension Stream {
static func streamPairToHost(
host: String,
port: Int
) -> (
inputStream: InputStream,
outputStream: OutputStream
) {
var inStream: InputStream? = nil
var outStream: OutputStream? = nil
Stream.getStreamsToHost(
withName: host,
port: port,
inputStream: &inStream,
outputStream: &outStream
)
return (inStream, outStream)
}
}
// configure streams and it's delegate
let streams = Stream.streamPairToHost(host: "192.168.4.1", port: 80)
inputStream = streams.0
outputStream = streams.1
inputStream?.delegate = self
outputStream?.delegate = self
inputStream?.schedule(in: .current, forMode: .common)
outputStream?.schedule(in: .current, forMode: .common)
inputStream?.open()
outputStream?.open()
// adopt [`StreamDelegate`](https://developer.apple.com/documentation/foundation/streamdelegate)
extension StreamSocketHandle: StreamDelegate {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case .hasBytesAvailable:
if aStream == inputStream {
var dataBuffer = Array<UInt8>(repeating: 0, count: 1024)
var len: Int
while inputStream?.hasBytesAvailable == true {
if length = inputStream?
.read(&dataBuffer, maxLength: 1024),
length > 0 {
let output = String(bytes: dataBuffer, encoding: .ascii)
}
}
}
case .endEncountered:
stopSession()
case .errorOccurred:
capture("ERR: \(String(describing: aStream.streamError?.localizedDescription))")
case .hasSpaceAvailable:
break
case .openCompleted:
if aStream.streamStatus == .open {
// ready to communicate
}
default:
break
}
}
}
// to send something
func sendMessage(_ string: String) {
if let dataToSend = string.data(using: .utf8),
let outputStream = outputStream {
_ = dataToSend.withUnsafeBytes({
if let pointer = $0.baseAddress?
.assumingMemoryBound(to: UInt8.self) {
let bytesWritten = outputStream.write(pointer, maxLength: dataToSend.count)
let isSuccess = bytesWritten > 0
if !isSuccess {
// handle error
}
}
})
} else {
// handle error
}
}
// to close session - clean up everything
func stopSession() {
inputStream?.close()
outputStream?.close()
inputStream?.remove(from: .current, forMode: .common)
outputStream?.remove(from: .current, forMode: .common)
inputStream?.delegate = nil
outputStream?.delegate = nil
}
This code is not a drag and use, I grabbed different parts from some project, where I connected to the local device in network - that was and openWRT system with custom PCB for some specific purpose.
If u want to know more about Stream, I recommend reading docs from Apple and a part about socket connection
CFNetwork
Why use CFNetwork
if we have a lot of ways to do socket communication? The only reason is only if u need to support some protocols that are not supported by more high-level API (like URLSession
).
With CFNetwork
we can use functions like CFStreamCreatePairWithSocketToHost
in combinations with streams:
var inStreamUnmanaged: Unmanaged<CFReadStream>?
var outStreamUnmanaged: Unmanaged<CFWriteStream>?
CFStreamCreatePairWithSocketToHost(
nil,
"192.168.1.168",
UInt32(40),
&inStreamUnmanaged,
&outStreamUnmanaged
)
inputStream = inStreamUnmanaged?.takeRetainedValue()
outputStream = outStreamUnmanaged?.takeRetainedValue()
if u check the official doc - this function already deprecated due to reasons described a bit earlier above
The code with example contains comments-explanation of how to perform connection to specific host with sockets and send message using CFNetwork
.
CFNetwork CFSocket example
// original host
let host = "127.0.0.1"
let port = 1234
// IPv4 addresses is a uint32_t, convert a string representation
// of the octets to the appropriate value
let inAddr = inet_addr(host)
if inAddr == INADDR_NONE {
// handle invalid addr
}
// create a socket ref
let socket: CFSocket = CFSocketCreate(
// This is a synonym for NULL
kCFAllocatorDefault,
// an address family that is used to designate the type
// of addresses that your socket can communicate with
// AF_INET -> Internet Protocol v4 addresses
AF_INET,
// reliable stream-oriented service or Stream Sockets
SOCK_STREAM,
// specifying the actual transport protocol to use
// These protocols are specified in file netinet/in.h.
// The value 0 may be used to select a default protocol from
// the selected domain and type.
// IPPROTO_TCP - > TCP
IPPROTO_TCP,
// callback
CFSocketCallBackType.readCallBack.rawValue,
{ (socket, callBackType, address, data, info) in
// handle callback
},
// context
nil
)
if socket == nil {
// socket not created
}
// the basic structures for all syscalls and functions that
// deal with internet addresses
/*
struct sockaddr_in {
short sin_family; // e.g. AF_INET
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // zero this if you want to
};
*/
// more -> https://www.gta.ufrj.br/ensino/eel878/sockets/sockaddr_inman.html
var sin = sockaddr_in()
// The length member, sin_len, was added with 4.3BSD-Reno,
// when support for the OSI protocols was added
// more -> https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch03lev1sec2.html
sin.sin_len = __uint8_t(MemoryLayout.size(ofValue: sin))
// Internet Protocol version 4 (IPv4)
sin.sin_family = sa_family_t(AF_INET)
// host byte order save way
sin.sin_port = UInt16(port).bigEndian
sin.sin_addr.s_addr = inAddr
// create addr to connect as CFData
let addressDataCF = Data(bytes: &sin, count: MemoryLayout.size(ofValue: sin)) as CFData
// connect
let timeInterval = 5
let socketErr = CFSocketConnectToAddress(
socket,
addressDataCF,
CFTimeInterval(timeInterval)
)
print(socketErr)
switch socketErr {
case .success:
print("success")
// send test message
let addr_data: CFData = addressDataCF
let string = "Hello, world! - form the test app"
let msg_data: CFData = string.data(using: .utf8)! as CFData
let socketErr: CFSocketError = CFSocketSendData(
socket,
addr_data,
msg_data,
0
)
print("message send \(socketErr)")
case .error:
break
// handle err
case .timeout:
break
// handle timeout
}
Testing
To test sockets we need some source that provides sockets for us.
The easiest way - is to create a server and use it on localhost.
To do so, we can use javaScript:
const net = require('net');
const server = net.createServer((socket) => {
socket.on('data', (data) => {
console.log(data.toString());
});
}).on('error', (err) => {
console.error(err);
});
// Open server on port 1234, localhost - 127.0.0.1
server.listen(1234, () => {
console.log('opened server on', server.address().port);
});
then, just run using node:
node server.js
With latest example that use CFNetwork
u can get something like this:
3rd party
There are a lot of 3rd party libs that provide similar functionality to us, but all of them (or at least 99% of them) under the hood use one of the core components.
The example, that I use some time ago was a SocketRocket - they use Streams under the hood.
Another one may be BlueSocket, under the hood - CFNetwork
.
And so on.
Resources
- REST
- SOAP
- RFC 6455 - The WebSocket Protocol
URLSession
- WIKI - WebSocket
- WIKI - Network Socket
Network
frameworkNSStream
(Stream
)CFNetwork
framework- Unix Socket faq
- AF - discussion
- CFLocalServer
- UDPEcho
- Low level socket programming in POSIX and Core Foundation
- Socket address structure
- WIKI - Barkeley sockets
- SO - What is SOCK_DGRAM and SOCK_STREAM?
- SO - Are sockaddr_in and sockaddr_in6 still using sin_len and sin6_len?
- SO -
ws:
vswss:
- Socket programming
- WIKI - WebSocket
Share on: