mirror of
https://github.com/apple/swift-nio.git
synced 2026-05-20 20:30:36 +00:00
Added extra documentation to `channel.remoteAddress` field to capture a scenario where this field might be `nil` ### Motivation: It is somewhat common for users to have code like `channel.remoteAddress!` in their implementation, as it is a reasonable assumption to think a socket connection will have an associated remote address. However, in at least one known situation this might not be the case. When that happens, user's code might crash due to the force unwrap of the optional field. ### Modifications: Introduced more documentation to make it clear that users should be prepared to handle the `nil` scenario. ### Result: Less frequent mishandling of `channel.remoteAddress`.
209 lines
8.0 KiB
Swift
209 lines
8.0 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2021 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 Dispatch
|
|
import NIOCore
|
|
import NIOPosix
|
|
|
|
private let newLine = "\n".utf8.first!
|
|
|
|
/// Very simple example codec which will buffer inbound data until a `\n` was found.
|
|
final class LineDelimiterCodec: ByteToMessageDecoder {
|
|
public typealias InboundIn = ByteBuffer
|
|
public typealias InboundOut = ByteBuffer
|
|
|
|
public var cumulationBuffer: ByteBuffer?
|
|
|
|
public func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
|
|
let readable = buffer.withUnsafeReadableBytes { $0.firstIndex(of: newLine) }
|
|
if let r = readable {
|
|
context.fireChannelRead(Self.wrapInboundOut(buffer.readSlice(length: r + 1)!))
|
|
return .continue
|
|
}
|
|
return .needMoreData
|
|
}
|
|
}
|
|
|
|
/// This `ChannelInboundHandler` demonstrates a few things:
|
|
/// * Synchronisation between `EventLoop`s.
|
|
/// * Mixing `Dispatch` and SwiftNIO.
|
|
/// * `Channel`s are thread-safe, `ChannelHandlerContext`s are not.
|
|
///
|
|
/// As we are using an `MultiThreadedEventLoopGroup` that uses more then 1 thread we need to ensure proper
|
|
/// synchronization on the shared state in the `ChatHandler` (as the same instance is shared across
|
|
/// child `Channel`s). For this a serial `DispatchQueue` is used when we modify the shared state (the `Dictionary`).
|
|
/// As `ChannelHandlerContext` is not thread-safe we need to ensure we only operate on the `Channel` itself while
|
|
/// `Dispatch` executed the submitted block.
|
|
final class ChatHandler: ChannelInboundHandler {
|
|
public typealias InboundIn = ByteBuffer
|
|
public typealias OutboundOut = ByteBuffer
|
|
|
|
// All access to channels is guarded by channelsSyncQueue.
|
|
private let channelsSyncQueue = DispatchQueue(label: "channelsQueue")
|
|
private var channels: [ObjectIdentifier: Channel] = [:]
|
|
|
|
public func channelActive(context: ChannelHandlerContext) {
|
|
// In production code, you should check if `context.remoteAddress` is actually
|
|
// present, as in rare situations it can be `nil`.
|
|
let remoteAddress = context.remoteAddress!
|
|
let channel = context.channel
|
|
self.channelsSyncQueue.async {
|
|
// broadcast the message to all the connected clients except the one that just became active.
|
|
self.writeToAll(
|
|
channels: self.channels,
|
|
allocator: channel.allocator,
|
|
message: "(ChatServer) - New client connected with address: \(remoteAddress)\n"
|
|
)
|
|
|
|
self.channels[ObjectIdentifier(channel)] = channel
|
|
}
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 64)
|
|
buffer.writeString("(ChatServer) - Welcome to: \(context.localAddress!)\n")
|
|
context.writeAndFlush(Self.wrapOutboundOut(buffer), promise: nil)
|
|
}
|
|
|
|
public func channelInactive(context: ChannelHandlerContext) {
|
|
let channel = context.channel
|
|
self.channelsSyncQueue.async {
|
|
if self.channels.removeValue(forKey: ObjectIdentifier(channel)) != nil {
|
|
// Broadcast the message to all the connected clients except the one that just was disconnected.
|
|
self.writeToAll(
|
|
channels: self.channels,
|
|
allocator: channel.allocator,
|
|
message: "(ChatServer) - Client disconnected\n"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
let id = ObjectIdentifier(context.channel)
|
|
var read = Self.unwrapInboundIn(data)
|
|
|
|
// 64 should be good enough for the ipaddress
|
|
var buffer = context.channel.allocator.buffer(capacity: read.readableBytes + 64)
|
|
|
|
// In production code, you should check if `context.remoteAddress` is actually
|
|
// present, as in rare situations it can be `nil`.
|
|
buffer.writeString("(\(context.remoteAddress!)) - ")
|
|
buffer.writeBuffer(&read)
|
|
self.channelsSyncQueue.async { [buffer] in
|
|
// broadcast the message to all the connected clients except the one that wrote it.
|
|
self.writeToAll(channels: self.channels.filter { id != $0.key }, buffer: buffer)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
private func writeToAll(channels: [ObjectIdentifier: Channel], allocator: ByteBufferAllocator, message: String) {
|
|
let buffer = allocator.buffer(string: message)
|
|
self.writeToAll(channels: channels, buffer: buffer)
|
|
}
|
|
|
|
private func writeToAll(channels: [ObjectIdentifier: Channel], buffer: ByteBuffer) {
|
|
for channel in channels {
|
|
channel.value.writeAndFlush(buffer, promise: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// access to the internal state is protected by `channelsSyncQueue`
|
|
extension ChatHandler: @unchecked Sendable {}
|
|
|
|
// We need to share the same ChatHandler for all as it keeps track of all
|
|
// connected clients. For this ChatHandler MUST be thread-safe!
|
|
let chatHandler = ChatHandler()
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
|
let bootstrap = ServerBootstrap(group: group)
|
|
// Specify backlog and enable SO_REUSEADDR for the server itself
|
|
.serverChannelOption(.backlog, value: 256)
|
|
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
|
|
|
|
// Set the handlers that are applied to the accepted Channels
|
|
.childChannelInitializer { channel in
|
|
// Add handler that will buffer data until a \n is received
|
|
channel.eventLoop.makeCompletedFuture {
|
|
try channel.pipeline.syncOperations.addHandler(ByteToMessageHandler(LineDelimiterCodec()))
|
|
try channel.pipeline.syncOperations.addHandler(chatHandler)
|
|
}
|
|
}
|
|
|
|
// Enable SO_REUSEADDR for the accepted Channels
|
|
.childChannelOption(.socketOption(.so_reuseaddr), value: 1)
|
|
.childChannelOption(.maxMessagesPerRead, value: 16)
|
|
.childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
|
|
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 = 9999
|
|
|
|
enum BindTo {
|
|
case ip(host: String, port: Int)
|
|
case unixDomainSocket(path: String)
|
|
}
|
|
|
|
let bindTarget: BindTo
|
|
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
|
|
bindTarget = .ip(host: h, port: p)
|
|
|
|
case (let portString?, .none, _):
|
|
// Couldn't parse as number, expecting unix domain socket path.
|
|
bindTarget = .unixDomainSocket(path: portString)
|
|
|
|
case (_, let p?, _):
|
|
// Only one argument --> port.
|
|
bindTarget = .ip(host: defaultHost, port: p)
|
|
|
|
default:
|
|
bindTarget = .ip(host: defaultHost, port: defaultPort)
|
|
}
|
|
|
|
let channel = try { () -> Channel in
|
|
switch bindTarget {
|
|
case .ip(let host, let port):
|
|
return try bootstrap.bind(host: host, port: port).wait()
|
|
case .unixDomainSocket(let path):
|
|
return try bootstrap.bind(unixDomainSocketPath: path).wait()
|
|
}
|
|
}()
|
|
|
|
guard let localAddress = channel.localAddress else {
|
|
fatalError(
|
|
"Address was unable to bind. Please check that the socket was not closed or that the address family was understood."
|
|
)
|
|
}
|
|
print("Server started and listening on \(localAddress)")
|
|
|
|
// This will never unblock as we don't close the ServerChannel.
|
|
try channel.closeFuture.wait()
|
|
|
|
print("ChatServer closed")
|