added connection pooling to gbcs-client
All checks were successful
CI / build (push) Successful in 3m55s

This commit is contained in:
2025-01-16 13:37:14 +08:00
parent 5af99330f8
commit 05a265e4b4
16 changed files with 144 additions and 165 deletions

View File

@@ -6,9 +6,9 @@ plugins {
id 'maven-publish' id 'maven-publish'
} }
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
allprojects { subproject -> allprojects { subproject ->
group = 'net.woggioni' group = 'net.woggioni'

View File

@@ -1,8 +1,6 @@
package net.woggioni.gbcs.cli.impl package net.woggioni.gbcs.cli.impl
import picocli.CommandLine import picocli.CommandLine
import java.net.URL
import java.util.Enumeration
import java.util.jar.Attributes import java.util.jar.Attributes
import java.util.jar.JarFile import java.util.jar.JarFile
import java.util.jar.Manifest import java.util.jar.Manifest

View File

@@ -1,8 +1,7 @@
package net.woggioni.gbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.client.GbcsClient
import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Path import java.nio.file.Path

View File

@@ -6,6 +6,7 @@ plugins {
dependencies { dependencies {
implementation project(':gbcs-base') implementation project(':gbcs-base')
implementation catalog.picocli implementation catalog.picocli
implementation catalog.slf4j.api
implementation catalog.netty.buffer implementation catalog.netty.buffer
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
} }

View File

@@ -8,6 +8,7 @@ module net.woggioni.gbcs.client {
requires java.xml; requires java.xml;
requires net.woggioni.gbcs.base; requires net.woggioni.gbcs.base;
requires io.netty.codec; requires io.netty.codec;
requires org.slf4j;
exports net.woggioni.gbcs.client; exports net.woggioni.gbcs.client;

View File

@@ -5,11 +5,13 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline import io.netty.channel.ChannelPipeline
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.nio.NioSocketChannel import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.DecoderException import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.http.DefaultFullHttpRequest import io.netty.handler.codec.http.DefaultFullHttpRequest
@@ -26,8 +28,14 @@ import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.ssl.SslContext import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.Future
import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.client.impl.Parser import net.woggioni.gbcs.client.impl.Parser
import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@@ -35,29 +43,36 @@ import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.Base64 import java.util.Base64
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import io.netty.util.concurrent.Future as NettyFuture import io.netty.util.concurrent.Future as NettyFuture
class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable { class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
private val group: NioEventLoopGroup private val group: NioEventLoopGroup
private var sslContext: SslContext private var sslContext: SslContext
private val log = contextLogger()
private val pool: ChannelPool
data class Configuration( data class Configuration(
val profiles : Map<String, Profile> val profiles: Map<String, Profile>
) { ) {
sealed class Authentication { sealed class Authentication {
data class TlsClientAuthenticationCredentials(val key: PrivateKey, val certificateChain: Array<X509Certificate>) : Authentication() data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication() data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
} }
data class Profile( data class Profile(
val serverURI: URI, val serverURI: URI,
val authentication : Authentication? val authentication: Authentication?,
val maxConnections : Int
) )
companion object { companion object {
fun parse(path : Path) : Configuration { fun parse(path: Path): Configuration {
return Files.newInputStream(path).use { return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it) Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse) }.let(Parser::parse)
@@ -67,9 +82,7 @@ class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
init { init {
group = NioEventLoopGroup() group = NioEventLoopGroup()
sslContext = SslContextBuilder.forClient().also { builder ->
this.sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials -> (profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.keyManager( builder.keyManager(
tlsClientAuthenticationCredentials.key, tlsClientAuthenticationCredentials.key,
@@ -77,6 +90,61 @@ class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
) )
} }
}.build() }.build()
val (scheme, host, port) = profile.serverURI.run {
Triple(
if (scheme == null) "http" else profile.serverURI.scheme,
host,
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
)
}
val bootstrap = Bootstrap().apply {
group(group)
channel(NioSocketChannel::class.java)
option(ChannelOption.TCP_NODELAY, true)
option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(host, port))
}
val channelPoolHandler = object : AbstractChannelPoolHandler() {
@Volatile
private var connectionCount = AtomicInteger()
@Volatile
private var leaseCount = AtomicInteger()
override fun channelReleased(ch: Channel) {
log.debug {
"Released lease ${leaseCount.decrementAndGet()}"
}
}
override fun channelAcquired(ch: Channel?) {
log.debug {
"Acquired lease ${leaseCount.getAndIncrement()}"
}
}
override fun channelCreated(ch: Channel) {
log.debug {
"Created connection ${connectionCount.getAndIncrement()}"
}
val pipeline: ChannelPipeline = ch.pipeline()
// Add SSL handler if needed
if ("https".equals(scheme, ignoreCase = true)) {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
}
// HTTP handlers
pipeline.addLast("codec", HttpClientCodec())
pipeline.addLast("decompressor", HttpContentDecompressor())
pipeline.addLast("aggregator", HttpObjectAggregator(1048576))
pipeline.addLast("chunked", ChunkedWriteHandler())
}
}
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
} }
fun get(key: String): CompletableFuture<ByteArray?> { fun get(key: String): CompletableFuture<ByteArray?> {
@@ -110,92 +178,69 @@ class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> { private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>() val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses
try { pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
val scheme = if (uri.scheme == null) "http" else uri.scheme override fun operationComplete(channelFuture: Future<Channel>) {
val host = uri.host if (channelFuture.isSuccess) {
var port = uri.port val channel = channelFuture.now
if (port == -1) { val pipeline = channel.pipeline()
port = if ("https".equals(scheme, ignoreCase = true)) 443 else 80 channel.pipeline().addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
} override fun channelRead0(
ctx: ChannelHandlerContext,
val bootstrap = Bootstrap() response: FullHttpResponse
bootstrap.group(group) ) {
.channel(NioSocketChannel::class.java) responseFuture.complete(response)
.handler(object : ChannelInitializer<SocketChannel>() { pipeline.removeLast()
override fun initChannel(ch: SocketChannel) { pool.release(channel)
val pipeline: ChannelPipeline = ch.pipeline()
// Add SSL handler if needed
if ("https".equals(scheme, ignoreCase = true)) {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
} }
// HTTP handlers override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
pipeline.addLast("codec", HttpClientCodec()) val ex = when (cause) {
pipeline.addLast("decompressor", HttpContentDecompressor()) is DecoderException -> cause.cause
pipeline.addLast("aggregator", HttpObjectAggregator(1048576)) else -> cause
pipeline.addLast("chunked", ChunkedWriteHandler())
// Custom handler for processing responses
pipeline.addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
override fun channelRead0(
ctx: ChannelHandlerContext,
response: FullHttpResponse
) {
responseFuture.complete(response)
ctx.close()
} }
responseFuture.completeExceptionally(ex)
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { ctx.close()
val ex = when (cause) { pipeline.removeLast()
is DecoderException -> cause.cause pool.release(channel)
else -> cause }
})
// Prepare the HTTP request
val request: FullHttpRequest = let {
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
uri.rawPath,
content ?: Unpooled.buffer(0)
).apply {
headers().apply {
if (content != null) {
set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM)
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
}
set(HttpHeaderNames.HOST, profile.serverURI.host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
// Add basic auth if configured
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
val auth = "${credentials.username}:${credentials.password}"
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
} }
responseFuture.completeExceptionally(ex)
ctx.close()
} }
})
}
})
// Connect to host
val channel: Channel = bootstrap.connect(host, port).sync().channel()
// Prepare the HTTP request
val request: FullHttpRequest = let {
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri.rawPath, content ?: Unpooled.buffer(0)).apply {
headers().apply {
if (content != null) {
set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM)
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
}
set(HttpHeaderNames.HOST, host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
// Add basic auth if configured
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
val auth = "${credentials.username}:${credentials.password}"
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
} }
} }
// Set headers
// Send the request
channel.writeAndFlush(request)
} }
} }
})
// Set headers
// Send the request
channel.writeAndFlush(request)
} catch (e: Exception) {
responseFuture.completeExceptionally(e)
}
return responseFuture return responseFuture
} }

View File

@@ -1,38 +0,0 @@
package net.woggioni.gbcs.client
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
import kotlin.random.Random
//object Main {
// @JvmStatic
// fun main(vararg args : String) {
// val pwd = "PO%!*bW9p'Zp#=uu\$fl{Ij`Ad.8}x#ho".toCharArray()
// val keystore = KeyStore.getInstance("PKCS12").apply{
// Files.newInputStream(Path.of("/home/woggioni/ssl/woggioni@c962475fa38.pfx")).use {
// load(it, pwd)
// }
// }
// val key = keystore.getKey("woggioni@c962475fa38", pwd) as PrivateKey
// val certChain = keystore.getCertificateChain("woggioni@c962475fa38").asSequence()
// .map { it as X509Certificate }
// .toList()
// .toTypedArray()
// GbcsClient.Configuration(
// serverURI = URI("https://gbcs.woggioni.net/"),
// GbcsClient.TlsClientAuthenticationCredentials(
// key, certChain
// )
// ).let(::GbcsClient).use { client ->
// val random = Random(101325)
// val entry = "something" to ByteArray(0x1000).also(random::nextBytes)
// client.put(entry.first, entry.second)
// val retrieved = client.get(entry.first).get()
// println(retrieved.contentEquals(entry.second))
// }
// }
//}

View File

@@ -55,7 +55,11 @@ object Parser {
} }
} }
} }
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication) val maxConnections = child.getAttribute("max-connections")
.takeIf(String::isNotEmpty)
?.let(String::toInt)
?: 50
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections)
} }
} }
} }

View File

@@ -19,6 +19,7 @@
</xs:choice> </xs:choice>
<xs:attribute name="name" type="xs:token" use="required"/> <xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/> <xs:attribute name="base-url" type="xs:anyURI" use="required"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="basicAuthType"> <xs:complexType name="basicAuthType">

View File

@@ -1,6 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id 'java-library' id 'java-library'
id 'maven-publish' id 'maven-publish'

View File

@@ -1,6 +1,5 @@
package net.woggioni.gbcs.memcached package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.MemcachedClient
import net.rubyeye.xmemcached.XMemcachedClientBuilder import net.rubyeye.xmemcached.XMemcachedClientBuilder
import net.rubyeye.xmemcached.command.BinaryCommandFactory import net.rubyeye.xmemcached.command.BinaryCommandFactory
import net.rubyeye.xmemcached.transcoders.CompressionMode import net.rubyeye.xmemcached.transcoders.CompressionMode

View File

@@ -16,7 +16,6 @@ import net.woggioni.gbcs.base.Xml.Companion.asIterable
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.TypeInfo import org.w3c.dom.TypeInfo
import java.lang.IllegalArgumentException
import java.nio.file.Paths import java.nio.file.Paths
object Parser { object Parser {

View File

@@ -1,6 +1,5 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.Xml
@@ -10,13 +9,9 @@ import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials
import net.woggioni.gbcs.utils.NetworkUtils import net.woggioni.gbcs.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path

View File

@@ -2,7 +2,6 @@ package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order

View File

@@ -1,15 +1,14 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.NetworkUtils import net.woggioni.gbcs.utils.NetworkUtils
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest

View File

@@ -3,33 +3,13 @@ package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials
import net.woggioni.gbcs.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.KeyStore.PasswordProtection
import java.time.Duration
import java.util.Base64
import java.util.zip.Deflater
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random
class TlsServerTest : AbstractTlsServerTest() { class TlsServerTest : AbstractTlsServerTest() {