Если вы читаете эту статью, значит, вы пользуетесь интернетом. А
интернет – это компьютерная сеть. Никогда не задумывались, как сети
работают? Ну, то есть, понятно, что есть кабели, которыми компьютеры
связываются между собой, еще какое-то оборудование (по секрету, это т.н.
активное сетевое оборудование, коммутаторы, маршрутизаторы и т.п.), но
это физическая сторона сети, а как происходит общение между двумя
компьютерами?
Для этого у них есть свои особые языки – протоколы, и если люди при
встрече могут сказать «Привет! Как дела?», а в другой раз просто
«Привет» (ну, нет сегодня настроения заводить долгую беседу), то языки
компьютерных сетей – протоколы – такого не допускают. Беседы должны быть
«протокольными» (простите за каламбур), т.е. каждый из них четко знает,
что он должен сказать, и приблизительно ожидает, что ему ответят. Если
на вопрос «Как погода?» в ответ придет «Меня зовут Саша» - разговор этот
не сладится.
В этом и суть протокола: процесс установки соединения для передачи
данных четко оговорен давным-давно и изложен в RFC (Request for
Comments), все эти документы есть в свободном доступе, большая часть из
них переведена на русский язык, и с ними можно ознакомиться, например, здесь (английские оригиналы) или здесь (русский репозиторий).
Один из этих документов описывает протокол Echo, суть которого, как
следует из его названия («Эхо») – дублировать полученные от клиента
данные. Для чего? Об этом было бы лучше спросить создателя протокола и
автора соответсвующего RFC862 (http://rfc.com.ru/rfc862.htm)
Джона Постела (J.Postel), но для нас в рамках этой обзорной статьи, это
не столь уж и важно (да и почил он ныне). Но можно предположить, что
протокол этот пригодится при тестировании соединения, например, мы
что-то послали серверу, и нам в ответ это же и вернулось, значит там
есть-таки кто-то живой.
Теперь перейдем к самому важному и интересному. Напишем свой
echo-клиент. Этот клиент будет отправлять на указанный нами echo-сервер
строки, которые мы будем вводить, и выводить полученные от сервера
данные, т.е. те же самые строки, а в конце – для проверки – выведет еще и
количество посланных и принятых байтов, чтобы мы, увидев, что числа эти
равны, окончательно убедились, что это было живое общение с сервером, а
не с собой.
Итак, писать свой echo-клиент мы будем на perl`е. Почему именно на
нем? Во-первых, perl идеально подходит для написания сетевых програм, в
нем уже есть мощная поддержка сетевого стека TCP/IP, а при необходимости
модули для получения дополнительного функционала ваших программ всегда
можно загрузить на сайте CPAN. Ну, и во-вторых, потому что я сам немного пишу на этом языке.
Приступим. Писать код можно в любом текстовом редакторе, если хотите, даже в Блокноте. Но лично я предпочитаю gvim, одним из неоспоримых достоинств которого является проверка синтаксиса многих языков программирования, в том числе и perl`а.
Для начала подключим необходимые модули:
Для проверки синтаксиса
use strict;для работы с сетевыми сокетами Berkeley
use Socket;
для использования функции autoflush, что позволит передавать данные сразу же, не копируя их в буфер.
use IO::Handle;
Теперь
опишем переменные. ($bytes_out,$bytes_in – это счетчики соответственно
отправленных и принятых байтов. Пока они у нас обнулены, мы еще ничего в
сеть не отправляли и ничего оттуда не получили.
my ($bytes_out,$bytes_in) = (0,0);$host
– это ip-адрес того echo-сервера, к которому му будем стучаться. Мы
получаем этот адрес либо как параметр программы при запуске (shift),
либо, если он не был введен, используем наш компьютер в качестве
echo-сервера. Здесь нужно оговориться, что операционная система Windows
не имеет своего встроенного echo-сервера, поэтому, чтобы проверить
работу этого скрипта, мне пришлось включить echo-сервер на одном из моих
серверов под управлением ОС FreeBSD.
my $host = shift || 'localhost';
$port
– сетевой порт, на которм слушает и ждет соединения echo-сервер. Мы
либо вводим номер порта, либо он назначается автоматически как результат
функции getservbyname, которой передается символическое имя протокола –
echo и имя протокола нижнего уровня – tcp – через который и работает
echo.
my $port = shift || getservbyname ('echo','tcp');
$protocol – номер порта tcp, полученный преобразованием его имени функцией getprotobynamе
my $protocol = getprotobyname ('tcp');
Преобразуем
ip-адрес нашего сервера в двоичную форму функцией inet_aton. В случае
ошибки преобразования будет выведено соответствующее предупреждение и
программа аварийно завершится.
$host = inet_aton($host) or die "$host: unknown host";
Открываем
сокет – канал межсетевого взаимодействия на логическом уровне. Для
этого функции socket передаем четыре параметра: имя сокета, чтобы его в
дальнейшем идентифицировать (мы ведь можем открыть несколько сокетов
одновременно, именно так мы и работаем в интернете: через один сокет
странички смотрим, через другой с FTP-сервера программу качаем и т.д.),
второй параметр – т.н. домен протоколов, для нас тут важно просто знать,
что мы используем стек TCP/IP, поэтому имя домена протокола - AF_INET,
третий параметр – тип протокола – потоковый (TCP) или дейтаграммный
(UDP), мы используем TCP – поэтому пишем SOCK_STREAM, последний –
protocol – номер порта TCP. Опять же, в случае проблем – вывод ошибки и
выход из программы.
socket (SOCK, AF_INET, SOCK_STREAM, $protocol) or die "Can`t open socket: $!";
Определяем структуру destination_address, состоящую из пары двоичных чисел – порт и адрес хоста.
my $destination_address = sockaddr_in($port,$host);
Все подготовительные этапы пройдены, можем открывать соединение.
connect (SOCK, $destination_address) or die "Connection failed: $!";
Включаем для нашего открытого сокета автосброс – будем слать напрямую, минуя буфер.
SOCK->autoflush(1);
Теперь, собственно, сам процесс общения с сервером.
Цикл
будет работать, пока мы не выйдем, например, через Ctrl+D (в Windows)
или Ctrl+Z (в UNIX). В цикле читается стандартный ввод – клавиатура, и
все, что введено, отправляется в открытый сокет SOCK – т.е. на
echo-сервер, который мы определили.
while (my $message_out = <>){
print SOCK $message_out;
Теперь читаем из сокета то, что пришло в ответ
my $message_in = <SOCK>;
Печатаем это в стандартный вывод – на экран
print $message_in;
Подсчитываем количество принятых и отправленных байтов
$bytes_in +=length($message_in);
$bytes_out +=length($message_out);
}
Закрываем сокет
close SOCK;
Печатаем в стандартный вывод ошибок количество принятых и отправленных байтов
print STDERR "bytes_sent = $bytes_out, bytes_received = $bytes_in\n";
Вот и все. Только что мы написали простейший сетевой клиент. Простейший
он потому, что сам протокол, реализацию которого мы изобразили, простой
донельзя. Конечно же, почтовый клиент или FTP-клиент будут заметно
сложнее, но об этом мы поговорим как-нибудь в другой раз.