Swift

SwiftでTCPサーバーを作ってみる

More than 3 years have passed since last update.

Swiftでは,Cの構造体でさえもExtensionでどんどん拡張できてしまうのは愉快痛快ですね.
C言語でのソケット関連のお約束をSwiftで拡張して使いやすくしてみようという試みです.

Cの,というかソケットAPIでは,

struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_len = sizeof(sin);
    sin.sin_family = AF_INET;
    sin.sin_port = htons(0);
    sin.sin_addr.s_addr= INADDR_ANY;

bind(sock, (struct sockaddr *)&sin, sizeof(sin))
...

という感じでsockaddr_inからsockaddrへキャストするのがお約束.swiftではこれをストレートに表現できないので,Extensionを使って直感的キャスト(コピー)できるようにしてみる.
swiftでは,Cのsockaddr_in, sockaddr構造体はそれぞれ次のようにコンバートされて定義されている.

struct sockaddr_in {
    var sin_len: __uint8_t
    var sin_family: sa_family_t
    var sin_port: in_port_t
    var sin_addr: in_addr
    var sin_zero: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
}

struct sockaddr {
    var sa_len: __uint8_t
    var sa_family: sa_family_t
    var sa_data: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
}

これらを眺めつつ,Extension使ってsockaddr_in/sockaddrの初期化処理を追加.引数にsockaddr_in/sockaddrタイプ変数を与えて初期化,つまり相互変換できるようにする.

extension sockaddr {
    init(_ saddr_in: sockaddr_in) { // cast(copy) from sockaddr_in
        self = saddr_in.copyAsSockAddr()
    }

    func copyAsSockAddrIn() -> sockaddr_in {
        let len = UInt8(sizeof(sockaddr_in))
        let family = self.sa_family
        let (hi, lo) = (self.sa_data.0, self.sa_data.1)
        let port = (hi.asUInt16 << 8) | lo.asUInt16       // what's asUInt16?
        let b = (UInt32(self.sa_data.2), UInt32(self.sa_data.3),
                 UInt32(self.sa_data.4), UInt32(self.sa_data.5))
        let sadr =  b.0 << 24 | b.1 << 16 | b.2 << 8 | b.3
        let addr = in_addr(s_addr: sadr)

        return sockaddr_in(sin_len: len, sin_family: family, sin_port: port,
                    sin_addr: addr, sin_zero: (0,0,0,0,0,0,0,0))
    }

    func unsafeCopyAsSockAddrIn() -> sockaddr_in {
        return unsafeBitCast(self, sockaddr_in.self)
    }
}

   (unsafeBitCastを使ってしまえば一発で可能みたいだけど,ここでは生々しくやっていきたい.)

struct sockaddr_inも同様な感じにExtensionして,

extension sockaddr_in {
    init(_ saddr: sockaddr) {
        self = saddr.copyAsSockAddrIn()
    }

    func copyAsSockAddr() -> sockaddr {
        let len = self.sin_len
        let family = self.sin_family
        let port = self.sin_port.bigEndian
        let (hi, lo) = ((port >> 8).asInt8, (port & 0x00ff).asInt8)
        let sadr: UInt32 = self.sin_addr.s_addr.bigEndian
        let b = (Int8( sadr >> 24        ),
                 Int8((sadr >> 16) & 0xff),
                 Int8((sadr >>  8) & 0xff),
                 Int8( sadr        & 0xff))
        let z: Int8 = 0
        let data = (hi, lo, b.0, b.1, b.2, b.3, z,z,z,z,z,z,z,z)
        return sockaddr(sa_len: len, sa_family: family, sa_data: data)
    }
}

ところで,copyAsSockAddrInメソッド内のhi, lo(Int8タイプ)変数のところのasInt8とかasUInt16なんていうプロパティは整数タイプにありましたっけ?
これも次のようにExtensionしちゃってます.これ,符号なし16ビット整数を符号付8ビット整数に強引に変換するのに必要なんですね.UInt16(Int8(-128))なんてことをやるとコンパイラに怒られてしまうので.

extension Int8 {
    var asUInt16: UInt16 {
        return UInt16(Int16(self) & 0xff)
    }
}

extension UInt16 {
    var asInt8: Int8 {
        let v = self & 0xff
        return Int8(v >> 4) << 4 | Int8(v & 0xf)
    }
}

 
さて,以上で準備は終わり.
swiftでソケットAPIを呼んでみましょう.(長ったらしいのでエラー処理は省略.)

class EchoServer {
    ...
    func start(port: UInt16) {
        serverSocket = socket(AF_INET, SOCK_STREAM, 0)
        ...
        var addrIn = sockaddr_in(port: port)
        var len = socklen_t(addrIn.sin_len)
        var addr = sockaddr(addrIn)

        bind(serverSocket, &addr, len)
        listen(serverSocket, 4)

        let src = my_create_disp_src(serverSocket)
        dispatch_source_set_event_handler(src, acceptClient)
        dispatch_resume(src)
    }

 
ほとんどC言語のお約束と同様にいけますね.listenした後は,仕事をGCDに丸投げします.

ひとつ残念なのが,現状(XCode6 Beta5)では,swiftからGCDのdispatch_source_create()の直接呼び出しがうまく機能せず,正しく機能させるためにC言語のブリッジを使わざるを得ないことです.その関数がmy_create_disp_src()でこんなふうに定義しています.

dispatch_source_t my_create_disp_src(int sock) {
    return dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                           sock, 0, dispatch_get_global_queue(0, 0));
}

 
次は,dispatch_source_set_event_handlerに渡すハンドラ(acceptClient)の定義です.

    func acceptClient() {
        var addrIn = sockaddr_in()
        var addr = sockaddr(addrIn)
        var len = socklen_t(addrIn.sin_len)

        let sock = accept(serverSocket, &addr, &len)

        addrIn = sockaddr_in(addr) // copy back
        let ipStr = addrIn.addressString
        let port = addrIn.sin_port
        println("Accepted client(=\(ipStr):\(port))")

        var csrc = my_create_disp_src(sock)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
        clientsAccepted[sock] = csrc
        dispatch_semaphore_signal(semaphore)

        dispatch_source_set_event_handler(csrc, readAndEchoBack(sock))
        dispatch_resume(csrc)
    }

acceptもほとんどお約束通りいけますね.
あと,inet_ntoa()なんか使わず,sockaddr_inにExtensionしてaddressStringプロパティを追加しました.addressStringでIPアドレスの文字列表現を取れるようにしてます.なるべくピュアswiftで行きたいものでして.

お次は,接続してきたクライアントに対するイベント処理です.イベントハンドラ登録関数(dispatch_source_set_event_handler)に渡すハンドラ関数(クロージャ)はタイプが無引数無戻値な'() -> ()'でないとダメです.ですが,クライアントからのread/へのwriteや,切断時の後始末のためにクライアントのソケットが必要で,どうしても整数引数を渡さないといけません.
そこで関数の部分適用の出番です.
これがサラッと書ける言語はほんと楽でいいですねー.
つまり,"(Int) -> () -> ()"な関数を作ってしまえばいいのでした.
上記コードのreadAndEchoBack(sock)に注目.これ関数コールしてますが戻り値は"() -> ()"です.
その実装は,

    func readAndEchoBack(sock: Int32) -> () -> () {
        let client = sock
        return {
            () -> () in dispatch_async(GlobalQueue) {
//                [unowned self] in    // <=== Linkage error in Xcode6 Beta5
                let bufSize = 64
                var buf = [UInt8](count: bufSize, repeatedValue: 0)
                let size = read(client, &buf, UInt(bufSize))

                if size > 0 {
                    write(client, &buf, UInt(size))
                } else if size == 0 {
                    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)

                    var csrc = self.clientsAccepted.removeValueForKey(client)
                    if let src = csrc {
                        dispatch_source_cancel(src)
                        dispatch_release(src)
                        close(client)
                    }

                    dispatch_semaphore_signal(self.semaphore)
                } else {/* size == -1 somethig wrong happned...*/}
            }
        }
    }

現状(XCode6 Beta5),"[unowned self]"を付けると何故かリンク時にエラーになってしまいます. (循環参照していないのでキャプチャリストは必要ありません.)

  
以上で,swiftでもそこそこ簡単に生ソケットAPI使ってTCPサーバーを実装できることが分かりました.SwiftでC言語用APIをサラッと使えるのはとても便利ですね.
(一部残念ですが,swift環境はまだベータ版,というかむしろアルファ版ですからね.我慢しましょう.)

Gistにソースを置きました.
Extensions.swift
EchoServer.swift