Skip to content

Instantly share code, notes, and snippets.

@op183
Last active May 14, 2017 20:20
Show Gist options
  • Save op183/0f466bfaa195ccb0a9e73d5d453a0a32 to your computer and use it in GitHub Desktop.
Save op183/0f466bfaa195ccb0a9e73d5d453a0a32 to your computer and use it in GitHub Desktop.
Multi-Client EchoServer written in pure Swift
//
// main.swift
// echoCLI
//
// Created by Ivo Vacek on 13/05/2017.
// Copyright © 2017 Ivo Vacek. All rights reserved.
//
// Minimalistic Multi-Client EchoServer written in Swift
//
import Darwin
import Dispatch
class EchoServer {
// syncQueue help us print and read/write safely from our internal storage
// while running, the main queue is blocking with readLine()
private let syncQueue = DispatchQueue(label: "syncQueue")
// to be able maintain curren status of all DispatchSources
// we store all the information here
var serverSources:[Int32:DispatchSourceRead] = [:]
var clientSources:[Int32:DispatchSourceRead] = [:]
deinit {
// first stop the server !!
stop()
print("Echo Server deinit")
}
func start() {
// create buffer for temporary c strings
var temp = [CChar](repeating: 0, count: 255)
// host name
gethostname(&temp, temp.count)
// create addrinfo based on hints
// if host name is nil or "" we can connect on localhost
// if host name is specified ( like "computer.domain" ... "My-MacBook.local" )
// than localhost is not aviable.
// if port is 0, bind will assign some free port for us
var port: UInt16 = 0
let hosts = ["localhost", String(cString: temp)]
var hints = addrinfo()
hints.ai_flags = AI_PASSIVE | AI_CANONNAME
hints.ai_family = PF_UNSPEC
hints.ai_socktype = SOCK_STREAM
for host in hosts {
print("\t\(host)")
print()
// retrieve the info
// getaddrinfo will allocate the memory, we are responsible to free it!
var info: UnsafeMutablePointer<addrinfo>?
defer {
if info != nil
{
freeaddrinfo(info)
}
}
let status: Int32 = getaddrinfo(host, String(port), nil, &info)
guard status == 0 else {
print(errno, String(cString: gai_strerror(errno)))
return
}
var p = info
var serverSocket: Int32 = 0
var i = 0
var ipFamily = ""
// for each address avaiable
while p != nil {
i += 1
// use local copy of info
var _info = p!.pointee
p = _info.ai_next
// (1) create server socket
serverSocket = socket(_info.ai_family, _info.ai_socktype, _info.ai_protocol)
if serverSocket < 0 {
continue
}
// set port is tricky, because we need to remap ai_addr differently
// for inet and for inet6 family
switch _info.ai_family {
case PF_INET:
_info.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1, { p in
p.pointee.sin_port = port.bigEndian
})
case PF_INET6:
_info.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1, { p in
p.pointee.sin6_port = port.bigEndian
})
default:
continue
}
// (2) bind
//
// associates a socket with a socket address structure, i.e. a specified local port number and IP address
// if port is set to 0, bind will set first free port for us and update
if bind(serverSocket, _info.ai_addr, _info.ai_addrlen) < 0 {
close(serverSocket)
continue
}
// (3) we need to know an actual address and port number after bind
if getsockname(serverSocket, _info.ai_addr, &_info.ai_addrlen) < 0 {
close(serverSocket)
continue
}
// (4) retrieve the address and port from updated _info
switch _info.ai_family {
case PF_INET:
_info.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1, { p in
inet_ntop(AF_INET, &p.pointee.sin_addr, &temp, socklen_t(temp.count))
ipFamily = "IPv4"
port = p.pointee.sin_port.bigEndian
})
case PF_INET6:
_info.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1, { p in
inet_ntop(AF_INET6, &p.pointee.sin6_addr, &temp, socklen_t(temp.count))
ipFamily = "IPv6"
port = p.pointee.sin6_port.bigEndian
})
default:
break
}
// (5) bind is OK and port is set, we can start listen
if listen(serverSocket, 5) < 0 {
close(serverSocket)
continue
}
print("\tsocket \(serverSocket)\t\(ipFamily)\t\(String(cString: temp))/\(port)")
// (6) accept remote connection
// by installing event handler for listenning socket
let serverSource = DispatchSource.makeReadSource(fileDescriptor: serverSocket)//, queue: echoQueue)
serverSource.setEventHandler { [weak self] in
let s = Int32(serverSource.handle)
var len = socklen_t(MemoryLayout<sockaddr_storage>.size)
let _pca = UnsafeMutablePointer<sockaddr_storage>.allocate(capacity: 1)
defer {
_pca.deallocate(capacity: 1)
}
// accept connecion and redirect it to clientSocket
let clientSocket = _pca.withMemoryRebound(to: sockaddr.self, capacity: 1, { (pca) -> Int32 in
let sock = accept(s, pca, &len)
return sock
})
if clientSocket < 0 {
return
}
// get string representation of peer's address
var clientPort: UInt16 = 0
switch Int32(_pca.pointee.ss_family) {
case AF_INET:
_pca.withMemoryRebound(to: sockaddr_in.self, capacity: 1, { p in
inet_ntop(AF_INET, &p.pointee.sin_addr, &temp, socklen_t(temp.count))
clientPort = p.pointee.sin_port.bigEndian
})
case AF_INET6:
_pca.withMemoryRebound(to: sockaddr_in6.self, capacity: 1, { p in
inet_ntop(AF_INET6, &p.pointee.sin6_addr, &temp, socklen_t(temp.count))
clientPort = p.pointee.sin6_port.bigEndian
})
default:
break
}
self?.syncQueue.async {
print(clientSocket,"\t\(String(cString: temp))/\(clientPort)")
}
// disable SIGPIPE
// if we send some bytes to broken pipe, the send wil generate an error
var flag: Int32 = 1
setsockopt(clientSocket, SOL_SOCKET, SO_NOSIGPIPE, &flag, socklen_t(MemoryLayout.size(ofValue: flag)))
// (7) to be able to echo receiving data, we need install event hadler
// for reading data from remote side of clientSocket
let clientSource = DispatchSource.makeReadSource(fileDescriptor: clientSocket)//, queue: self?.echoQueue)
self?.syncQueue.sync {
self?.clientSources[clientSocket] = clientSource
}
clientSource.setEventHandler { [weak self] in
let c = Int32(clientSource.handle)
// we will consume all data in max 64 byte long chunks
var buffer = [Int8](repeating: 0, count: 64)
let received = read(c, &buffer, buffer.count)
if received < 1 {
// peer close the connection
close(c)
clientSource.cancel()
self?.syncQueue.sync {
print(c,"\tclosed")
self?.clientSources[clientSocket] = nil
}
return
}
// send all data
var totalSended = 0
repeat {
let sended = send(c, &buffer + totalSended, received - totalSended, 0)
if sended < 0 {
// no able to send, for any reason
// will close the connection and cancel this handler
close(c)
clientSource.cancel()
self?.syncQueue.sync {
print(c, "\tclosed")
self?.clientSources[clientSocket] = nil
}
return
}
totalSended += sended
} while totalSended < received
}
clientSource.resume()
}
serverSources[serverSocket] = serverSource
serverSource.resume()
}
print()
}
}
func stop() {
for (socket, source) in serverSources {
source.cancel()
close(socket)
print(socket,"\tclosed")
}
serverSources.removeAll()
syncQueue.sync {
for (socket, source) in clientSources {
source.cancel()
close(socket)
print(socket,"\tclosed")
}
clientSources.removeAll()
}
}
}
print()
print("Hello, World! EchoServer is listenig ...")
print("Connect any number of clients /telnet, nc, .../ to any of:")
print()
var echoServis: EchoServer?
echoServis = EchoServer()
echoServis?.start()
print("Press CTRL+D to exit")
print()
defer {
echoServis = nil
print("By by, World!")
}
while let input = readLine(){}
@op183
Copy link
Author

op183 commented May 14, 2017

echocli

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment