How to convert that Python code code on Swift (socket client/server)

Hi!

I have that code wrote in Python:

CLIENT:
import socket

clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(('localhost', 8089))
clientsocket.send('hello')

SERVER:
import socket

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('192.168.0.132', 8089))
serversocket.listen(5) # become a server socket, maximum 5 connections

while True:
connection, address = serversocket.accept()
buf = connection.recv(64)
if len(buf) > 0:
print (buf)
break

This is a very, very simple client/server async communication.
Is possible to port that code on swift?
I haven't found any library or another easy way....

If you're looking to write for Apple platforms only, you should take a look at Network.framework. If you're looking to write for Linux or cross platform, then you could use SwiftNIO.

A pretty much equivalent (but improved in many ways) program can be written easily in SwiftNIO.

Client

import NIO

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let client = try ClientBootstrap(group: group)
    .connect(host: "localhost", port: 8089)
    .wait() // This makes it "blocking", ie. synchronous.

try client.writeAndFlush(ByteBuffer(string: "hello")).flatMap {
    client.close()
}.wait()

Server

final class PrintEverythingReceivedHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let buffer = self.unwrapInboundIn(data)
        
        print(String(buffer: buffer))
    }
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let server = try ServerBootstrap(group: group)
    // Sets the listen backlog to 5. Contrary to your comment this doesn't set a
    // maximum of 5 connections. I'd recommend to delete this line but I left it
    // because you have it
    .serverChannelOption(ChannelOptions.backlog, value: 5)

    // You don't have this in your example but I'd recommend it
    .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)

    // You only read a maximum of 64 bytes at time. I wouldn't recommend it,
    // I'd just delete this line. But again, you've got it :)
    .childChannelOption(ChannelOptions.recvAllocator, value: FixedSizeRecvByteBufferAllocator(capacity: 64))
    .childChannelInitializer { channel in
        channel.pipeline.addHandler(PrintEverythingReceivedHandler())
    }
    .bind(host: "localhost", port: 8089)
    .wait()
try server.closeFuture.wait()

Comments

Yes, the SwiftNIO program has more lines. And yes, SwiftNIO is overkill (and too much ceremony) for tiny network programs that just send the string "hello" over the network. SwiftNIO is built as an abstraction that lets you develop & compose real network programs. Also it's asynchronous (because it is supposed to scale to thousands of connections) which -- at the moment -- makes things more complicated.

Once async/await lands we (or anyone really) will be able to offer asynchronous and yet super easy APIs for toy programs like the one you suggest.

What I mean with network protocol composability is that SwiftNIO makes it easy to add other protocols in the pipeline such as TLS, HTTP1/2, WebSocket, SSH, ... . For example you really should use TLS these days and that can be done using only very slightly modified code (see below).

Also, I should note that on Apple platforms, you should not be using the BSD Socket API anymore. So your Python program (and the NIO program I posted above) would -- on Apple platforms -- use the deprecated APIs. With SwiftNIO however it's very easy to make SwiftNIO use Network.framework instead of the Sockets API by using SwiftNIOTransportServices. Code using "plain" SwiftNIO which runs on BSD Sockets or SwiftNIOTransportServices (which runs on Network.framework) is pretty much the same, you just need to swap out the ClientBootstrap for a NIOTSConnectionBootstrap and the ServerBootstrap for a NIOTSListenerBootstrap. Packages like GRPC Swift, AsyncHTTPClient, or the NIOSMTP example automatically pick the best backend, they will choose NIOTransportServices (Network.framework) for modern Apple OSes and BSD Sockets on Linux (and old Apple OSes).

Client

let tlsConfiguration = TLSConfiguration.forClient(/* optional config if you don't have real certs */)
let sslContext = try! NIOSSLContext(configuration: tlsConfiguration)

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let client = try ClientBootstrap(group: group)
    .channelInitializer { channel in
        // add TLS
        channel.pipeline.addHandler(try! NIOSSLClientHandler(context: sslContext, serverHostname: "localhost"))
    }
    .connect(host: "localhost", port: 8089)
    .wait()

try client.writeAndFlush(ByteBuffer(string: "hello")).flatMap {
    client.close()
}.wait()

Server

final class PrintEverythingReceivedHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let buffer = self.unwrapInboundIn(data)
        
        print(String(buffer: buffer))
    }
}
    
let certificateChain = try NIOSSLCertificate.fromPEMFile("/path/to/your/cert")
let sslContext = try! NIOSSLContext(configuration: TLSConfiguration.forServer(certificateChain: certificateChain.map { .certificate($0) },
                                    privateKey: .privateKey(.init(file: "/path/to/your/private/key",
                                                            format: .pem))))

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let server = try ServerBootstrap(group: group)
    .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
    .childChannelInitializer { channel in
        channel.pipeline.addHandlers(
            // add TLS
            NIOSSLServerHandler(context: sslContext),
            PrintEverythingReceivedHandler())
    }
    .bind(host: "localhost", port: 8089)
    .wait()
try server.closeFuture.wait()
5 Likes

Thanks a lot for the reply!

If I write the application for a command line execution works well, but if I try to write it for the UI won't work because when I use "try" Xcode says me "Errors thrown from here are not handled"

I've tried with "try!" and I can compile the program, but when I execute it, stop to work.

SERVER:

//
//  ViewController.swift
//  Server_NIO_UI
//
//  Created by Gian Luca on 26/03/21.
//

import Cocoa
import NIO

var received = ""

class ViewController: NSViewController {

@IBOutlet weak var Text1: NSTextField!

override func viewDidLoad() {
    super.viewDidLoad()
    

    // Do any additional setup after loading the view.
}

override var representedObject: Any? {
    didSet {
        
        final class PrintEverythingReceivedHandler: ChannelInboundHandler {
            typealias InboundIn = ByteBuffer
            
            func channelRead(context: ChannelHandlerContext, data: NIOAny) {
                let buffer = self.unwrapInboundIn(data)
                
               received = String(buffer: buffer)
                Text1.stringValue = received <- **Class declaration cannot close over value 'self' defined in outer scope**        }
        }

        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let server = try! ServerBootstrap(group: group)
            // Sets the listen backlog to 5. Contrary to your comment this doesn't set a
            // maximum of 5 connections. I'd recommend to delete this line but I left it
            // because you have it
            .serverChannelOption(ChannelOptions.backlog, value: 5)

            // You don't have this in your example but I'd recommend it
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)

            // You only read a maximum of 64 bytes at time. I wouldn't recommend it,
            // I'd just delete this line. But again, you've got it :)
            .childChannelOption(ChannelOptions.recvAllocator, value: FixedSizeRecvByteBufferAllocator(capacity: 64))
            .childChannelInitializer { channel in
                channel.pipeline.addHandler(PrintEverythingReceivedHandler())
            }
            .bind(host: "localhost", port: 8089)
            .wait()
        try! server.closeFuture.wait()
    // Update the view, if already loaded.
    }
}


}

CLIENT:

//
//  ViewController.swift
//  Client_NIO_UI
//
//  Created by Gian Luca on 26/03/21.
//

import Cocoa
import NIO

var stringa = ""

class ViewController: NSViewController {

@IBOutlet weak var Text1: NSTextField!
@IBOutlet weak var Text2: NSTextField!
override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
}

override var representedObject: Any? {        didSet {
    // Update the view, if already loaded.
    }
}


@IBAction func Button1(_ sender: Any) {

stringa = Text2.stringValue

    let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let client = try! ClientBootstrap(group: group)
    .connect(host: "localhost", port: 8089)
    .wait() // This makes it "blocking", ie. synchronous.

try! client.writeAndFlush(ByteBuffer(string: stringa)).flatMap {
    client.close()
}.wait()
    
}

    

    }

And another issue: in the server side, I receive the error "Class declaration cannot close over value 'self' defined in outer scope" what it means?

1 Like

Right, to run it inside a UI application you actually want to make it asynchronous and not block the main thread. You could have a look at the ViewController in NIOIMAP. Basically you want to remove the .wait() calls because they make the otherwise asynchronous SwiftNIO block.

I'll repeat that on Apple platforms you should be using SwiftNIO TransportServices or Network.framework directly and not the BSD Sockets API.

Full example code example (server & client in one) for iOS with NIOTS
import UIKit

import NIO
import NIOTransportServices

class ViewController: UIViewController {
    var group = NIOTSEventLoopGroup()
    var server: Channel?

    override func viewDidLoad() {
        super.viewDidLoad()

        self.runServer().whenSuccess {
            self.runClient()
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        try? self.server?.close().wait()
        try? self.group.syncShutdownGracefully()
    }

    func runClient() {
        NIOTSConnectionBootstrap(group: self.group)
            .connect(host: "localhost", port: 8089)
            .whenSuccess { client in
                client.writeAndFlush(ByteBuffer(string: "hello")).whenComplete { _ in
                    client.close(promise: nil)
                }
            }
    }

    func runServer() -> EventLoopFuture<Void> {
        final class PrintEverythingReceivedHandler: ChannelInboundHandler {
            typealias InboundIn = ByteBuffer

            func channelRead(context: ChannelHandlerContext, data: NIOAny) {
                let buffer = self.unwrapInboundIn(data)

                print(String(buffer: buffer))
            }
        }

        return NIOTSListenerBootstrap(group: self.group)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)

            .childChannelInitializer { channel in
                channel.pipeline.addHandler(PrintEverythingReceivedHandler())
            }
            .bind(host: "localhost", port: 8089)
            .map { channel in
                self.server = channel
            }
    }
}

Thanks again!

Last question: do you have the same example for MacOS instead IOS?

It's almost the same. If you let us know where you got stuck, we may be able to help.