Библиотека сайта rus-linux.net
Создание собственного сервера: Интерфейс прикладного программирования Socket API, Часть 2
Оригинал: "Creating Your Own Server: The Socket API, Part 2"Автор: Pankaj Tanwar
Дата публикации: September 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июль 2012 г.
Начало серии статей о Socket API
Ранее мы создали простой сервер и простую клиентскую программу, в которых использовался интерфейс прикладного программирования Socket API. На этот раз мы сначала начнем с программы, а затем объясним, что было сделано. Итак, запустите свою систему и приготовьтесь углубиться в программировании сокетов.
Как уже было сказано, давайте сразу начнем с кода.
Версия сервера для IPv6
Ниже показана версия сервера, созданного нами в предыдущей статье, для протокола IPv6. Существенных изменений нет, за исключением появления в коде цифры "6". Давайте назовем этот файл serverin6.c:
#include <stdio.h%gt; #include <unistd.h%gt; #include <sys/types.h%gt; #include <sys/socket.h%gt; #include <netinet/in.h%gt; int main() { int sfd, cfd; char ch; socklen_t len; struct sockaddr_in6 saddr, caddr; sfd= socket(AF_INET6, SOCK_STREAM, 0); saddr.sin6_family=AF_INET6; saddr.sin6_addr=in6addr_any; saddr.sin6_port=htons(1205); bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr)); listen(sfd, 5); while(1) { printf("Waiting...n"); len=sizeof(cfd); cfd=accept(sfd, (struct sockaddr *)&caddr, &len); if(read(cfd, &ch, 1)<0) perror("read"); ch++; if(write(cfd, &ch, 1)<0) perror("write"); close(cfd); } }
Теперь давайте рассмотрим различия. Первое находится в строке 6 (sockaddr_in6) и оно понятно; для адресов IPv6 нам нужно в этой адресной структуре хранить адрес, порт и тип адресов так, как мы это делали в строках 13, 14 и 15. В строке 14 находится универсальный символ in6addr_any, используемый для всех адресов IPv6, такой же, как и INADDR_ANY для IPv4. Есть также изменения и в accept(), с которыми можно легко разобраться. И вот наш сервер работает с версией протокола IPv6. Вы можете с правами пользователя root настроить протокол IPv6 с помощью следующей команды:
ip -f inet6 addr add face:1f::ea54:a dev eth0
Здесь -f указывает семейство протоколов (inet6), а addr предназначен для хранения адреса - мы добавили face:1f::ea54:a (когда мы записываем адреса IPv6, лидирующие нули можно опустить, указанный выше адрес, в действительности, является адресом face:001f::ea54:000a). Вы можете задать любой адрес, и чтобы он ни с чем не совпадал, вы можете использовать свой MAC-адрес. В параметре dev указывается устройство, для которого мы устанавливаем адрес, в данном случае - eth0. Вы можете проверить результаты с помощью команды ifconfig.
Скомпилируйте и запустите сервер следующим образом:
cc serverin6.c -o serverin6 ./serverin6
Прежде, чем писать клиентскую программу, вы можете увидеть, что сервер работает даже с нашим клиентом для версии IPv4, а адрес IPv4, также указывает на ту же самую машину. Мы можем для того, чтобы проверить, работает ли сервер так, как ожидалось, подключаться к каждому из серверов с помощью telnet, например:
telnet localhost 1205
Введите символ, который вы хотите отправить на сервер, и нажмите клавишу Enter, и сервер ответит следующим символом ASCII, а затем закроет соединение. Помните, что для версии IPv4 localhost будет равен 127.0.0.1, а для IPv6 — будет ::1.
Клиентская программа (версия для IPv6)
Давайте теперь напишем версию клиентской программы для IPv6 и назовем ее clientin6.c:
include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char **argv) { int cfd; struct sockaddr_in6 addr; char ch; if(argc!=3) { printf("Usage: %s in6addr charactern", argv[0]); return -1; } if( ! inet_pton(AF_INET6, argv[1], &(addr.sin6_addr))) { /* returns 0 on error */ printf("Invalid Addressn"); return -1; } ch=argv[2][0]; /* Set the character to second argument */ cfd=socket(AF_INET6, SOCK_STREAM, 0); addr.sin6_family=AF_INET6; addr.sin6_port=htons(1205); if(connect(cfd, (struct sockaddr *)&addr, sizeof(addr))<0) { perror("connect error"); return -1; } if(write(cfd, &ch, 1)<0) perror("write"); if(read(cfd, &ch, 1)<0) perror("read"); printf("Server sent: %cn", ch); close(cfd); return 0; }
Здесь изменения снова незначительны и понятны - просто добавляется символ "6". Клиентская программа получает в качестве аргументов адрес IP и символы, так что для того, чтобы запустить эту программу, просто введите следующую команду:
cc clientin6.c -o clientin6 ./clientin6 ::1 d
Когда посылается символ d, сервер отвечает символом e. Результат будет такой же, как и в последних примерах, показанных на рисунках в предыдущей статье. Чтобы обработать адрес, мы должны использовать функцию inet_pton(), которую мы рассмотрим более подробно в следующем разделе.
Погружаемся глубже
Во-первых, давайте взглянем на функцию, которую мы использовали в нашей клиентской программе для преобразования адреса из строки в число. В двоичном виде адрес 192.168.1.23 будет равен 11000000 10101000 00000001 00010111 (32-битная строка из единиц и нулей). Для версии IPv6 это будет 128-битная строка (64-битный адрес). Адреса 192.168.1.23 или face:1f::ea54:a, которые более удобны для восприятия человеком, являются "презентационной" формой (функция p), а n является числовой (двоичной) формой. Прототип функции представлен в <arpa/inet.h> в виде:
int inet_pton (int family, const char *strptr, void *addrptr);
С помощью этой функции можно в машине в случае необходимости конвертировать как адреса IPv4, так и адреса IPv 6, из строки в двоичную форму. Первым аргументом, конечно, является семейство протоколов: AF_INET или AF_INET6. Аргумент *strptr является адресом в строковом виде, а *addrptr будет тем местом, где будет сохранен адрес в числовом формате; это должна быть структура (sin_addr — для IPv4, и sin6_addr - для IPv6). Функция в случае успеха возвращает 1 и 0 - в случае ошибки. Следующей функцией будет:
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
Эта функция делает ровно обратное по сравнению с предыдущей функцией; она преобразует числовое представление (addrptr) и сохраняет его в презентационной форме (strptr). Последним аргументом является len, размер/длина целевой переменной, которая используется для того, чтобы избежать переполнения. Для удобства в netinet/in.h определены константы, задающие размер, а именно:
#define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
Есть и другие функции, используемых для той же самой цели - inet_aton() и inet_ntoa(), но только для версии IPv4; поэтому мы их не используем.
Теперь давайте посмотрим на функции, используемые для преобразования байтов между стеком сетевого протокола и хоста и получения машинно-независимого кода. Архитектуры машин делятся на машины с обратным порядком хранения байтов и с прямым порядком хранения байтов, то есть различаются тем, как в машине хранятся данные. В машине с прямым порядком хранения байтов старший байт имеет больший адрес, а младший байт — меньший адрес. С другой стороны, в машине с обратным порядком хранения байтом старший байт имеет меньший адрес, а младший байт — больший адрес.
Например, если у нас есть 2-байтовое целое, хранящее значение 0x1234 по адресу 0×0200, оно будет занимать два байта: 0x0200 и 0x0201. Если у нас в 0x0200 находится значение 0x12, а в 0x0201 - значение 0x34, то машина с прямым порядком хранения байтов. Если значения хранятся в обратном порядке, то машина с обратным порядком хранения байтов. В книге "Сетевое программирование для Unix" Ричарда Стивенса (Unix Network Programming, W Richard Stevens) приводится программа, которая определяет, какая у вас машина. Давайте теперь взглянем на функции, которые определены в netinet/in.h:
uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue);
Эти две функции возвращают 16-битное и 32-битноее значения соответственно; htons() является сокращением от host to network short и htonl()- host to network long. Они преобразуют байты из порядка, используемого на хосте, в порядок, используемый в сетевом стеке. Функции ntohs() и ntohl() делают обратное (преобразуют порядок байтов, используемый в сети, в порядок байтов, используемый в хосте).
Эти функции особенно полезны в случае, когда мы просим систему предоставить нам информацию о хосте, на котором работает клиентская программа или сервер. Теперь давайте попробуем получить от клиентской программы некоторую информацию. Просто добавьте на сервер следующие строчки:
char address[INET6_ADDRSTRLEN]; /* at declaration section */ printf("Connected to client %s at port %dn", inet_ntop(AF_INET6, &caddr.sin6_addr, buff, sizeof(buff)), ntohs(caddr.sin6_port)); /* after the call to accept() */
После того, как эти строки будут изменены, результат станет таким, как это показано на рис.1. Сам код понятен, мы просто использовали рассмотренные выше функции с целью общего ознакомления.
Рис.1: Работающий сервер
Небольшое упражнение
Теперь для того, чтобы сделать что-нибудь более полезное, чем то, что мы делали ранее, поместите в соответствующую часть программы-сервера следующий код:
if(read(cfd, &ch, 1)<0) perror("read"); while( ch != EOF) { if((ch>='a' && ch<='z') || (ch>='A' && ch<='Z')) ch^=0x20; /* EXORing 6th bit will result in change in case */ if(write(cfd, &ch, 1)<0) perror("write"); if(read(cfd, &ch, 1)<0) perror("read"); }
Да, вы должны сделать это правильно: добавить код после вызова функции accept(). Теперь, если вы засыпаете точно также, как и я, просто обратитесь к серверу с помощью telnet; либо двигайтесь дальше и напишите собственную клиентскую программу.
Да, прежде, чем закрывать соединение, давайте взглянем на данные, выданные в сессии telnet. Запустите в вашей командной оболочке telnet ::1 1205 и начните набирать текст. На рисунке 2 приведен пример выдаваемых данных.
Рис.2: Используем telnet
При нажатии клавиш Ctrl+D сервер закроет соединение и будет ждать новое соединение. А для тех лентяев, кому неохота писать свою собственную клиентскую программу, нужно просто поместить в программу следующий код, а не только вызовы функций write() и read(); результат показан на рис.3:
while(1) { ch=getchar(); if(write(cfd, &ch, 1)<0) perror("write"); if(read(cfd, &ch, 1)<0) perror("read"); printf("%c", ch); }
Рис.3: Работа клиентской программы
Скомпилируйте и запустите вашу клиентскую программу.
Чтобы закрыть клиентскую программу, используйте сочетание клавиш Ctrl+D, которая пошлет символ EOF на сервер, а сервер закроет соединение с клиентом. Не нужно закрывать сервер с помощью нажатия клавиш Ctrl-C. Либо вы также можете закрывать соединение из клиентской программы; мы решим эту проблему позже с помощью обработчики сигналов. Теперь, я думаю, что нужно немного отдохнуть. Спокойной ночи и FOSS — это круто!
Продолжение серии статей о Socket API