Н.Ромоданов
Оригинал: "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 - это круто!