Интерфейс прикладного программирования Socket API, Часть 1: Создание собственного сервераИсточник: rus-linux Н.Ромоданов
Оригинал: "Creating Your Own Server: The Socket API, Part 1" В этой серии статей, передназначенных для новичков сетевого программирования (знание языка C является обязательным условием), мы узнаем, как с помощью интерфейса прикладного программирования Socket API системы UNIX создавать сервера и сетевые клиентские программы. Мы начнем с создания простых программ типа "клиент-сервер", а затем попробуем сделать что-нибудь более сложное. Мы также попытаемся понять, как работают различные сервера. Я постарался включить в опиание множество подробностей, но если вы обнаружите, что некоторая информация отсутствует, то, пожалуйста, не стесняйтесь сообщить мне об этом в комментариях. Поскольку мы при сетевом программировании рассматриваем сокеты, новички должны сначала разобраться с уровнями модели OSI и с протоколами, используемыми на этих уровнях. Каждый уровень в этой модели отвечает за выполнение определенной работы, что в результате делает возможным передачу данных по сети. В каждом уровне происходит абстрагирование работы, выполняемой на более низких уровнях, и представление этой работы на уровень, находящийся выше. Если вы не знакомы с эталонной моделью взаимодействия открытых систем ISO OSI (Open Systems Interconnection) Reference Model, я рекомендую о ней почитать. Хорошей отправной точкой является Здесь мы сосредоточимся на сессионном уровне (в котором происходит создание сессий и поддержка работы с ними) и транспортном уровне, на котором обеспечивает надежная или ненадежная передача данных от отправителя к получателю. Есть несколько протоколов - TCP (для надежных соединений), UDP (для ненадежных соединений) и SCTP (расширенный протокол с возможностью множественного подключения). Информацию о протоколах TCP / IP, пожалуйста, смотрите Протокол Transmission Control Protocol (TCP)Протокол TCP является протоколом, ориентированным на соединения, который обеспечивает надежный полнодуплексный поток байтов, идущий к пользователям. Здесь мы, когда используем протокол TCP, напрямую обращаемся на транспортный уровень с уровня приложений, на котором пользователи могут взаимодействовать с программой. Протокол TCP обладает рядом важных особенностей. Это надежный протокол (в отличие от не обрабатывающего соединения протокола UDP, который мы рассмотрим в следующих статьях). После того, как пакет будет передан, протокол ждет подтверждение о приеме; если оно не вернулось, то пакет ретранслируется несколько раз (в зависимости от реализации). Если данные не могут быть переданы, протокол уведомляет пользователя и закрывает соединение. В протоколе TCP также определяется, как долго ждать подтверждения, - для этого используется оценочное значение RTT (Round Trip Time - время прохождения маршрута), задаваемое между сервером и клиентом. В протоколе также происходит назначение сегментам порядковых номеров, так что если сегменты принимаются в неправильной последовательности, их можно будет переупорядочить на принимающей стороне. Благодаря этому можно игнорировать дублирующие сегменты (передаваемые повторно из-за задержек). В протоколе TCP осуществляется управление потоком данных: принимающая сторона может сообщить отправителю, сколько байтов данных будет принято, так что медленно работающий приемник не будет выведен из строя слишком большим количеством данных. Соединения TCP являются полнодуплексными - приложение может одновременно отправлять и получать данные. Простые сервераТеперь для того, чтобы разобраться с сокетами, используемыми в интернете, давайте создадим простой сервер (server.c). Первоначально наш код будет создан для версии протокола IPv4, но в последующих статьях мы рассмотрим версию протокола IPv6, а затем перейдем к коду, не зависящему от версии протокола. #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main() { int sfd, cfd; int ch='k'; struct sockaddr_in saddr, caddr; sfd= socket(AF_INET, SOCK_STREAM, 0); saddr.sin_family=AF_INET; /* Set Address Family to Internet */ saddr.sin_addr.s_addr=htonl(INADDR_ANY); /* Any Internet address */ saddr.sin_port=htons(29008); /* Set server port to 29008 */ /* select any arbitrary Port >1024 */ bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr)); listen(sfd, 1); while(1) { printf("Server waitingn"); cfd=accept(sfd, (struct sockaddr *)NULL, NULL); if(read(cfd, &ch, 1)<0) perror("read"); ch++; if(write(cfd, &ch, 1)<0) perror("write"); close(cfd); } } О кодеПервое, на что нужно взглянуть, это структура сstruct sockaddr_in. Эта структура используется для хранения интернет адреса (IP) в поле sin_addr, которое является структурой типа struct in_addr и в которой может храниться 32-битное беззнаковое целочисленное значение. Номер порта хранится в полеsin_port, это беззнаковое 16-битное целое (посольку номер порта должен быть меньше 65536). Далее, давайте посмотрим на вызов функции socket() (в includes требуется указать sys/types.h и sys/socket.h): int socket(int domain, int type, int protocol); Если обращение к socket() будет успешным, то будет возвращен дескриптор, который будет использован на завершающей стадии соединения. В первом аргументе, domain, определяется домен соединения - семейство протоколов, которые будут использоваться при соединении. Согласно sys/sockets.h, это следующие протоколы:
AF является сокращением от Address Family - семейство адресов. Здесь мы используем AF_INET - интернет-протокол IPv4. В следующем аргументе, type, указывается тип соединения; может использоваться один из следующих вариантов:
В аргументе protocol определяется протокол, который будет использоваться совместно с сокетом. Как правило, для поддержки конкретного типа сокета в заданном семействе протоколов существует только один протокол (который выше был указан в скобках). В таком случае, этот аргумент равен 0. Далее давайте поместим адрес в поле sin_addr, так как это было показано выше. Когда происходит обращение к функции socket(), то создается сокет, но ему адрес не назначается. Поэтому нам нужна функция bind(): Эта функция используется для того, чтобы связать дескриптор сокета sockfd с адресом addr; а в addrlen указывается длина адреса. Эта операция называется назначением имени сокету. Затем будем слушать сокет с помощью listen(): int listen(int sockfd, int backlog); С помощью вызова listen() соответствующий сокет помечается демоном sockfd как пассивный сокет - т. е. такой, который будет использоваться для приема входящих подключений. В качестве типа сокета должен быть SOCK_STREAM или SOCK_SEQPACKET, т.е. должно обеспечиваться надежное соединение. В аргументеbacklog определяется максимальная длина очереди ожидающих соединений с sockfd. Если очередь превысит указанное значение, то клиентской программе будет отказано в соединении. Далее, давайте войдем в бесконечный цикл, который используется для обслуживания запросов клиентов. Здесь мы должны создать еще один дескриптор сокета для клиента, вызвав для этого функцию accept. Теперь, все, что будет записано в этот дескриптор, передается клиенту, а все, что читается из этого дескриптора, является данными, которые клиент отправляет на сервер: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); Системный вызов accept() используется с типами сокетов SOCK_STREAM и SOCK_SEQPACKET. Он извлекает первый запрос на соединение из очереди запросов, ожидающих соединений с демоном sockfd, слушающим сокеты, создает сокет нового соединения и возвращает новый дескриптор, относящийся к этому сокету - в нашей программе это cfd. Новый сокет не находится в состоянии прослушивания. Исходный сокет sockfd не оказывает влияние на этот вызов. В аргументе addr находится адрес удаленного компьютера, с которым мы связываемся, но т. к. заранее мы не знаем адрес клиента, здесь это значение равно NULL. Затем давайте с помощью операции read() прочитаем из дескриптора символ (отправленный клиентом на сервер), увеличим его значение на единицу и с помощью команды write() запишем в дескриптор его новое значение, которое будет отправлено клиенту. Затем закроем дескриптор с помощью вызоваclose(). Обработка ошибок, которую я выбрал, базируется на том, что в случае неудачи эти функции возвращают отрицательные значения; для того, чтобы отобразить сообщение о номере ошибки, я использую функцию perror(). Клиентская программаИ теперь клиентская программа (client.c). Этот клиент посылает символ на сервер, работающий на порту 29008 (или любом другом произвольном порту): #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_in addr; char ch='r'; cfd=socket(AF_INET, SOCK_STREAM, 0); addr.sin_family=AF_INET; addr.sin_addr.s_addr=inet_addr("127.0.0.1"); /* Check for server on loopback */ addr.sin_port=htons(29008); 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("nReply from Server: %cnn",ch); close(cfd); return 0; } Порядок работы клиентской программы аналогичен порядку работы сервера. Первое отличие заключается в том, что в sin_addr указывается интернет-адрес сервера (адрес localhost, указывающий на ту же самую машину). Далее, вместо прослушивания вызывается системный вызов connect(), с помощью которого выполняется подключение sockfd по адресу, указанному в addr. Возвращаемый дескриптор будет использоваться для связи с указанным адресом. Затем в программе мы для того, чтобы отправить символ на сервер и получить символ, используем команды write() и read(), а затем - закрываем дескриптор. Запуск программКомпиляция программы осуществляется следующим образом: cc server.c -o server cc client.c -o client Затем, запускаем программы: ./server & ./client Чтобы было проще следить за работой каждой из программ, запускайте их в разных терминалах. На рис.1 показаны данные, выдаваемые на терминал сервером, а на рис.2 - клиентской программой. Рис.1: Работающий сервер Рис.2: Данные, выдаваемые клиентской программой Хорошее начало, не прада ли? В следующей статье мы рассмотрим, как переписать обе эти программы для протокола IPv6 и будем двигаться дальше к UDP. И да, FOSS - это круто! |