Лекция №1 17.02.2009 Гурьев Сегодня мы с вами рассмотрим вопрос программирования сетевых а… Разбиение протоколов по уровням стек TCP/IP делится на 4 уровня: канальный, межсетевой, транспортный и прикладной. Канальный уровень реализуется аппаратным обеспечением, межсетевой и транспортный – ядром операционной системы, а прикладной – приложениями. Поэтому между ядром и приложениями потребовался жесткий системный интерфейс, потому что это разное программное обеспечение. Возник интерфейс API, который называется socket. Об этом мы и будем говорить. Сокет – программная абстракция, коммуникационный примитив. Если говорить о tcp/ip, то сокет это пара (ip адрес, номер порта). Вообще говоря, если сокет участвует в открытом соединении, то там есть еще два других числа (ip адрес другой стороны, номер порта другой стороны). Во вторых сокеты имеют дескрипторы. Дескриптор сокета это целое число, которое является указателем на этот коммуникационный примитив. На Юниксе дескриптор сокета это род файлового дескриптора. На других платформах это не так. Этот интерфейс спрограммирован с большой общностью, что бы его можно было использовать другими протоколами. На Юниксе кроме сокетов tcp/ip еще есть сокеты юникс-домен. Это способы обмена информацией между приложениями на одном хосте. На юникс сокеты задуманы не только как средсвто сетевого обмена информацией, но и как решает процессы синхронизации. Спроектировано все с большой общностью. Функция socket имеет три параметра: домен, тип, протокол. Возвращает она дескриптор сокета. int socket (int domain, int type, int protocol) 1. Домен – коммуникационный домен (communication domain), то есть наиболее общий тип сокетов. Домен это целове число, которое где-то заранее зарегистрировано. Есть домен, который называется pf_unix (pf_local) - это локальные сокеты не имеющие отношения к сетям. pf_inet - это как раз TCP/IP. 2. Type – тип сокета. Сокеты бывают разных типов sock_stream – сокет с возможностью передачи как в tcp sock_dgram – тип сокета, который обеспечивает возможность udp sock_raw – возможность самостоятельно полностью формировать дейтаграмму, её заголовки. sock_seqpacket– передача отдельных сообщений с гарантированной доставкой (это не TCP/IP) 3. Протокол – указывается номер протокола (как указывается в ipдейтаграме и записано в rfc1700), если поля тип недостаточно. Но как правило 0 (если тип сокета и домен однозначно определяет протокол) Эта функция возвращает дескриптор сокета. Этот сокет имеет уже определенную заложенную функциональность, но у него нет ни адреса, ни номера порта. Это просто дескриптор, с которым дальше можно что-то делать. Следуюшая функция bind (sd, sock_addr, address_length) - привязывает к дескриптору локальный адрес, то есть адрес на данном хосте. 1. sd - дестриптор сокета, то что выдала функция socket) 2. sock_addr – этот самый адрес. 3. Address_lenght – подсказывает какая длина этого адреса. Абстрактно эта структура sock_addr определяется так: struct sock_addr { u_char sock_len, so_family family. char so_data. } Беззнаковый байт – длина самой структуры (еще раз повторяется) дальше Address Family – константа, идентифицирующая домен. И далее поле неопределенной длины, которое называется адрес Если взаимодействие tcp/ip, то структура называется sockaddr_in. Sock_addr_internet_in Далее беззнаковое 16 битное целое sin_port Структура интернет адрес sin_addr (содерджит айпиадрес и порт), это можно заполнить нулями, тогда тут будет адрес данного хоста, и какой-то порт, который сейчас свободен. struct sockaddr_in { u_char sin_len u_char sin_family; u_short sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; } Тип sa_family_t эквивалентен типу unsigned short int. Структура in_addr состоит всего из одного элемента и имеет следующий формат struct in_addr { u_int32_t s_addr; } Следующая функция int connect(int sockfd, struct sockaddr *remote_addr, int addrlen); Эта функция подсоединяет к сокету адрес с которым она должна соединяться. (тогда как bind – прописывает локальный адрес), и устанавливает соединение (если необходимо). Нули в адресе и порте хоста нельзя. Возвращает 0 если все хорошо, -1 если ошибка. Эту операцию может выполнить клиент, а для сервера есть две другие операции. int listen(int sockfd, int backlog); - перевод сокета в режим ожидания соединения дескритор сокета и максимальное число соединений в очереди соединений, ожидающих обработки. Возвращает 0 если все хорошо, -1 если ошибка. Другая функция для серверов int accept(int sockfd, struct sockaddr *remote_addr, int *addrlen); Дескриптор сокета (который находится в режиме ожидания соединения), структура адрес сокета (адрес, порт вызвавшей стороны), и длина (передается по ссылке, так как сюда записывается длина) Возвращает дескриптор дочернего сокета для обмена по установленному соединению. Когда сервер ожидает соединения, он создает один сокет что бы эти вызовы принимать, и для каждого обращения клиента создается еще один сокет что бы эти вызовы обслуживать. Все параметры он наследует от родителя. int send(int sockfd, void *buf, int len, unsigned int flags); дескриптор сокета, указатель на данные которые надо передать, длина данных, флаги (значимы только для конкретных протоколов) Возвращает -1 если ошибка, число переданных байт int recv(int sockfd, void *buf, int len, unsigned int flags); дексриптор сокета, указатель на буфер куда принимать данные, длина и флаги. Гарантируется, что больше длины которую здесь указали, то больше её не будет. Возвращает -1 если ошибка, иначе число принятых байт Типичный сценарий. Сервер Клиент Сервер создает сокет, клиент создает сокет Сервер выполняет bind, клиент выполняет bind Сервер выполняет listen Потом отдыхает пока не придет первое обращение Клиент выполняет операцию connect. В этот момент проходит процедура трехстороннего рукопожатия. Если все проходит хорошо, то возвращает номер сокета После этого send – recv идут от сервера к клиенту и от клиента к серверу. Когда им надоело, он выполняют операцию close http://dfe3300.karelia.ru/koi/posob/socks/socks.html По такой же схеме можно было бы и udp программировать, но там connect это просто привязывание ip адреса другой стороны к сокету. Но удобнее использовать другие функции, которые немного отличаются. ssize_t sendto( int sockfd, void *buff, size_t nbytes, int flags, const struct sockaddr* to, socklen_t addrlen); номер сокета, буфер, длина буфера, адрес куда посылать, длина адреса Возвращает число байтов, или -1 если ощибка. ssize_t recvfrom( int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr* from, socklen_t *fromaddrlen); дексриптор сокета, буфер для приема данных, его длина, буфер для приема адреса, и его длина. Эта функция работает, пока не пришло сообщение, а когда оно приходит, то оно содержит и содержание сообщения, и адрес отправителя. В своем классическом варианте все эти функции блокирующие. Процесс приостанавливается, пока не будет завершена затребованная операция. Позднее появились дополнительные флаги, которые делают их не блокирующими, но это поддерживается не всеми. Возникает вопрос, а как же тогда реализовать программы, которые поддерживают несколько tcp или udp обменов. Этот механизм основан для функции select. Это такой синхронизационный примитив. Функция ожидать любого события из заданной группы событий. int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); целое число, структура множество файлов-дескрипторов. Для работы с этим множеством есть несколько макросов. Обнуление, включение элементов в множество, исключение элементов из множества, проверка элемента на вхождение в множество. Readfds – множество дескрипторов, в которых произошла операция чтения (пришло сообщение которое можно прочитать) Writefds – если возможность передачи. Exceptfds – множество дескрипторов с которыми произошли исключения (произошли ошибки) struct timeval – туда можно записать интервал времени timeout – может быть nul, или пока не истечет этот интервал. Возвращает общее число событий, а структуры она модифицирует. В множества дескрипторов, с которыми действительно произошли такие события.