Guide to Network Сетевое программирование от Биджа
Скачать 1.34 Mb.
|
20 http://beej.us/guide/bgnet/examples/showip.c 19 Beej's Guide to Network Как видите, код передаёт getaddrinfo() всё, что вы укажете в командной строке, она заполняет связанный список, на который указывает res, и мы можем пройтись по нему, распечатывая содержимое или что-то ещё. Есть некоторое уродство в том, что нам нужно копаться в различных типах struct sockaddr в зависимости версии IP. Простите за это Яне уверен, что можно лучше) Пример работает Всем нравится распечатка $ showip www.example.net IP addresses for www.example.net: IPv4: 192.0.2.88 $ showip ipv6.example.com IP addresses for ipv6.example.com: IPv4: 192.0.2.101 IPv6: 2001:db8:8c00:22::171 Теперь у нас всё под контролем и мы передадим полученные от getaddrinfo() результаты другой функции сокета ив конце концов, установим сетевое соединение Продолжайте читать 5.2. soket() -‐ Получи дескриптор файла Полагаю, откладывать больше нельзя, мне нужно рассказать о системном вызове socket(). Вот схема #include #include ! int socket(int domain, int type, int protocol); Что это за аргументы Они позволяют указать какой тип сокета вам нужен (IPv4 или IPv6, потоковый или дейтаграммный и TCP или UDP). Люди обычно жёстко устанавливают эти значения ивы можете поступить абсолютно также. (ставится или PF_INET6, это или SOCK_DGRAM и может быть установлен в 0 для выбора правильного протокола для заданного типа. Или вы можете вызвать и выбрать нужный протокол, “tcp” or “udp”). это близкий родственник AF_INET , который вы используете при инициализации поля в вашей структуре sockaddr_in. Они настолько близкие родственники, что имеют одинаковое значение и многие программисты при вызове передают socket() в первом аргументе AF_INET вместо PF_INET. Теперь возьмите молока и печенья, поскольку настало время сказания. Однажды, давным-давно, людям представилось, что может быть семейство адресов (“AF” в “AF_INET”) сможет поддерживать несколько протоколов, определяемых их семейством протоколов (“PF” в “PF_INET”). Этого не случилось. И жили они долго и счастливо. Конец. Так что правильней всего использовать в вашей struct sockaddr_in ив вашем вызове socket() ) В любом случае, хватит об этом. Что вам действительно нужно, так это взять данные из результатов вызова getaddrinfo() и передать их socket(), прямо как здесь int s; struct addrinfo hints, *res; // поиск // полагаем, что структура “hints" уже заполнена getaddrinfo("www.example.com", "http", &hints, &res); ! // нужно проверить выход getaddrinfo() на ошибки и просмотреть // связанный список "res" на действительный элемент, не полагаясь // на то, что это первый (как во многих других примерах 21 Beej's Guide to Network Programming // Примеры смотри в разделе Клиент-Сервер.] ! s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); просто возвращает вам дескриптор сокета, который вы можете позже использовать в системных вызовах или -1 в случае ошибки. Глобальная переменная содержит код ошибки (см. подробности в man странице и кратких заметкиах по использованию в многопоточных программах) Прекрасно, прекрасно, но что хорошего в этих сокетах? Само по себе - ничего, ивам нужно читать дальше и делать больше системных вызовов, чтобы хоть что-нибудь почувствовать. 5.3. bind() -‐ На каком я порте Коли у вас есть сокет, вам может понадобиться связать его спортом на вашей локальной машине. (Так обычно делается если вы хотите слушать (listen()) входные подключения на специальном порте - сетевые игры делают так, когда говорят вам подключитесь к 192.168.5.10 порт 3490”.) Номер порта используется ядром при сравнении входящего пакета с дескриптором сокета конкретного процесса. Если вы собираетесь выполнить только connect() (поскольку вы клиента не сервер, это может быть ненужным. В любом случае читайте, просто для забавы. Вот форма системного вызова bind(): #include #include ! int bind(int sockfd, struct sockaddr *my_addr, int addrlen); это файловый дескриптор сокета, возвращенный socket()-ом. это указатель на struct sockaddr, которая содержит информацию о вашем адресе, а именно, порти. длина этого адреса в байтах. Вот так так Это же глотается одним куском. Давайте рассмотрим пример, в котором привяжем сокет к порту 3490 хоста, на котором выполняется программа struct addrinfo hints, *res; int sockfd; ! // сначала заполнить адресные структуры с помощью getaddrinfo(): ! memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // использовать либо IPv4 либо IPv6 hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // заполнить мой IP для меня ! getaddrinfo(NULL, "3490", &hints, &res); ! // создать сокет: ! sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); ! // связать спортом, полученным из getaddrinfo(): ! bind(sockfd, res->ai_addr, res->ai_addrlen); Указав флаг AI_PASSIVE, я сказал программе привязаться к IP хоста, на котором выполняется. Если вы хотите соединиться с отдельным локальным IP адресом, опустите AI_PASSIVE и укажите IP адрес в первом аргументе getaddrinfo(). В случае ошибки также возвращает -1 и устанавливает в код ошибки. 22 Beej's Guide to Network Много старого кода перед вызовом bind() заполняет struct sockaddr_in вручную. Это явно специфично для IPv4, но ничто не удерживает вас оттого чтобы делать таки с IPv6, разве что использование getaddrinfo(), в общем-то, проще. В любом случае, старый код выглядит примерно так // !!! ЭТО СТАРЫЙ СПОСОБ !!! ! int sockfd; struct sockaddr_in my_addr; ! sockfd = socket(PF_INET, SOCK_STREAM, 0); ! my_addr.sin_family = AF_INET; my_addr.sin_port = htons(MYPORT); // short, порядок байтов сети my_addr.sin_addr.s_addr = inet_addr("10.12.110.57"); memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero); ! bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr); В коде выше вы можете присвоить INADDR_ANY полю s_addr если хотите подключиться к локальному IP адресу (подобно флагу AI_PASSIVE выше) IPv6 версия это глобальная переменная in6addr_any, записанная в поле sin6_addr вашей struct sockaddr_in6. (Также есть макрос IN6ADDR_ANY_INIT, который вы можете использовать при инициализации переменной) Ещё одна вещь, за которой нужно следить при вызове bind(): не опускайте номера ваших портов ниже планки. Все порты ниже 1024 ЗАРЕЗЕРВИРОВАНЫ (если только вы не суперпользователь Вы можете обладать любым номером порта аж допри условии, что он не используется другой программой) Иногда вы могли заметить, вы перезапускаете сервер, и сбоит, заявляя Адрес уже занят. Что это значит Это кусочек подключавшегося сокета до сих пор висит в ядре и это загаживает порт. Вы можете подождать, пока он очистится (минуту или около того, или добавить в программу код, позволяющий использовать порт повторно, вот как здесь int yes=1; //char yes='1'; // Это для пользователей Solaris // устранить противное сообщение "Address already in use" ! if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); } Ещё одно финальное замечание побывают времена, когда вы абсолютно не можете её вызвать. Если вы подключаетесь (connect()) к уделённой машине и вас не заботит номер вашего локального порта (как в случае с telnet, где вам нужен только удалённый порт) вы можете просто вызвать connect(), он проверит, подключён ли порти, если необходимо, вызовет bind() и подключит свободный локальный порт. 5.4. connect() -‐ Эй, вы там Давйте на несколько минут притворимся, что вы telnet приложение. Ваш пользователь приказал вам (как в кино TRON) получить файловый дескриптор сокета. Вы подчинились и вызываете socket(). Потом пользователь говорит вам подключиться к “ 10.12.110.57 ” на порт “ 23 ” (стандартный telnet порт) Ой Что вам делать К счастью для вас, программа, вы сейчас внимательно читаете раздел по connect() - как подключиться к удалённому хосту. Так что яростно вперёд! Не терять времени 23 Beej's Guide to Network вызывается вот так #include #include ! int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); это файловый дескриптор сокета нашего доброго соседа, возвращённый вызовом socket(), serv_addr это struct sockaddr , содержащая порти адрес назначения, addrlen это длина этой структуры в байтах. Всю эту информацию можно наскрести из результатов вызова getaddrinfo(). Вы уже начали чувствовать Я отсюда вас не слышу и просто надеюсь, что это так. Давайте для примера подключим сокет к “www.example.com”, порт 3490: struct addrinfo hints, *res; int sockfd; ! // сначала заполнить адресные структуры с помощью getaddrinfo(): ! memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; ! getaddrinfo("www.example.com", "3490", &hints, &res); ! // создать сокет: ! sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); ! // подключить ! connect(sockfd, res->ai_addr, res->ai_addrlen); И снова, программы старой школы заполнены своими собственными struct sockaddr_in для передачи в connect(). Если хотите, можете делать так. Смотрите подобное замечание в разделе bind() выше. Не забудьте проверить возвращаемое значение connect(), в случае ошибки она вернёт -1 и установит переменную errno. Отметьте также, что мы не вызывали bind(). В основном, нас заботит не наш локальный порта тот, куда мы подсоединяемся (удалённый порт. Ядро подберёт для нас локальный порти подключённый сайт будет получать от нас информацию. Не тревожьтесь. 5.5. listen() -‐ Позвони мне, позвони… О’кей, пора сменить ритм. Что если вы не хотите подключаться к удалённому хосту. Скажем, для забавы вы хотите дождаться входящих подключений и затем их как-то обрабатывать. Это двухшаговый процесс сначала выслушаете, затем принимаете - accept() (см. ниже) Вызов listen() очаровательно прост, но требует некоторого разъяснения int listen(int sockfd, int backlog); sockfd это обычный файловый дескриптор сокета из системного вызова socket(). backlog это число разрешённых входных подключений во входной очереди. Что это значит Хорошо, входящие подключения будут ждать в очереди пока вы их не примете (accept() см. ниже) и это предел сколько их там может быть. Большинство систем молчком ограничивает их числом порядка 20, наверное вы можете остановиться на 5 или 10. 24 Beej's Guide to Network И снова, как обычно, возвращает -1 и устанавливает errno. Как вы, наверное, поняли нам нужно вызвать bind() до вызова listen(), так что сервер работает на определенном порте. (Вы должны знать, как сказать вашим приятелям, к какому порту подключаться) Так что, если вы собираетесь слушать входящие подключения, то вам нужно выполнить следующую последовательность вызовов getaddrinfo(); socket(); bind(); listen(); /* accept() будет тут */ Я просто поставил строку в код, поскольку это само всё объясняет. (Код в разделе accept() ниже более полный) Наиболее мудрёная часть этого предприятия это вызов accept(). 5.6. accept() -‐ Спасибо за звонок на порт 3490.” Приготовьтесь, вызов accept() весьма причудлив. Что произойдёт в таком случае некто очень далёкий будет пытаться подключиться вызовом connect() к вашей машине на порт, который выслушаете вызовом listen(). Это соединение будет поставлено в очередь ждать а. Вы вызываете accept() и говорите ему принять ожидающие подключения. Он вернёт совершенно новый файловый дескриптор сокета для использования с одним подключением Всё верно, внезапно у вас появилось два файловых дескриптора сокета по цене одного Исходный до сих пор слушает новые подключения, а вновь созданный полностью готовки. Вот так Вызов выглядит так #include #include ! int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); sockfd это дескриптор слушающего сокета. Достаточно просто. addr обычно будет указателем на локальную структуру sockaddr_storage. Сюда приходит информация о входящих подключениях (се помощью вы можете определить какой хост вызывает вас и с какого порта. addrlen is это локальная целая переменная, которая должна содержать размер struct sockaddr_storage до того как её адрес будет передан accept(). accept() больше, чем указано, байтов вне запишет. Если запишет меньше указанного, то изменит значение. Догадались в случае ошибки возвращает -1 и устанавливает errno. Спорим, и не снилось. Как и раньше, здесь есть много чему внимать зараз, так что этот фрагмент кода смотрите внимательно #include #include #include #include ! #define MYPORT "3490" // номер моего порта для подключения пользователей #define BACKLOG 10 // размер очереди ожидающих подключений int main(void) { struct sockaddr_storage their_addr; socklen_t addr_size; 25 Beej's Guide to Network Programming struct addrinfo hints, *res; int sockfd, new_fd; ! // !! не забудьте проверить ошибки для этих вызовов !! // сначала заполнить адресные структуры с помощью getaddrinfo(): ! memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // использовать либо IPv4 либо IPv6 hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // заполнить мой IP для меня ! getaddrinfo(NULL, MYPORT, &hints, &res); ! // создать сокет, связать и слушать ! sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); bind(sockfd, res->ai_addr, res->ai_addrlen); listen(sockfd, BACKLOG); ! // принять входящие подключения ! addr_size = sizeof their_addr; new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size); ! // связываемся по дескриптору сокета new_fd! Опять отметим, что мы будем использовать дескриптор сокета для всех вызовов и recv(). Если вы всегда только слушаете одно подключение, вы можете закрыть (close() ) слушающий sockfd, чтобы остановить входящие подключения, если вам так уж хочется. 5.7. send() и recv() - Поговори со мною, бэби! Эти две функции обеспечивают связь по потоковыми подключённым дейтаграммным сокетам. Если вы хотите использовать обычные неподключаемые дейтаграммные сокеты, обратитесь в раздел об sendto() и recvfrom() ниже. Вызов send(): int send(int sockfd, const void *msg, int len, int flags); это дескриптор сокета, куда вы хотите отправить данные (возможно он был возвращён или получен accept().) это указатель на посылаемые данные. это длина этих данных в байтах. просто установите в 0 . (См. man страницу вызова send(), там есть информация относительно flags.) Пример кода может быть таким char *msg = "Beej was here!"; int len, bytes_sent; len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); send() возвращает количество действительно посланных байтов - это может быть меньше числа байтов, указанного для передачи Видите ли, иногда вы посылаете такую кучу данных , что она не может их обработать. Она выпалит столько данных, сколько сможет, и поверит, что вы пошлёте оставшиеся позже. Помните, если количество байтов, 26 Beej's Guide to Network Programming возвращённых send() не совпадает сто вам надо послать остаток строки. Хорошая новость такова если пакет невелик (меньше Кили около того, он, возможно будет послан одним куском. Опять же, в случае ошибки send() возвращает -1 и устанавливает errno. Вызов recv() во многом подобен int recv(int sockfd, void *buf, int len, int flags); sockfd это дескриптор сокета для чтения, buf это буфер куда читать, len это максимальная длина буфера, flags can опять может быть установлен в 0 (см. man страницу recv().) recv() возвращает действительное количество записанных в буфер байтов или -1 при ошибке ( и соответственно установив errno ). Погодите recv() может возвращать 0. Это означает, что удалённая сторона закрыла для вас подключение Это способ сказать вам об этом. Это было просто, неправда ли Теперь вы можете гонять данные туда-сюда по потоковым сокетам! Чудо Вы теперь Сетевой Программист Unix! 5.8. sendto() и recvfrom() -‐ Поговори со мной, стиль Слышу, как выговорите Это всё чудесно и прелестно, но что мне делать с неподключаемыми сокетами?” No problemo, amigo. У нас с собой было. Поскольку дейтаграммные сокеты не подключены к удалённому хосту, угадайте, какую информацию мы должны задать до посылки пакета Правильно Адрес назначения Сенсационная новость int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, socklen_t tolen); Как видите, этот вызов в основном подобен вызову send() с добавлением двух крупиц информации. to это указатель на struct sockaddr (которая может быть struct sockaddr_in, struct sockaddr_in6 или struct sockaddr_storage, приведёнными в последний момент, содержащую IP адрес назначения и порт. tolen, в глубине души целое число, можно просто установить вили. Структуру с адресом назначения можно получить из или recvfrom() ниже, или заполняйте сами. Как и send(), sendto() возвращает число действительно отправленных байт которое, опять же, может быть меньше, чем вы указали) или -1 при ошибке. Точно также recvfrom() подобна recv(). Вызов recvfrom(): int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen); И опять, он такой же, как и |