Наши партнеры

UnixForum





Библиотека сайта 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