Порт сокетных приложений из Unix в Windows
Как известно, концепция сокетов была разработана в Беркли, и затем реализована сначала в BSD, затем в Linux и, наконец, с некоторыми изменениями, и в Windows. Таким образом, это делает сетевое программирование на обеих платформах очень сильно похожим и перенос Unix приложения на Win-платформу становится не очень сложным делом.
Эта статья — попытка помочь тем, кто впервые решил осуществить порт сокетного приложения из Unix (Linux, BSD) в Windows. Зачем? Кто-то решает поднять свой уровень кодинга на новую высоту, кому-то это просто интересно, а кто-то ищет новые задачи для решения. Совет: не пытайтесь сразу портировать что-нибудь достаточно большое и сложное, начните с простых утилит вроде traceroute, nslookup и с прочтения этой статьи :-). Базовых знаний приемов сетевого программирования на C++ будет вполне достаточно. Все примеры из статьи компилировались на VC++ 6.0 под Win2k prof. и WinXP prof.
UNIX и Windows по разному обращаются с сокетами: в UNIX сокеты обрабатываются системой точно так же, как дескрипторы файлов integer типа, в то время как Windows это хэндл unsigned типа — SOCKET. В Unix все I/O действия выполняются чтением или записью в соответствующий дескриптор — число (integer) ассоциированное с открытым файлом, сетевым соединением, терминалом и т.п.
В Unix коды ошибок доступны через переменную errno, в Windows нужно использовать функцию WSAGetLastError().
И в Unix и в Windows порт определяется параметром, переданном функции htons(), но в Windows некоторые, наиболее часто используемые порты предопределены в
winsock.h:
IPPORT_ECHO — 7
IPPORT_DISCARD — 9
IPPORT_SYSTAT — 11
IPPORT_DAYTIME — 13
IPPORT_NETSTAT — 15
IPPORT_FTP — 21
IPPORT_TELNET — 23
IPPORT_SMTP — 25
IPPORT_TIMESERVER — 37
IPPORT_NAMESERVER — 42
IPPORT_WHOIS — 43
IPPORT_MTP — 57
Заголовочные файлы
Вот список функций Unix и соответствующих им .h-файлов
socket()
[ #include ]
[ #include ]
bind()
[ #include ]
[ #include ]
connect()
[ #include ]
[ #include ]
listen()
[ #include ]
[ #include ]
Эти два файла к сокетам не относятся, но обычно присутствуют в Unix программах:
Т.е. типичное начало сетевой UNIX программы выглядит так:
#include
#include
#include
#include
#include
При переносе в Windows, все эти строки заменяются на одну:
Объявление winsock.h уже включено в windows.h.
Вторым шагом будет линковка приложению Wsock32.lib (Для VC++: меню Project->Settings, на вкладке Link, дописать wsock32.lib к списку библиотек).
UNIX и Windows имеют ряд общих, выполняющих одинаковые функций
процедур. Это большинство функций работы с TCP/UDP, все функции преобразования + используемые ими структуры. Это, например, функции htons() и inet_addr() и структуры sockaddr и sockaddr_in.
Вот список этих функций:
socket()
bind()
listen()
connect()
accept()
sendto()
recvfrom()
gethostname()
А вот список функций, делающих одно и то же, различающихся только названиями:
Unix Windows
close() closesocket()
ioctl() ioctlsocket()
read() recv()
write() send()
Дополнительно к вышесказанному, каждое сокетное приложение Windows должно содержать вызовы функций WSASStartup() и WSACleanup(), которые подготавливают к использованию Winsock и освобождают его, соответственно.
Для начала, перенесем что-нибудь простенькое. Например утилиту, определяющую IP-адрес хоста по его имени. Вот UNIX код:
#include
#include /* Этот файл нужен функции
gethostbyname() */
#include
#include
#include
int main(int argc, char *argv[])
<
struct hostent *he;
if ((he=gethostbyname(argv[1]))==NULL)
<
printf («gethostbyname() error\n»);
exit (-1);
>
printf («Hostname : %s\n»,he->h_name); /* Вывод имени хоста */
printf («IP Address: %s\n»,inet_ntoa(*((struct in_addr *)he->h_addr))); /* И его IP-адреса
*/
>
Попытка скомпилировать этот код без изменений была горячо воспринята VC++ — компилятор ругнулся на отсутствие .h файлов и сообщил, что компилировать программу он не собирается :-). Первым делом удаляем 2-5 строки, заменив их на:
и немного изменим строку 6:
void main(int argc, char **argv)
не забыв прилинковать wsock32.lib к проекту. Теперь программа компилируется без проблем, но при попытке ею воспользоваться выдает лаконичное: «gethostbyname() error». В чем дело? Все просто, не были вызваны WSAStartup() и WSACleanup()! Добавляем вызовы этих функций и код нормально компилируется.
Вот код портированного, работающего Win приложения:
void main(int argc, char **argv)
<
WSADATA wsdata;
WSAStartup(0x0101,&wsdata);
struct hostent *he;
if (argc! = 2)
<
printf(«Usage: %s hostname\n»,argv[0]);
>
if ((he = gethostbyname(argv[1])) == NULL)
<
printf(«gethostbyname() error\n»);
>
printf («Hostname : %s\n»,he->h_name);
printf(«IP Address: %s\n»,inet_ntoa(*((struct in_addr *)he->h_addr)));
Конечно, это — очень простое приложение. Попробуем перенести программу посложнее
— TCP streaming server.
#include
#include
#include
#include
#define PORT 3550 /* Порт, открываемый программой */
#define BACKLOG 2 /* Число соединений */
main()
<
int fd, fd2; /* дескрипторы */
struct sockaddr_in server; /* информация о сервере */
struct sockaddr_in client; /* информация о клиенте */
int sin_size;
if ((fd=socket(AF_INET, SOCK_STREAM, 0)) == -1 )
< /* вызов socket() */
printf («socket() error\n»);
exit (-1);
>
server.sin_family = AF_INET;
server.sin_port = htons (PORT);
server.sin_addr.s_addr = INADDR_ANY;
bzero (&(server.sin_zero),8);
printf («You got a connection from %s\n», inet_ntoa(client.sin_addr) );
send (fd2,»Welcome to my server.\n»,22,0);
close (fd2);
>>
Итак, сначала повторим шаги из предыдущего примера — оставим объявления только windows.h и stdio.h и прилинкуем wsock32.lib. Попытка компиляции приносит две ошибки: одна — по поводу функции bzero(), вторая — по поводу функции
close() — компилятор сообщает, что она — invalid identifier :-). Первая ошибка лечится очень просто — удаляем всю 21-ю строку. Для исправления второй, смотрим в таблицу, приведенную выше и заменяем close() на ее Win-аналог — closesocket(). Добавляем вызовы WSAStartup() и WSACleanup() и voila — программа компилируется без проблем. После запуска программы видим пустую командную строку, все правильно — сервер ждет клиента :-).
Windows код сервера:
#include
#include
#define PORT 3550
#define BACKLOG 2
main()
<
WSADATA wsdata;
WSAStartup(0x0101,&wsdata);
struct sockaddr_in server;
struct sockaddr_in client;
int sin_size;
if ((fd=socket(AF_INET, SOCK_STREAM, 0)) == -1 )
<
printf(«socket() error\n»);
exit(-1);
>
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = INADDR_ANY;
if (listen (fd,BACKLOG) == -1)
<
printf («listen() error\n»);
exit (-1);
>
while (1)
<
sin_size=sizeof (struct sockaddr_in);
printf («You got a connection from %s\n»,inet_ntoa(client.sin_addr) );
send (fd2,»Welcome to my server.\n»,22,0);
closesocket (fd2);
TCP streaming client
#include
#include
#include
#include
#include /* необходим для struct hostent */
#define PORT 3550 /* Порт, к которому будем коннектиться */
#define MAXDATASIZE 100 /* Макс. размер данных в байтах */
int main (int argc, char *argv[])
<
int fd, numbytes; /* дескрипторы */
char buf[MAXDATASIZE]; /* здесь будем хранить полученный текст */
struct hostent *he;
struct sockaddr_in server;
if ((he=gethostbyname(argv[1]))==NULL)
<
printf(«gethostbyname() error\n»);
exit(-1);
>
if ((fd=socket(AF_INET, SOCK_STREAM, 0))==-1)
<
printf(«socket() error\n»);
exit(-1);
>
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(server.sin_zero),8);
if ((numbytes=recv(fd,buf,MAXDATASIZE,0)) == -1)
<
printf(«recv() error\n»);
exit(-1);
>
buf[numbytes]=’\0′;
printf(«Server Message: %s\n»,buf);
close(fd);
>
И, без лишних слов, Windows код — для его получения нужно проделать те же, вышеописанные, шаги.
#define PORT 3550
#define MAXDATASIZE 100
int main(int argc, char *argv[])
<
WSADATA wsdata;
WSAStartup(0x0101,&wsdata);
int fd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in server;
if ((he=gethostbyname(argv[1])) == NULL)
<
printf(«gethostbyname() error\n»);
exit(-1);
>
if ((fd=socket(AF_INET, SOCK_STREAM, 0))==-1)
<
printf(«socket() error\n»);
exit(-1);
>
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr);
if ((numbytes=recv(fd,buf,MAXDATASIZE,0)) == -1)
<
printf(«recv() error\n»);
exit(-1);
>
buf[numbytes] = ‘\0’;
printf(«Server Message: %s\n»,buf);
closesocket(fd);
WSACleanup();
return -1;
>
Запустив клиента (при запущенном сервере, естественно) с аргументом localhost (что-то вроде tcp_client.exe localhost) видим приветствие сервера — «Welcome to my server». С чем я тебя и поздравляю
:-).
Вот те шаги, которые нужно делать в первую очередь, при переносе приложений:
1. Заменить объявления заголовочных файлов UNIX на windows.h и прилинковать wsock32.lib
2. Добавить вызовы функций WSAStartup() и WSACleanup()
3. Заменить функции вроде close() и ioctl() на их Windows-аналоги
Можно даже написать небольшую программу делающую это автоматически (например на Perl). Она здорово облегчит процесс порта приложений.
Все примеры из статьи (exe + исходники) можно скачать
здесь.
Сетевое программирование для разработчиков игр. Часть 2: прием и передача пакетов данных
Прием и передача пакетов данных
Введение
Привет, меня зовут Гленн Фидлер и я приветствую вас в своей второй статье из цикла “Сетевое программирование для разработчиков игр”.
В предыдущей статье мы обсудили различные способы передачи данных между компьютерами по сети, и в конце решили использовать протокол UDP, а не TCP. UDP мы решили использовать для того, чтобы иметь возможность пересылать данные без задержек, связанных с ожиданием повторной пересылки пакетов.
А сейчас я собираюсь рассказать вам, как на практике использовать UDP для отправки и приема пакетов.
BSD сокеты
В большинстве современных ОС имеется какая-нибудь реализация сокетов, основанная на BSD сокетах (сокетах Беркли).
Сокеты BSD оперируют простыми функциями, такими, как “socket”, “bind”, “sendto” и “recvfrom”. Конечно, вы можете обращаться к этим функциями напрямую, но в таком случае ваш код будет зависим от платформы, так как их реализации в разных ОС могут немного отличаться.
Поэтому, хоть я далее и приведу первый простой пример взаимодействия с BSD сокетами, в дальнейшем мы не будем использовать их напрямую. Вместо этого, после освоения базового функционала, мы напишем несколько классов, которые абстрагируют всю работу с сокетами, чтобы в дальнейшем наш код был платформонезависимым.
Особенности разных ОС
Для начала напишем код, который будет определять текущую ОС, чтобы мы могли учесть различия в работе сокетов:
Теперь подключим заголовочные файлы, нужные для работы с сокетами. Так как набор необходимых заголовочных файлов зависит от текущей ОС, здесь мы используем код #define, написанный выше, чтобы определить, какие файлы нужно подключать.
В UNIX системах функции работы с сокетами входят в стандартные системные библиотеки, поэтому никакие сторонние библиотеки нам в этом случае не нужны. Однако в Windows для этих целей нам нужно подключить библиотеку winsock.
Вот небольшая хитрость, как можно это сделать без изменения проекта или makefile’а:
Мне нравится этот прием потому, что я ленивый. Вы, конечно, можете подключить библиотеку в проект или в makefile.
Инициализация сокетов
В большинстве unix-like операционных систем (включая macosx) не требуется никаких особых действий для инициализации функционала работы с сокетами, но в Windows нужно сначала сделать пару па — нужно вызвать функцию “WSAStartup” перед использованием любых функций работы с сокетами, а после окончания работы — вызвать “WSACleanup”.
Давайте добавим две новые функции:
Теперь мы имеем независимый от платформы код инициализации и завершения работы с сокетами. На платформах, которые не требуют инициализации, данный код просто не делает ничего.
Создаем сокет
Теперь мы можем создать UDP сокет. Это делается так:
Далее мы должны привязать сокет к определенному номеру порта (к примеру, 30000). У каждого сокета должен быть свой уникальный порт, так как, когда приходит новый пакет, номер порта определяет, какому сокету его передать. Не используйте номера портов меньшие, чем 1024 — они зарезервированы системой.
Если вам все равно, какой номер порта использовать для сокета, вы можете просто передать в функцию “0”, и тогда система сама выделит вам какой-нибудь незанятый порт.
Теперь наш сокет готов для передачи и приема пакетов данных.
Но что это за таинственная функция “htons” вызывается в коде? Это просто небольшая вспомогательная функция, которая переводит порядок следования байтов в 16-битном целом числе — из текущего (little- или big-endian) в big-endian, который используется при сетевом взаимодействии. Ее нужно вызывать каждый раз, когда вы используете целые числа при работе с сокетами напрямую.
Вы встретите функцию “htons” и ее 32-битного двойника — “htonl” в этой статье еще несколько раз, так что будьте внимательны.
Перевод сокета в неблокирующий режим
По умолчанию сокеты находится в так называемом “блокирующем режиме”. Это означает, что если вы попытаетесь прочитать из него данные с помощью “recvfrom”, функция не вернет значение, пока не сокет не получит пакет с данными, которые можно прочитать. Такое поведение нам совсем не подходит. Игры — это приложения, работающие в реальном времени, со скоростью от 30 до 60 кадров в секунду, и игра не может просто остановиться и ждать, пока не придет пакет с данными!
Решить эту проблему можно переведя сокет в “неблокирующий режим” после его создания. В этом режиме функция “recvfrom”, если отсутствуют данные для чтения из сокета, сразу возвращает определенное значение, показывающее, что нужно будет вызвать ее еще раз, когда в сокете появятся данные.
Перевести сокет в неблокирующий режим можно следующим образом:
Как вы можете видеть, в Windows нет функции “fcntl”, поэтому вместе нее мы используем “ioctlsocket”.
Отправка пакетов
UDP — это протокол без поддержки соединений, поэтому при каждой отправке пакета нам нужно указывать адрес получателя. Можно использовать один и тот же UDP сокет для отправки пакетов на разные IP адреса — на другом конце сокета не обязательно должен быть один компьютер.
Переслать пакет на определенный адрес можно следующим образом:
Обратите внимание — возвращаемое функцией “sendto” значение показывает только, был ли пакет успешно отправлен с локального компьютера. Но оно не показывает, был ли пакет принят адресатом! В UDP нет средств для определения, дошел ли пакет по назначению или нет.
В коде, приведенном выше, мы передаем структуру “sockaddr_in” в качестве адреса назначения. Как нам получить эту структуру?
Допустим, мы хотим отправить пакет по адресу 207.45.186.98:30000.
Запишем адрес в следующей форме:
И нужно сделать еще пару преобразований, чтобы привести его к форме, которую понимает “sendto”:
Как видно, сначала мы объединяем числа a, b, c, d (которые лежат в диапазоне [0, 255]) в одно целое число, в котором каждый байт — это одно из исходных чисел. Затем мы инициализируем структуру “sockaddr_in” нашими адресом назначения и портом, при этом не забыв конвертировать порядок байтов с помощью функций “htonl” и “htons”.
Отдельно стоит выделить случай, когда нужно передать пакет самому себе: при этом не нужно выяснять IP адрес локальной машины, а можно просто использовать 127.0.0.1 в качестве адреса (адрес локальной петли), и пакет будет отправлен на локальный компьютер.
Прием пакетов
После того, как мы привязали UDP сокет к порту, все UDP пакеты, приходящие на IP адрес и порт нашего сокета, будут ставиться в очередь. Поэтому для приема пакетов мы просто в цикле вызываем “recvfrom”, пока он не выдаст ошибку, означающую, что пакетов для чтения в очерели не осталось.
Так как протокол UDP не поддерживает соединения, пакеты могут приходить с множества различных компьютеров сети. Каждый раз, когда мы принимаем пакет, функция “recvfrom” выдает нам IP адрес и порт отправителя, и поэтому мы знаем, кто отправил этот пакет.
Код приема пакетов в цикле:
Пакеты, размер которых больше, чем размер буфера приема, будут просто втихую удалены из очереди. Так что, если вы используете буфер размером 256 байтов, как в примере выше, и кто-то присылает вам пакет в 300 байт, он будет отброшен. Вы не получите просто первые 256 байтов из пакета.
Но, поскольку мы пишем свой собственный протокол, для нас это не станет проблемой. Просто всегда будьте внимательны и проверяете, чтобы размер буфера приема был достаточно большим, и мог вместить самый большой пакет, который вам могут прислать.
Закрытие сокета
На большинстве unix-like систем, сокеты представляют собой файловые дескрипторы, поэтому для того, чтобы закрыть сокеты после использования, можно использовать стандартную функцию “close”. Однако, Windows, как всегда, выделяется, и в ней нам нужно использовать “closesocket”.
Так держать, Windows!
Класс сокета
Итак, мы разобрались со всеми основными операциями: создание сокета, привязка его к порту, перевод в неблокирующий режим, отправка и прием пакетов, и, в конце, закрытие сокета.
Но, как вы могли заметить, все эти операции немного отличаются от платформы к платформе, и, конечно, трудно каждый раз при работе с сокетами вспоминать особенности разных платформ и писать все эти #ifdef.
Поэтому мы сделаем класс-обертку “Socket” для всех этих операций. Также мы создадим класс “Address”, чтобы было проще работать с IP адресами. Он позволит не проводить все манипуляции с “sockaddr_in” каждый раз, когда мы захотим отправить или принять пакет.
Итак, наш класс Socket:
Использовать их для приема и передачи нужно следующим образом:
Как видите, это намного проще, чем работать с BSD сокетами напрямую. И также этот код будет одинаков для всех ОС, потому весь платформозависимый функционал находится внутри классов Socket и Address.
Заключение
Теперь у нас есть независимый от платформы инструмент для отправки и према UDP пакетов.
UDP не поддерживает соединения, и мне хотелось сделать пример, который бы четко это показал. Поэтому я написал небольшую программу, которая считывает список IP адресов из текстового файла и рассылает им пакеты, по одному в секунду. Каждый раз, когда программа принимает пакет, она выводит в консоль адрес и порт компьютера-отправителя и размер принятого пакета.
Вы можете легко настроить программу так, чтобы даже на локальной машине получить несколько узлов, обменивающихся пакетами друг с другом. Для этого просто разным экземплярам программы задайте разные порты, например:
> Node 30000
> Node 30001
> Node 30002
И т.д…
Каждый из узлов будет пересылать пакеты всем остальным узлам, образуя нечто вроде мини peer-to-peer системы.
Я разрабатывал эту программу на MacOSX, но она должна компилироваться на любой unix-like ОС и на Windows, однако если вам для этого потребуется делать какие-либо доработки, сообщите мне.