Сетевое программирование в Linux (1119562), страница 2
Текст из файла (страница 2)
По большому счёту, это излишняя предосторожность, поэтому на эти две строки можно смело забить (по крайней мере не нужно пытаться понять, каконо там работает).linger linger_opt = { 1, 0 };setsockopt(s0, SOL_SOCKET, SO_LINGER, &linger_opt, sizeof(linger_opt));3.1.3. Приём входящих соединений и функция acceptНаконец-то наступает кульминационный момент в жизни гнезда s0.
Всё, что было написано выше, былонужно ради следующих двух команд:listen(s0, 1);sockaddr_in CliAddr;socklen_t AddrLen;int s1 = accept(s0, (struct sockaddr*) &CliAddr, &AddrLen);Вызвав функцию listen, мы заставляем систему ждать прихода запроса на соединение от клиента. Второйпараметр указывает на требуемое количество подключений — в нашем случае нужен только один клиент. Следует помнить, что эта функция будет ждать вечно, если никто не захочет подключаться к нашему серверу. Но3будем считать, что хоть один юзер догадался выполнить функцию connect, да ещё к тому же угадал номерпорта и IP-адрес нашей станции. Тогда listen завершится, и можно будет выяснить, что же это за юзер —по крайней мере, мы сможем узнать IP-адрес его компьютера и номер порта, на котором он сидит. А делатьэто мы будем так: заведём ещё одну переменную типа sockaddr_in, назвав её CliAddr, и вызовем функциюaccept, передав в качестве первого аргумента дескриптор того гнезда, на которое пришло сообщение, а в качестве второго — указатель на структуру для адреса клиента.
Функция accept занесёт туда все данные о нашемновом клиенте, а её возвращаемое значение — дескриптор гнезда, по которому можно будет предавать клиентуданные.С этого момента установка соединения со стороны сервера считается успешной. Далее про всякие accept’ы,bind’ы и listen’ы можно забыть. Они относятся к инициализационной части программы и далее нам нужнобудет только посылать и принимать данные с помощью функций read/write.
Более того, гнездо s0 теперьможно даже закрыть — оно больше не пригодится. Закрытие гнезда производится функцией close.3.2. Клиентская частьТеперь посмотрим, что в это время происходит на другом конце провода.3.2.1. Получение адреса сервераТот вид соединений, который мы здесь используем (т. е. connection-oriented), предполагает, что сервер сидиттихо и только «слушает». Клиент же, напротив, должен послать запрос на соединение по вполне конкретномуадресу. Но откуда, позвольте спросить, он возьмёт этот адрес? Конечно, может быть так: два человека захотелисыграть по сети, и один из них знает IP-адрес компьютера другого. Тогда всё тривиально: указываем нужныйадрес и подключаемся туда, где нас уже ждут.
Но как быть, когда мы не знаем адреса сервера? Тут разумновоспользоваться функцией gethostbyname, позволяющей по имени сервера определить его адрес. Это эквивалентно тому, как если бы мы стали искать в адресной книге адрес требуемого учреждения (естественно, еслимы знаем, как оно называется).hostent * Host = gethostbyname(HostName);Здесь HostName есть строка с именем сервера.
Среди всех допустимых имён зарезервировано специальноеимя localhost. Если выполняется подключение к localhost, то подключение происходит «к самому себе»,т. е. клиент и сервер должны работать на одном и том же компьютере. С тем же успехом можно вместослова localhost указать IP-адрес 127.0.0.1, также означающий себя самого. Функция gethostbyname возвращаетадрес некоторой структуры типа hostent, у которой есть поле h_addr_list, представляющее собой массивадресов, соответствующих данному серверу.
Мы выберем из этого массива первый элемент и засунем его вструктуру. . . нет, совсем забыл, сначала надо же ещё объявить переменную sockaddr_in SrvAddr, почиститьеё memset’ом, и только потом скопировать адрес. Ну, а про настройку типа адреса и номера порта всё уже былосказано выше — тут ничего нового нет.sockaddr_in SrvAddr;memset(&SrvAddr, 0, sizeof(SrvAddr));SrvAddr.sin_family = AF_INET;SrvAddr.sin_port = htons(Port);memmove(&(SrvAddr.sin_addr.s_addr), Host->h_addr_list[0], 4);3.2.2.
Подключение к серверуТут всё предельно просто. Для начала создадим гнездо — точно также, как и на сервере, а затем вызовемдолгожданный connect:int s0 = socket(AF_INET, SOCK_STREAM, 0);connect(s0, (sockaddr*) &SrvAddr, sizeof(SrvAddr));Вторым параметром передаётся тот самый адрес, которой был получен столь долгим и мучительным путём.Если на другом конце провода нас не ждут, то connect вернёт отрицательное значение. В противном случаеможно считать, что соединение установлено.4.
Сетевой обмен даннымиНастало время разобраться с тем, ради чего мы заварили всю эту кашу с сокетами (надеюсь, пока у читателяв голове подобной каши ещё не возникло). С посылкой данных всё совсем просто: делаем массив, ну, скажем,на 1024 байта (назовём его SendBuf), запихиваем данные в этот массив и вызываем функцию write:4write(s1, SendBuf, 1024 * sizeof(char));Естественно, так выглядит команда посылки со стороны сервера.
Для клиента — почти так же, тольковместо s1 нужно написать s0. А вот с приёмом пакетов дела обстоят несколько хитрее. Часто в сетевых программах (например в играх) нельзя ждать, пока придет очередное сообщение, а нужно выполнять какие-тодругие действия (например, перерисовывать экран). Поэтому имеет смысл воспользоваться функцией select.Она позволяет определить, есть ли у гнезда новые данные, или нет. Кроме того, эта функция умеет ждатьприхода сообщения в течение определённого промежутка времени, и если он проходит, а сообщения всё нет, тофункция завершается.
Эта функция позволяет отслеживать очень маленькие промежутки времени — с точностью до одной микросекунды. Напишем функцию bool CheckMessage(int Socket, char * Buffer), котораябудет проверять, пришло ли сообщение на сокете Socket. Если да, то она возвращает true и «сливает 1024 байтаиз входной трубы сокета» в массив Buffer, а если нет, то просто возвращает false.bool CheckMessage(int Socket, int * Buffer){fd_set DSet;FD_ZERO(&DSet);FD_SET(Socket, &DSet);timeval tv;tv.tv_sec = 0;tv.tv_usec = 10000;if (select(Socket+1, &DSet, 0, 0, &tv) > 0){read(Socket, Buffer, 1024);return true;}else return false;}1секунды данМаксимальный интервал ожидания установлен в 10000 микросекунд, т.
е. если в течение 100ные не появятся, будет выдано значение false. Использовать функцию CheckMessage надо примерно так: послеустановки соединения делаем бесконечный цикл, в котором вызываем эту функцию, и если результат положительный, то обрабатываем данные, иначе — продолжаем цикл.Скажем пару слов о том, что такое fd_set и select. Как уже было сказано, сокет — это дескриптор потокаввода-вывода. В недрах ОС содержится информация по каждому дескриптору, и разработчики предусмотрелимеханизм получения этой информации.
Структура fd_set описывает битовое множество всех дескрипторов.Существует три типа событий, сигнализирующих о состоянии каждого дескриптора:• Есть данные для чтения• Гнездо готово к записи данных• Произошла ошибкаЗа эти три типа событий отвечают второй, третий и четвёртый параметры функции select соответственно.Чтобы получить информацию о каком-либо дескрипторе, нужно установить в соответствующем битовом множестве бит этого дескриптора и вызвать функцию select.
Первый параметр ограничивает максимальный номеринтересующего нас дескриптора плюс один.Нас интересует только информация о данных на чтение, поэтому третий и четвёртый параметры установленыв нули. Последний параметр — это величина таймаута в микросекундах. Функция select будет «прослушивать»указанные дескрипторы в течение этого времени, и, если в каком-либо из выбранных дескрипторов произошлособытие требуемого типа, вернёт положительное число. Чтобы узнать, в каком именно, вообще говоря, нужнопроверить битовое множество DSet, но в нашем случае дескриптор всего один, и проверять ничего не нужно.Если бы таковая проверка была бы необходима, её следовало бы выполнять с помощью макроса FD_ISSET. Болееподробную информацию о select всегда можно посмотреть в справочной системе Linux.5.
Пример программыСтрого говоря, теория изложена в полном объёме. Остаётся немного попрактиковаться и написать какую-нибудь программу. Но чтобы читателю не было скучно, мы кратко изложим на словах её алгоритм, а в качествеработоспособного примера приведём другую программу.55.1. Упражнение: пересылка файла по сетиНапишите самостоятельно программу, пересылающую двоичный файл с одного компьютера на другой.
Длячтения и записи двоичных (т. е. не обязательно текстовых) файлов есть функцииint fread(void * Buffer, int Size, int Count, FILE * F)int fwrite(void * Buffer, int Size, int Count, FILE * F)Пусть файл передаётся от сервера к клиенту. Тогда алгоритм работы будет примерно следующий: сервер вцикле считывает файл кусками (например, по 1024 байта) и отправляет клиенту сообщения такого формата:Размер i-го куска (4 байта)i-й кусок файла (6 1024 байт)Клиент же записывает эти куски последовательно себе на диск в какой-нибудь файл, а после записи очередного куска отправляет подтверждение серверу. Сервер же в это время ждёт подтверждения, и когда оноприходит, посылает очередной кусок файла.
Подтверждения нужны не для того, чтобы гарантировать доставкуданных, а для того, чтобы в сети не произошло «затора» из пакетов. Запись на диск — медленная операция (покрайней мере, медленнее, чем чтение), и может получиться так, что клиент не будет успевать «сливать водуиз трубы-сокета». Для маленьких файлов это будет незаметно, а вот при попытке переслать файл мегабайт надесять могут начаться всякие глюки...5.2. Пример: игра «Быки и коровы»Напомним, что игра состоит в угадывании четырёхзначного числа, загаданного противником, который информирует игрока о количестве точных совпадений, или «быков» (правильная цифра на правильном месте) иколичестве неточных совпадений, или «коров» (правильная цифра не на своём месте).В данном случае клиент и сервер фактически равноправны, поэтому код сервера и клиента отличается толькомеханизмом установки соединения.5.2.1.
Серверная часть#include#include#include#include#include#include#include<signal.h><stdio.h><string.h><stdlib.h><unistd.h><sys/socket.h><netinet/in.h>const int MAX = 64;void SigHandler(int SigID);bool WaitMessage(int Socket, int * Buffer);bool GetAnswer(int N, int GN, int * B, int * C);// Обработчик ошибок гнезда// Функция ожидания данных// Вычисляет количество быков и коровint main(int argc, char * argv[]){printf("Welcome to the game ’Bulls & Cows’.