mike-neckのブログ

Java or Groovy or Swift or Golang

Swift-NIO で http クライアントの書き方

Swift-NIO で http クライアントの書き方。

f:id:mike_neck:20180609045321p:plain

以前失敗してたけど、リベンジしたのでその記録。


以前失敗した記事

mike-neck.hatenadiary.com

Package.swift は以下の通り

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "HTTP-CLIENT",
    products: [
        .executable(name: "HTTP-CLIENT", targets: ["HTTP-CLIENT"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-nio.git", from: "1.7.2")
        ,.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "1.0.0")
    ],
    targets: [
        .target(
            name: "HTTP-CLIENT",
            dependencies: ["NIO", "NIOHTTP1", "NIOOpenSSL"]),
        .testTarget(
            name: "HTTP-CLIENTTests",
            dependencies: ["HTTP-CLIENT"]),
    ]
)

ハンドラーはごくごく簡単なもの

import NIO
import NIOHTTP1
import NIOOpenSSL

private final class HTTPResponseHandler: ChannelInboundHandler {

    let promise: EventLoopPromise<Void>

    init(_ promise: EventLoopPromise<Void>) {
        self.promise = promise
    }

    typealias InboundIn = HTTPClientResponsePart

    func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
        let httpResponsePart = unwrapInboundIn(data)
        switch httpResponsePart {
        case .head(let httpResponseHeader):
            print("\(httpResponseHeader.version) \(httpResponseHeader.status.code) \(httpResponseHeader.status.reasonPhrase)")
            for (name, value) in httpResponseHeader.headers {
                print("\(name): \(value)")
            }
        case .body(var byteBuffer):
            if let responseBody = byteBuffer.readString(length: byteBuffer.readableBytes) {
                print(responseBody)
            }
        case .end(_):
            break
        }
        _ = ctx.channel.close()
        promise.succeed(result: ())
    }

    func errorCaught(ctx: ChannelHandlerContext, error: Error) {
        print("Error: ", error)
        _ = ctx.channel.close()
        promise.succeed(result: ())
    }
}

EventLoopGroup と同期のために EventLoopPromise を作る(main.swift)

let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let promise: EventLoopPromise<Void> = eventLoopGroup.next().newPromise()
defer {
    try! promise.futureResult.wait()
    try! eventLoopGroup.syncShutdownGracefully()
}

次に OpenSSLClientHandler を作る(main.swift)

前回やったときは、 OpenSSLClientHandler のイニシャライザーに serverHostname を指定してないためにエラーが発生したようだった。

let tlsConfiguration = TLSConfiguration.forClient()
let sslContext = try! SSLContext(configuration: tlsConfiguration)
let openSslHandler = try! OpenSSLClientHandler(context: sslContext, serverHostname: "httpbin.org")

Bootstrap を組み立てる。(main.swift)

let bootstrap = ClientBootstrap(group: eventLoopGroup)
        .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
        .channelInitializer { channel in
            _ = channel.pipeline.add(handler: openSslHandler)
            _ = channel.pipeline.addHTTPClientHandlers()
            return channel.pipeline.add(handler: HTTPResponseHandler(promise))
        }

接続後にリクエストを書き込むための関数を作る。(main.swift)

func sendRequest(_ channel: Channel) {
    var request = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1), method: HTTPMethod.GET, uri: "https://httpbin.org/get?query=param")
    request.headers = HTTPHeaders([
        ("Host", "httpbin.org"),
        ("User-Agent", "swift-nio"),
        ("Accept", "application/json")
    ])
    _ = channel.write(HTTPClientRequestPart.head(request))
    _ = channel.writeAndFlush(HTTPClientRequestPart.end(nil))
}

最後にサーバーにつないで、リクエストを送信します(main.swift)

bootstrap.connect(host: "httpbin.org", port: 443)
        .whenSuccess { sendRequest($0) }

すると、このような感じで表示されるはず…

HTTP/1.1 200 OK
Connection: keep-alive
Server: gunicorn/19.9.0
Date: Sat, 01 Sep 2018 14:18:51 GMT
Content-Type: application/json
Content-Length: 260
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
{
  "args": {
    "query": "param"
  }, 
  "headers": {
    "Accept": "application/json", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "swift-nio"
  }, 
  "origin": "192.168.1.2", 
  "url": "https://httpbin.org/get?query=param"
}