Skip to content
Permalink
Browse files

Add a HTTP1 client example. It is only a simple echo which closely re…

…sembles the TCP and UDP echo examples. (#941)

Motivation:
To provide a simple example of the HTTP1 client pipeline.
To create a comparable example to the TCP and UDP demos to show what extra steps are needed for a HTTP layer.

Modifications:
Adds a NIOHTTP1Client example in a main file.
Adds a readme to the NIOHTTP1Client example.

Result:
There is now a simple example of how to send a HTTP1 client request to the server.
  • Loading branch information...
tigerpixel authored and weissi committed Apr 5, 2019
1 parent 39c6b87 commit 06649bb8c704a042fc07f2013ae429e2d646e7bb
Showing with 148 additions and 0 deletions.
  1. +3 −0 Package.swift
  2. +11 −0 Sources/NIOHTTP1Client/README.md
  3. +134 −0 Sources/NIOHTTP1Client/main.swift
@@ -39,6 +39,8 @@ var targets: [PackageDescription.Target] = [
dependencies: ["NIO", "NIOConcurrencyHelpers"]),
.target(name: "NIOHTTP1Server",
dependencies: ["NIO", "NIOHTTP1", "NIOConcurrencyHelpers"]),
.target(name: "NIOHTTP1Client",
dependencies: ["NIO", "NIOHTTP1", "NIOConcurrencyHelpers"]),
.target(name: "CNIOHTTPParser"),
.target(name: "NIOTLS", dependencies: ["NIO"]),
.target(name: "NIOChatServer",
@@ -77,6 +79,7 @@ let package = Package(
.executable(name: "NIOChatServer", targets: ["NIOChatServer"]),
.executable(name: "NIOChatClient", targets: ["NIOChatClient"]),
.executable(name: "NIOHTTP1Server", targets: ["NIOHTTP1Server"]),
.executable(name: "NIOHTTP1Client", targets: ["NIOHTTP1Client"]),
.executable(name: "NIOWebSocketServer",
targets: ["NIOWebSocketServer"]),
.executable(name: "NIOPerformanceTester",
@@ -0,0 +1,11 @@
# NIOHTTP1Client

This sample application provides a simple echo client that will send a basic HTTP request, containing a single line of text, to the server and wait for a response. Invoke it using one of the following syntaxes:

```bash
swift run NIOHTTP1Client # Connects to a server on ::1, port 8888.
swift run NIOHTTP1Client 9899 # Connects to a server on ::1, port 9899
swift run NIOHTTP1Client /path/to/unix/socket # Connects to a server using the given UNIX socket
swift run NIOHTTP1Client echo.example.com 9899 # Connects to a server on echo.example.com:9899
```

@@ -0,0 +1,134 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO
import NIOHTTP1

print("Please enter line to send to the server")
let line = readLine(strippingNewline: true)!

private final class HTTPEchoHandler: ChannelInboundHandler {
public typealias InboundIn = HTTPClientResponsePart
public typealias OutboundOut = HTTPClientRequestPart

public func channelRead(context: ChannelHandlerContext, data: NIOAny) {

let clientResponse = self.unwrapInboundIn(data)

switch clientResponse {
case .head(let responseHead):
print("Received status: \(responseHead.status)")
case .body(var byteBuffer):
if let string = byteBuffer.readString(length: byteBuffer.readableBytes) {
print("Received: '\(string)' back from the server.")
} else {
print("Received the line back from the server.")
}
case .end:
print("Closing channel.")
context.close(promise: nil)
}
}

public func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error: ", error)

// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.close(promise: nil)
}

public func channelActive(context: ChannelHandlerContext) {
print("Client connected to \(context.remoteAddress!)")

// We are connected. It's time to send the message to the server to initialize the ping-pong sequence.
var buffer = context.channel.allocator.buffer(capacity: line.utf8.count)
buffer.writeString(line)

var headers = HTTPHeaders()
headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
headers.add(name: "Content-Length", value: "\(buffer.readableBytes)")

// This sample only sends an echo request.
// The sample server has more functionality which can be easily tested by playing with the URI.
// For example, try "/dynamic/count-to-ten" or "/dynamic/client-ip"
let requestHead = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1),
method: .GET,
uri: "/dynamic/echo",
headers: headers)

context.write(self.wrapOutboundOut(.head(requestHead)), promise: nil)

context.write(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil)

context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
}
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = ClientBootstrap(group: group)
// Enable SO_REUSEADDR.
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.channelInitializer { channel in
channel.pipeline.addHTTPClientHandlers(position: .first,
leftOverBytesStrategy: .fireError).flatMap {
channel.pipeline.addHandler(HTTPEchoHandler())
}
}
defer {
try! group.syncShutdownGracefully()
}

// First argument is the program path
let arguments = CommandLine.arguments
let arg1 = arguments.dropFirst().first
let arg2 = arguments.dropFirst(2).first

let defaultHost = "::1"
let defaultPort: Int = 8888

enum ConnectTo {
case ip(host: String, port: Int)
case unixDomainSocket(path: String)
}

let connectTarget: ConnectTo
switch (arg1, arg1.flatMap(Int.init), arg2.flatMap(Int.init)) {
case (.some(let h), _ , .some(let p)):
/* we got two arguments, let's interpret that as host and port */
connectTarget = .ip(host: h, port: p)
case (.some(let portString), .none, _):
/* couldn't parse as number, expecting unix domain socket path */
connectTarget = .unixDomainSocket(path: portString)
case (_, .some(let p), _):
/* only one argument --> port */
connectTarget = .ip(host: defaultHost, port: p)
default:
connectTarget = .ip(host: defaultHost, port: defaultPort)
}

let channel = try { () -> Channel in
switch connectTarget {
case .ip(let host, let port):
return try bootstrap.connect(host: host, port: port).wait()
case .unixDomainSocket(let path):
return try bootstrap.connect(unixDomainSocketPath: path).wait()
}
}()

// Will be closed after we echo-ed back to the server.
try channel.closeFuture.wait()

print("Client closed")

0 comments on commit 06649bb

Please sign in to comment.
You can’t perform that action at this time.