48
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-08-07

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

48
52
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?