diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2024-07-28 14:55:56 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2024-07-28 14:55:56 -0600 |
commit | 06649f47a8b1d65590ee8a8f78668d064e92f56b (patch) | |
tree | d3566b5e9bcc233864407e2389c51fc6b9657e3e /Sources/SwiftGopherClient | |
parent | d983c019c74c75eb18e68dfc6d31c5e4ddb2f6f6 (diff) |
rename and capitalize
Diffstat (limited to 'Sources/SwiftGopherClient')
-rw-r--r-- | Sources/SwiftGopherClient/gopherClient.swift | 161 | ||||
-rw-r--r-- | Sources/SwiftGopherClient/gopherRequestResponseHandler.swift | 113 |
2 files changed, 274 insertions, 0 deletions
diff --git a/Sources/SwiftGopherClient/gopherClient.swift b/Sources/SwiftGopherClient/gopherClient.swift new file mode 100644 index 0000000..3fd61ec --- /dev/null +++ b/Sources/SwiftGopherClient/gopherClient.swift @@ -0,0 +1,161 @@ +// +// gopherClient.swift +// +// +// Created by Navan Chauhan on 12/12/23. +// + +import Foundation +import GopherHelpers +import NIO +import NIOTransportServices + +/// `GopherClient` is a class for handling network connections and requests to Gopher servers. +/// +/// This client utilizes Swift NIO for efficient, non-blocking network operations. It automatically +/// chooses the appropriate `EventLoopGroup` based on the running platform: +/// - On iOS/macOS 10.14+, it uses `NIOTSEventLoopGroup` for optimal performance. +/// - On Linux or older Apple platforms, it falls back to `MultiThreadedEventLoopGroup`. +/// +/// The client supports both synchronous (completion handler-based) and asynchronous (Swift concurrency) APIs +/// for sending requests to Gopher servers. +public class GopherClient { + /// The event loop group used for managing network operations. + private let group: EventLoopGroup + + /// Initializes a new instance of `GopherClient`. + /// + /// This initializer automatically selects the appropriate `EventLoopGroup` based on the running platform. + public init() { + #if os(Linux) + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + #else + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { + self.group = NIOTSEventLoopGroup() + } else { + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + } + #endif + } + + /// Cleans up resources when the instance is deinitialized. + deinit { + self.shutdownEventLoopGroup() + } + + /// Sends a request to a Gopher server using a completion handler. + /// + /// This method asynchronously establishes a connection, sends the request, and calls the completion + /// handler with the result. + /// + /// - Parameters: + /// - host: The host address of the Gopher server. + /// - port: The port of the Gopher server. Defaults to 70. + /// - message: The message to be sent to the server. + /// - completion: A closure that handles the result of the request. It takes a `Result` type + /// which either contains an array of `gopherItem` on success or an `Error` on failure. + public func sendRequest( + to host: String, + port: Int = 70, + message: String, + completion: @escaping (Result<[gopherItem], Error>) -> Void + ) { + let bootstrap = self.createBootstrap(message: message, completion: completion) + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + /// Sends a request to a Gopher server using Swift concurrency. + /// + /// This method asynchronously establishes a connection and sends the request, + /// returning the result as an array of `gopherItem`. + /// + /// - Parameters: + /// - host: The host address of the Gopher server. + /// - port: The port of the Gopher server. Defaults to 70. + /// - message: The message to be sent to the server. + /// + /// - Returns: An array of `gopherItem` representing the server's response. + /// + /// - Throws: An error if the connection fails or the server returns an invalid response. + @available(iOS 13.0, *) + @available(macOS 10.15, *) + public func sendRequest(to host: String, port: Int = 70, message: String) async throws + -> [gopherItem] + { + return try await withCheckedThrowingContinuation { continuation in + let bootstrap = self.createBootstrap(message: message) { result in + continuation.resume(with: result) + } + + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection Closed") + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + /// Creates a bootstrap for connecting to a Gopher server. + /// + /// This method sets up the appropriate bootstrap based on the platform and configures + /// the channel with a `GopherRequestResponseHandler`. + /// + /// - Parameters: + /// - message: The message to be sent to the server. + /// - completion: A closure that handles the result of the request. + /// + /// - Returns: A `NIOClientTCPBootstrapProtocol` configured for Gopher communication. + private func createBootstrap( + message: String, + completion: @escaping (Result<[gopherItem], Error>) -> Void + ) -> NIOClientTCPBootstrapProtocol { + let handler = GopherRequestResponseHandler(message: message, completion: completion) + + #if os(Linux) + return ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(handler) + } + #else + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { + return NIOTSConnectionBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(handler) + } + } else { + return ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(handler) + } + } + #endif + } + + /// Shuts down the event loop group, releasing any resources. + /// + /// This method is called during deinitialization to ensure clean shutdown of network resources. + private func shutdownEventLoopGroup() { + do { + try group.syncShutdownGracefully() + } catch { + print("Error shutting down event loop group: \(error)") + } + } +} diff --git a/Sources/SwiftGopherClient/gopherRequestResponseHandler.swift b/Sources/SwiftGopherClient/gopherRequestResponseHandler.swift new file mode 100644 index 0000000..10da67e --- /dev/null +++ b/Sources/SwiftGopherClient/gopherRequestResponseHandler.swift @@ -0,0 +1,113 @@ +// +// gopherRequestResponseHandler.swift +// +// +// Created by Navan Chauhan on 12/12/23. +// + +import Foundation +import GopherHelpers +import NIO + +final class GopherRequestResponseHandler: ChannelInboundHandler { + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + + private var accumulatedData: ByteBuffer + private let message: String + private let completion: (Result<[gopherItem], Error>) -> Void + + init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { + self.message = message + self.completion = completion + self.accumulatedData = ByteBuffer() + } + + func channelActive(context: ChannelHandlerContext) { + var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) + buffer.writeString(message) + context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + var buffer = unwrapInboundIn(data) + accumulatedData.writeBuffer(&buffer) + } + + func channelInactive(context: ChannelHandlerContext) { + if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { + parseGopherServerResponse( + response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", + originalBytes: dataCopy) + } + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + print("Error: ", error) + context.close(promise: nil) + } + + func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) + -> gopherItem + { + var item = gopherItem(rawLine: rawLine) + item.parsedItemType = itemType + item.rawData = rawData + + if rawLine.isEmpty { + item.valid = false + } else { + let components = rawLine.components(separatedBy: "\t") + + // Handle cases where rawLine does not have any itemType in the first character + item.message = String(components[0].dropFirst()) + + if components.indices.contains(1) { + item.selector = String(components[1]) + } + + if components.indices.contains(2) { + item.host = String(components[2]) + } + + if components.indices.contains(3) { + item.port = Int(String(components[3])) ?? 70 + } + } + + return item + } + + func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { + var gopherServerResponse: [gopherItem] = [] + + print("parsing") + let carriageReturnCount = response.filter({ $0 == "\r" }).count + let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count + print( + "Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)" + ) + + if newlineCarriageReturnCount == 0 { + for line in response.split(separator: "\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + } else { + for line in response.split(separator: "\r\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + } + + print("done parsing") + + completion(.success(gopherServerResponse)) + } +} |