сетевое программирование в Unix. Часть 1
Полудохлому высшему техническому образованию Беларуси посвящается...
редакционное предисловие
Опять сокетное программирование?!? Сколько можно?!? — слышу я возгласы некоторых старых читателей СР.
Можно было бы, конечно, в ответ на это ограничиться старым добрым «повторением-мать-учением», но я все-таки приведу несколько развернутых доводов, почему мы решили опять (или снова :) вернуться к этой теме.
Да, кстати, СР вообще пора бы записать в Книгурекордов Гинесакак «самое неповторяющееся издание» — за все время существования журнала (а до журнала — одноименной газеты) мы возвращались к обсуждавшейся ранее теме буквально с десяток раз, при том что наши российские конкуренты занимаются этим каждые пару месяцев ;) Думаю, уже решительно наступило время кое в чем повториться, поскольку не все чистатели хранят подшивку СР за все годы (в редакции и то ранних номеров нет ;)
Но мы отвлеклись. Итак, почему опять про сокеты:
1. Потому, что каждый год армия IT-специалистов пополняется новобранцами за счет выпускников технических вузов (ну и самоучек, конечно). Про состояние дел с сетевым программированием в ВУЗах нам поведает автор в своем предисловии. Что же касается самоучек, то им приходится иметь дело либо с англоязычной документацией (не у всех, к сожалению, с английским ОК), либо с мутной и/или занудной русскоязычной.
2. А вот этот цикл материалов занудным как раз не назовешь: написано ЧЕЛОВЕЧЕСКИМ (а не «научно-техническим» языком, с забавными шутками и остроумными примерами/аллегориями. Даже знакомым с предметом рекомендую почитать просто как худ. литературу.
3. Остальное см. в авторском предисловии :)
предисловие
На этот труд меня подвигло несколько причин, сложно сказать какая стала решающей. То ли получение моим дедом литературной премии финского культурного общества имени М.А. Кастрена. То ли дебильный курсовой, который пришлось выполнять по просьбе супруги. Скорее всего последней каплей стал прочувствованный рассказ глав. ред. "Сетевых решений" с лейтмотивом "А ты записался на фронт борьбы с Деникиным?" (речь шла о нехватке квалифицированных кадров в "СР" :)
Итак, что это будет? Это будет рассказ о программировании, программистах, unix-е и сетях. Авторская тут только подача материала, первоисточники будут указаны в конце. 100% плагиат, но не все в состоянии купить/прочитать монографии Стивенса, а белорусские ВУЗы (из мне известных БГУ, БГУИР, БНТУ, etc) этому почти не учат, ибо некому.
Кстати, все здесь написанное войдет целиком в БГУИРовский учебник по Unix (если он когда-нибудь будет дописан).
Рассказ коснется интерфейса сокетов, UNIX API, многозадачности, многопоточности, основ создания серверных приложений, коснусь, конечно, кое-каких культурных особенностей, которые делают Unix Unix'ом. Примеры написаны на Си. Примеры проверяются на Debian GNU/Linux и (по мере возможностей) на FreeBSD 4.9. Постараюсь не перегружать изложение деталями, дам только самые необходимые концепции и системные вызовы.
начальные сведения
Сокеты (sockets) впервые появились в начале 80-х в составе 4.2BSD как программный интерфейс к стеку TCP/IP. С тех пор их часто называют "BSD сокеты". И до и после были попытки создавать сетевые API, но сокеты прижились лучше всего. Возможно потому что при их создании учитывались характерные особенности Unix как среды программирования: файла, как основы взаимодействия процессов и принципа "keep it simple stupid".
Кстати, в русскоязычной литературе и сленге имеется множество вариантов перевода и произношения термина "socket": сОкет, сокЕт, гнездо (!!!) и т.п. словотворчество. Весь этот зоопарк обозначает одно и то же.
В основе сокетов лежит модная концепция "клиент-сервер". Сервер реализует некоторый сервис, клиент его использует. Два процесса, используя сеть, обмениваются данными. При создании соединения, с каждой из сторон создается сокет. Протоколы TCP/IP отвечают за передачу между ними данных. Чтобы определить кто и куда передает информацию, бы ли введены два понятия: адрес и порт. Адрес — сетевой адрес компьютера, порт — это номер сокета на конкретном компьютере. Причем каждый порт связан с конкретным процессом и поэтому операционная система знает кому отдавать пришедшие данные. Сокет — это пара адрес:порт.
В итоге — передача данных использует 2 сокета, по одному на каждого участника обмена информацией. Между ними устанавливается виртуальный канал и нет больше об этом заботы в чешуйчатой зеленой голове программиста. Что вписано в сокет на одной стороне, то появится на другой, и наоборот.
Для иллюстрации: поглядеть 'netstat -natp' (в linux).
Работа с сокетом идет через файловый дескриптор, что очень характерно для Unix.
Однако есть 2 вида передачи информации: с предварительным соединением и без. Мы подробно прожуем и проглотим оба вида передачи. Так же очень основательно будет рассмотрен серверный и клиентский режимы работы.
Согласно общепринятой нотации, за названиями системных вызовов (syscall's) и библиотечных функций будет указываться номер раздела man в скобках (например socket(2) или printf(3) ).
все есть файл
Эта концепция заслуживает отдельного упоминания.
Работа с сокетом ведется через дескриптор, являющийся также и дескриптором файла. То есть уже открытое соединение ничем не отличается с точки зрения программиста от открытого файла или терминала. Разработчик работает с сокетом теми же методами, которыми он работает с обычным (regular) файлом. Несколько отклоняясь от темы, замечу, что то же самое мы наблюдаем в отношении и других специальных типов файлов (pipe, fifo, block и character devices).
Это подымает термин «файл» на совершенно новый уровень понимания: "мы работаем с этим как с файлом и плевать, что там в реализации". Файл может быть физическим устройством, сокетом, каналом, каталогом, текстовым файлом, но мы только открываем их по разному. Чтение/запись и управление режимами работы — через одни и те же системные вызовы.
Потрясающий по своей красоте и выразительности пример: запись компакт диска по сети в 1 строку.
$cat 4.9-i386-disc1.iso|ssh user@host.localdomain cdrecord -
Здесь используется сразу многие типы файлов, объединенных командой shell в единое целое.
cat читает файл с диска и выводит на экран. Оболочка перенаправляет вывод cat через pipe на вход команды ssh.
ssh запускает на удаленном хосте команду записи дисков. Причем таким образом чтобы cdrecord кушал данные со стандартного ввода.
ssh доставляет ему этот ввод через сокет. Еще один неявный момент — cdrecord использует файл блочного устройства резака для записи полученного образа компакта. Подсчитаем: regular file, pipe, socket, block device — 4 различных вида файлов.
Нашему примеру можно придать изящный финальный штрих — избавится от временного файла в лице образа компакта и создавать его на лету:
$mkisofs /home/user2/for_write|ssh user@host.localdomain cdrecord -
Нет временных файлов, только безостановочное движение потока данных из одного состояния в другое, подкрепленное могучей концепцией "бывают разные файлы, но мы работаем с ними одинаково, просто пишем и читаем".
передача с предварительным соединением и без
Есть две модели передачи информации посредством сокетов. С установлением соединения и без оного.
Мы в основном будем останавливаться на первой.
1. Наиболее важные отличия сonnection-oriented соединения (назовем его условно TCP):
а) требует установления соединения перед передачей;
b) гарантирует доставку данных (создание виртуального канала);
c) передача идет потоком данных. TCP/IP гарантирует то что данные дойдут так, как посылались;
d) основан на протоколе TCP.
2. connectionless (условно назовем его UDP):
a) не требует соединения;
b) ничего не гарантирует;
c) передача идет пакетами, которые могут пропадать, дублироваться и т.п. Контроль за целостностью информации целиком в руках программиста;
d) основан на протоколе UDP.
Итого:
TCP имеет бОльшие накладные расходы чем UDP. Но UDP проще организован, и если нужна высокая скорость и компьютеры стоят в одном сегменте сети — то очень хорошо подойдет. В свою очередь, TCP больше подходит для работы по ненадежным каналам.
Прибегнув к скользкой аналогии,
UDP — это выстрел вслепую, пули могут пропасть, а могут и размножится по дороге.
TCP — телефон: что сказано с обеих сторон, то и услышано. Конечно если ворона не села на провод и не перекусила его.
основные системные вызовы
Примечание: в Solaris, унаследовавшей реализацию сокетов от System V Release 4, сокеты не входят в ядро, а реализованы библиотекой. Поэтому man по ним содержится в 3-м разделе.
Вот небольшой аннотированный список системных вызовов, тем или иным образом качающихся сокетов. В списке есть как socket-specific, так и общие вызовы, связанные с файлами.
accept(2) — принять соединение ( используется сервером);
bind(2) — связать сокет с конкретным адресом (обычно используется только сервером);
connect(2) — соединиться с удаленным сервером (клиентский);
close(2) — закрыть файл или сокет;
listen(2) — начать прослушивание сокета (серверный);
read(2) — чтение данных из файла;
recv(2) — чтение из сокета;
select(2) — проверка изменения статуса открытых файлов;
send(2) — послать данные через сокет;
shutdown(2) — закрыть соединение;
socket(2) — создать сокет;
write(2) — запись данных в файл.
заключение
Следующим разделом пойдет наполнение этой краткой теории конкретными примерами. Будут внимательно прожеваны и проглочены примеры реализации клиентов и серверов, как tcp, так и udp.
mend0za.
обсуждение статьи
редакционное предисловие
Опять сокетное программирование?!? Сколько можно?!? — слышу я возгласы некоторых старых читателей СР.
Можно было бы, конечно, в ответ на это ограничиться старым добрым «повторением-мать-учением», но я все-таки приведу несколько развернутых доводов, почему мы решили опять (или снова :) вернуться к этой теме.
Да, кстати, СР вообще пора бы записать в Книгурекордов Гинесакак «самое неповторяющееся издание» — за все время существования журнала (а до журнала — одноименной газеты) мы возвращались к обсуждавшейся ранее теме буквально с десяток раз, при том что наши российские конкуренты занимаются этим каждые пару месяцев ;) Думаю, уже решительно наступило время кое в чем повториться, поскольку не все чистатели хранят подшивку СР за все годы (в редакции и то ранних номеров нет ;)
Но мы отвлеклись. Итак, почему опять про сокеты:
1. Потому, что каждый год армия IT-специалистов пополняется новобранцами за счет выпускников технических вузов (ну и самоучек, конечно). Про состояние дел с сетевым программированием в ВУЗах нам поведает автор в своем предисловии. Что же касается самоучек, то им приходится иметь дело либо с англоязычной документацией (не у всех, к сожалению, с английским ОК), либо с мутной и/или занудной русскоязычной.
2. А вот этот цикл материалов занудным как раз не назовешь: написано ЧЕЛОВЕЧЕСКИМ (а не «научно-техническим» языком, с забавными шутками и остроумными примерами/аллегориями. Даже знакомым с предметом рекомендую почитать просто как худ. литературу.
3. Остальное см. в авторском предисловии :)
предисловие
На этот труд меня подвигло несколько причин, сложно сказать какая стала решающей. То ли получение моим дедом литературной премии финского культурного общества имени М.А. Кастрена. То ли дебильный курсовой, который пришлось выполнять по просьбе супруги. Скорее всего последней каплей стал прочувствованный рассказ глав. ред. "Сетевых решений" с лейтмотивом "А ты записался на фронт борьбы с Деникиным?" (речь шла о нехватке квалифицированных кадров в "СР" :)
Итак, что это будет? Это будет рассказ о программировании, программистах, unix-е и сетях. Авторская тут только подача материала, первоисточники будут указаны в конце. 100% плагиат, но не все в состоянии купить/прочитать монографии Стивенса, а белорусские ВУЗы (из мне известных БГУ, БГУИР, БНТУ, etc) этому почти не учат, ибо некому.
Кстати, все здесь написанное войдет целиком в БГУИРовский учебник по Unix (если он когда-нибудь будет дописан).
Рассказ коснется интерфейса сокетов, UNIX API, многозадачности, многопоточности, основ создания серверных приложений, коснусь, конечно, кое-каких культурных особенностей, которые делают Unix Unix'ом. Примеры написаны на Си. Примеры проверяются на Debian GNU/Linux и (по мере возможностей) на FreeBSD 4.9. Постараюсь не перегружать изложение деталями, дам только самые необходимые концепции и системные вызовы.
начальные сведения
Сокеты (sockets) впервые появились в начале 80-х в составе 4.2BSD как программный интерфейс к стеку TCP/IP. С тех пор их часто называют "BSD сокеты". И до и после были попытки создавать сетевые API, но сокеты прижились лучше всего. Возможно потому что при их создании учитывались характерные особенности Unix как среды программирования: файла, как основы взаимодействия процессов и принципа "keep it simple stupid".
Кстати, в русскоязычной литературе и сленге имеется множество вариантов перевода и произношения термина "socket": сОкет, сокЕт, гнездо (!!!) и т.п. словотворчество. Весь этот зоопарк обозначает одно и то же.
В основе сокетов лежит модная концепция "клиент-сервер". Сервер реализует некоторый сервис, клиент его использует. Два процесса, используя сеть, обмениваются данными. При создании соединения, с каждой из сторон создается сокет. Протоколы TCP/IP отвечают за передачу между ними данных. Чтобы определить кто и куда передает информацию, бы ли введены два понятия: адрес и порт. Адрес — сетевой адрес компьютера, порт — это номер сокета на конкретном компьютере. Причем каждый порт связан с конкретным процессом и поэтому операционная система знает кому отдавать пришедшие данные. Сокет — это пара адрес:порт.
В итоге — передача данных использует 2 сокета, по одному на каждого участника обмена информацией. Между ними устанавливается виртуальный канал и нет больше об этом заботы в чешуйчатой зеленой голове программиста. Что вписано в сокет на одной стороне, то появится на другой, и наоборот.
Для иллюстрации: поглядеть 'netstat -natp' (в linux).
Работа с сокетом идет через файловый дескриптор, что очень характерно для Unix.
Однако есть 2 вида передачи информации: с предварительным соединением и без. Мы подробно прожуем и проглотим оба вида передачи. Так же очень основательно будет рассмотрен серверный и клиентский режимы работы.
Согласно общепринятой нотации, за названиями системных вызовов (syscall's) и библиотечных функций будет указываться номер раздела man в скобках (например socket(2) или printf(3) ).
все есть файл
Эта концепция заслуживает отдельного упоминания.
Работа с сокетом ведется через дескриптор, являющийся также и дескриптором файла. То есть уже открытое соединение ничем не отличается с точки зрения программиста от открытого файла или терминала. Разработчик работает с сокетом теми же методами, которыми он работает с обычным (regular) файлом. Несколько отклоняясь от темы, замечу, что то же самое мы наблюдаем в отношении и других специальных типов файлов (pipe, fifo, block и character devices).
Это подымает термин «файл» на совершенно новый уровень понимания: "мы работаем с этим как с файлом и плевать, что там в реализации". Файл может быть физическим устройством, сокетом, каналом, каталогом, текстовым файлом, но мы только открываем их по разному. Чтение/запись и управление режимами работы — через одни и те же системные вызовы.
Потрясающий по своей красоте и выразительности пример: запись компакт диска по сети в 1 строку.
$cat 4.9-i386-disc1.iso|ssh user@host.localdomain cdrecord -
Здесь используется сразу многие типы файлов, объединенных командой shell в единое целое.
cat читает файл с диска и выводит на экран. Оболочка перенаправляет вывод cat через pipe на вход команды ssh.
ssh запускает на удаленном хосте команду записи дисков. Причем таким образом чтобы cdrecord кушал данные со стандартного ввода.
ssh доставляет ему этот ввод через сокет. Еще один неявный момент — cdrecord использует файл блочного устройства резака для записи полученного образа компакта. Подсчитаем: regular file, pipe, socket, block device — 4 различных вида файлов.
Нашему примеру можно придать изящный финальный штрих — избавится от временного файла в лице образа компакта и создавать его на лету:
$mkisofs /home/user2/for_write|ssh user@host.localdomain cdrecord -
Нет временных файлов, только безостановочное движение потока данных из одного состояния в другое, подкрепленное могучей концепцией "бывают разные файлы, но мы работаем с ними одинаково, просто пишем и читаем".
передача с предварительным соединением и без
Есть две модели передачи информации посредством сокетов. С установлением соединения и без оного.
Мы в основном будем останавливаться на первой.
1. Наиболее важные отличия сonnection-oriented соединения (назовем его условно TCP):
а) требует установления соединения перед передачей;
b) гарантирует доставку данных (создание виртуального канала);
c) передача идет потоком данных. TCP/IP гарантирует то что данные дойдут так, как посылались;
d) основан на протоколе TCP.
2. connectionless (условно назовем его UDP):
a) не требует соединения;
b) ничего не гарантирует;
c) передача идет пакетами, которые могут пропадать, дублироваться и т.п. Контроль за целостностью информации целиком в руках программиста;
d) основан на протоколе UDP.
Итого:
TCP имеет бОльшие накладные расходы чем UDP. Но UDP проще организован, и если нужна высокая скорость и компьютеры стоят в одном сегменте сети — то очень хорошо подойдет. В свою очередь, TCP больше подходит для работы по ненадежным каналам.
Прибегнув к скользкой аналогии,
UDP — это выстрел вслепую, пули могут пропасть, а могут и размножится по дороге.
TCP — телефон: что сказано с обеих сторон, то и услышано. Конечно если ворона не села на провод и не перекусила его.
основные системные вызовы
Примечание: в Solaris, унаследовавшей реализацию сокетов от System V Release 4, сокеты не входят в ядро, а реализованы библиотекой. Поэтому man по ним содержится в 3-м разделе.
Вот небольшой аннотированный список системных вызовов, тем или иным образом качающихся сокетов. В списке есть как socket-specific, так и общие вызовы, связанные с файлами.
accept(2) — принять соединение ( используется сервером);
bind(2) — связать сокет с конкретным адресом (обычно используется только сервером);
connect(2) — соединиться с удаленным сервером (клиентский);
close(2) — закрыть файл или сокет;
listen(2) — начать прослушивание сокета (серверный);
read(2) — чтение данных из файла;
recv(2) — чтение из сокета;
select(2) — проверка изменения статуса открытых файлов;
send(2) — послать данные через сокет;
shutdown(2) — закрыть соединение;
socket(2) — создать сокет;
write(2) — запись данных в файл.
заключение
Следующим разделом пойдет наполнение этой краткой теории конкретными примерами. Будут внимательно прожеваны и проглочены примеры реализации клиентов и серверов, как tcp, так и udp.
mend0za.
обсуждение статьи
Сетевые решения. Статья была опубликована в номере 12 за 2003 год в рубрике программирование