Игры с сетевыми котами: изобретем /usr/bin/yes еще раз
Автор: (C) Жаовай [zhaoway]
Перевод: (C) Сергей Скороходов

Кошка Нет и и кошка Да [Netcat and Yescat]

Первая (но не главная) задача этой статьи: познакомить вас с прелестной сетевой "тулзой": /usr/bin/netcat, которую можно без труда найти в одноименном пакете Debian GNU/Linux (упражнение: выполните apt-get install netcat -- и готово!). Автор программы, пожелавший остаться неизветстным, снабдил ее хорошо написанной документацией, на основе которой мои коллеги-разработчики Debian сделали прекрасно отформатированную страницу руководства Unix. Читать эту документацию -- одно удовольствие. Благородному читателю наверняка придет в голову, что да, действительно есть такие существа -- UNIX гуру -- живущие где-то там, в Дальнем Мире. И они, исключительно из харерских побуждений, сохраняют анонимность после написания столь превосходного программного обеспечения. Только подлинные гуру Unix'а могут так поступить!

Раз уж документация по netcat так хороша, я ее здесь повторять не буду. Однако, я советую всем прочесть ее, прежде чем приступать к этой статье. Для нетерпеливых же сообщаю: netcat может перенаправлять поток данных из стандартного ввода в TCP или UDP сокет, а из TCP или UDP сокета -- в стандартный вывод. Точно так, как команда cat перенаправляет из стандартного ввода [stdin] в стандартный вывод [stdout]. По непроверенным данным, именно от этой утилиты произошло название программы netcat.

Вторая, но зато основная задача этой статьи -- показать, сколь скучным и невежественным можте быть автор статей (вроде меня) при описании программы без графического интерфейса пользователя или интерактивной справки. Если я типа не могу "заграбастать" хотя бы пары скриншотов -- то я просто шизею!

А теперь, для целей, которые станут понятными далее, мы представим пикантную программку /usr/bin/yes. Почти никто не обращает на нее внимание. А она тихонько лежит там, в уголке /usr/bin уже так давно, что едва ли кто-либо из нас, недавно пришедших в мир Linux, хотя бы подозревает о ее присутствии в системе. Происхождение этой программы покрыто тайной. А популярность у нее -- не меньше, чем у /sbin/init! Что она делает? Давайте посмотрим:

zw@q ~ % yes
y
y
y
y
y
y
y

Ну, разве не чудо? ;-) (Нажмите ctrl-C для того, чтобы остановить поток 'y'-ов, иначе они будут вечно маршировать по экрану.) Между прочем, программа может сказать и нет!

zw@q ~ % yes no
no
no
no
no
no

В последующих разделах мы разработаем две вспомогательные утилиты, с помощью которых мы сделаем свой вариант /usr/bin/yes, естественно с помощью /usr/bin/netcat! В путь!

Hub и cable

Источником вдохновения при создании утилит hub (hub.c) и cable (cable.c), безусловно, явился netcat, пересылающий поток данных от сокета к стандартному выводу и со стандартного входа -- на сокет. Разве я забыл порекомендовать прочесть сопровождающую netcat документацию? ;-) Hub задуман, как сервер, а cable -- как клиент. Вместо того, чтобы перенаправлять данные между stdin, stdout и сокетами, hub и cable направляют и мультиплексируют данные от сокета на другие сокеты. Вот откуда взялись их имена. Они работают, как hub и cable в Ethernet. Взгляните на скриншот. О-о, скриншот! ;-)

zw@q ~ % ./hub
Лаборатория Сетевых Колыбельных: hub (сервер) $Revision: 1.5 $
Copyright (C) 2001  zhaoway <[email protected]>

Использование: hub [размер буфера] [номер порта tcp] [число портов в "хабе"]

o размер буфера указывается в байтах. например 10240.
o номер порта tcp должен быть не меньше 1024 для того, чтобы не требовались права root'а.
o в хабе должно быть не менее двух портов. будьте счасливы.
zw@q ~ %

Hub слушает порты TCP также, как работает Ethernet hub. Данные, поступающие с одного порта hub'а, будут направлятся на остальные его порты. С помощью netcat можно протестировать hub не прибегая к cable. Учтите: nc -- это сокращение для netcat.

  1. Запустите hub в консоли. Вот так: ConA % ./hub 10240 10000 2
  2. Из консоли B подсоединитесь netcat'ом: ConB % nc localhost 10000
  3. Из консоли C подключите еще один netcat: ConC % nc localhost 10240
  4. Можете печатать текст в ConC, а читать в ConB и наоборот.

А теперь займемся cable:

zw@q ~ % ./cable
Лаборатория Сетевых Леденцов: cable ( клиент) $Revision: 1.14 $
Copyright (C) 2001  zhaoway <[email protected]>

Использовать: cable [размер буфера] [1-й ip] [1-й порт] [2-1 ip] [2-й порт] ...

o размер буфера указывается в байтах. например 10240.
o порты должны прослушиваться, иначе попытка соединения закончится неудачей.
o должно быть указано по меньшей мере два набора ip:port.
zw@q ~ %

Cable работает примерно как разделямый коаксиальный кабель Ethernet. Он перенаправляет и мультиплексирует данные между слушающими демонами сокетов. Давайте испытаем и его.

  1. Запускаем демон netcat в ConA: ConA % nc -l -p 10000
  2. Запускаем другой демон netcat в ConB: ConB % nc -l -p 10001
  3. Организуем cable: ConC % ./cable 10240 127.0.0.1 10000 127.0.0.1 10001
  4. Теперь можно вводить текст в ConA, а читать в ConB и наоборот.

В разработке hub и cable применены любопытные приемы. Особенно советую обратить внимание на вызов функции select(). А пока сосредоточимся на повторном измобретении /usr/bin/yes ;-).

Изобретаем колесо заново

Не так-то просто с помощью netcat, hub и cable изобрести /usr/bin/yes еще раз. Могу намекнуть: поэтому мне и потребовалось устанавливать размер буфера в аргументе командной строки. Тем не менее, приступим!

Основная идея такова. Сначала мы настроем трех-портовый hub, потом соединим два "хаба" с помощью "кабеля", а затем мы сможем использовать netcat для того, чтобы повторять любой символ в свободные порты "хаба". Как на диаграмме:

            |        кабель-cable
           \|/        ,---------,
            |         |         |
            V         V         V
        ,--[ ]-------[ ]-------[ ]--.
        |   A         B         C   |
        | трехпортовый хаб-hub      |
        ---------------------------'

Природа hub'а такова, что данные, посылаемые на порт А будут перенаправлены на порты B и C, а так как порты B и C соеденены кабелем, данные, поступившие из "хаба" отправятся равнехонько назад и будут мультеплексированы и направлены в порт А, циркулируя в петле кабеля до бесконечности. В конце концов порт А получит бесконечное число копий изначально введенных данных.

Соберем устройство.

  1. В ConA запускаем трехпортовый "хаб": ConA % ./hub 10240 10000 3
  2. В ConB замыкаем "кабель": ConB % ./cable 10240 127.0.0.1 10000 127.0.0.1 10000

Теперь, когда мы завершили сборочные работы, можно с помощью netcat наконец-то завершить наше повторное изобретение /usr/bin/yes.

ConC % echo "y" | nc localhost 10000
y
y
y
y
y
y

Хитрое упражнение для читателя: что случится, если мы изменим размер буфера (и для hub, и для cable) с 10240 байт до 1 байта? Можете попробовать сами.

Так что не скучайте и удачи!

Код примеров

Клиент cable

/* -*- C -*-
 * Лаборатория Сетевых Колыбельных: cable (клиент)
 *
 * Copyright (C) 2001  zhaoway <[email protected]>
 *
 * $Id: cable.c,v 1.15 2001/12/14 01:33:55 zw Exp $
 *
 * Комприляция: gcc -Wall -g -o cable cable.c
 *
 * Pri perevode ispol'zovalas' kodirovka KOI8-R
 */

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/socket.h>

void banner(void)
{
  printf("Лаборатория Сетевых Колыбельных: cable (клиент) $Revision: 1.14 $\n"
  "Copyright (C) 2001  zhaoway <[email protected]>\n\n");
}

void usage(void)
{
  banner();
  printf("Использование: cable [размер буфера] [1-й ip] [1-й порт] [2-й ip] [2-й порт] ..\n\n"
  "o размер буфера указывается в байтах. например 10240.\n"
  "o порты должны прослушиваться, иначе попытка соединения закончится неудачей.\n"
  "o должно быть указано по меньшей мере два набора ip:port.\n");
}

/* При неудаче возвращает -1. */
int make_socket(struct sockaddr_in *servaddr)
{
  int sockfd;

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (connect(sockfd, (struct sockaddr *) servaddr, sizeof(struct sockaddr_in)) == -1)
    {
      perror("connect");
      return -1;
    }
  return sockfd;
}

/* Возвращает число открытых портов, в случае неудачи оно < 2. */
int cmdline(int argc, char *argv[], int *sockfd[], int *maxline)
{
  struct sockaddr_in *servaddr, tmpaddr;
  int ports = 2, i;

  /* У нас должно быть по меньшей мере два соединения. */
  servaddr = malloc(ports * sizeof(struct sockaddr_in));
  if (servaddr == NULL)
    {
      fprintf(stderr, "не хватило памяти!\n");
      return 1;
    }
  memset(servaddr, 0, sizeof(servaddr));
  for (i = 0; i < ports; i++) servaddr[i].sin_family = AF_INET;
  if (argc < 6
      || (*maxline = strtol(argv[1], NULL, 10)) == 0
      || (inet_pton(AF_INET, argv[2], &servaddr[0].sin_addr)) <= 0
      || (servaddr[0].sin_port = htons(strtol(argv[3], NULL, 10))) == 0
      || (inet_pton(AF_INET, argv[4], &servaddr[1].sin_addr)) <= 0
      || (servaddr[1].sin_port = htons(strtol(argv[5], NULL, 10))) == 0)
    {
      usage();
      return 0;
    }
  banner();
  for ( ; ; )
    {
      if ((argc -= 2) < 6) break;
      memset(&tmpaddr, 0, sizeof(struct sockaddr_in));
      if ((inet_pton(AF_INET, argv[ports * 2 + 2], &tmpaddr.sin_addr)) <= 0)
 break;
      if ((tmpaddr.sin_port = htons(strtol(argv[ports * 2 + 3], NULL, 10))) == 0)
 break;
      servaddr = realloc(servaddr, (ports + 1) * sizeof(struct sockaddr_in));
      servaddr[ports].sin_addr = tmpaddr.sin_addr;
      servaddr[ports].sin_port = tmpaddr.sin_port;
      ports++;
    }
  *sockfd = malloc(ports * sizeof(int));
  if (*sockfd == NULL)
    {
      fprintf(stderr, "не хватило памяти!\n");
      return 1;
    }
  for (i = 0; i < ports; i++)
    {
      if (((*sockfd)[i] = make_socket(&servaddr[i])) == -1)
 {
   if (i >= --ports) break;
   servaddr[i] = servaddr[ports];
   i--;   /* retry */
 }
    }
  return ports;
}

int main(int argc, char *argv[])
{
  int *sockfd, ports, size, maxline, i, j;
  char *buff;
  fd_set rset, rset_memo, wset, wset_memo;
  struct timeval nowait = { 0, 0 };

  if ((ports = cmdline(argc, argv, &sockfd, &maxline)) < 2) return 1;
  buff = (char *) malloc(maxline * sizeof(char));
  if (buff == NULL)
    {
      fprintf(stderr, "не хватило памяти!\n");
      return 1;
    }
  FD_ZERO(&rset_memo);
  FD_ZERO(&wset_memo);
  for (i = 0; i < ports; i++)
    {
      FD_SET(sockfd[i], &rset_memo);
      FD_SET(sockfd[i], &wset_memo);
    }
  /* Ignore this to receive EPIPE. */
  signal(SIGPIPE, SIG_IGN);
  /* Main loop. */
  for ( ; ; )
    {
      if (ports < 2)
 {
   fprintf(stderr,
    "кабель поврежден и только %i соединение(я) по прежнему отркрыто(ы)\n", ports);
   return 1;
 }
      /* Готовимся читать. */
      rset = rset_memo;
      /* С какого-нибудь порта посылаются данные? */
      if ((select(FD_SETSIZE, &rset, NULL, NULL, &nowait)) <= 0) continue;
      /* Ищем порт, из которого можно читать. */
      for (i = 0; i < ports; i++)
 {
   /* Можно ли что-нибудь прочесть из этого порта? */
   if (! FD_ISSET(sockfd[i], &rset)) continue;
   /* А если не получится, то... */
   else if ((size = recv(sockfd[i], buff, maxline, 0)) == -1)
     {
       perror("recv err");
       return errno;
     }
   /* А если мы прочли в точности ничего? */
   else if (size == 0) continue;
   /* Что-то прочли, приготовимся к записи. */
   wset = wset_memo;
   /* А если писать мы не можем? */
   if ((select(FD_SETSIZE, NULL, &wset, NULL, &nowait)) <= 0)
     {
       fprintf(stderr, "Кабель \"поломатый\", с записью ничего не вышло!\n");
       return 2;
     }
   /* Write. */
   for (j = 0; j < ports; j++)
     {
       /* Не пишите ответов в порт, посылающий данные. */
       if (j != i && FD_ISSET(sockfd[j], &wset))
  {
    /* Проверим возможность записи. */
    if ((send(sockfd[j], buff, size, 0)) == -1)
      {
        if (errno == EPIPE)
   {
     FD_CLR(sockfd[j], &wset);
     FD_CLR(sockfd[j], &rset);
     close(sockfd[j]);
     sockfd[j] = sockfd[--ports];
   }
        else
   {
     perror("send err");
     return errno;
   }
      }
  }
     }
 } /* Цикл, ищущий порт, из которого можно читать. */
    } /* Главный цикл. */
}

Сервер hub

/* -*- C -*-
 * Лаборатория Сетевых Колыбельных: cable (клиент)
 *
 * Copyright (C) 2001  zhaoway <[email protected]>
 *
 * $Id: hub.c,v 1.6 2001/12/14 01:34:41 zw Exp $
 *
 * Компиляция: gcc -Wall -g -o hub hub.c
 *
 * Pri perevode ispol'zovalas' kodirovka KOI8-R
 */

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>

void banner(void)
{
  printf("Лаборатория Сетевых Колыбельных: hub (сервер) $Revision: 1.6 $\n"
  "Copyright(C) 2001  zhaoway <[email protected]>\n\n");
}

void usage(void)
{
  banner();
  printf("Использование: hub [размер буфера] [номер порта tcp] [число портов в хабе]\n\n"
  "o размер буфера указывается в байтах. например 10240.\n"
  "o номер порта tcp должен быть не меньше 1024 для того, чтобы не требовались права root\'а.\n"
  "o в хабе должно быть не менее двух портов. будьте счасливы.\n");
}

/* При неудаче возвращает -1. */
int make_socket(short int port)
{
  int listenfd, val;
  struct sockaddr_in servaddr;

  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(port);
  if ((bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr))) == -1)
    {
      perror("bind err");
      return -1;
    }
  listen(listenfd, 1);
  /* Установим сокет неблокирующим. */
  val = fcntl(listenfd, F_GETFL);
  fcntl(listenfd, F_SETFL, val | O_NONBLOCK);
  return listenfd;
}

int main(int argc, char *argv[])
{
  int listenfd, *connfd, maxline, size, port, ports, i, j;
  fd_set rset, rset_memo, wset, wset_memo;
  char *buff;
  struct timeval nowait = { 0, 0 };

  if (argc != 4
      || (maxline = strtol(argv[1], NULL, 10)) == 0
      || (port = strtol(argv[2], NULL, 10)) == 0
      || (ports = strtol(argv[3], NULL, 10)) < 2)
    {
      usage();
      return(0);
    }
  banner();
  buff = (char *) malloc(maxline * sizeof(char));
  if (buff == NULL)
    {
      fprintf(stderr, "не хватило памяти!\n");
      return 1;
    }
  if ((listenfd = make_socket(port)) == -1)
    {
      usage();
      return -1;
    }
  printf("Hub слушает на порте TCP %i\n", port);
  connfd = malloc(ports * sizeof(int));
  if (connfd == NULL)
    {
      fprintf(stderr, "не хватило памяти!\n");
      return 1;
    }
  for (i = 0; i < ports; i++)
    {
      connfd[i] = -1;
    }
  FD_ZERO(&rset_memo);
  FD_ZERO(&wset_memo);
  /* Игнорируем получение EPIPE. */
  signal(SIGPIPE, SIG_IGN);
  /* Главный цикл. */
  for ( ; ; )
    {
      for (i = 0; i < ports; i++)
 {
   if (connfd[i] == -1)
     {
       connfd[i] = accept(listenfd, NULL, NULL);
       if (connfd[i] != -1)
  {
    FD_SET(connfd[i], &rset_memo);
    FD_SET(connfd[i], &wset_memo);
  }
     }
 }
      /* Подготовка к чтению. */
      rset = rset_memo;
      /* Нечего читать? */
      if ((select(FD_SETSIZE, &rset, NULL, NULL, &nowait)) <= 0) continue;
      /* Ищем порт, из которого можно что-нибудь прочесть. */
      for (i = 0; i < ports; i++)
 {
   /* Порт подсоединен и из него можно читать? */
   if (connfd[i] == -1 || ! FD_ISSET(connfd[i], &rset)) continue;
   /* Проблемы с чтением? */
   if ((size = recv(connfd[i], buff, maxline, 0)) == -1)
     {
       perror("recv err");
       return errno;
     }
   /* На самом деле ничего не прочитано? */
   if (size == 0) continue;
   /* Готовимся писать. */
   wset = wset_memo;
   /* Нет порта, открытого на запись? */
   if ((select(FD_SETSIZE, NULL, &wset, NULL, &nowait)) <= 0) continue;
   /* В цикле ищем любой пригодный для записи порт. */
   for (j = 0; j < ports; j++)
     {
       /* Не пишите в порт, если из него читается. */
       if (j == i || connfd[j] == -1 || ! FD_ISSET(connfd[j], &wset))
  continue;
       if ((send(connfd[j], buff, size, 0)) == -1)
  {
    if (errno == EPIPE)
      {
        FD_CLR(connfd[j], &wset);
        FD_CLR(connfd[j], &rset);
        close(connfd[j]);
        connfd[j] = -1;
      }
    else
      {
        perror("send err");
        return errno;
      }
  }
     }
 }
    } /* Главный цикл. */
}
Но хватит болтать, я обещал сделать "работу над ошибками". В статье про "сетевых котов" я допустил ошибку, в исходном коде было:Launch hub in the console A: ConA % ./hub 10240 10000 2 From console B, connect a netcat: ConB % nc localhost 10000 From console C, connect another netcat: ConC % nc localhost 10000 Then you could type in ConC and read the output in ConB, vice versa. А у меня:Запустите hub в консоли. Вот так: ConA % ./hub 10240 10000 2 Из консоли B подсоединитесь netcat'ом: ConB % nc localhost 10000 Из консоли C подключите еще один netcat: ConC % nc localhost 10240 Можете печатать текст в ConC, а читать в ConB и наоборот. На что и обратил внимание Павел Цибулин (надеюсь, не переврал фамилию, в письме она была в транслите):Обратите внимание на аргументы у вызовов nc Хотя лично я бы попробовал из ConB цепляться к порту 10001 Исправляюсь. Кроме того, Павел написал:Хотя по поводу статьи, как оригинала, как так и перевода есть некоторые мысли(шки): 1) Может быть Вы помните, но в дни моего далекого детства :-) был такой журнальчик "Юный техник", в котором статьи из цикла "делай с нами, делай..." просто изобиловали ошибками. Но эти ошибки не были по сути фатальными, как и в Вашем переводе. Но, если у человека было некоторое терпение и он доходил до сути сам, докапываясь "почему не работает, как написано", то его познания в этой области были все-таки значительно больше, чем у того, кто просто бездумно втыкнул, как есть - и вдруг оно все само собой заработало, как в MS XP :-)). Я не призываю делать насильственные ошибки, но может таки оно так и лучше, с учетом "отечественного менталитета"? 2) Лично я бы слегка переделал программу hub. Опять-же, это мое личное мнение: а) Вместо того, чтобы принимать n соединений по одному порту, я бы принимал по одному соединению на n последовательных портах, что IMHO более соответствует реальному хабу и более наглядно для применения cable, в какие дырки хаба он втыкается. Т.е. ./hub 10240 10000 3 слушал бы на трех портах, причем гнездо A соответствовало бы порту 10000, B - 10001, C - 10002 Иначе втыкать два конца кабеля в один порт 10000 - как-то странно... б) Приделал бы примитивную ASCII-мордочку, отображающую состояние хаба, какие в нем "дырочки" есть, в какие кабель воткнут и по каким идет реальная передача данных. Мне это кажется разумным (не про желательность ошибок, конечно:). Хотя я и не ожидал, что кто-то непременно попробует внимательно прочесть заметку, собрать программу и даже предложить ее улучшение:) И еще, другой читатель пожаловался, что у него примеры не собирались:( Я даже попросил прислать неработающий код мне -- все напрасно:( У меня собирается, у него -- нет. Ну, правда мы не слишком рогом уприрались. Так что не все так просто, как может показаться:) Сергей Скороходов

Жаовай [Zhaoway]

Жаовай живет в Наньдзине, в Китае. Свое время он делит между прелестной подружкой, стареьнким "пнем" и чистой математикой. (Он занимается "высшематематическим самообразованием", так что если у вас завалялось несколько лишних, но драгоценных марок и/или книг по высшей математике, то не стесняйтесь послать ему копию). Еще он на добровольной основе участвует в проекте Debian GNU/Linux.


Copyright (C) 2002, zhaoway.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 74 of Linux Gazette, January 2002

Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин

Со всеми предложениями, идеями и комментариями обращайтесь
к Сергею Скороходову ([email protected])

Сайт рассылки: http://gazette.linux.ru.net
Эту статью можно посмотреть по адресу: http://gazette.linux.ru.net/lg74/articles/rus-zhaoway.html