I'm trying to make a proxy server by ktor, it proxy a http-flv infinite stream, after client close, it should do something to release resource. like
client.get<HttpStatement>(url.value.url).execute { response ->
val channel: ByteReadChannel = response.receive()
while (!channel.isClosedForRead) {
logger.info("status ${call.request.receiveChannel().isClosedForRead}")
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
logger.info("flushing...")
withContext(Dispatchers.IO) {
write(packet.readBytes())
flush()
}
}
// do something to clean resource
}
try to use router event and flush writer, didn't work
Related
I'm trying to build a performant tcp proxy to bi-directionally forward connections from client A to server B via a proxy server C.
The connection would look like so:
A <——> C <——> B
My initial plan was to use KTor raw TCP Sockets, but the initial example on the website was pretty lack luster and didn’t really point me in the direction of accomplishing my task.
Some things I wanted to be able to do:
register callbacks for when the client is closed
register callbacks for when the server is closed
clean up resources when either the client or server close
point the data from one tcp buffer to another without copying it over
currently I have a very naive implementation as follows:
fun main() {
runBlocking {
launch(Dispatchers.IO) {
val proxySelectorManager = SelectorManager(Dispatchers.IO)
val proxyServer = aSocket(proxySelectorManager).tcp().bind(HOST, PORT)
println("Listening on: ${HOST}:${PORT}")
while (true) {
val clientSocket = proxyServer.accept()
val serverSelectorManager = SelectorManager(Dispatchers.IO)
val serverSocket = aSocket(serverSelectorManager).tcp().connect(REMOTE_HOST, REMOTE_PORT)
println("Client joined at: ${clientSocket.remoteAddress} -> ${clientSocket.localAddress}")
println("Connected to Server at: ${serverSocket.localAddress} -> ${serverSocket.remoteAddress}")
launch(Dispatchers.IO) {
val clientReadChannel = clientSocket.openReadChannel()
val clientWriteChannel = clientSocket.openWriteChannel(true)
val serverReadChannel = serverSocket.openReadChannel()
val serverWriteChannel = serverSocket.openWriteChannel(true)
launch(Dispatchers.IO) {
while (true) {
try { serverWriteChannel.writeByte(clientReadChannel.readByte()) }
catch (e: Exception) { break }
}
}
launch(Dispatchers.IO) {
while (true) {
try { clientWriteChannel.writeByte(serverReadChannel.readByte()) }
catch (e: Exception) { break }
}
}
}
}
}
}
}
My main issues currently are that I have no way of really knowing when a connection was closed (either the server or the client) and my current method of copying over the buffers is extremely slow and has lots of overhead.
Any thoughts or ideas on how I can accomplish the following are greatly appreciated.
emphasized textI am trying to use Kotlin Flow to process some data asynchronously and in parallel, and stream the responses to the client as they occur, as opposed to waiting until all the jobs are complete.
After unsuccessfully trying to just send the flow itself to the response, like this: call.respond(HttpStatusCode.OK, flow.toList())
... I tinkered for hours trying to figure it out, and came up with the following. Is this correct? It seems there should be a more idiomatic way of sending a Flow<MyData> as a response, like one can with a Flux<MyData> in Spring Boot.
Also, it seems that using the below method does not cancel the Flow when the HTTP request is cancelled, so how would one cancel it in Ktor?
data class MyData(val number: Int)
class MyService {
fun updateAllJobs(): Flow<MyData> =
flow {
buildList { repeat(10) { add(MyData(Random.nextInt())) } }
// Docs recommend using `onEach` to "delay" elements.
// However, if I delay here instead of in `map`, all elements are held
// and emitted at once at the very end of the cumulative delay.
// .onEach { delay(500) }
.map {
// I want to emit elements in a "stream" as each is computed.
delay(500)
emit(it)
}
}
}
fun Route.jobRouter() {
val service: MyService by inject() // injected with Koin
put("/jobs") {
val flow = service.updateAllJobs()
// Just using the default Jackson mapper for this example.
val mapper = jsonMapper { }
// `respondOutputStream` seems to be the only way to send a Flow as a stream.
call.respondOutputStream(ContentType.Application.Json, HttpStatusCode.OK) {
flow.collect {
println(it)
// The data does not stream without the newline and `flush()` call.
write((mapper.writeValueAsString(it) + "\n").toByteArray())
flush()
}
}
}
}
The best solution I was able to find (although I don't like it) is to use respondBytesWriter to write data to a response body channel. In the handler, a new job to collect the flow is launched to be able to cancel it if the channel is closed for writing (HTTP request is canceled):
fun Route.jobRouter(service: MyService) {
put("/jobs") {
val flow = service.updateAllJobs()
val mapper = jsonMapper {}
call.respondBytesWriter(contentType = ContentType.Application.Json) {
val job = launch {
flow.collect {
println(it)
try {
writeStringUtf8(mapper.writeValueAsString(it))
flush()
} catch (_: ChannelWriteException) {
cancel()
}
}
}
job.join()
}
}
}
I have been trying to get Json data from dummyjson and place them into an Array in the ktor server. But it just won't work.
Application.kt:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(ContentNegotiation) {
gson {
setDateFormat(DateFormat.LONG)
setPrettyPrinting()
}
}
val client = HttpClient(CIO)
val products: Array<Product> = client.get("https://dummyjson.com/products").body()
configureRouting()
}.start(wait = true)
}
I could create the client but I am not allowed to use the get method because it says Suspend function 'get' should be called only from a coroutine or another suspend function.
Then, in configureRouting.kt I defined the client and used the get method. It works and returns the String to the client.
fun Application.configureRouting() {
val client = HttpClient(CIO)
routing {
get("/get-all-products"){
val product: String = client.get("https://dummyjson.com/products").body()
call.respond(product)
}
}
However, what I want to achieve is without the client's request, the server can automatically load all the JSON file from dummyjson, and place it in a list. But it just keep giving me the same error if I place this get method outside the routing.
I am receiving back this kotlinx.coroutines.channels.ClosedReceiveChannelException upon about 50% of my api calls to a post url through Ktor HttpClient.
Our code looks like
Module.kt
bind<ServiceClient>() with singleton {
val client = HttpClient(CIO) {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
ServiceClient( client, instance() )
}
and our implementation of the call is
suspend fun post(request: RequestObject): List<ResponseObjects> =
client.post(endpoint) {
contentType(ContentType.Application.Json)
body = request
}
Sometimes I am able to receive back the expected results and other times I get the client closed exception. What am I doing wrong here?
I am on Ktor version 1.6.4
How do I monitor request progress in Ktor http client?
For example: I have request like this:
val response = HttpClient().get<String>("https://stackoverflow.com/")
and I want to monitor request progress with progress bar like this:
fun progress(downloaded: Long, contentLength: Long) {
// Update progress bar or whatever
}
How do I set progress() to be called by HttpClient?
edit: This is Kotlin Multiplatform project. Relevant dependencies are:
implementation 'io.ktor:ktor-client-core:1.2.5'
implementation 'io.ktor:ktor-client-cio:1.2.5'
Starting with Ktor 1.6.0, you can react to download progress change using the onDownload extension function exposed by HttpRequestBuilder:
val channel = get<ByteReadChannel>("https://ktor.io/") {
onDownload { bytesSentTotal, contentLength ->
println("Received $bytesSentTotal bytes from $contentLength")
}
}
There is also the onUpload function that can be used to display upload progress:
onUpload { bytesSentTotal, contentLength ->
println("Sent $bytesSentTotal bytes from $contentLength")
}
Here are runnable examples from the Ktor documentation:
download-file
upload-file
How to emit the download progress into a Flow?
I want to observe the download progress by a Flow, so I write a function like this:
suspend fun downloadFile(file: File, url: String): Flow<Int>{
val client = HttpClient(Android)
return flow{
val httpResponse: HttpResponse = client.get(url) {
onDownload { bytesSentTotal, contentLength ->
val progress = (bytesSentTotal * 100f / contentLength).roundToInt()
emit(progress)
}
}
val responseBody: ByteArray = httpResponse.receive()
file.writeBytes(responseBody)
}
}
but the onDownload will be called only once. If I remove the emit(progress) it will work.
#andrey-aksyonov