Networking, 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:

TCPsockets.jpg



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)
  }
}
To use - init with address, and call needed functions:
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.

from official doc.

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:

result.png



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