Руководство Beej по сетевому программированию, используя интернет-сокеты — Архив WASM.RU
Содержание:
- 1. Интро
- 1.1 Аудиенция
- 1.2 Платформа и Компилятор
- 1.3 Официальная страничка
- 1.4 Замечание для Solaris/SunOS программеров
- 1.5 Замечание для Windows программеров
- 1.6 Замечания по обратной связи
- 1.7 Зеркала
- 1.8 Замечание для переводчиков
- 1.9 Копирайт и Распространение
- 2 Что такое сокет?
- 2.1 Два Вида Интернет Сокетов
- 2.2 Low level Nonsense and Network Theory
- 3 struct и Управление Данными
- 3.1 Сконвертируй Natives!
- 3.2 IP адреса и Как Ими Управлять
- 4 Системные Вызовы
- 4.1 socket() -- Получи Файловый Дескриптор!
- 4.2 bind() -- На каком я порту?
- 4.3 connect() -- Эй, ты!
- 4.4 listen() -- Кто-нибудь мне позвонит?
- 4.5 accept() -- Спасибо, что обратились к порту 3490
- 4.6 send() и recv() -- Поговори со мной, крошка!
- 4.7 sendto() и recvfrom() -- Поговори со мной в DGRAM-стиле
- 4.8 close() и shutdown() -- Убирайся прочь!
- 4.9 getpeername() -- Кто ты?
- 4.10 gethostname() -- Кто я?
- 4.11 DNS -- Ты говоришь "whitehouse.gov", я же говорю "198.137.240.92"
- 5 Технология Клиент-Сервер
- 5.1 Простой Поточный Сервер
- 5.2 Простой Поточный Клиент
- 5.3 Datagram Сокеты
- 6 Более Продвинутые Техники
- 6.1 Блокировка
- 6.2 select() -- Синхронный I/O мультиплексинг
- 6.3 Управление Частичными send()
- 6.4 Сын Инкапсуляции Данных
- 7 Частые Вопросы
- 8 Дисклэймер и Помощь
1. Интро
Эй! Программирование сокетов тебя утомляет? Ты считаешь его чересчур сложным, чтобы изучать его по man страницам? Ты хочешь круто программировать под Инет, но у тебя нет времени, чтобы разобраться со всеми этими структурами, пытаясь выяснить, должен ли ты вызвать bind() перед connect(), итд, итд?
Что ж, знаешь что? Я уже сделал все эти мерзкие дела, и я хочу поделиться этой информацией со всеми! Ты пришел по нужному адресу. Этот документ даст среднему компетентному Си программеру необходимый ему/ей уровень, чтобы разобраться со всем этим сетевым шумом.
1.1 Публика
Этот документ был написан как туториал. Вероятнее всего, он наиболее подойдет тем, кто только начал разбираться с программированием сокетов и ищет надежную основу. Но это не полноценное руководство. Надеюсь, однако, что этого будет достаточно для того, чтобы понять все, что вы узнавали из man страниц...
1.2 Платформа и Компилятор
Код, приведенный в этом документе, был скомпилирован на Linux PC используя GNU gcc компилятор. Он подойдет и к любой иной платформе, которая использует gcc. На самом деле это не относится к тебе, если ты программируешь под Windows -- если так, то смотри раздел "Замечание для Windows программеров".
1.3 Официальная Страничка
Этот документ расположен в Калифорнийском Университете, Чико, по адресу http://www.ecst.csuchico.edu/~beej/guide/net/ .
1.4 Замечание для Solaris/SunOS программеров
Когда компилируете под Solaris или SunOS, вам необходимо указывать некоторые дополнительные ключи в командой строке для линковки с нужными библиотеками. Для того чтобы добиться этого, просто добавьте "-lnsl -lsocket -lresolv" к концу команды компилеру, как показано ниже:
$ cc -o server server.c -lnsl -lsocket -lresolv
Если вы все еще получаете ошибки, попробуйте далее добавить "-lxnet" к концу командной строки. Я не знаю в точности, для чего это, но некоторые нуждались в этом. Другая область, где вы можете натолкнуться на проблемы - это вызов setsockopt(). Его прототип различается от прототипа на моем Линуксе, поэтому вместо
int yes=1; попробуйте ввести этот вариант:
char yes='1';
Т.к. у меня нету Sun дистрибутива, то я не тестировал вышенаписанное -- это всего лишь советы, которые я получал по эл.почте.
1.5 Замечание для Windows Программистов
Я очень не люблю Виндоус, и советую тебе пересесть на Linux, BSD, или Unix. Тем не менее, как уже было сказано, ты все же можешь использовать весь этот материал и под Винды. Для начала забудь про большинство системных заголовочных файлов, о которых я далее буду упоминать. Все что тебе потребуется - это:
#include «winsock.h»
Стоп! Тебе также понадобится обратиться к WSAStartup() перед началом работы с этой библиотекой сокетов. Это сделать очень просто:
Код (Text):
#include «winsock.h» // пропущено WSADATA wsaData; // если это не пойдет, //WSAData wsaData; // то попробуй этот вариант if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { fprintf(stderr, "WSAStartup failed.\n"); exit(1); }Также тебе нужно будет сообщить своему компилятору, чтобы он слинковал Winsock Библиотеку, обычно называемую wsock32.lib, winsock32.lib, или еще как-нибудь. Под VC++ это можно сделать из раздела Settings в Project menu... Кликни вкладку Link, и найди "Object/library modules". Попросту добавь туда "wsock32.lib".
Наконец, тебе понадобится вызвать WSACleanup() когда ты закончишь работать с этой библиотекой. За подробностями обращайся к онлайн хелпу.
Когда ты завершишь все эти приготовления, ты сможешь компилировать все примеры ниже, за редкими исключениями. К примеру, если ты не можешь сделать close() чтобы закрыть сокет, попробуй closesocket() вместо этого. Также, функция select() работает только с дескрипторами сокета, а не с файловыми дескрипторами (как 0 в stdin).
За большей информацией по Winsock читай Winsock FAQ.
Далее, я слышал, что Windows не имеет системного вызова fork(), к которому, к сожалению, я обращаюсь в некоторых своих примерах. МОжет быть тебе стоит слинковать библиотеку POSIX или еще что-нибудь, чтобы заставить fork() работать, или же просто использовать CreateProcess() вместо этого. fork() не принимает аргументов, в то время как CreateProcess() принимает около 48 миллиардов аргументов. Добро пожаловать в великолепный мир Win32 программирования.
1.6 Замечания по обратной связи
Моя эл.почта доступна для вопросов, но я не гарантирую ответ. Я веду насыщенный образ жизни и бывают моменты, когда я попросту не могу ответить на ваши вопросы. В таком случае я обычно удаляю сообщение. Ничего личного. У меня просто нету времени чтобы детально ответить на ваш вопрос.
Как правило, чем сложнее вопрос, тем менее вероятно что я на него отвечу. Если вы можете заранее немного разобраться в своем вопросе и включить доп. информацию (платформу, компилятор, сообщения об ошибках, и все остальное, что вы думаете может помочь с вопросом) прежде чем мылить мне, то более вероятно, что вы получите ответ. Если же нет, то поразбирайтесь с вопросом самостоятельно, пытайтесь найти ответ, и, если он все еще не получен, напишите мне еще раз, включив полезную информацию, которую вы раскопали. Надеюсь теперь этого будет достаточно для меня чтобы ответить на ваш вопрос.
Теперь, объяснив вам как задавать мне вопросы, я хотел бы сказать вам, что я в полной мере ощущаю полезность этого своего туториала за все эти годы. Это большая моральная поддержка и я очень рад слышать, что этот туториал оказывается полезным! Спасибо вам!
1.7 Зеркала
Вы без проблем можете создавать зеркала этого сайта (в нашем случае - документа - прим. перев.), как приватные, так и публичные. Если вы выкладываете публичное зеркало этого сайта, и хотите чтобы я поместил на вас ссылку на своей страничке, черкните мне пару строк на это мыло beejATpiratehaven.org
1.8 Замечания для переводчиков
Если вы хотите перевести это руководство, напишите мне и я добавлю ссылку на ваш перевод на своей страничке. Вы можете свободно добавлять ваше имя и эл.почту в переводы. Извините, но из-за ограничения места я не могу сам выкладывать переводы.
1.9 Копирайты и Распространение
Руководство Beej по Программированию Сокетов защищено правами. Copyright © 1995-2001 Brian "Beej" Hall. Руководство может быть сводобно перепечатано с учетом соблюдения копирайтов.
2. Что такое сокет?
Ты слышишь разговоры о сокетах все время, и, вероятно, ты задумываешься о том, что же это такое. Что ж, сокеты - это просто способ общаться программам, используя стандартные Юниксовые дескрипторы файлов. Что?
Ок -- ты наверно слышал на каком-нибудь хакерском сайте утверждения в духе: "Эй, в Юниксе все - это файл!". О чем это они? Эти люди имели ввиду факте, согласно которому когда Юниксовые программы манипулируют с Вводом/Выводом, они делают это через чтение или запись в файловый дескриптор. Файловый дескриптор - это просто целое число, ассоциируемое с открытым файлом. Но такой файл может быть и сетевым соединением, и FIFO, и pipe, и терминалом, и обычным файлом на диске, итд. Все в Юниксе - это файл! Поэтому когда ты хочешь обменяться данными с др. программой по Инету, тебе придется делать это через файловый дескриптор. В это время ты поймешь, о чем я. "Откуда же я получу этот файловый дескриптор для коммуникации с сетью, Мр. Умные-Штаны (в оригинале: "Mr. Smarty-Pants")" - вероятно, это последний вопрос, будоражащий твой ум на этот момент. Я отвечу на него следующим образом: Ты обращаешься к системной функции socket(), она возвращает файловый дескриптор, и затем ты можешь общаться с сетью при помощи специальных системных сокетных вызовов send() и recv(). "Но...эй!" - удивитесь вы. "Раз это всего лишь файловый дескриптор, то какого фига я не могу обойтись стандартными read() и write() для коммуникации через сокет?". Короткий ответ: "Ты можешь!". Длинный ответ: "Ты можешь, но send() и recv() предоставляют гораздо больший контроль над передачей твоих данных." Что дальше? Как насчет этого: существуют все виды сокетов. Есть DARPA Интернет адреса (Интернет Сокеты), имена путей на локальной машине (Юникс Сокеты), CCITT X.25 адреса (X.25 Сокеты, которые ты можешь проигнорировать), и, вероятно, множество других, зависящих от Юникс дистрибутива, который у тебя стоит. Этот документ описывает работу только с первыми: Интернет Сокетами.
2.1 Два вида Интернет Сокетов
Что это? Существует два вида Интернет сокетов? Да. Эээ, нет. Я лгу. Существует больше, но я не хочу пока что тебя ими пугать. Я только собираюсь рассказать тебе о двух их типах. Но я хотел бы сказать, что "Raw Сокеты" также очень мощные и я бы советовал тебе глянуть на них.
Ладно, что это за два вида? Первый - это "Stream сокеты"; другие - это "Datagram сокеты", которые также называются "SOCK_STREAM" и "SOCK_DGRAM" соответственно. Datagram сокеты иногда называются "connectionless sockets". Stream сокеты - это надежные двухсторонне-соединенные коммуникационные потоки. Если ты отправляешь два элемента в сокет в порядке "1,2", то они прибудут на др. сторону в порядке "1,2". Они также будут без ошибок. Всякие ошибки, которые ты обнаружишь - это вымысел твоего расстроенного мозга, и они не будут здесь обсуждаться.
Что используют stream сокеты? Что ж, ты наверно уже слышал о telnet приложениях? Так вот, они используют stream сокеты. Все символы, которые ты набираешь, должны быть переданы в том же самом порядке на др. сторону, правильно? Также, веб-браузеры используют HTTP протокол, который использует stream сокеты для того, чтобы получать странички. Правда, если ты зателнетишься на вебсайт на порт 80 и введешь "GET /", telnet-приложение сдампит тебе весь HTML код!
Каким образом stream сокеты достигают этого высокого уровня при трансмиссии данных? Они используют протокол, получивший название "The Transmission Control Protocol", проще говоря, "TCP" (смотри RFC-793 для более подробной информации по этому протоколу). TCP проверяет, доставлены ли твои последовательные данные, и не произошли ли ошибки. Вероятно ты слышал о "TCP" ранее как о наиболее лучшей части "TCP/IP", где "IP" обозначает "Internet Protocol" (смотри RFC-791). IP главным образом обслуживает Internet роутинг и не предназначен для интеграции данных.
Круто. А как насчет Datagram сокетов? Почему они называются "connectionless"? Каково их предназначение? Почему они ненадежные? Что ж, вот некоторые факты: если ты отправляешь датаграмму, она может прибыть до назначения, хотя порядок датаграммы может быть изменен. Если датаграмма приходит, то данные этого пакета будут без ошибок.
Сокеты датаграмм также используют IP для роутинга, но они не могут применять TCP; они используют вместо этого "User Datagram Protocol", т.е. "UDP" (смотри RFC-768).
Почему они "connectionless"? Что ж, главным образом это потому, что ты не поддерживаешь открытое соединение, как ты делал это при работе с сокетами потоков. Ты просто создаешь пакет, вносишь в IP заголовок (header) информацию о месте назначения, и отправляешь его. Не нужно никаких соединений. Они главным образом используются для передач информации от пакета к пакету. Примеры некоторых приложений: tftp, bootp, итд. "Хватит" - крикнешь ты. "Как могут эти программы работать, если датаграммы могут быть потеряны?!" Что ж, мой друг, каждая датаграмма имеет свой собственный протокол в верхушке из UDP. К примеру, протокол tftp говорит, что для каждого пакета, который будет отправлен, получатель должен отправить пакет, который скажет "Я получил его!" (т.н. "ACK" пакет). Если же отправитель исходного пакета не получит ответа, скажем, в течение 5 секунд, он переотправит пакет заново, пока не получит ACK. Эта допущенная процедура очень важна, когда разрабатываются SOCK_DGRAM приложения.
2.2 Low level Nonsence and Network Theory
Т.к. я упомянул о слоях протоколов, то настало время поговорить об основных принципах работы сетей, и привести некоторые примеры создания SOCK_DGRAM пакетов. Что касается практики, то вы можете пропустить эту секцию. Хотя, это хорошее введение.
Эй, детишки, пришло время изучить Инкапсуляцию Данных! Это очень очень важно. Это настолько важно, что ты можешь поступить на курсы по сетям, для того чтобы изучить все это, здесь, в Штате Чико (Chico). Главным образом инкапсуляция данных говорит о том, что пакет был создан и перенесен ("инкапсулирован") в заголовок (и реже в низ - "footer") первым протоколом (скажем, TFTP), затем весь пакет (включая TFTP заголовок) инкапсулируется снова следующим протоколом (скажем, UDP), затем снова следующим (IP), и, наконец, финальным протоколом, уже на хардварном (физическом) уровне (скажем, Ethernet).
Когда др. компьютер получает пакет, железо "разбирает" Ethernet заголовок, ядро "разбирает" IP и UDP заголовки, программа TFTP - TFTP заголовок, и, наконец, получает данные.
Теперь я могу наконец-то поговорить о неизвестной Layered Network Model. Эта Сетевая Модель описывает систему функциональностей сети, которая имеет множество преимуществ перед др. моделями. Например, ты можешь написать программу с сокетами, которые в точности те же самые, не забивая себе голову о том, "как физически передаются данные" (Ethernet, AUI, итд), потому что это все обрабатывают программы на более низких уровнях. Действительное сетевое железо и топология остаются, таким образом, прозрачными для программера сокетов.
Ниже я привожу такую полноценную модель. Запомни ее для экзаменов на уроках по сетям:
- Приложение (Application)
- Представление (Presentation)
- Сессия (Session)
- Передача (Transport)
- Сеть (Network)
- Связь Данных (Data Link)
- Физический уровень (Physical)
Физический уровень - это железо. Уровень приложения далек от физического уровня настолько, насколько ты можешь себе это представить. Это то место, где юзер взаимодействует с сетью.
Это общая модель, и ты можешь пользоваться ею также как инструкцией автомобиля для его починки. Та же самая модель, но уже применительно к Unix системам может выглядеть след. образом:
Уровень приложения (Application Layer) - telnet, ftp, итд
Уровень передачи данных от хоста к хосту (Host-to-Host Transport Layer) (TCP, UDP)
Интернет уровень (Internet Layer) (IP и routing)
Уровень Интернет доступа (Network Access Layer) (Ethernet, ATM, or whatever)
Теперь ты можешь увидеть каким образом эти слои взаимодействуют чтобы инкапировать исходные данные.
Видишь как много нужно сделать чтобы построить простой пакет? А ты должен набрать заголовки пакета самостоятельно, используя "cat"! Шучу. Все что тебе нужно сделать для сокетов потока - это вызвать send(). Для датаграмм сокетов тебе нужно инкапсулировать пакет любым методом и применить sendto(). Ядро выстроит Уровень Передачи и Интернет Уровень для тебя, а железо обеспечит Network Access Layer. Ах, современная технология!
Итак, заканчиваем наше введение в теорию сетей. О да, я забыл рассказать тебе о роутинге! Это так, я вообще не рассказал об этом. Роутер разделяет пакет на IP заголовок, вносит коррективы по своей роутинг таблице, бла бла бла... Глянь в IP RFC если ты очень этим интересуешься. Если ты никогда не изучал это, что ж, ты выживешь.
3. структуры и Управление Данными
Что ж, наконец-то мы здесь. Пришло время поговорить о программировании. В этом разделе я освещу различные типы данных, используемые для интерфейса сокетов, т.к. некоторые из них по-настоящему скучные для рассмотрения.
Для начала самый простой: дескриптор сокетов. Дескриптор сокетов имеет тип int. Обычный int.
Вся фишка начинается отсюда, поэтому читай все и разбирайся вместе со мной. Запомни это: существует два порядка байтов: либо сначала первый наиболее значимый байт (иногда называемый "октет"), либо последний. Последний наз. "Сетевой Байтовый Порядок" ("Network Byte Order"). Некоторые машины хранят свои числа в "Network Byte Order", а некоторые - нет. Когда я говорю, что что-то должно быть в в "Network Byte Order", ты должен вызвать функцию (такую как htons()) для того, чтобы поменять это что-то с "Host Byte Order"а. Если же я не упоминаю "Network Byte Order", то ты должен оставить значение в "Host Byte Порядке".
Для любопытных, "Network Byte Order" также известен как "Big-Endian Byte Order".
Моя первая StructTM -- struct sockaddr. Эта структура содержит информацию об адресе сокета для большинства сокетов:
Код (Text):
struct sockaddr { unsigned short sa_family; // адрес семейства, AF_xxx char sa_data[14]; // 14 байт адреса протокола };sa_family может быть различным, но у нас будет только AF_INET на протяжении всего документа. sa_data содержит адрес назначения и номер порта для сокета.
Чтобы общаться с struct sockaddr, программеры создали параллельную структуру: struct sockaddr_in ("in" для "Internet").
Код (Text):
struct sockaddr_in { short int sin_family; // адрес семейства unsigned short int sin_port; // номер порта struct in_addr sin_addr; // Internet адрес unsigned char sin_zero[8]; // такого же размера, как и struct sockaddr };Эта структура позволяет легко ссылаться на элементы адреса сокета. Заметь, что sin_zero (который включен чтобы совместить структуру по длине со структурой sockaddr) должен быть выставлен в нули через функцию memset(). Также, указатель на структуру sockaddr_in может быть приведен к указателю на struct sockaddr, и наоборот. Поэтому, даже если socket() "хочет" структуру sockaddr*, ты все же можешь использовать struct sockaddr_in и перевести ее в последнюю минуту! Заметь далее, что sin_family обращается к sa_family в struct sockaddr и должна быть выставлена в "AF_INET". Наконец-то, sin_port и sin_addr должны быть в Network Byte order!
"Но" - возразишь ты, - "как может целая структура - struct in_addr sin_addr быть в Network Byte Order?" Этот вопрос требует внимательного изучения структуры struct in_addr, одного из наихудших объединений в мире:
Код (Text):
// Internet адрес (структура чисто по историческим причинам) struct in_addr { unsigned long s_addr; // 32-бита, т.е. 4 байта };Что ж, раньше она была объединением (union), но в наши дни не используется. Хорошее избавление. Поэтому, если ты объявил ina как struct sockaddr_in, то ina.sin_addr.s_addr ссылается на 4-байтный адрес (в Network Byte порядке). Заметь, что даже если твоя система все еще использует богопротивный union для struct in_addr, ты тем не менее можешь обращаться к 4-байтному IP адресу в точности также, как я делал это выше (это из-за #defines).
3.1 Сконвертируй Natives!
Мы уже порядочно говорили о переводе порядка байтов, поэтому настало время окончательно разобраться с самим этим переводом.
Ладно. Есть два вида, которые ты можешь сконвертировать: short (два байта) и long (четыре байта). Эти функции также работают и с беззнаковыми варициями. Скажем, ты хочешь перевести short с Host Byte Order в Network Byte Order. Начни название функции с "h" (для "host"), добавь "to", затем "n" (от "network"), и "s" (т.е. "short"). Получим h-to-n-s, т.е. htons(). Это очень просто...
Ты можешь использовать различные комбинации вышеприведенных сокращений, тем самым получая следующие названия функций:
- htons() -- "Host to Network Short"
- htonl() -- "Host to Network Long"
- ntohs() -- "Network to Host Short"
- ntohl() -- "Network to Host Long"
Ты наверно уже подумал, что разобрался с этим вопросом. Ты можешь спросить: "А как же мне к примеру изменить порядок байтов на символ?". Наверно, ты думаешь, что это невозможно. Также ты наверно думаешь, что твой компьютер уже использует Network Byte Order, и поэтому тебе не нужно вызывать htonl() для своего IP адреса. Ты был бы прав, но если бы ты обратился к машине, имеющей обратный порядок байтов, то твоя программа, вероятнее всего, вылетела. Будь портируемым! Это мир Unix! (настолько, насколько Б.Гейтс думает с точностью до наоборот). Помни: переводи свои байты в Network Byte Order перед тем, как отправлять их в сеть.
Последний вопрос: почему sin_addr и sin_port должны быть размещены в Network Byte Order в struct sockaddr_in, но не должны в sin_family? Ответ: sin_addr и sin_port инкапсулируются в пакет в IP и UDP разделы соответственно. Таким образом, они должны быть в Network Byte Order. Однако, поле sin_family используется только лишь ядром для определения типа адреса, который содержит структура, поэтому оно должно быть в Host Byte Order. Также, т.к. sin_family не отправляется в сеть, то это поле опять таки может быть в Host Byte Order.
3.2 IP адреса и Как Ими Управлять
К нашему счастью существует целый раздел функций, позволяющих нам управлять IP адресами. Не нужно ничего самому придумывать на коленке.
Для начала, скажем, что у тебя есть struct sockaddr_in ina, и у тебя есть IP "10.12.110.57", который ты хочешь сохранить в этой структуре. Для этого тебе понадобится функция inet_addr(), которая конвертирует IP адрес, записанный в привычном нам виде цифр, разделенных точками, в unsigned long. Это можно, к примеру, сделать следующим образом:
Код (Text):
ina.sin_addr.s_addr = inet_addr("10.12.110.57");Обрати внимание на то, что inet_addr() возвращает адрес уже в Network Byte Order, посему тебе не нужно далее вызывать htonl().
Строчка кода выше не очень грамотна, т.к. отсутствует проверка на ошибки. Функция inet_addr() возвращает -1 в случае ошибки. Помнишь бинарные числа? Беззнаковое -1 в этом случае обозначает 255.255.255.255! А это уже широковещательный адрес! Неправильно!! Поэтому не забывай самостоятельно делать проверку на ошибки.
Вообще-то существует более ясный интерфейс, который ты можешь применять вместо inet_addr(). Это - функция inet_aton() ("aton" обозначает "ascii to network").
Пример объявления:
Код (Text):
#include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» int inet_aton(const char *cp, struct in_addr *inp);А вот пример простейшего использования этой функции:
Код (Text):
struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; // Host Byte Order my_addr.sin_port = htons(MYPORT); // короткое целое, в Network Byte Order inet_aton("10.12.110.57", &(my_addr.sin_addr)); memset(&(my_addr.sin_zero), '\0', 8); // обнуляем всю оставшуюся структуруinet_aton(), в отличии от практически всех остальных связанных с сокетами функций, возвращает не нулевое значение в случае успеха, и нуль при ошибке. (Если кто-то знает почему, пожалуйста, разъясните.)
К сожалению, не все платформы поддерживают inet_aton(), поэтому, хоть и ее использование предпочтительно, в этом руководстве мы все же будем использовать более старую общую функцию inet_addr().
Хорошо, теперь ты можешь конвертировать строковый IP адрес в их бинарное представление. А как насчет других способов? Что если у тебя есть struct in_addr и ты захочешь напечатать ее в стандартном виде? В этом случае тебе понадобится обратиться к функции inet_ntoa() ("ntoa" обозначает "network to ascii") например так:
Код (Text):
printf("%s", inet_ntoa(ina.sin_addr));Эта строчка выведет IP адрес. Заметь, что inet_ntoa() в качестве своего аргумента принимает struct in_addr, а не long. Также заметь, что она возвращает указатель на char. Он указывает на статически сохраненный char массив в inet_ntoa(), поэтому каждый раз, когда ты вызываешь inet_ntoa(), функция перезаписывает последний IP адрес. Например:
Код (Text):
char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); // это 192.168.4.14 a2 = inet_ntoa(ina2.sin_addr); // а это 10.12.110.57 printf("address 1: %s\n",a1); printf("address 2: %s\n",a2);напечатает
Код (Text):
address 1: 10.12.110.57 address 2: 10.12.110.57Поэтому, если тебе нужно сохранить адрес, скопируй его через strcpy() в свой символьный массив. Это все по этому вопросу. Далее ты научишься конвертировать строки типа "whitehouse.gov" в их соответствующий IP адрес (смотри DNS ниже).
4. Системные вызовы
Это тот раздел, где мы обратимся к системным вызовам, которые позволят тебе почувствовать сетевую функциональность твоего Unix дистрибутива. При вызове этих функций всю работу за тебя выполняет ядро.
Главная проблема, возникающая у большинства, - это порядок, в котором вызывать эти функции. В этом случае не помогут man'ы, как ты уже наверно выяснил. Что ж, для того чтобы помочь в этой страшной ситуации, я попробую расположить эти системные вызовы в следующих разделах в том же самом порядке их вызова, который и нужен для твоих программ.
4.1 socket() -- Получи Файловый Дескриптор!
Я полагаю, что я больше не могу откладывать этот разговор на будущее. Я должен объяснить вам работу с socket(). Вот ее объявление:
Код (Text):
#include «sys/types.h» #include «sys/socket.h» int socket(int domain, int type, int protocol);Но что это за аргументы? Первый - тип домена; должен быть выставлен в "AF_INET", также как и в struct sockaddr_in (выше). Следующий аргумент говорит ядру о том, какой это сокет: SOCK_STREAM или SOCK_DGRAM. Последний должен быть установлен в "0", чтобы сокет мог выбрать корректный протокол, базируясь на типе. Заметь, что есть гораздо больше доменов и типов, чем я здесь перечислил. Посмотри socket() man страницу. Также, есть более "хороший" способ получиь протокол - смотри getprotobyname() man страницу.
socket() просто возвращает тебе сокетовый дескриптор, который ты далее можешь использовать в системных вызовах, или же -1 в случае ошибки. Глобальная переменная errno устанавливается в значение ошибки (смотри perror() man страницу).
В некоторых документациях упоминается мистическое "PF_INET". Этот странный зверь редко встречается в природе, но я мог бы немного рассказать о нем здесь. Когда-то, много лет назад, думали, что семейство адреса (для которого служит "AF" в "AF_INET") может поддерживать несколько протоколов, которые были в struct sockaddr_in и PF_INET в твоем вызове socket(). Ты можешь использовать AF_INET повсюду. И, т.к. W.Richard Stevens использовал его в своей книге, то и я буду в этом туториале.
Отлично, отлично, но чем же хорош сокет? Сам по себе сокет не представляет нам никакого интереса. Продолжай читать и изучать больше системных вызовов, чтобы найти ответы на этот вопрос.
4.2 bind() -- На каком я порту?
После того, как у тебя есть сокет, ты можешь захотеть "связать" этот сокет с портом на своей локальной машине. Обычно так делают, когда хотят "прослушать" (listen() ) входящие соединения на указанный порт. Номер порта используется ядром, чтобы ассоциировать полученный пакет с сокетовым дескриптором определенного процесса. Если же ты собираешься просто подсоединиться через connect(), то тогда bind() здесь будет лишней. В любом случае изучи ее.
Вот ее объявление:
Код (Text):
#include «sys/types.h» #include «sys/socket.h» int bind(int sockfd, struct sockaddr *my_addr, int addrlen);sockfd - это сокетовый дескриптор файла, полученный от socket(). my_addr - это указатель на struct sockaddr, которая содержит информацию о твоем адресе, о порте, и о IP. addrlen() может быть выставлена в sizeof(struct sockaddr). Фьюю...Вот небольшой пример для большего понимания:
Код (Text):
#include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #define MYPORT 3490 main() { int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); // сделай проверку на ошибки! my_addr.sin_family = AF_INET; // Host Byte Order my_addr.sin_port = htons(MYPORT); // short, Network Byte Order my_addr.sin_addr.s_addr = inet_addr("10.12.110.57"); memset(&(my_addr.sin_zero), '\0', 8); // обнулим все оставшееся у структуры // не забудь о проверке на ошибки! bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); . . .Вот несколько вещей, о которых нужно здесь рассказать: my_addr.sin_port и my_addr.sin_addr.s_addr находятся в Network Byte Order. Также стоит обратить внимание на заголовочные файлы, которые могут различаться на разных системах. ЧТобы быть уверенным, тебе потребуется проверить это по своим man страницам. Наконец, стоит добавить, что некоторые процессы получения твоего собственного IP адреса и/или порта могут быть автоматизированы:
Код (Text):
my_addr.sin_port = 0; // возьмем неиспользуемый порт, чтобы bind() сама взяла порт my_addr.sin_addr.s_addr = INADDR_ANY; // используем мой IP адресВидишь, при помощи установки my_addr.sin_port в нуль ты говоришь функции bind(), чтобы она выбрала за тебя порт самостоятельно. Похожим образом при установке my_addr.sin_addr.s_addr в INADDR_ANY ты говоришь функции, чтобы она самостоятельно заполнила IP адрес компьютера, на котором запущен этот процесс.
Если ты немного поразмышляешь, то ты можешь заметить, что я не поместил INADDR_ANY в Network Byte Order! ПОругай меня. Однако, у меня есть внутренняя информация: INADDR_ANY в действительности нуль! Нуль все еще имеет нуль в битах, даже при изменении порядка байтов.
Сейчас мы так портабельны, как только ты можешь себе это представить! Я просто хотел обратить на это внимание, т.к. бОльшая часть кода, с которым ты встретишься, не будет содержать INADDR_ANY в htonl().
bind() также возвращает -1 при ошибках и выставляет errno в значение этой ошибки.
Другая вешь, на которую стоит обратить внимание при вызове bind() - это то, что не стоит перебарщивать с номером порта. Все порты ниже 1024 ЗАРЕЗЕРВИРОВАНЫ! Ты можешь иметь любой номер порта выше них до 65535.
Иногда ты заметишь, что при перезапуске сервера bind() вылетает с ошибкой, заявляя, что "Address already in use". Что это значит? Что ж, часть сокета, которая была соединена, все еще "висит" в ядре, тем самым, удерживая порт. Ты можешь либо подождать, пока она очистится (около минуты), или же добавить код в свою программу, позволяющий переиспользовать порт. Например, следующим образом:
Код (Text):
int yes=1; //char yes='1'; // под "Solaris" используем этот вариант if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); }Последнее замечание о bind(): бывают моменты, когда ты не совсем хочешь вызывать bind(). Если ты подсоединен (через connect() ) к удаленному компьютеру, и ты не беспокоишься о том, какой у тебя локальный порт (как в случае telnet, где тебе важен номер удаленного порта), ты можешь просто вызвать connect(). Она проверит, назначен ли сокет, и забиндит его через bind() на неиспользуемый локальный порт, если это необходимо.
4.3 connect() -- Эй, ты!
Давайте на несколько минут представим, что мы - telnet приложение. Юзеры командуют нам (прям как в фильме TRON), чтобы мы передали им сокетовый файловый дескриптор. Мы подчиняемся и вызываем socket(). Затем юзер говорит нам, чтобы мы подсоединились к "10.12.110.57" на "23" порт. Йоу! Что нам тогда делать?
К счастью для тебя, программы, ты сейчас изучаешь раздел, посвященный connect(). Поэтому читай внимательно! Не теряй времени!
Объявление connect():
Код (Text):
#include «sys/types.h» #include «sys/socket.h» int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);sockfd - это наш дружественный сосед сокетового файлового дескриптора, который мы получили после вызова socket(), serv_addr - это struct sockaddr, содержащая порт назначения и IP адрес, а addrlen может быть выставлена в sizeof(struct sockaddr).
Разве все это не проясняет все? Давайте посмотрим на пример:
Код (Text):
#include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #define DEST_IP "10.12.110.57" #define DEST_PORT 23 main() { int sockfd; struct sockaddr_in dest_addr; // будет содержать addr назначения sockfd = socket(AF_INET, SOCK_STREAM, 0); // проверь на ошибки! dest_addr.sin_family = AF_INET; //Host Byte Order dest_addr.sin_port = htons(DEST_PORT); // short, Network Byte Order dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); memset(&(dest_addr.sin_zero), '\0', 8); //обнулим оставшуюся часть struct // не забудь проверить эту connect() на ошибки! connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)); . . .Еще раз, не забудь проверь возвращенное connect() значение. Она вернет -1 при ошибке.
Также заметь, что мы не вызываем bind(). Вообще, мы не беспокоимся по поводу нашего локального номера порта; нам нужен только порт назначения (удаленный порт). Ядро выберет для нас локальный порт, и сайт, на который мы подсоединяемся, автоматически получит от нас эту информацию. Нечего переживать.
4.4 listen() -- Кто-нибудь мне позвонит?
Окей, подходящее время настало. Что если ты не хочешь подсоединяться к удаленному хосту? Скажем, ты всего лишь хочешь подождать входящего соединения и обработать их каким-нибудь образом? Процесс разделяется на два этапа: сначала ты слушаешь - listen(), затем принимаешь - accept() (смотри ниже).
Вызов listen() довольно простой, но требует некоторых объяснений:
Код (Text):
int listen(int sockfd, int backlog);soskfd - это обыкновенный сокетовый файловый дескриптор от socket(), backlog - это кол-во соединений, разрешенный во входящей очереди. ЧТо все это значит? Что ж, входящие соединения станут ждать этой очереди пока ты не примешь ( accept() ) их. Это лимит того, сколько очередь может вмещать. Большинство систем ограничивают это число двадцатью.
Опять же, как и обычно, listen() возвращает -1.
Как ты уже наверно представил, нам потребуется вызвать bind() перед тем как мы сможем слушать, или же ядро по-дефолту посчитает нас слушающими случайный порт. Поэтому если ты собираешься прослушивать входящие соединения, то последовательность системных вызовов, которые тебе нужно будет вызвать, следующая:
Код (Text):
socket(); bind(); listen(); /* здесь идет accept() */Я просто покажу все это далее в разделе accept(), т.к. код очень простой. В действительности, самая хитрая часть во всем этом колдовсттве - это вызов accept();
4.5 accept() -- Спасибо, что обратились к порту 3490
Будь готов - вызов accept() немного странный! Что случится здесь, так это вот что: кто-нибудь очень далекие попробует подсоединиться через connect() к твоему компьютеру на порт, который ты слушаешь через listen(). Их соединение будет ждать определенное время для соединения. Ты вызовешь accept() и ты скажешь функции, чтобы она получила ожидающее соединение. Она возвратит тебе нвый сокетный файловый дескриптор. Таким образом, ты будешь иметь два сокетных файловых дескриптора по цене одного! Исходный дескриптор все еще прослушивает твой локальный порт, а новый созданный сокет уже готов для операций получения и отправки. Объявление следующее:
Код (Text):
#include «sys/socket.h» int accept(int sockfd, void *addr, int *addrlen);sockfd - Это прослушивающий сокетный дескриптор. Вполе просто. addr обычно будет указателем на локальную struct sockaddr_in. Это то место, куда попадет информация о входящих соединениях (и с этим указателем ты сможешь определить, какой хост тебя вызвал, и с какого порта). addrlen - это локальная переменная целого типа, которая должна была быть выставлена в sizeof(struct sockaddr_in) перед тем, как передавать ее адрес в accept(). accept() не будет передавать байт больше, чем под них отведено места в addr. Если же она передаст меньше, то она изменит величину addrlen для соответствия.
accept() возвращает -1 и выставляет errno в значение ошибки если произойдет ошибка. Как и раньше, вот наглядный пример фрагмента кода, который демонстрирует все вышеописанное:
Код (Text):
#include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #define MYPORT 3490 // порт, на который будут соединяться юзеры #define BACKLOG 10 // сколько ожидающих соединений будет вмещать очередь main() { int sockfd, new_fd; // sockfd для listen, new_fd для нового соединения struct sockaddr_in my_addr; // информация о моем адресе struct sockaddr_in their_addr; // информация о адресе подсоединяющегося int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); // не забудь о проверке на ошибки! my_addr.sin_family = AF_INET; // Host Byte Order my_addr.sin_port = htons(MYPORT); // short, Network Byte Order my_addr.sin_addr.s_addr = INADDR_ANY; // автозаполнение с моим IP memset(&(my_addr.sin_zero), '\0', 8); // обнуляем оставшуюся часть структуры //не забудь про поверку на ошибки! bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); . . .Опять таки, заметь, что мы будем использовать сокетный дескриптор new_fd для вызова send() и recv(). Если жы ты только получаешь единственное соединение, то ты можешь закрыть слушающий sockfd через close() для того, чтобы предотвратить подрубление новых соединений на тот же самый порт.
4.6 send() и recv() -- Поговори со мной, крошка!
Эти две функции отвечают за общение через stream сокеты или через подсоединенные датаграмм сокеты. Если ты хочешь использовать регулярные неподрубленные датаграмм сокеты, то тебе потребуется обратиться к разделу о sendto() и recvfrom() ниже.
Вызов send():
Код (Text):
int send(int sockfd, const void *msg, int len, int flags);sockfd - это дескриптор сокета, на который ты хочешь отправить данные (это либо полученный через socket(), или же дескриптор, полученный через accept() ). msg - это указатель на данные, которые ты хочешь отослать, а len - это длина этих данных в байтах. Просто поставь флаги в 0. Вот наглядный примерчик:
Код (Text):
char *msg = "Beej was here!"; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); . . .sendto() возвращает количество байтов, которые в действительности были отправлены. Оно может быть меньше, чем количество, которое ты хотел отослать! Смотри, иногда ты говоришь функции отправить целый блок данных, а она попросту не может его обработать. Она отправит такое количество байт, какое только сможет, и предупредит тебя, чтобы ты отослал оставшееся после. Запомни, что если значение, возвращенное send() не совпадает с величиной len, то отправка всех оставшихся данных полностью возлагается на твои плечи. Хорошая новость - если пакет маленький (меньше 1K, или около того), то скорее всего функция сможет справиться с данными и отправит их одним махом. опять же, возвращенное -1 сигнализирует о ошибке, посмотреть которую можно в errno.
Вызов recv() похож на многие другие:
Код (Text):
int recv(int sockfd, void *buf, int len, unsigned int flags);sockfd - это дескриптор сокета, из которого нужно читать данные, buf - это буфер, в который будем читать информацию, len - это максимальная длина буфера, а флаги могут быть опять выставлены в 0. recv() возвращет число байт, которые были получены в действительности, или -1 при ошибке.
Подожди! recv() может вернуть 0. Это будет значить, что удаленная сторона закрыла для тебя соединение. Таким образом, возвращенный 0 сообщит тебе о том, что это произошло. Йиии! Ты Unix Network Программер!
4.7 sendto() и recvfrom() -- Поговори со мной в DGRAM-стиле
Я слышу твое: "Это все круто, но что делать с несоединенными датаграмм сокетами?". Нет проблем, амиго. Мы сейчас с этим всем и разберемся. Т.к. датаграмм сокеты не соединены с удаленным хостом, то угадай, что нам нужно знать перед тем как отправить пакет? Правильно! Адрес назначения! Вот прототип:
Код (Text):
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);Как видишь, этот системный вызов практически такой же как и вызов send(), только с добавочной информацией: to - это указатель на struct sockaddr, которая содержит IP адрес назначения и порт; tolen - может быть просто равен sizeof(struct sockaddr).
Также как и send(), sendto() возвращает число отосланных байт (которое, опять таки, может быть меньше чем число байт, которые ты в действительности хотел отослать!), или -1 при ошибке. Вызов recvfrom() такой же как и recv():
Код (Text):
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);from - это указатель на локальную struct sockaddr, в который запишется IP адрес и порт компьютера. fromlen - это указатель на int, который должен быть проинициализирован в sizeof(struct sockaddr). При выходе из функции fromlen будет содержать длину адреса, действительно сохраненного в from.
recvfrom() возвращает число полученных байт, или же -1 при ошибке.
Помни, что если ты используешь в connect() датаграмм сокет, то ты попросту можешь обойтись одними send() и recv() для всех своих транзакций. Сокет сам по себе - это датаграмм сокет, а пакеты все еще используют UDP, но интерфейс сокетов сам автоматически за тебя добавит всю необходимую информацию об адресе назначения и адресе отправителя.
4.8 close() и shutdown() -- Убирайся прочь!
Фьюю! Наверно ты уже вдоволь наотправлял и наполучал данных при помощи send() и recv(). Пришло время закрыть соединение. Это очень просто. Для этого тебе нужно вызвать функцию close. Вот ее описание:
Код (Text):
close(sockfd);Она предотвратит возможные в дальнейшем попытки читать и записывать в сокет. Любой, кто попробует прочесть или записать данные в твой сокет с удаленного компьютера, получит сообщение об ошибке.
В случае если ты хочешь еще более проконтролировать закрытие твоего сокета тебе стоит взглянуть на функцию shutdown(). Она позволит тебе обрубить все указанные тобой соединения:
Код (Text):
int shutdown(int sockfd, int how);sockfd - это сокетный файловый дескриптор, который ты хочешь закрыть. а how может быть следующей:
- 0 -- запретить дальнейшее получение данных
- 1 -- запретить дальнейшую отпавку данных
- 2 -- запретить и получение, и отправку данных (как в close() ).
shutdown() возвращает 0 при успехе, и -1 в случае ошибки.
Если ты попробуешь применить shutdown() к уже отсоединенному датаграмм сокету, т он попросту станет недоступным для дальнейших send() и recv() функций (помни, что чтобы их использовать, тебе нужно сперва вызвать connect() для твоего датаграмм сокета).
Важно помнить, что shutdown() в действительности не закрывает файловый дескриптор - она попросту меняет его "юзабилити". Чтобы освободить сокетный дескриптор нужно вызвать close().
4.9 getpeername() -- Кто ты?
Это очень простая функция.ВОт она. Функция getpeername() сообщит тебе, кто находится на другой стороне соединного stream сокета. Ее объявление:
Код (Text):
#include «sys/socket.h» int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);sockfd 0 Это дескриптор соединенного stream сокета, addr - это указатель на struct sockaddr (или на struct sockaddr_in), в который далее поместится информация о другой стороне соединения, а addrlen - это указатель на int, который должен быть проинициализирован в sizeof(struct sockaddr).
Функция возвращает -1 при ошибке.
Теперь, когда у тебя есть их адреса, ты можешь использовать inet_ntoa() или gethostbyaddr() для того, чтобы вывести больше информации. Нет, ты не получишь их логины (Ок, ок. Если на другой стороне запущен ident даемон, то это возможно. Это, однако, за пределами данного документа).
4.10 gethostname() -- Кто я?
Еще проще чем getpeername(). Она возвращает имя компьютера, на котором запущена твоя программа. Имя может применяться в gethostbyname(), которая будет рассмотрена ниже, для того, чтобы определить IP адрес твоей локальной машины.
Что может быть более интересным? Я могу придумать несколько вещей, но они не относятся к сокет программингу. В любом случае, вот ее объявление:
Код (Text):
#include «unistd.h» int gethostname(char *hostname, size_t size);Аргументы: hostname - это указатель на массив из char, в который далее закинется информация о имени хоста в ходе работы этой функции. size - это длина в байтах этого массива.
4.11 DNS -- Ты говоришь "whitehouse.gov", я же говорю "198.137.240.92"
В том случае, если ты не знаешь что такое DNS, я поясню. DNS Обозначает "Domain Name Service". В консоли ты говоришь этому сервису о имени сайта в человеко-понятной форме, а этот сервис дает тебе его IP адрес (так, что ты теперь сможешь использовать bind(), connect(), sendto(), и все что тебе далее понадобится.). Т.е. когда кто-нибудь вводит:
Код (Text):
$ telnet whitehouse.govtelnet может разузнать, что ему нужно сделать connect() к "198.137.240.92".
Но как это все работает? Тебе понадобится вызвать функцию gethostbyname():
Код (Text):
#include «netdb.h» struct hostent *gethostbyname(const char *name)Как видишь, она возвращает указатель на struct hostent, которая выглядит след. образом:
Код (Text):
struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0]А вот и описания полей этой структуры:
- h_name -- официальное имя хоста
- h_aliases -- массив альтернативных имен хоста, заканчивающийся NULL
- h_addrtype -- тип используемого адреса; обычно AF_INET
- h_length -- длина адреса в байтах
- h_addr_list --массив сетевых адресов для хоста, завершающийся нулем. Хост адреса находятся в Network Byte Order.
- h_addr -- первый адрес в h_addr_list.
gethostbyname() возвращает указатель на заполненную структуру hostent, или NULL при ошибке.
Но как ее применять? Иногда (как мы видим из различных компьютерных мануалов), просто выдать читателю информацию зачастую недостаточно. Эта функция гораздо проще применять, чем ее вид.
Вот пример программы:
Код (Text):
/* ** getip.c -- a hostname lookup demo */ #include «stdio.h» #include «stdlib.h» #include «errno.h» #include «netdb.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» int main(int argc, char *argv[]) { struct hostent *h; if (argc != 2) { // проверь на ошибки данные, переданные в командной строке fprintf(stderr,"usage: getip address\n"); exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { //получаем инфу о хосте herror("gethostbyname"); exit(1); } printf("Host name : %s\n", h->h_name); printf("IP Address : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr))); return 0; }С этой функцией ты не можешь использовать perror() чтобы вывести сообщение об ошибке (т.к. errno не используется). Вместо этого вызывай herror(). Это очень удобно. Ты просто передаешь строчку, которая содержит имя машины ("whitehouse.gov") в нашу функцию gethostbyname(), а она грабит информацию из возвращенной struct hostent.
Единственная возможная странность может быть при печати IP адреса выше. h->h_addr - это char*, но inet_ntoa() требует, чтобы ей была передана struct in_addr. ПОэтому я перевожу h->h_addr в struct in_addr*, а затем разыменовываю ее для получения данных.
5 Технология Клиент-Сервер
Это клиент-сервер мир, детка. Все вокруг в сети работает с клиентными процессами, обращающимися к серверам, и наоборот. Возьми например telnet. Когда ты подсоединяешься с ним к удаленному хосту на порт 23 (клиент), программа на том хосте (называемая telnetd - сервером) приступает к своей работе. Она управляет входящим telnet соединением, запрашивает у тебя логин, итд.
Обмен информацией представлен на рис 1.
Рис 1. Взаимодействие клиента и сервера
Код (Text):
|----запрос----- >| клиент сервер |<-----ответ------|Обрати внимание на то, что пара клиент-сервер может "говорить" на SOCK_STREAM, SOCK_DGRAM, и на любом другом "языке". Хорошие примеры пар клиент-сервер - это telnet/telnetd, ftp/ftpd, bootp/bootpd. Каждый раз, когда ты используешь ftp, имеется удаленная программа, ftpd, которая и обслуживает тебя.
Зачастую, имеется только лишь один сервер на машине, и ое оперирует множеством клиентов через fork(). Принцип работы этой базовой функции следующий: сервер ждет соединения, принимает его через accept(), и делает fork(). Это то, что наш простой пример сервера будет делать в следующем разделе.
5.1 Простой Поточный Сервер
Все что делает этот сервер - это отправка строчки "Hello, World!\n" через stream соединение. Все что тебе потребуется для тестирования этого сервера работает в одном окне, и телнетится к нему из другого при помощи:
Код (Text):
$ telnet remotehostname 3490, где remotehostname - это имя машины, на которой ты работаешь.
Код сервера:
Код (Text):
/* ** server.c -- a stream socket server demo */ #include «stdio.h» #include «stdlib.h» #include «unistd.h» #include «errno.h» #include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» #include «sys/wait.h» #include «signal.h» #define MYPORT 3490 // порт, на который будут коннектиться юзеры #define BACKLOG 10 // максимум соединений void sigchld_handler(int s) { while(wait(NULL) > 0); } int main(void) { int sockfd, new_fd; // sock_fd для слушанья, new_fd для нового соединения struct sockaddr_in my_addr; // инфа о моем адресе struct sockaddr_in their_addr; // инфа о адресе подсоединившегося int sin_size; struct sigaction sa; int yes=1; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); } my_addr.sin_family = AF_INET; // Host Byte Order my_addr.sin_port = htons(MYPORT); // short, network byte order my_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP memset(&(my_addr.sin_zero), '\0', 8); // zero the rest of the struct if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } sa.sa_handler = sigchld_handler; // рипаем все мертвые процессы sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } while(1) { // главный accept() цикл sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) perror("accept"); continue; } printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr)); if (!fork()) { // это child process close(sockfd); // child doesn't need the listener if (send(new_fd, "Hello, world!\n", 14, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); // родителю он не нужен } return 0; }Если ты любопытен, то я поместил весь код в одну большую функцию main() просто для наглядности. Можешь свободно разбить его на меньшие функции, если так тебе будет проще и яснее.
Также, что касается всей sigaction, которая может быть для тебя незнакомой - не беспокойся, все ОК. Этот код отвечает за рипанье зомби процессов, которые находятся после за-fork()-нутых child процессов. Если ты создашь кучу зомби и не погрохаешь их, твой сисадмин будет не очень этому рад.
Ты можешь получать данные от этого сервера при помощи клиента, который расписан в следующем разделе.
5.2 Простой Поточный Клиент
Этот парень даже проще, чем его клиент. Все что он делает - это подсоединение к хосту, который ты указал ему в командной строке, на порт 3490. Он получает строчку, которую отправляет сервер.
Исходник клиента:
Код (Text):
/* ** client.c -- a stream socket client demo */ #include «stdio.h» #include «stdlib.h» #include «unistd.h» #include «errno.h» #include «string.h» #include «netdb.h» #include «sys/types.h» #include «netinet/in.h» #include «sys/socket.h» #define PORT 3490 // порт, на который будет коннектиться клиент #define MAXDATASIZE 100 // максимум байтов, которые мы можем получить за раз int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; // инфа об адресе коннектора if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // получим инфу хоста perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // Host Byte Order their_addr.sin_port = htons(PORT); // short, Network Byte Order their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), 8); // обнулим оставшуюся структуру if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0; }Заметь, что если ты не запустишь сервер перед тем как запустить клиент, то connect() возвратит "Connection refused". Очень полезно.
5.3 Datagram Сокеты
У меня не так много сказать на этот счет, поэтому я просто представлю тебе пару простых программ: talker.c и listener.c
listener сидит на компьютере и ждет приходящего пакета на порт 4950. talker отправляет пакет на этот порт на указанную машину, которую указывает юзер в командной строке.
Вот исходник listener.c:
Код (Text):
/* ** listener.c -- a datagram sockets "server" demo */ #include «stdio.h» #include «stdlib.h» #include «unistd.h» #include «errno.h» #include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» #define MYPORT 4950 // порт, на который будут коннектиться юзеры #define MAXBUFLEN 100 int main(void) { int sockfd; struct sockaddr_in my_addr; // инфа о моем адресе struct sockaddr_in their_addr; // инфа об адресе коннектора int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; // Host Byte Order my_addr.sin_port = htons(MYPORT); // short, Network Byte Order my_addr.sin_addr.s_addr = INADDR_ANY; // автоматически заполнит моим IP memset(&(my_addr.sin_zero), '\0', 8); // обнуляем остаток структуры if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr)); printf("packet is %d bytes long\n",numbytes); buf[numbytes] = '\0'; printf("packet contains \"%s\"\n",buf); close(sockfd); return 0; }Заметь, что в нашем вызове socket() мы наконец-то использовали SOCK_DGRAM. Также, обрати внимание что нам не нужно в этом случае использовать listen() или accept(). Это одна из фич использования неподсоединенных датаграмм сокетов!
Далее следует исходный код для talker.c:
Код (Text):
/* ** talker.c -- a datagram "client" demo */ #include «stdio.h» #include «stdlib.h» #include «unistd.h» #include «errno.h» #include «string.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» #include «netdb.h» #define MYPORT 4950 //порт, на который будут подсоединяться юзеры int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // инфа об адресе коннектора struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // получаем инфу о хосте perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // Host Byte Order their_addr.sin_port = htons(MYPORT); // short, Network Byte Order their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // обнуляем остаток структуры if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; }И это все на этот счет. Запусти listener на компьютере, затем запусти talker на другой. Посмотри, как они будут взаимодействовать!
6 Более Продвинутые Техники
Они не совсем навороченные, но они выходят из уровня материала, который мы изучали. В действительности, если ты изучил все вышенаписанное, то ты уже смело считать, что ты изучил все основные моменты в сетевом программировании под Unix! Поздравляю!
Поэтому здесь мы шагнем в храбрый новый мир, содержащий кое-какие эзотерические вещи, которым ты можешь захотеть обучиться в сокет программировании. Поехали!
6.1 Блокировка
Блокировка. Ты слышал об этом, но не знал что же это такое. В консоли, "block" - это жаргонный аналог "sleep". Наверно ты заметил, что когда работал с listener (рассмотренный выше), то он попросту сидел и ждал пока к нему прилетит пакет. Что происходило - это то, что он вызывал recvfrom(). recvfrom() сообщалось о том, чтобы она уснула до тех пор, пока не придут данные.
Многие функции могут выполнять блокировку. Им просто разрешено ее выполнять. Когда ты вначале создаешь сокетный дескриптор через socket(), то ядро устанавливает его в блокировку. Если ты не хочешь, чтобы сокет был заблокирован, то ты вызываешь fcntl():
Код (Text):
#include «tunistd.h» #include «fcntl.h» . . sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . .Устанавливая сокет в незаблокированное состояние, ты можешь эффективнее обращаться с ним. Говоря главным образом, однако, этот тип состояния - плохая идея. Если ты изменишь свою программу на "занятую-ожидающую данные от сокета", то ты повесишь процессорное время. Более элегантное решение для проверки того, есть ли данные, которые ожидают своего чтения, описывается в разделе, посвященном функции select().
6.2 select() -- Синхронный I/O мультиплексинг
Эта функция странная, но очень полезная. Возьмем след. ситуациж: ты - сервер, и ты хочешь прослушать входящие соединения, а также продолжать читать данные из соединений, которые у тебя, скорее всего, есть. "Нет проблем", - скажешь ты. - "Просто используй accept() и пару recv()". Не так быстро, торопыга! А что, если ты заблокируешь вызов accept()? Как ты тогда будешь получать данные через recv() в это время? "Используй неб кируемые сокеты!" Ни в коем случае! Ты же не хочешь отжирать CPU? Что же делать?
select() дает тебе возможность мониторить несколько сокетов в одно и то же время. Я расскажу тебе попозже, какие готовы для чтения, какие готовы для записи, а какие вызвали исключения, если ты этого захочешь.
Без всяких дальнейших рассуждений, я просто покажу тебе объявление select():
Код (Text):
#include «sys/time.h» #include «sys/types.h» #include «unistd.h» int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);Функция мониторит набор файловых дескрипторов; в частности - readfds, writefds и exceptfds. Если ты хочешь выяснить, можешь ли ты читать со стандартного ввода и некоторых сокетных дескрипторов, просто добавь файловые дескрипторы 0 и sockfd в перечень readfds. Параметр numfds должен быть установлен в размер самого большого файлового дескриптора плюс один. В этом примере он должен быть установлен в sockfd+1, т.к. он больше чем стандартный ввод(0).
После работы select() readfds будет изменен так, чтобы отражать то, какие дескрипторы из тех, что ты выбрал, готовы для чтения. Ты можешь проверить их с macro FD_ISSET, ниже.
Перед тем как углубляться в детали, я расскажу о том, как управлять этими перечнями (set). Каждый перечень имеет тип fd_set. Следующий макрос оперирует этим типом:
- FD_ZERO(fd_set *set) -- очищает перечень файловых дескрипторов
- FD_SET(int fd, fd_set *set) -- добавляет в перечень fd
- FD_CLR(int fd, fd_set *set) -- удаляет из перечня fd
- FD_ISSET(int fd, fd_set *set) -- проверяет, находится ли fd в перечне
Наконец, что же странного в struct timeval? Что ж, иногда ты не хочешь ждать бесконечно того, что тебе пошлют данные. Может быть, каждые 96 секунд ты решишь выводить: "Still going..." на терминал, даже несмотря на то, что ничего не происходит. Эта структура времени позвоит тебе указать период таймаута. Если это время истечет, а select() все еще не получила никакого файлового дескриптора, то она завершится, и тем самым ты сможешь продолжать работу своей программы.
Структура struct timeval имеет след. поля:
Код (Text):
struct timeval { int tv_sec; // секунды int tv_usec; // микросекунды };Просто поставь tv_sec в число секунд, которые ты хочешь ждать, и установи tv_usec в число микросекунд. Да, это микросекунды, а не миллисекунды. В одной миллисекунде 1000 микросекунд, а в одной секунде 1000 миллисекунд. Таким образом, в секунде 1000000 микросекунд. А почему микросекунды обозначаются "usec"? Эта "u" выглядит очень похоже на греческую букву µ ("мю"), которую мы используем для "микро". Также, когда функция возвращает значение, то таймаут может быть обновлен для того, чтобы отразить все еще оставшееся время. Это зависит от дистрибутива Unix.
Йоу! У нас есть таймер с точностью дл микросекунды! Что ж, не ручайся на него. Стандартный Unix "timeslice" около 100 миллисекунд, поэтому, вероятно, ты будешь должен ждать это время, независимо от того, как мало время в твоей struct timeval.
Еще одна интересная фишка: если ты установишь свои поля в struct timeval в 0, то select() сразу же сработает. Если ты установишь параметр timeout в NULL, то он никогда не сработает, и будет ждать до тех пор, пока не станет готов первый файловый дескриптор. Наконец-таки, если ты не очень беспокоишься о том, сколько ждать для каждого перечня, ты просто можешь установить таймаут в NULL в вызове select();
Следующий код ждет 2,5 секунды, ожидая появления чего-нибудь на стандартном вводе.
Код (Text):
/* ** select.c -- a select() demo */ #include «stdio.h» #include «sys/time.h» #include «sys/types.h» #include «unistd.h» #define STDIN 0 // файловый дескриптор для стандартного ввода int main(void) { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); // не заморачиваемся по поводу writefds and exceptfds: select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf("A key was pressed!\n"); else printf("Timed out.\n"); return 0; }Теперь некоторые из вас могут решить, что это великолепный способ ожидания данных в датаграмм сокетах, и вы будете правы: возможно, это так. Некоторые Юниксы могут действовать в этом духе, а некоторые - нет. Тебе нужно проверить это в своем мане, если ты хочешь использовать такой метод.
Некоторые Юниксы обновляют время в твоей локальной struct timeval для того , чтобы отражать количество времени, оставшееся до таймаута. Но другие так не действуют. Не полагайся на это, если ты хочешь быть портируемым. Применяй gettimeofday() если тебе надо учитывать пройденное время.
А что случится, если сокет в состоянии чтения закроет соединение? Что ж , в этом случае select() возвратится с сокетным дескриптором, выставленным в "ready to read". Когда ты на самом деле далее вызовешь recv(), то recv() вернет 0. Это способ, по которому ты можешь узнать, что клиент закрыл соединение.
Еще одна интересная вещь, связанная с select(): если у тебя есть сокет, который прослушивает через listen(), то ты можешь проверить, появилось ли новое соединение при помощи помещения этого сокетного файлового дескриптора в readfds set.
Вот и все вкратце, мои друзья, о всемогущей функции select().
Но, по многочисленным запросам я все же покажу подробный пример.
Эта программа действует как простой многопользовательский чат сервер. Запусти его в одном окне, затем зателнеться на него ("telnet hostname 9034") со многих других окон. Когда ты введешь что-нибудь в одной telnet сессии, эта строчка должна отображаться во всех других.
Код (Text):
/* ** selectserver.c -- a cheezy multiperson chat server */ #include «stdio.h» #include «stdlib.h» #include «string.h» #include «unistd.h» #include «sys/types.h» #include «sys/socket.h» #include «netinet/in.h» #include «arpa/inet.h» #define PORT 9034 // порт, который мы прослушиваем int main(void) { fd_set master; // master file descriptor list fd_set read_fds; // temp file descriptor list for select() struct sockaddr_in myaddr; // server address struct sockaddr_in remoteaddr; // client address int fdmax; // maximum file descriptor number int listener; // listening socket descriptor int newfd; // newly accept()ed socket descriptor char buf[256]; // buffer for client data int nbytes; int yes=1; // for setsockopt() SO_REUSEADDR, below int addrlen; int i, j; FD_ZERO(&master); // clear the master and temp sets FD_ZERO(&read_fds); // get the listener if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } // lose the pesky "address already in use" error message if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt"); exit(1); } // bind myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = INADDR_ANY; myaddr.sin_port = htons(PORT); memset(&(myaddr.sin_zero), '\0', 8); if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) { perror("bind"); exit(1); } // listen if (listen(listener, 10) == -1) { perror("listen"); exit(1); } // add the listener to the master set FD_SET(listener, &master); // keep track of the biggest file descriptor fdmax = listener; // so far, it's this one // main loop for(;;) { read_fds = master; // copy it if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(1); } // run through the existing connections looking for data to read for(i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { // we got one!! if (i == listener) { // handle new connections addrlen = sizeof(remoteaddr); if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen)) == -1) { perror("accept"); } else { FD_SET(newfd, &master); // add to master set if (newfd > fdmax) { // keep track of the maximum fdmax = newfd; } printf("selectserver: new connection from %s on " "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd); } } else { // handle data from a client if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) { // got error or connection closed by client if (nbytes == 0) { // connection closed printf("selectserver: socket %d hung up\n", i); } else { perror("recv"); } close(i); // bye! FD_CLR(i, &master); // remove from master set } else { // we got some data from a client for(j = 0; j <= fdmax; j++) { // send to everyone! if (FD_ISSET(j, &master)) { // except the listener and ourselves if (j != listener && j != i) { if (send(j, buf, nbytes, 0) == -1) { perror("send"); } } } } } } // it's SO UGLY! } } } return 0; }Заметь что у меня два списка файловых дескрипторов в коде: master и read_fds. Первый содержит все сокетные дескрипторы, которые в данный момент подсоединены, также как и сокетный дескриптор, который прослушивает новые соединения.
Причина, по которой я использую mastel set - это то, что select() изменяет список, который ты передаешь ей, чтобы отразить какие сокеты готовы для чтения. Т.к. я хочу следить за соединениями от одной select() к другой, то я должен осторожненько сохранить их куда-нибудь еще. В последний момент я копирую master в read_fds, и затем вызываю select().
Но значит ли это, что каждый раз, когда я получаю новое соединение, я должен добавить его к master set? Йееп! И каждый раз, когда соедиение закрывается, я должен удалять его из моего master set? Да, должен.
Если клиентская recv() вернет не ноль, я буду знать, что некоторые данные были получены. Поэтому я получаю их, а затем просматриваю master list и отправляю эти данные всем остальным клиентам.
И это, мои друзья, наиболее простой обзор всемогущей функции select().
6.3 Управление Частичными send()
Возвращаясь к предыдущему разделу о send() выше, помнишь, что я говорил о том, что send() может и не отослать все байты, которые ты хотел отослать? Т.е. допустим, что ты хотел отослать 512 байт, а функция вернула только 412. Что же случилось с остальной сотней байт?
Что ж, они все еще в твоем маленьком буфере, и они все еще ждут своей отправки. В зависимости от обстоятельств ядро ядро решило не отсылать все эти данные за один присест, и сейчас, мой друг, отправка оставшейся сотни байт легла на твои плечи.
Ты мог бы написать функцию примерно в духе вот этой, чтобы разрулить эту ситуацию:
Код (Text):
#include «sys/types.h» #include «sys/socket.h» int sendall(int s, char *buf, int *len) { int total = 0; // сколько байт мы отправили int bytesleft = *len; // сколько еще сталось для отправки int n; while(total < *len) { n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; // возвращает число в действительности отосланных байт return n==-1?-1:0; // вернет-1 при неудаче, 0 при успехе }В этом примере s - это сокет, в который ты хочешь послать данные; buf - это буфер, содержащий данные; len - указатель на int, содержащий число байт в буфере.
Функция вернет -1 при ошибке. Также, число действительно посланных байт возвращается в len. Это будет такое же число байт, которые ты попросил функцию отослать, если не произошла ошибка. sendall() сделает все наилучшим образом.
Для завершенности, вот простой пример к функции:
Код (Text):
char buf[10] = "Beej!"; int len; len = strlen(buf); if (sendall(s, buf, &len) == -1) { perror("sendall"); printf("We only sent %d bytes because of the error!\n", len); }Что произойдет на стороне получателя, если прибудет часть пакета? Если пакеты разной длины, то как получатель узнает, когда заканчивается первый пакет и начинается следующий? Ты, наверно, должен воспользоваться инкапсуляцией (помнишь про нее из раздела про инкапсуляцию данных в самом начале?). Прочти его!
6.4 Сын Инкапсуляции Данных
Что это значит - инкапсулировать данные? В самом простом случае это обозначает, что ты снабжаешь заголовок (header) либо с идентифицирующей его информацией, либо же с длиной пакета, или и с тем, и с другим.
Каким тогда должен быть заголовок? Что ж, это просто бинарные данные, которые представляют все, что ты считаешь нужным для включения в свой проект.
Вау. Это непонятно.
Окей. Например, давай скажем, что у тебя есть многопользовательская чат программа, использующая SOCK_STREAM сокеты. Когда юзер печатает ("говорит") что-нибудь, то два куска информации должны быть переданны на сервер: чтО было сказано, и ктО сказал это.
И все так просто? "А какие могут быть проблемы" - спросишь ты.
Проблема в том, что сообщения могут быть разной длины. Один человек по имени "tom", может сказать "Hi", а другой по имени "Benjamin" скажет "Hey guys what is up?".
Поэтому если ты отправишь через send() все данные как они есть к клиентам, то твой выходящий поток данных будет выглядеть примерно так:
Код (Text):
t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?И так далее. Как клиент сможет узнать, где заканчивается одно сообщение и начинается другое? Ты мог бы, если конечно захочешь, делать все сообщения одинаковой длины и просто вызвать sendall(), которую мы написали выше. Но это будет нерационально! Мы не хотим слать через send() 1024 байта для того, чтобы отправить всего лишь то, что "tom" сказал "Hi".
Поэтому мы инкапсулируем данные в маленьком заголовке и структуре пакета. Клиент и сервер оба знают, как запаковать и распаковать эти данные (это иногда называют "marshal" и "unmarshal"). Мы начиннаем определять протокол, который бы описывал способ, с помощью которого общаются клиент и сервер!
В этом случае давайте положим, что имя юзера имеет фиксированную длину в 8 символов, завершающуюся '\0'. И затем давай положим, что данные имеют различную длину, но не больше 128 символов. Давай возьмем для рассмотрения кусок структуры пакета, который мы бы могли использовать в этой ситуации:
- len (1 byte, unsigned) --Полная длина пакета, учитывая 8-ибайтовое имя юзера и данные чата
- name(8 byte) -- имя юзеа, заканчивающееся NULL, если необходимо
- chatdata(n-byte) -- сами данные, но не более чем 128 байт. Длина пакета должна быть посчитана как длина его данных плюс 8 (длина поля для имени выше)
Почему я выбрал 8-ибайтовое и 128-ибайтовое ограничения для полей? Я лишил их воздуха, пологая что они будут достаточно длинными. Может быть, хотя, 8 байт - очень мало для твоих нужд, и ты можешь иметь 30-ибайтовое поле под имя, или еще что-нибудь. Дело выбора остается за тобой.
Используя расписанное выше описание пакета, первый пакет мог бы состоять из след. информации (в hex и ASCII):
Код (Text):
0A 74 6F 6D 00 00 00 00 00 48 69 (length) T o m (padding) H iА следующий:
Код (Text):
14 42 65 6E 6A 61 6D 69 6E 48 65 79 20 67 75 79 73 20 77 ... (length) B e n j a m i n H e y g u y s w ...Длина сохранена в Network Byte Order, конечно же. В этом случае это только один байт, поэтому это не важно, но, в общем говоря, ты захочешь, чтобы твои бинарные целые числа были сохранены в Network Byte Order в твоих пакетах.
Когда ты отправляешь эти данные, тебе нужно быть осторожным и использовать команды в духе sendall() выше, чтобы ты всегда знал, что твои данные были отосланы, даже если это займет несколько вызовов send(), чтобы получить их все.
Похожим образом, когда ты получаешь данные, тебе нужно проделать немного дополнительной работы. Чтобы быть осторожным, тебе нужно понимать, что ты можешь получить частичные данные (например, мы могли бы получить 00 14 42 65 6E" от Benjamin выше). Нам нужно вызывать recv() опять и опять, пока мы не получим полностью весь пакет.
Но как? Что ж, мы занем суммарное число байт для пакета, которые надо получить. Нам также известен и максимальный размер пакета, который равен 1+8+128 = 137 байт (следуя из нашего описания пакета).
Мы можем объявить достаточно большой массив для двух пакетов. Это твое дело - куда ты перебросишь данные полученных пакетов.
Каждый раз, когда ты получаешь через recv() данные, ты отправляешь их в рабочий буфер и смотришь, не закончился ли пакет. Таким образом, число байт в буфере больше или равно длине, указанной в заголовке (+1, т.к. длина в буфере не включает байт на саму длину). Если число байт в буфере меньше чем 1, то очевидно, что пакет не завершен. Ты должен предусмотреть это, т.к. первый байт - это мусор, и ты не можешь полагаться на него как на корректную длину пакета.
Когда пакет будет завершен, ты можешь делать с ним все что пожелаешь. Используй его, а затем удаляй из своего рабочего буфера.
Фьюю! Ты отложил что-нибудь в своей голове? Ты также можешь прочесть конец первого пакета в буфер, а за ним и начало следующего через один вызов recv(). Т.о. у тебя будет рабочий буфер с одним полноценным пакетом, и с частичкой следующего! Bloody heck! Именно поэтому ты и сделал свой буфер достаточно большим, чтобы поместить в него аж 2 пакета.
Т.к. ты знаешь длину первого пакета из заголовка, то ты можешь отследить число байт в рабочем буфере, и подсчитать кол-во байт, относящихся уже ко второму (неполному) пакету. Когда ты закончил с первым пакетом, ты можешь удалить его из рабочего буфера и переместить ту часть второго пакета к началу. И теперь ты опять будешь готов для следующего recv().
Некоторые из вас могут решить, что перемещение частичные пакеты в начало рабочего буфера занимает много времени, и что программу можно написать при помощи другого метода. Можно, но рассмотрение таких методов выходит за рамки этой документации. Если вам все еще любопытно - достаньте какую-нибудь книжку по структурам данных.
Я никогда не говорил, что это было легко. ОК, я сказал, что это было легко И вот что: вам нужно просто практиковаться и очень скоро ты все поймешь. Клянусь Экскалибуром!
7. Частые Вопросы
Q: Где я могу взять эти заголовочные файлы
A:Если у тебя нет их, то тебе скорее всего они не нужны. Посмотри в мануале по твоей платформе. Если ты под Виндами, то тебе нужно только #include «winsock.h»Q: Что мне делать когда bind() возвращает: "Address already in use"?
A: Тебе надо использовать setsockopt() с опцией SO_REUSEADDR для слушающего сокета. Глянь в секцию bind() и в секцию про select().Q: Как мне получить список открытых сокетов на моей системе?
A: Используй netstat.
Код (Text):
$ netstatQ: Как я могу просмотреть routing table?
A: Выполни команду rout (в /sbin на большинстве Линуксов) или команду netstat -r.Q: Как я могу запустить клиент и сервер программы если у меня только один компьютер? Разве мне не нужна сеть для написания сетевых программ?
A: К счастью для тебя, виртуально все машины описывают loopback network девайс, который сидит в ядре и заменяет собой сетевую карточку.Q: Как я могу узнать, что удаленная сторона закрыла соединение?
A: Ты можешь узнать это, если recv() вернет 0.Q: Как мне написать утилиту ping? Что такое ICMP? Где мне узнать больше о raw сокетах и SOCK_RAW?
A: Сначала удали Винды и поставь Линух или BSD Нет, на самом деле, просто загляни в раздел, посвященный Винде в самом начале.Q: Как мне писАать под Solaris/SunOS? Я продолжаю получать ошибки линковщика при компиляции.
A: Линкер выдает ошибки, потому что операционка Sun не автоматически компилирует в сокет библиотеки. Посмотри раздел по Solaris/SunOS в начале.Q: Почему select() вылетает при сигнале?
A: Сигналы заставляют блокируемые системные вызовы возвращать -1 с errno, поставленным в EINTR. Когда ты настраиваешь signal handler с sigaction(), ты можешь установить flag SA_RESTART, который предназначен для рестарта системного вызова после того, как он был прерван. Но это не всегда работает. Мое любимое решение этой поблемы - использовать goto:
Код (Text):
select_restart: if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) { if (errno == EINTR) { // какой-то сигнал прервал нас, поэтому рестартим... goto select_restart; } // обрабатываем настоящие ошибки здесь: perror("select"); }Конечно же, тебе не нужно использовать goto в твоем случае; ты можешь применить другие структуры для контроля. Но я думаю, что goto гораздо яснее.
Q: Как я могу описать таймаут для вызова recv()?
A: Юзай select()! Он позволит тебе указать таймаут параметр для сокетного дескриптора, из которого ты собираешься читать. Или же, ты можешь вынести всю функциональность в единственную функцию, типа этой:
Код (Text):
#include «unistd.h» #include «sys/time.h» #include «sys/types.h» #include «sys/socket.h» int recvtimeout(int s, char *buf, int len, int timeout) { fd_set fds; int n; struct timeval tv; // настраиваем file descriptor set FD_ZERO(&fds); FD_SET(s, &fds); // настраиваем время на таймаут tv.tv_sec = timeout; tv.tv_usec = 0; // ждем таймаута или полученных данных n = select(s+1, &fds, NULL, NULL, &tv); if (n == 0) return -2; // timeout! if (n == -1) return -1; // error // данные должны быть здесть, поэтому делаем обычный recv() return recv(s, buf, len, 0); } // простой вызов recvtimeout(): . . n = recvtimeout(s, buf, sizeof(buf), 10); // 10 second timeout if (n == -1) { // была ошибка perror("recvtimeout"); } else if (n == -2) { // вышло время } else { // получини данные в buf } . .Заметь, что recvtimeout() возвращает -2 в случае таймаута. Почему не 0? Если ты вспомнишь, возвращаемый recv() 0 значит, что удаленная сторона прикрыла соединение.
Q: Как мне зашифровать или сжать данные перед отправкой через сокет?
A: Самый легкий способ - использовать SSL (secure socket layer)Но, полагая, что ты хочешь воткнуть свой собственный компрессом или систему шифровки, это всего лишь принцип понимания твоих данных как данных, проходящих последовательность шагов между двумя сторонами. Каждый шаг изменяет данные своим образом.
- сервер читает данные с файла (или еще с чего-нибудь)
- сервер шифрует данные (ты это сам напишешь)
- сервер шлет зашифрованные данные
Другая сторона:
- клиент получает зашифрованные данные
- клиент расшифровывает данные (это ты пишешь сам)
- клиент пишет данные в файл ( или еще куда-нибудь)
Ты также можешь сжать данные по такой же схеме. Или же и сжать, и зашифровать! Просто помни, что сжимать данные нужно перед шифрованием
Поэтому все что тебе нужно сделать с моим кодом - это найти место между тем, где данные отсылаются и получаются из сети, и воткнуть туда свой код по шифровке/сжатию.
Q: Что такое "PF_INET", который я заметил? Он связан с "AF_INET"?
A: Да, да, это так. Глянь раздел про socket() для подробностей.Q: Как я могу написать сервер, который бы принимал шелл команды от клиента и выполнял их?
A: Для простоты, скажем, что клиент connect(), send() и close() соединение. Схема, по которой будет действовать клиент, следующая:
- connect() к серверу
- send ("/sbin/ls > /tmp/client.out")
- close() соединение
Немного погодя сервер выполняет команды:
- accept() соединение от клиента
- recv(str) командную строку
- close() соедиение
- system(str) выполняем комаду
Будь осторожен! Иметь сервер, выполняющий клиентские команды - это то же самое, что дать удаленный шелл доступ. Подумай, что если клиент отправит "rm -rf ~"? Он удалит все в твоем аккаунте, вот что!
Поэтому, будь умным, и запрещай клиентам использовать все кроме нескольких утилит, которые для тебя неопасны, например след. образом:
Код (Text):
if (!strcmp(str, "foobar")) { sprintf(sysstr, "%s > /tmp/server.out", str); system(sysstr); }Но ты и сейчас не секьюрен: что будет, если клиент введет "foobar; rm -rf ~"? Самая безопасная вещь, которую можно сделать, - это написать маленькую функцию, которая будет добавлять эскейп ("\") символ впереди всех не буквенно-циферных символов (включая пробелы) в аргументы команды.
Как видишь, безопасность - это очень большая тема для разговора, когда сервер начинает исполнять все инструкции, которые ему посылает клиент.
Q: Когда я отсылаю 1460 байта, то я получаю только 536 из них за раз. Но если я запущу эту свою программу на моем компьютере, то я получаю все данные в то же время. Что происходит?
A: Ты превысил MTU - максимум размера, который может обработать физический medium. На твоем компьютере ты используешь loopback девайс, который может оперировать 8К или даже большими без проблем. Но на ethernet, который может оперировать только 1500 байтами с заголовком, ты превышаешь лимит. Через модем с 576 MTU (опять таки, с заголовком), ты превысишь даже меньший лимит.Ты должен убедиться, что все данные были отосланы. Когда ты убедился в этом, тебе нужно будет recv() в цикле, пока не отправятся все твои данные.
Q: Я под Виндой и у меня нет функции fork(). Как быть?
A: Если она есть, то она должна быть в POSIX библиотеках, которые могли идти с твоим компилером. Т.к. у меня нету Винды, то я не могу тебе точно сказать ответ. В крайнем случае можно заменить fork() Виндовым эквивалентом CreateProcess().8. Дисклеймер и Помощь.
что по крайней мере вся эта информация была точной и аккуратной, и я искренне надеюсь, что я не допустил досадных ошибок. Что ж, конечно же, они есть всегда.
Поэтому, пусть это предупредит тебя! Я извиняюсь за все неточности. Кроме всего, я провел много часов, разбирая и просматривая весь этот документ, описал несколько TCP/IP сетевых утилит для работы, написал мультиплеерный игровой движок итд. Но я не бог сокетов. Я просто парень.
Кстати, если у кого-нибудь есть конструктивные(или деструктивные) высказывания по этому документу, пожалуйста, шлите их на beej @ piratehaven.org и я попробую приложить усилия чтобы со всем разобраться.
Если ты удивляешься тому, а зачем же я написал все это, что ж, я сделал это за деньги. Ха! Нет, я написал все это потому что много людей задавали мне вопросы про сокеты, и, когда я сказал им, что я собираюсь поместить все ответы в документ по сокетам, они сказали "Круто!". Кроме этого, я чувствую, что все эти с трудом добытые знания утратятся, если я не поделюсь ими с другими. Веб просто стал идеальным двигателем. Я призываю всех делиться информацией, когда это только случается возможным.
Хватит об этом -- назад к кодингу! © Брайан "Beej" Холл, пер. varnie
Руководство Beej по сетевому программированию, используя интернет-сокеты
Дата публикации 2 ноя 2005