Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

 W. R. Stevens  : Глава 4

Элементарные сокеты TCP

В этой главе будут описаны функции , необходимые для работоспособного клиента и сервера TCP. Сам клиент и сервер будут созданы в следующей , 5-й главе.

Будут описаны параллельные серверы - когда множество клиентов соединяются с одним и тем же сервером. При этом сервер каждый раз будет выполнять функцию fork , порождающую новый серверный процесс.

Типичный сценарий взаимодействия между клиентом и сервером :


 								 TCP-сервер
 								------------
 								| socket() |
 								------------
 								     |
 								------------
 								|  bind()  |
 								------------
 								     |
 								------------
 								| listen() |
 								------------
 								     |
 								------------
 								| accept() |
    TCP-клиент							------------
   ------------							     |
   | socket() |						Блокировка для соединения
   ------------							с клиентом
        |     						 	     |   
   ------------	         Установка соединения            	     |
   | connect()|		<---------------------> 		     |
   ------------	       (3-этапное рукопожатие TCP)		     |
        |                                      			     |
   ------------         Данные(запрос)  			-------------
   | write()  | ---------------------------------------> 	| read()    |
   ------------                          			-------------
        |							     |
        |							     |	
   ------------         Данные (ответ)            		-------------
   |  read()  |    <------------------------------------------  | write()   |
   ------------                                			-------------
        |							     !	
        |							     !	
   ------------      Уведомление о конце файла       		-------------
   | close()  |   ------------------------------->  		| read()    |
   ------------                                      		-------------
 								     |
 								-------------
 								| close()   |
 								-------------
 

Функция socket


 	int socket(int family , int type , int protocol);
Константа family может принимать значения :

 	AF_INET
 	AF_INET6
 	AF_LOCAL
 	AF_ROUTE
 	AF_KEY
Константа type может принимать значения:

 	SOCK_STREAM
 	SOCK_DGRAM
 	SOCK_RAW
protocol обычно равен нулю , за исключением символьных сокетов

Не все сочетания family и type допустимы . Допустимые сочетания :


 	===========================================================================
 	 AF_INET          AF_INET6    AF_LOCAL     AF_ROUTE     AF_key
 	===========================================================================
 	SOCK_STREAM 	  TCP          TCP         Да           
 	SOCK_DGRAM        UDP          UDP         Да
 	SOCK_RAW          IPv6                     Да            Да
 	===========================================================================
При успешном выполнении функции socket() возвращается целое неотрицательное число - sockfd.

Функция connect

Функция используется клиентом.

 	int connect(int sockfd , const struct sockaddr *servaddr,socklen_t addrlen);
Возвращает либо 0 , либо -1.
В случае протокола TCP функция инициирует 3-этапное рукопожатие. Возможные ошибки :

 	ETIMEOUT
 	ECONNREFUSED
 	EHOSTUNREACH
 	ENETUNREACH 
 

Функция bind

Функция связывает сокет с локальным адресом протокола. Это комбинация 32-разрядного (IPv4) либо 128-разрядного (IPv6) адреса протокола с 16-разрядным номером порта TCP либо UDP.

 	int bind(int sockfd , const struct sockaddr *myaddr,socklen_t addrlen);
Функция позволяет нам либо задать ip и порт , либо ничего не задавать. Если мы зададим нулевой порт , то при вызове bind ядро само динамически выберет порт.

Функция listen

Функция вызывается на сервере и выполняет 2 действия :
1. Переводит сокет из состояния CLOSED в состояние LISTEN
2. 2-й аргумент этой функции задает максимальное число соединений , которое ядро может поместить в очередь этого сокета.

  int listen(int sockfd , int backlog);

Для сокета ядро может поддерживать 2 очереди :
1. Очередь не полностью установленных соединений.Эти сокеты находятся в состоянии SYN_RCVD
2. Очередь полностью установленных соединений. Эти сокеты находятся в состоянии ESTABLISHED.
Аргумент backlog задает максимальное значение для обоих очередей.Не нужно этому аргументу присваивать ноль, лучше просто закрыть сокет. Этот параметр является одним из основных в серверах http , и он может быть явно больше 5 - например , 64. Если очередь заполнена , то протокол TCP будет просто игнорировать вновь приходящий SYN.

Функция accept

Функция вызывается сервером TCP для возвращения следующего установленного соединения из начала очереди полностью установленных соединений. Если очередь пуста , процесс переходит в состояние ожидания

 	int accept(int sockfd , struct sockaddr *cliaddr,socklen_t addrlen);
Если функция возвращает положительное число , это означает , что ядро создало новый дескриптор. Этот дескриптор будет использоваться конкретным клиентом. Первый аргумент функции - прослушивающий сокет , существующий как правило в единственном экземпляре . Затем ядро создает по одному присоединенному сокету для каждого клиентского соединения , принятого с помощью accept. cliaddr - адрес протокола клиентского процесса. addrlen - размер адреса. Можно cliaddr и addrlen сделать пустыми указателями , если они нам не нужны.
В следующем листинге показано , как присоединенный сокет закрывается при каждом прохождении цикла , но прослушиваемый сокет остается открытым. Здесь 2-й и 3-й аргументы accept - пустые указатели , поскольку нам не нужна идентификация клиента.

  // intro/daytimetcpsrv1.c
 #include	"unp.h"
 #include	< time.h>
 
 int main(int argc, char **argv)
 {
 	int					listenfd, connfd;
 	socklen_t			len;
 	struct sockaddr_in	servaddr, cliaddr;
 	char				buff[MAXLINE];
 	time_t				ticks;
 
 	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 
 	bzero(&servaddr, sizeof(servaddr));
 	servaddr.sin_family      = AF_INET;
 	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 	servaddr.sin_port        = htons(13);	/* daytime server */
 
 	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
 
 	Listen(listenfd, LISTENQ);
 
 	for ( ; ; ) {
 		len = sizeof(cliaddr);
 		connfd = Accept(listenfd, (SA *) &cliaddr, &len);
 		printf("connection from %s, port %d\n",
 			   Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
 			   ntohs(cliaddr.sin_port));
 
         ticks = time(NULL);
         snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
         Write(connfd, buff, strlen(buff));
 
 		Close(connfd);
 	}
 }
Мы вызываем здесь функцию inet_ntop для преобразования 32-битного IP-адреса в строку ASCII (точечно-десятичную запись) , затем вызываем ntohs для преобразования сетевого порядка байтов в порядок байтов узла. Адрес IP-сервера - 127.0.0.1 - это т.н. loopback address , здесь и клиент , и сервер запускаются на одной машине.Сервер должен обладать админскими правами.

Функции fork и exec


 	pid_t fork(void);
 
Функция fork возвращает : 0 в дочернем процессе , идентификатор дочернего процесса в родительском процессе. В случае ошибки возвращает -1. Функция , вызываемая 1 раз , возвращает 2 значения :-) Дочернему процессу возвращается ноль , а не id-шник родителя . потому что у дочернего процесса всегда есть возможность узнать идентификатор родителя - функция getppid. Все дескрипторы . открытые в родительском процессе , становятся доступны дочернему после fork. Существует 2 типичных случая использования fork :
1. Процесс создает копию , чтобы та могла обработать одно задание - применяется серверами.
2. Запуск другой программы - после fork идет вызов функции exec . Exec - пожалуй единственный способ запустить файл на выполнение.

Параллельные серверы

Сервер , представленный в предыдущем листинге , является последовательным. Он связан с одним клиентом . В следующем примере показан параллельный сервер , обслуживающий несколько клиентов одновременно.

 	pid_t pid;
 	int listenfd , connfd;
 	listenfd = Socket( ...);
 	Bind(listenfd , ...);
 	Listen(listenfd,LISTENQ);
 
 	for(;;) {
 		connfd = Accept(listenfd , ...);
 		if ((pid == Fork()) ==0) {
 			Close(listenfd);
 			doit(connfd);
 			Close(connfd);
 			exit(0);
 		}
 		Close(connfd);
 	}
Здесь после установки соединения форкается дочерний процесс , который обслуживает клиента , а родительский процесс ждет другое соединение. doit -условная функция для обслуживания клиента. Вызов функции Close сразу после doit , кстати , необязателен , поскольку последующий exit все равно всё прибьет . Стивенс тут выражается так - это , мол , уже дело вкуса программера. А вот вызов Close после цикла обязателен , иначе TCP -соединение не закроется.

Функции getsockname и getpeername

Эти функции возвращают либо локальный , либо удаленный адрес сокета.

 	int getsockname(int sockfd, struct sockaddr *localadd, socklen_t *addrlen); 
     int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
 
Функция getsockname нужна клиенту для получения IP-адреса и номера локального порта. Если функция bind вызвана с номером порта 0 , то функция getpeername вернет нужный номер порта. Сервер с помощью функции getsockname может получить локальный ip-адрес соединения.

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье