Добавить в избранное | Сделать стартовой страницей

Большая Linux библиотека для пользователей OS Linux и ПО для нее
Есть что сказать? Нужен совет? Посети наш форум.


При поддержке
Продвижение сайта
Продвижение сайта
Раскрутка сайта
Создание сайта
Оптимизация сайта
Интернет реклама
Аудит сайта
Администрирование сервера
настройка сервера
установка сервера
аренда сервера
Администрирование сервера
администрирование сервера
настройка сервера
аренда сервера
Rambler's Top100


Игры с сетевыми котами: изобретем /usr/bin/yes еще раз.

Автор : Жаовай [zhaoway]
Перевод : Сергей Скороходов

Первая (но не главная) задача этой статьи: познакомить вас с прелестной сетевой "тулзой": /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 

Использование: 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 

Использовать: 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 
 *
 * $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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void banner(void)
{
  printf("Лаборатория Сетевых Колыбельных: cable (клиент) $Revision: 1.14 $\n"
  "Copyright (C) 2001  zhaoway \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 
 *
 * $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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void banner(void)
{
  printf("Лаборатория Сетевых Колыбельных: hub (сервер) $Revision: 1.6 $\n"
  "Copyright(C) 2001  zhaoway \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-мордочку, отображающую состояние хаба, какие в нем "дырочки" есть, в какие кабель воткнут и по каким идет реальная передача данных. Мне это кажется разумным (не про желательность ошибок, конечно:). Хотя я и не ожидал, что кто-то непременно попробует внимательно прочесть заметку, собрать программу и даже предложить ее улучшение:) И еще, другой читатель пожаловался, что у него примеры не собирались:( Я даже попросил прислать неработающий код мне -- все напрасно:( У меня собирается, у него -- нет. Ну, правда мы не слишком рогом уприрались. Так что не все так просто, как может показаться:) Сергей Скороходов
Обсудить данную тему на нашем форуме "Все о Linux"