LINUX.ORG.RU

Получение переменной из колбек-функции

 , , ,


0

1

Всем привет,

Имеется прототип асинхронного, на уровне сокетов и файловых операций, http сервера. Функция GetData является колбеком на событие получение данных выхлопа внешней программы которая генерирует картинку. Нужно получить количество записанных в сокет данных и передать их в хедер Content-Length который определяется уровнем выше. Как можно такое реализовать? Не хотелось бы перемещать формирование хедера в GetData.

socket -server handle_client 8080
proc handle_client {chan addr port} {
    puts $chan "HTTP/1.1 200 OK\nContent-type: image/jpeg\nConnection: close\n"
    set fd [open "|cat test.jpg" r]
    fconfigure $fd -translation binary -encoding binary -blocking 0 -buffering none
    fconfigure $chan -translation binary -blocking 0
    fileevent $fd readable [list GetData $fd $chan]
}
proc GetData {fd chan} {                                                                                                       
    if {[eof $fd] && ![fblocked $fd]} {
        fileevent $fd readable {}
        close $fd
        close $chan
    } else {
        set data [read $fd]
        puts -nonewline $chan $data
        # puts [string length $data]
        if {[string length $data]} {
            incr bytes_sent [string length $data]
        }
    }
}
vwait forever
★★★★★

Ответ на: комментарий от no-dashi-v2

Твоя функция должна возвращать

Сама колбэк функция ничего не может возвращать, так как является обработчиком события записи в пайп. Причем вызывается каждый раз когда в пайпе появится порция данных. То бишь, read $fd читает за раз ровно столько, сколько накапало в пайп.

Создать еще один канал, пробросить его в колбек функцию и писать туда — так себе затея. Слишком накладно и к тому же, это значение уже будет не нужно когда данные уже будут в целевом сокете.

Единственный вариант как можно в теории посчитать количество данных - копировать из пайпа не в сокет, а куда-то в промежуточный буфер в виде глобальной переменной. После чего делать все манипуляции с данными в буфере и пулять его в сокет. Но тогда весь принцип асинхронности - коту под хвост. Но прикол еще в том, значение в глобальную переменную не сохраняется. Вот по чему изначально и возник вопрос.

iron ★★★★★
() автор топика
Ответ на: комментарий от iron

Ты не понял.

Сделай новую функцию.

Открывай файл в ней

Возвращай из функции структуру в которой упакованы размер файла и файловый дескриптор

Размер файла выводи в заголовки ответа, файловый дескриптор дальше отдавай в свою GetData

Если размер файла определить не можешь (то есть это не файл а стрим неизвестной длины) то без вараинтов, либо ты не указываешь размер либо читаешь всё в буфер и потом смотришь размер буфера. И обсуждать тут в общем то нечего.

no-dashi-v2 ★★
()
Ответ на: комментарий от no-dashi-v2

либо ты не указываешь размер либо читаешь всё в буфер и потом смотришь размер буфера.

Пришлось реализовать два варианта:

  • синхронный - при котором данные читаются в буфер, по завершению считается общий размер, в сокет отправляются хедеры с Content-Length и затем данные из буфера.
  • асинхронный - перед началом чтения фала отправляются хедеры без Content-Length и затем сразу пишутся в сокет по мере появления данных в файловом дескрипторе.

Выглядит это примерно так:

chan configure $fd -translation binary -encoding binary -blocking 0 -buffering none
set co_name "File_Read_Coroutine$chan"
coroutine $co_name Process_Pipe_Output $fd $chan $cmdline $url $headers $time_start
chan event $fd readable [list $co_name]

array set data_buffer {}
proc append_data_buffer {chan chunk} {
    global data_buffer
    if {![info exists data_buffer($chan)]} {
        set data_buffer($chan) ""
    }
    append data_buffer($chan) $chunk
}

proc Process_Pipe_Output {fd chan cmdline url headers time_start} {
    global log_level disable_async_send
    yield
    set first_chunk [chan read $fd]
    if {$first_chunk ne ""} {
        if {$disable_async_send} {
            append_data_buffer $chan $first_chunk
        } elseif {[catch { puts -nonewline $chan "$headers\n$first_chunk" } err]} {
            if {$log_level} {logger "$err"}
            close $fd
        } elseif {[eof $fd]} {
            handle_eof $fd $chan $url $cmdline $headers $time_start
            rename [info coroutine] {}
        }
    }
    yield
    while {1} {
        set chunk [chan read $fd]
        if {$chunk ne ""} {
            if {$disable_async_send} {
                append_data_buffer $chan $chunk
            } elseif {[catch {puts -nonewline $chan $chunk}] != 0} {
                logger "Cannot send data to socket: client closed connection"
                chan event $fd readable {}
                close $fd
                rename [info coroutine] {}
            }
        } elseif {[eof $fd]} {
            handle_eof $fd $chan $url $cmdline $headers $time_start
            rename [info coroutine] {}
        }
        yield
    }
}

proc handle_eof {fd chan url cmdline headers time_start} {
    global command_timeout log_level disable_async_send data_buffer
    set tcl_precision 4
    set time_end [clock milliseconds]
    set time_diff [expr [clock milliseconds] - $time_start]
    if {$time_diff>1000} {
        set time_diff "[expr $time_diff.000 / 1000]s"
    } else {
        set time_diff "${time_diff}ms"
    }
    chan event $fd readable {}
    chan configure $fd -blocking 1
    try {
        close $fd
    } trap CHILDSTATUS {result options} {
        if {[lindex [dict get $options -errorcode] 2] == 124} {
            return_404 $chan "$url $time_diff Error: timeout from `command` (${command_timeout} sec). Process killed.\nCommand: $cmdline"
        } else {
            return_404 $chan "$url $time_diff `command` returned error\nCommand: $cmdline"
        }
    } on ok {} {
        if {$disable_async_send} {
            puts $chan "${headers}Content-Length: [string length $data_buffer($chan)]\n"
            puts -nonewline $chan $data_buffer($chan)
            unset data_buffer($chan)
        }
        logger "200 $url $time_diff"
        close_socket_handler $chan
    }
}

Далее столкнулся с тем, что логирование на диск, особенно на HDD, при запредельных нагрузках существенно замедляло все операции. Пришлось навелосипедить неблокирующий логгер и наколеночный механизм синхронизации. В итоге логи пишутся только тогда, когда event loop полностью свободен. Т.е. нет никакой нагрузки - пишем логи. Есть - ждем когда завершаться все приоритетные операции.

set log_queue_switch 0
set log_queue_lock {}

coroutine async_log_flush_callback apply {{} {
    global log_queue log_queue_switch log_queue_lock log_chan
    while {1} {
        yield
        set cur_log_queue_switch $log_queue_switch
        set log_queue_switch [expr {!$log_queue_switch}]
        puts $log_chan [join $log_queue($cur_log_queue_switch) "\n"]
        flush $log_chan
        set log_queue($cur_log_queue_switch) {}
        set log_queue_lock {}
    }
}}

proc logger {log} {
    global log_level log_chan log_queue log_queue_switch log_queue_lock
    if {$log_level == 1} {
        lappend log_queue($log_queue_switch) "[time_now]: $log"
        if {$log_queue_lock eq ""} {
            set log_queue_lock [after idle async_log_flush_callback]
        }
    } elseif {$log_level == 2} {
        puts "[time_now]: $log"
    }
}
Протестил все это. Потолок скрипта так и не смог нащупать. На скоростях около 3-4 гигобайт в сек уперся либо в потолок скорости передачи данных через пайп, либо в потолок передачи данных через сокет на лупбеке - в общем, в ядро. Load average был около тысячи и мышка еле двигалась в то время как сам скрипт кушал 30-40% одного ядра процессора.

Если у кого-то есть мысли как улучшить выше написанный код - то велкам.

iron ★★★★★
() автор топика

а статик классов или синглтонов в этом вашем тикле не бывает, вдруг сработает как глобал? Чтобы оно было потокобезопасно/асинхронно можно использовать некий (сессионный) идентификатор для хранения инстансов этого синглтона

https://stackoverflow.com/questions/37937649/can-we-define-static-functions-in-tcloo

Syncro ★★★★★
()
Ответ на: комментарий от Syncro

статик классов или синглтонов в этом вашем тикле не бывает, вдруг сработает как глобал?

Синглтоны есть, если я правильно понял вопрос. Изначальную задачу решил при помощи корутин, по сему глобальные переменные уже не актуальны. В качестве идентификатора выступает обслуживаемый сокет, т.е. коннект.

iron ★★★★★
() автор топика