struct protoent { char *p_name; /* Название протокола */ char **p_aliases; /* Массив указателей на альтернативные имена протокола*/ int p_proto; /* Номер протокола */ };
Для получения информации о протоколе по его названию или номеру используются функции:
struct protoent *getprotobyname(const char *name); struct protoent *getprotobynumber(int proto);
В случае возникновения ошибок функции возвращают NULL.
struct servent { char *s_name; /* Имя сервиса */ char **s_aliases; /* Альтернативные имена сервиса */ int s_port; /* Номер порта занимаемый сервисом */ char *s_proto; /* Имя протокола используемого сервисом */ }
Для получения информации о сервисе по номеру порта и наоборот используются, соответственно, функции:
struct servent *getservbyport(int port, const char *proto); struct servent *getservbyname(const char *name, const char *proto);
В случае возникновения ошибок функции возвращают NULL.
struct hostent { char *h_name; /* Официальное имя хоста */ char **h_aliases; /* Массив псевдонимов хоста */ int h_addrtype; /* Тип адреса (обычно AF_INET) */ int h_length; /* Длина адреса в байтах */ char **h_addr_list; /* Список адресов хоста */ }
Функция gethostbyname позволяет получить адрес хоста по его имени:
struct hostent *gethostbyname(const char *name);
Функция gethostbyaddr позволяет определить имя хоста по его адресу. В качестве аргументов функции передаются указатель на адрес хоста, длина адреса и его тип (AF_INET для IPv4):
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
В случае возникновения ошибок функции возвращают NULL. Код ошибки помещается в переменную h_errno.
Ниже приведен пример реализации серверной части daytime:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <time.h> #include <string.h> main(){ int s, clen, rd, proto; struct sockaddr_in saddr, caddr; struct sockaddr *sa, *ca; struct hostent *rhost; time_t itime; char buf[2048], *tstr, *host; sa=&saddr; ca=&caddr; // Получаем номер протокола UDP proto=getprotobyname("udp")->p_proto; // Создаем сокет s=socket(PF_INET, SOCK_DGRAM, proto); if(s<0) { perror("udps: не удается создать сокет"); exit(1); } // Резервируем порт 13 saddr.sin_family=AF_INET; saddr.sin_addr.s_addr=INADDR_ANY; saddr.sin_port=htons(13); if(bind(s, sa, sizeof(saddr))==-1) { perror("udps: не удается занять порт"); exit(1); } caddr.sin_family=AF_INET; clen=sizeof(caddr); while(1) { // Ожидаем поступления запроса rd=recvfrom(s, buf, 1, 0, ca, &clen); if(rd==-1){ perror("udps: ошибка при получении данных"); exit(1); } // Преобразуем адрес хоста отправителя в его имя rhost=gethostbyaddr((char*)(&caddr.sin_addr), sizeof(caddr.sin_addr), AF_INET); if(h_errno){ printf("gethostbyaddr error: %d\n", h_errno); host=inet_ntoa(caddr.sin_addr); } else{ host=rhost->h_name; } // Получаем строку содержащую дату и время itime=time(NULL); tstr=ctime(&itime); // Выводим время поступления запроса, // адрес и порт отправителя printf("%s request from %s:%d\n", tstr, host, htons(caddr.sin_port)); // Отправляем дату и время клиенту sendto(s, tstr, strlen(tstr), 0, ca, sizeof(caddr)); } }
Реализация клиентской части приведена ниже. Клиент устанавливает ограничение на время ожидания поступления данных, посылает широковещательный запрос и ожидает поступления ответа. Получив ответ клиент выводит его на экран и ожидает поступления других ответов. Если функция recv завершается с ошибкой превышения времени ожидания ответа, то клиент считает что все ответы получены и завершает выполнение.
#include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <errno.h> #include <string.h> main(){ int s, so, clen, rd, proto; struct sockaddr_in saddr, caddr; struct sockaddr *sa, *ca; struct hostent *rhost; struct timeval timeout; char buf[100], *host; sa=&saddr; ca=&caddr; // Получаем номер протокола UDP proto=getprotobyname("udp")->p_proto; // Создаем сокет s=socket(AF_INET, SOCK_DGRAM, proto); if(s<0) { perror("udpc: не удается создать сокет"); exit(1); } // Разрешаем отпраку широковещательных пакетов so=1; rd=setsockopt(s,SOL_SOCKET,SO_BROADCAST,&so,sizeof(so)); if(rd==-1) { perror("udpc: не удается установить параметры сокета"); exit(1); } // Устанавливаем предельное время ожидания ответа timeout.tv_sec=3; timeout.tv_usec=0; rd=setsockopt(s,SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); if(rd==-1) { perror("udpc: не удается установить параметры сокета"); exit(1); } // Резервируем порт caddr.sin_family=AF_INET; caddr.sin_addr.s_addr=INADDR_ANY; caddr.sin_port=0; if(bind(s, ca, sizeof(caddr))==-1) { perror("udpc: не удается занять порт"); exit(1); } // Задаем адрес получателя saddr.sin_family=AF_INET; saddr.sin_port=htons(13); saddr.sin_addr.s_addr=INADDR_BROADCAST; clen=sizeof(saddr); // Отправляем запрос rd=sendto(s, buf, 1, 0, sa, clen); if(rd==-1){ perror("udpc: ошибка при отправке запроса"); exit(1); } while(1){ // Ожидаем ответ rd=recvfrom(s ,buf ,99 ,0 ,sa , &clen); if(rd==-1){ // Если превышено время ожидания ответа, то выход if(errno==EAGAIN) break; // Иначе ошибка perror("udpc: ошибка при получении ответа"); exit(1); } buf[rd]=(char)0; // Преобразуем адрес хоста отправителя в его имя rhost=gethostbyaddr((char*)(&saddr.sin_addr), sizeof(saddr.sin_addr), AF_INET); if(h_errno){ printf("gethostbyaddr error: %d",h_errno); host=inet_ntoa(caddr.sin_addr); } else{ host=rhost->h_name; } // Выводим информацию о поступившем ответе printf("%s - reply from %s\n", buf, host); } }
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> main() { int s, c, sz; struct sockaddr_in ssa, csa; struct sockaddr *sp, *cp; struct hostent *rhost; char *host, *tstr; time_t itime; sp=(struct sockaddr *)&ssa; cp=(struct sockaddr *)&csa; sz=sizeof(ssa); // Создаём сокет s=socket(AF_INET, SOCK_STREAM, 0); if(s == -1){ perror("Невозможно создать сокет"); exit(1); } // Резервируем порт 13 ssa.sin_family = AF_INET; ssa.sin_port = htons(13); ssa.sin_addr.s_addr = INADDR_ANY; if(bind(s, sp, sz) == -1){ perror("Невозможно занять порт"); exit(1); } // Переводим сокет в режим ожидания соединения if(listen(s, 0) == -1){ perror("Невозможно перейти в режим ожидания"); exit(1); } while(1){ // Принимаем соединение if((c = accept(s, cp, &sz)) == -1) { perror("Ошибка при выполнении accept"); exit(1); } // Преобразуем адрес хоста отправителя в его имя rhost=gethostbyaddr((char*)(&csa.sin_addr), sizeof(csa.sin_addr), AF_INET); if(h_errno){ printf("gethostbyaddr error: %d\n", h_errno); host=inet_ntoa(csa.sin_addr); } else { host=rhost->h_name; } // Получаем строку, содержащую дату и время if((itime = time(NULL)) < 0){ perror("Не удалось получить время"); exit(1); } tstr = ctime(&itime); // Выводим время поступления запроса, // адрес и порт отправителя printf("%s request from %s:%d\n", tstr, host, htons(csa.sin_port)); // Отправляем дату и время клиенту send(c, tstr, strlen(tstr), 0); // Закрываем соединение close(c); } }
Теперь рассмотрим реализацию клиента. Обратите внимание, что клиент не должен выполнять вызов функции bind, порт выделяется автоматически при выполнении connect.
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define BUFSZ 128 main(int argc, char *argv[]) { int s, sz, i; struct sockaddr_in ssa; struct sockaddr *sp; struct in_addr sip; char buf[BUFSZ]; sp=(struct sockaddr *)&ssa; sz=sizeof(ssa); if(argc!=2){ // Помощь по использованию команды printf("Использование: %s ip-адрес\n",argv[0]); exit(1); } if(inet_aton(argv[1], &sip) != 1){ printf("Неправильно задан адрес сервера\n"); exit(1); } // Создаём сокет s=socket(AF_INET, SOCK_STREAM, 0); if(s == -1){ perror("Невозможно создать сокет"); exit(1); } // Задаём адрес сервера ssa.sin_family = AF_INET; ssa.sin_port = htons(13); ssa.sin_addr = sip; // Устанавливаем соединение if(connect(s, sp, sz) == -1){ perror("Не удалось установить соединение"); exit(1); } // Получаем данные от сервера while((i=recv(s, buf, BUFSZ, 0)) > 0) write(1, buf, i); }
Интерфейс сокетов впервые появился в операционной системе 4.2BSD. С тех пор он получил широкое распространение и реализован практически во всех операционных системах. Интерфейс сокетов обеспечивает возможность взаимодействия между процессами независимо от того, выполняются они на одном компьютере или на разных. Кроме того, интерфейс предоставляет единый набор функций для работы с различными стеками протоколов.
В данном документе описываются основные функции, предназначенные для работы с сокетами, и методы их использования. Последняя версия документа доступна по адресу http://www.arh.ru/~zwon.
struct sockaddr { sa_family_t sa_family; char sa_data[]; };
На практике, в зависимости от используемого сетевого протокола, используются другие структуры. Сокеты для использования с протоколом IP определены следующим образом:
struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; };
Структура in_addr определена следующим образом:
struct in_addr { in_addr_t s_addr; };где in_addr_t это целый беззнаковый тип длиной 32 бита.
Сокеты создаются при помощи системного вызова socket:
int socket(int domain, int type, int protocol);
Аргументы функции socket имеют следующее значение:
Функция socket возвращает дескриптор файла сокета, используемый в дальнейшем для работы с сокетом. В случае возникновения ошибки функция возвращает значение -1.
Функция socket создает "безымянный" сокет, т.е. не связанный ни с локальным адресом, ни с номером порта. Связать сокет с адресом компьютера и номером порта можно при помощи функции bind:
int bind(int socket, const struct sockaddr *address, socklen_t address_len}
Аргументы функции bind:
Если вызов функции bind завершается успешно, то возвращаемое значение равно нулю. В случае возникновения ошибки возвращается значение -1. Код ошибки содержится в переменной errno.
При присвоении значений номеру порта и адресу следует учитывать, что порядок следования байтов на разных архитектурах различен. При передаче данных по сети общепринятым является представление чисел в формате big-endian, в котором самый старший байт целого числа имеет наименьший адрес, а самый младший байт имеет наибольший адрес. Компьютеры построенные на архитектуре Intel x86 используют схему представления целых чисел little-endian, в которой наименьший адрес имеет самый младший байт, а наибольший адрес имеет самый старший байт. Для преобразования числа из той схемы которая используется на компьютере к той которая используется в сети, и наоборот, применяются функции:
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohl(uint16_t netshort);
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); int getsockopt(int socket, int level, int option_name, void *option_value, socklen_t *option_len);
Функция setsockopt устанавливает параметр, заданный аргументом option_name на уровне протокола определенного аргументом level, в значение на которое указывает параметр option_value. Для присвоения параметра на уровне библиотеки сокетов, аргументу level присваивается значение SOL_SOCKET. Для установки параметра на другом уровне, аргументу level присваивается номер соответствующего протокола. На уровне библиотеки сокетов допустимыми являются следущие параметры:
На уровне протокола TCP допустимы следующие параметры:
Параметры имеющие логическое значение являются целыми. Значение 0 обозначает, что соответствующий параметр будет отключен, значение 1 обозначает, что параметр будет включен. В случае успешного завершения функция фозвращает ноль, если возникли ошибки, то результат равен -1.
Функция getsockopt возвращает значение указанного параметра. Помимо вышеперечисленных параметров могут использоваться следующие:
socket() // Создание сокета bind() // Привязка сокета к номеру порта listen() // Создание очереди соединений while(){ accept() // Прием запроса на установление соединения ... // Обмен данными close() // Закрытие соединения }
Вызов listen включает прием соединений и ограничивает очередь входящих соединений.
int listen(int socket, int backlog)Параметр socket содержит идентификатор сокета, который будет принимать соединения. Параметр backlog содержит длину очереди входящих запросов на установление соединения.
После приема запроса на установление соединения сервер создает новый сокет для работы с соединением. Для создания этого сокета используется вызов accept:
int accept(int socket, struct sockaddr *address, socklen_t *address_len);Функция accept извлекает первый запрос из очереди ожидающих соединений, создает новый сокет, с тем же протоколом и семейством адресов что и исходный, и возвращает дескриптор файла для этого сокета. Аргумент socket определяет дескриптор сокета который принимает запросы на установление соединений. Аргумент address либо NULL, либо указатель на структуру, в которую будет помещен адрес удаленного сокета после возврата из функции. address_len - указатель на переменную, в которой хранится длина структуры address.
Функция возвращает дескриптор файла сокета для установленного соединения или -1 в случае ошибки. Полученный в результате вызова функции accept дескриптор файла сокета используется, в дальнейшем, для работы с установленым соединением, он не может использоваться для установления других соединений.
Схема действий активной стороны выглядит следующим образом:
socket() // Создание сокета connect() // Установление соединения ... // Обмен данными close() // Закрытие соединения
После создания сокета пассивная сторона сразу устанавливает соединение. Для установки соединения используется функция connect:
int connect(int socket, const struct sockaddr *address, socklen_t *address_len);
Аргумент socket определяет сокет, который будет использоваться для установки соединения. address указывает на структуру содержащую адрес сервера. address_len содержит длину структуры address.Если сокет еще не был привязан к локальному номеру порта, то функция connect сделат это сама. Возвращаемое значение равно нулю в случае успеха и -1 в противном случае.
ssize_t send(int socket, const void *buffer, size_t length, int flags);
Функция выполняет передачу данных через указанный сокет партнеру. Аргумент socket определяет дескриптор файла сокета, через который отправляются данные. buffer указывает на буфер, содержащий данные для передачи. Длина передаваемых данных определяется аргументом length. Аргумент flags определяет тип передачи данных. Значение flags является результатом логического ИЛИ нуля или большего числа следующих констант:
В случае успешного завершения send возвращает число переданных байт. В противном случае возвращаемое значение равно -1.
Для приема данных используется функция recv.
ssize_t recv(int socket, void *buffer, size_t length, int flags);
Функция recv принимает данные из сокета, заданного первым аргументом. Аргумент buffer указывает на буфер в который будут помещены принятые данные. length определяет длину буфера. Аргумент flags определяет параметры получения данных.Значение flags является результатом логического ИЛИ нуля или большего числа следующих констант:
В случае успешного завершения функция возвращает число принятых байт. В противном случае возвращается -1.
ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);Функция sendto предназначена для отправки данных. Аргументы функции имеют следующее значение:
Функция возвращает число переданных байт в случае успешного завершения и -1 в противном случае. Следует заметить, что успешное выполнение функции sendto не гарантирует доставку данных получателю. Возврат значения -1 происходит только в случае локально обнаруженных ошибок.
ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);
Функция recvfrom принимает данные из сокета. Аргументы функции имеют следующее значение:
Функция возвращет количество данных, записаных в буфер. Если при выполнении функции возникли ошибки, то возвращается значение -1. Для протокола UDP, данные, пришедшие в одном пакете, должны быть прочитаны одним вызовом функции recvfrom. Если длина буфера недостаточна для размещения всех данных, то лишние байты отбрасываются.