it-swarm.it

Linux non bloccante fifo (registrazione su richiesta)

Mi piace registrare l'output di un programma 'su richiesta'. Per esempio. l'output viene registrato sul terminale, ma un altro processo può collegarsi all'uscita corrente in qualsiasi momento. 

Il modo classico sarebbe: 

myprogram 2>&1 | tee /tmp/mylog

e su richiesta

tail /tmp/mylog

Tuttavia, ciò creerebbe un file di registro sempre crescente anche se non utilizzato fino a quando l'unità non esaurisce lo spazio. Quindi il mio tentativo è stato:

mkfifo /tmp/mylog
myprogram 2>&1 | tee /tmp/mylog

e su richiesta 

cat /tmp/mylog

Ora posso leggere/tmp/mylog in qualsiasi momento. Tuttavia, qualsiasi output blocca il programma finché non viene letto/tmp/mylog. Mi piace il fifo per scaricare tutti i dati in arrivo non letti. Come farlo?

29
dronus

Ispirato dalla tua domanda ho scritto un semplice programma che ti permetterà di fare questo:

$ myprogram 2>&1 | ftee /tmp/mylog 

Si comporta in modo simile a tee ma clona lo stdin allo stdout e ad una named pipe (un requisito per ora) senza bloccare. Ciò significa che se si desidera effettuare il log in questo modo può accadere che perderà i dati del registro, ma suppongo che sia accettabile nel proprio scenario. Il trucco è bloccare il segnale SIGPIPE e ignorare l'errore nella scrittura a un fifo rotto. Questo esempio può essere ottimizzato in vari modi, naturalmente, ma fino ad ora fa il lavoro che immagino.

/* ftee - clone stdin to stdout and to a named pipe 
(c) [email protected]
WTFPL Licence */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int readfd, writefd;
    struct stat status;
    char *fifonam;
    char buffer[BUFSIZ];
    ssize_t bytes;

    signal(SIGPIPE, SIG_IGN);

    if(2!=argc)
    {
        printf("Usage:\n someprog 2>&1 | %s FIFO\n FIFO - path to a"
            " named pipe, required argument\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    fifonam = argv[1];

    readfd = open(fifonam, O_RDONLY | O_NONBLOCK);
    if(-1==readfd)
    {
        perror("ftee: readfd: open()");
        exit(EXIT_FAILURE);
    }

    if(-1==fstat(readfd, &status))
    {
        perror("ftee: fstat");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    if(!S_ISFIFO(status.st_mode))
    {
        printf("ftee: %s in not a fifo!\n", fifonam);
        close(readfd);
        exit(EXIT_FAILURE);
    }

    writefd = open(fifonam, O_WRONLY | O_NONBLOCK);
    if(-1==writefd)
    {
        perror("ftee: writefd: open()");
        close(readfd);
        exit(EXIT_FAILURE);
    }

    close(readfd);

    while(1)
    {
        bytes = read(STDIN_FILENO, buffer, sizeof(buffer));
        if (bytes < 0 && errno == EINTR)
            continue;
        if (bytes <= 0)
            break;

        bytes = write(STDOUT_FILENO, buffer, bytes);
        if(-1==bytes)
            perror("ftee: writing to stdout");
        bytes = write(writefd, buffer, bytes);
        if(-1==bytes);//Ignoring the errors
    }
    close(writefd); 
    return(0);
}

Puoi compilarlo con questo comando standard:

$ gcc ftee.c -o ftee

Puoi verificarlo rapidamente eseguendo per esempio:

$ ping www.google.com | ftee /tmp/mylog

$ cat /tmp/mylog

Nota anche - questo non è un multiplexer. È possibile avere un solo processo che esegue $ cat /tmp/mylog alla volta.

45
racic

Questa è una (molto) vecchia discussione, ma mi sono imbattuto in un problema simile negli ultimi tempi. In effetti, ciò di cui avevo bisogno è una clonazione di stdin per lo stdout con una copia in una pipe che non è bloccante. la proposta di risposta nella prima risposta ci è stata di grande aiuto, ma è stata (per il mio caso d'uso) troppo volatile. Significa che ho perso i dati che avrei potuto elaborare se avessi ottenuto in tempo. 

Lo scenario che ho dovuto affrontare è che ho un processo (qualche_processo) che aggrega alcuni dati e scrive i risultati ogni tre secondi per lo stdout. L'installazione (semplificata) era simile a questa (nella configurazione reale sto usando una named pipe):

some_process | ftee >(onlineAnalysis.pl > results) | gzip > raw_data.gz

Ora, raw_data.gz deve essere compresso e deve essere completo. questo lavoro fa molto bene Ma la pipa che sto usando nel mezzo era troppo lenta per afferrare i dati svuotati - ma era abbastanza veloce per elaborare tutto se potesse arrivare ad esso, che è stato testato con un tee normale. Tuttavia, un normale tee blocca se qualcosa accade alla pipe senza nome e, poiché voglio essere in grado di connettersi su richiesta, tee non è un'opzione. Torna all'argomento: è migliorato quando ho inserito un buffer intermedio, che ha prodotto:

some_process | ftee >(mbuffer -m 32M| onlineAnalysis.pl > results) | gzip > raw_data.gz

Ma questo stava ancora perdendo i dati che avrei potuto elaborare. Quindi sono andato avanti e ho esteso le funzioni proposte prima a una versione con buffer (bftee). Ha ancora tutte le stesse proprietà, ma usa un buffer interno (inefficiente?) Nel caso in cui una scrittura fallisca. Ancora perde i dati se il buffer è pieno, ma funziona perfettamente per il mio caso. Come sempre c'è molto margine di miglioramento, ma quando ho copiato il codice da qui mi piacerebbe condividerlo con le persone che potrebbero esserne utili.

/* bftee - clone stdin to stdout and to a buffered, non-blocking pipe 
    (c) [email protected]
    (c) [email protected]
    WTFPL Licence */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <signal.h>
    #include <unistd.h>

    // the number of sBuffers that are being held at a maximum
    #define BUFFER_SIZE 4096
    #define BLOCK_SIZE 2048

    typedef struct {
      char data[BLOCK_SIZE];
      int bytes;
    } sBuffer;

    typedef struct {
      sBuffer *data;  //array of buffers
      int bufferSize; // number of buffer in data
      int start;      // index of the current start buffer
      int end;        // index of the current end buffer
      int active;     // number of active buffer (currently in use)
      int maxUse;     // maximum number of buffers ever used
      int drops;      // number of discarded buffer due to overflow
      int sWrites;    // number of buffer written to stdout
      int pWrites;    // number of buffers written to pipe
    } sQueue;

    void InitQueue(sQueue*, int);              // initialized the Queue
    void PushToQueue(sQueue*, sBuffer*, int);  // pushes a buffer into Queue at the end 
    sBuffer *RetrieveFromQueue(sQueue*);       // returns the first entry of the buffer and removes it or NULL is buffer is empty
    sBuffer *PeakAtQueue(sQueue*);             // returns the first entry of the buffer but does not remove it. Returns NULL on an empty buffer
    void ShrinkInQueue(sQueue *queue, int);    // shrinks the first entry of the buffer by n-bytes. Buffer is removed if it is empty
    void DelFromQueue(sQueue *queue);          // removes the first entry of the queue

    static void sigUSR1(int);                  // signal handled for SUGUSR1 - used for stats output to stderr
    static void sigINT(int);                   // signla handler for SIGKILL/SIGTERM - allows for a graceful stop ?

    sQueue queue;                              // Buffer storing the overflow
    volatile int quit;                         // for quiting the main loop

    int main(int argc, char *argv[])
    {   
        int readfd, writefd;
        struct stat status;
        char *fifonam;
        sBuffer buffer;
        ssize_t bytes;
        int bufferSize = BUFFER_SIZE;

        signal(SIGPIPE, SIG_IGN);
        signal(SIGUSR1, sigUSR1);
        signal(SIGTERM, sigINT);
        signal(SIGINT,  sigINT);

        /** Handle commandline args and open the pipe for non blocking writing **/

        if(argc < 2 || argc > 3)
        {   
            printf("Usage:\n someprog 2>&1 | %s FIFO [BufferSize]\n"
                   "FIFO - path to a named pipe, required argument\n"
                   "BufferSize - temporary Internal buffer size in case write to FIFO fails\n", argv[0]);
            exit(EXIT_FAILURE);
        }

        fifonam = argv[1];
        if (argc == 3) {
          bufferSize = atoi(argv[2]);
          if (bufferSize == 0) bufferSize = BUFFER_SIZE;
        }

        readfd = open(fifonam, O_RDONLY | O_NONBLOCK);
        if(-1==readfd)
        {   
            perror("bftee: readfd: open()");
            exit(EXIT_FAILURE);
        }

        if(-1==fstat(readfd, &status))
        {
            perror("bftee: fstat");
            close(readfd);
            exit(EXIT_FAILURE);
        }

        if(!S_ISFIFO(status.st_mode))
        {
            printf("bftee: %s in not a fifo!\n", fifonam);
            close(readfd);
            exit(EXIT_FAILURE);
        }

        writefd = open(fifonam, O_WRONLY | O_NONBLOCK);
        if(-1==writefd)
        {
            perror("bftee: writefd: open()");
            close(readfd);
            exit(EXIT_FAILURE);
        }

        close(readfd);


        InitQueue(&queue, bufferSize);
        quit = 0;

        while(!quit)
        {
            // read from STDIN
            bytes = read(STDIN_FILENO, buffer.data, sizeof(buffer.data));

            // if read failed due to interrupt, then retry, otherwise STDIN has closed and we should stop reading
            if (bytes < 0 && errno == EINTR) continue;
            if (bytes <= 0) break;

            // save the number if read bytes in the current buffer to be processed
            buffer.bytes = bytes;

            // this is a blocking write. As long as buffer is smaller than 4096 Bytes, the write is atomic to a pipe in Linux
            // thus, this cannot be interrupted. however, to be save this should handle the error cases of partial or interrupted write none the less.
            bytes = write(STDOUT_FILENO, buffer.data, buffer.bytes);
            queue.sWrites++;

            if(-1==bytes) {
                perror("ftee: writing to stdout");
                break;
            }

            sBuffer *tmpBuffer = NULL;

            // if the queue is empty (tmpBuffer gets set to NULL) the this does nothing - otherwise it tries to write
            // the buffered data to the pipe. This continues until the Buffer is empty or the write fails.
            // NOTE: bytes cannot be -1  (that would have failed just before) when the loop is entered. 
            while ((bytes != -1) && (tmpBuffer = PeakAtQueue(&queue)) != NULL) {
               // write the oldest buffer to the pipe
               bytes = write(writefd, tmpBuffer->data, tmpBuffer->bytes);

               // the  written bytes are equal to the buffer size, the write is successful - remove the buffer and continue
               if (bytes == tmpBuffer->bytes) {
                 DelFromQueue(&queue);
                 queue.pWrites++;
               } else if (bytes > 0) {
                 // on a positive bytes value there was a partial write. we shrink the current buffer
                 //  and handle this as a write failure
                 ShrinkInQueue(&queue, bytes);
                 bytes = -1;
               }
            }
            // There are several cases here:
            // 1.) The Queue is empty -> bytes is still set from the write to STDOUT. in this case, we try to write the read data directly to the pipe
            // 2.) The Queue was not empty but is now -> bytes is set from the last write (which was successful) and is bigger 0. also try to write the data
            // 3.) The Queue was not empty and still is not -> there was a write error before (even partial), and bytes is -1. Thus this line is skipped.
            if (bytes != -1) bytes = write(writefd, buffer.data, buffer.bytes);

            // again, there are several cases what can happen here
            // 1.) the write before was successful -> in this case bytes is equal to buffer.bytes and nothing happens
            // 2.) the write just before is partial or failed all together - bytes is either -1 or smaller than buffer.bytes -> add the remaining data to the queue
            // 3.) the write before did not happen as the buffer flush already had an error. In this case bytes is -1 -> add the remaining data to the queue
            if (bytes != buffer.bytes)
              PushToQueue(&queue, &buffer, bytes);
            else 
              queue.pWrites++;
        }

        // once we are done with STDIN, try to flush the buffer to the named pipe
        if (queue.active > 0) {
           //set output buffer to block - here we wait until we can write everything to the named pipe
           // --> this does not seem to work - just in case there is a busy loop that waits for buffer flush aswell. 
           int saved_flags = fcntl(writefd, F_GETFL);
           int new_flags = saved_flags & ~O_NONBLOCK;
           int res = fcntl(writefd, F_SETFL, new_flags);

           sBuffer *tmpBuffer = NULL;
           //TODO: this does not handle partial writes yet
           while ((tmpBuffer = PeakAtQueue(&queue)) != NULL) {
             int bytes = write(writefd, tmpBuffer->data, tmpBuffer->bytes);
             if (bytes != -1) DelFromQueue(&queue);
           }
        }

        close(writefd);

    }


    /** init a given Queue **/
    void InitQueue (sQueue *queue, int bufferSize) {
      queue->data = calloc(bufferSize, sizeof(sBuffer));
      queue->bufferSize = bufferSize;
      queue->start = 0;
      queue->end = 0;
      queue->active = 0;
      queue->maxUse = 0;
      queue->drops = 0;
      queue->sWrites = 0;
      queue->pWrites = 0;
    }

    /** Push a buffer into the Queue**/
    void PushToQueue(sQueue *queue, sBuffer *p, int offset)
    {

        if (offset < 0) offset = 0;      // offset cannot be smaller than 0 - if that is the case, we were given an error code. Set it to 0 instead
        if (offset == p->bytes) return;  // in this case there are 0 bytes to add to the queue. Nothing to write

        // this should never happen - offset cannot be bigger than the buffer itself. Panic action
        if (offset > p->bytes) {perror("got more bytes to buffer than we read\n"); exit(EXIT_FAILURE);}

        // debug output on a partial write. TODO: remove this line
        // if (offset > 0 ) fprintf(stderr, "partial write to buffer\n");

        // copy the data from the buffer into the queue and remember its size
        memcpy(queue->data[queue->end].data, p->data + offset , p->bytes-offset);
        queue->data[queue->end].bytes = p->bytes - offset;

        // move the buffer forward
        queue->end = (queue->end + 1) % queue->bufferSize;

        // there is still space in the buffer
        if (queue->active < queue->bufferSize)
        {
            queue->active++;
            if (queue->active > queue->maxUse) queue->maxUse = queue->active;
        } else {
            // Overwriting the oldest. Move start to next-oldest
            queue->start = (queue->start + 1) % queue->bufferSize;
            queue->drops++;
        }
    }

    /** return the oldest entry in the Queue and remove it or return NULL in case the Queue is empty **/
    sBuffer *RetrieveFromQueue(sQueue *queue)
    {
        if (!queue->active) { return NULL; }

        queue->start = (queue->start + 1) % queue->bufferSize;
        queue->active--;
        return &(queue->data[queue->start]);
    }

    /** return the oldest entry in the Queue or NULL if the Queue is empty. Does not remove the entry **/
    sBuffer *PeakAtQueue(sQueue *queue)
    {
        if (!queue->active) { return NULL; }
        return &(queue->data[queue->start]);
    }

    /*** Shrinks the oldest entry i the Queue by bytes. Removes the entry if buffer of the oldest entry runs empty*/
    void ShrinkInQueue(sQueue *queue, int bytes) {

      // cannot remove negative amount of bytes - this is an error case. Ignore it
      if (bytes <= 0) return;

      // remove the entry if the offset is equal to the buffer size
      if (queue->data[queue->start].bytes == bytes) {
        DelFromQueue(queue);
        return;
      };

      // this is a partial delete
      if (queue->data[queue->start].bytes > bytes) {
        //shift the memory by the offset
        memmove(queue->data[queue->start].data, queue->data[queue->start].data + bytes, queue->data[queue->start].bytes - bytes);
        queue->data[queue->start].bytes = queue->data[queue->start].bytes - bytes;
        return;
      }

      // panic is the are to remove more than we have the buffer
      if (queue->data[queue->start].bytes < bytes) {
        perror("we wrote more than we had - this should never happen\n");
        exit(EXIT_FAILURE);
        return;
      }
    }

    /** delete the oldest entry from the queue. Do nothing if the Queue is empty **/
    void DelFromQueue(sQueue *queue)
    {
        if (queue->active > 0) {
          queue->start = (queue->start + 1) % queue->bufferSize;
          queue->active--;
        }
    }

    /** Stats output on SIGUSR1 **/
    static void sigUSR1(int signo) {
      fprintf(stderr, "Buffer use: %i (%i/%i), STDOUT: %i PIPE: %i:%i\n", queue.active, queue.maxUse, queue.bufferSize, queue.sWrites, queue.pWrites, queue.drops);
    }

    /** handle signal for terminating **/
    static void sigINT(int signo) {
      quit++;
      if (quit > 1) exit(EXIT_FAILURE);
    }

Questa versione richiede un altro argomento (opzionale) che specifica il numero dei blocchi da bufferare per la pipe. La mia chiamata di esempio ora si presenta così:

some_process | bftee >(onlineAnalysis.pl > results) 16384 | gzip > raw_data.gz

con conseguente blocco di 16384 blocchi prima che avvengano gli scarti. questo usa circa 32 Mbyte di memoria in più, ma ... a chi importa?

Ovviamente, nell'ambiente reale sto usando una named pipe in modo che possa attaccare e staccare secondo necessità. C'è un aspetto simile a questo:

mkfifo named_pipe
some_process | bftee named_pipe 16384 | gzip > raw_data.gz &
cat named_pipe | onlineAnalysis.pl > results

Inoltre, il processo reagisce sui segnali come segue: SIGUSR1 -> stampa i contatori su STDERR SIGTERM, SIGINT -> prima chiude il ciclo principale e svuota il buffer sul tubo, il secondo termina il programma immediatamente.

Forse questo aiuta qualcuno in futuro ... Divertiti

11
Fabraxias

Tuttavia, ciò creerebbe un file di registro sempre crescente anche se non utilizzato fino a quando l'unità non esaurisce lo spazio.

Perché non ruotare periodicamente i registri? C'è anche un programma per farlo logrotate.

C'è anche un sistema per generare messaggi di log e fare cose diverse con loro in base al tipo. Si chiama syslog.

Potresti anche combinare i due. Chiedi al tuo programma di generare messaggi syslog, configura syslog per inserirli in un file e usa logrotate per assicurarti che non riempiano il disco.


Se risulta che stavi scrivendo per un piccolo sistema embedded e l'output del programma è pesante, ci sono una varietà di tecniche che potresti prendere in considerazione.

  • Syslog remoto: invia i messaggi syslog a un server syslog sulla rete.
  • Utilizzare i livelli di gravità disponibili in syslog per fare cose diverse con i messaggi. Per esempio. scartare "INFO" ma accedere e inoltrare "ERR" o superiore. Per esempio. per consolare
  • Utilizzare un gestore di segnale nel programma per rileggere la configurazione su HUP e variare la generazione dei registri "su richiesta" in questo modo.
  • Fai in modo che il tuo programma ascolti su un socket unix e scriva messaggi quando aperto. Potresti anche implementare e console interattiva nel tuo programma in questo modo.
  • Utilizzando un file di configurazione, fornire un controllo granulare dell'output della registrazione.
8
MattH

BusyBox spesso utilizzato su dispositivi embedded può creare un registro bufferizzato da ram

syslogd -C

che può essere riempito da 

logger

e letto da 

logread

Funziona abbastanza bene, ma fornisce solo un registro globale.

6
dronus

Se è possibile installare la schermata sul dispositivo incorporato, è possibile eseguire "myprogram" al suo interno e scollegarlo e ricollegarlo ogni volta che si desidera visualizzare il registro. Qualcosa di simile a:

$ screen -t sometitle myprogram
Hit Ctrl+A, then d to detach it.

Ogni volta che vuoi vedere l'output, ricollegalo:

$ screen -DR sometitle
Hit Ctrl-A, then d to detach it again.

In questo modo non dovrai preoccuparti dell'output del programma utilizzando lo spazio su disco.

4
holygeek

Sembra che l'operatore di reindirizzamento <> di bash ( 3.6.10 Apertura dei descrittori di file per la lettura e la scrittura ) renda la scrittura su file/fifo aperto con esso non bloccante. Questo dovrebbe funzionare:

$ mkfifo /tmp/mylog
$ exec 4<>/tmp/mylog
$ myprogram 2>&1 | tee >&4
$ cat /tmp/mylog # on demend

Soluzione fornita da gniourf_gniourf on #bash IRC canale.

4
Piotr Dobrogost

Il problema con l'approccio fifo dato è che l'intera cosa si bloccherà quando il buffer pipe si riempirà e non si sta verificando alcun processo di lettura.

Per l'approccio fifo al lavoro penso che dovresti implementare un modello client-server pipe identico a quello menzionato in BASH: migliore architettura per la lettura da due flussi di input (vedi codice leggermente modificato di seguito, codice di esempio 2 ).

Per una soluzione alternativa, è possibile utilizzare un costrutto while ... read anziché teeing stdout in una pipe denominata implementando un meccanismo di conteggio all'interno del ciclo while ... read che sovrascriverà periodicamente il file di log in base a un numero specificato di righe. Ciò impedirebbe un file di registro sempre crescente (codice di esempio 1).

# sample code 1

# terminal window 1
rm -f /tmp/mylog
touch /tmp/mylog
while sleep 2; do date '+%Y-%m-%d_%H.%M.%S'; done 2>&1 | while IFS="" read -r line; do 
  lno=$((lno+1))
  #echo $lno
  array[${lno}]="${line}"
  if [[ $lno -eq 10 ]]; then
    lno=$((lno+1))
    array[${lno}]="-------------"
    printf '%s\n' "${array[@]}" > /tmp/mylog
    unset lno array
  fi
  printf '%s\n' "${line}"
done

# terminal window 2
tail -f /tmp/mylog


#------------------------


# sample code 2

# code taken from: 
# https://stackoverflow.com/questions/6702474/bash-best-architecture-for-reading-from-two-input-streams
# terminal window 1

# server
(
rm -f /tmp/to /tmp/from
mkfifo /tmp/to /tmp/from
while true; do 
  while IFS="" read -r -d $'\n' line; do 
    printf '%s\n' "${line}"
  done </tmp/to >/tmp/from &
  bgpid=$!
  exec 3>/tmp/to
  exec 4</tmp/from
  trap "kill -TERM $bgpid; exit" 0 1 2 3 13 15
  wait "$bgpid"
  echo "restarting..."
done
) &
serverpid=$!
#kill -TERM $serverpid

# client
(
exec 3>/tmp/to;
exec 4</tmp/from;
while IFS="" read -r -d $'\n' <&4 line; do
  if [[ "${line:0:1}" == $'\177' ]]; then 
    printf 'line from stdin: %s\n' "${line:1}"  > /dev/null
  else       
    printf 'line from fifo: %s\n' "$line"       > /dev/null
  fi
done &
trap "kill -TERM $"'!; exit' 1 2 3 13 15
while IFS="" read -r -d $'\n' line; do
  # can we make it atomic?
  # sleep 0.5
  # dd if=/tmp/to iflag=nonblock of=/dev/null  # flush fifo
  printf '\177%s\n' "${line}"
done >&3
) &
# kill -TERM $!


# terminal window 2
# tests
echo hello > /tmp/to
yes 1 | nl > /tmp/to
yes 1 | nl | tee /tmp/to
while sleep 2; do date '+%Y-%m-%d_%H.%M.%S'; done 2>&1 | tee -a /tmp/to


# terminal window 3
cat /tmp/to | head -n 10
3
chad

Se il tuo processo scrive su qualsiasi file di log e poi cancella il file e ricomincia ogni tanto, quindi non diventa troppo grande, o usa logrotate

tail --follow=name --retry my.log

É tutto quello di cui hai bisogno. Otterrai tutto lo scroll-back del tuo terminale. 

Niente di non standard è necessario. Non l'ho provato con i file di registro piccoli, ma tutti i nostri registri ruotano in questo modo e non ho mai notato perdere le linee. 

2
teknopaul

La registrazione può essere diretta a un socket UDP. Dal momento che UDP è senza connessione, non bloccherà il programma di invio. Naturalmente i log andranno persi se il ricevitore o la rete non riescono a tenere il passo.

myprogram 2>&1 | socat - udp-datagram:localhost:3333

Quindi, quando si desidera osservare la registrazione:

socat udp-recv:3333 -

Ci sono altri vantaggi interessanti come l'essere in grado di collegare più ascoltatori allo stesso tempo o trasmetterli a più dispositivi.

0
wally