Last active
May 14, 2017 20:20
-
-
Save op183/0f466bfaa195ccb0a9e73d5d453a0a32 to your computer and use it in GitHub Desktop.
Multi-Client EchoServer written in pure Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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(){} | |
Author
op183
commented
May 14, 2017
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment