Закрытие TCP-соединения
При работе с сокетами важно корректно реагировать на завершение работы абонентского процесса. Так как сокет является двусторонним механизмом связи, то нельзя предсказать заранее, когда произойдет разрыв соединения – во время чтения или записи. Поэтому нужно учитывать оба возможных варианта.
Если процесс пытается записать данные в оборванный сокет при помощи вызова fdwrite или send, то он получит сигнал SIGPIPE, который может быть перехвачен соответствующим обработчиком сигнала. При чтении обрыв диагностируется проще.
В случае разорванной связи вызов fdread или recv возвращает нулевое значение. Поэтому для вызовов fdread и recv необходимо всегда проверять возвращаемое значение, чтобы не зациклиться при приеме данных.
Закрываются сокеты так же, как и обычные дескрипторы файлового ввода/вывода, – при помощи системного вызова fdclose. Для сокета типа
SOCK_STREAM ядро гарантирует, что все записанные в сокет данные будут переданы принимающему процессу. Это может вызвать блокирование операции закрытия сокета до тех пор, пока данные не будут доставлены. (Если сокет имеет тип SOCK_DGRAM, то сокет закрывается немедленно.)
Теперь можно привести полный текст примера клиента и сервера, добавив в серверный процесс обработку сигналов и вызов fdclose в обе программы. В данном случае эти меры могут показаться излишними, но в реальном клиент/серверном приложении обязательна надежная обработка всех исключительных ситуаций. Приведем окончательный текст программы сервера:
(* Серверный процесс *)
uses sockets,stdio,linux;
const
SIZE=sizeof(tinetsockaddr);
server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);
var
newsockfd:longint;
procedure catcher (sig:integer);cdecl;
begin
fdclose (newsockfd);
halt (0);
end;
var
sockfd:longint;
c:char;
act:sigactionrec;
mask:sigset_t;
client:tinetsockaddr;
clientaddrlen:longint;
begin
act.handler.sh := @catcher;
sigfillset (@mask);
act.sa_mask:=mask.__val[0];
sigaction (SIGPIPE, @act, nil);
(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;
(* Связать адрес с абонентской точкой *)
if not bind (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова bind');
halt (1);
end;
(* Включить прием соединений *)
if not listen (sockfd, 5) then
begin
perror ('ошибка вызова listen');
halt (1);
end;
while true do
begin
(* Прием запроса на соединение *)
newsockfd := accept (sockfd, client, clientaddrlen);
if newsockfd = -1 then
begin
perror ('Ошибка вызова accept');
continue;
end;
(* Создать дочерний процесс для работы с соединением *)
if fork = 0 then
begin
while recv (newsockfd, c, 1, 0) > 0 do
begin
c := upcase (c);
send (newsockfd, c, 1, 0);
end;
(* После того, как клиент прекратит передачу данных,
* сокет может быть закрыт и дочерний процесс
* завершает работу *)
fdclose (newsockfd);
halt (0);
end;
(* В родительском процессе newsockfd не нужен *)
fdclose (newsockfd);
end;
end.
И клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;
const
SIZE=sizeof(tinetsockaddr);
server:tinetsockaddr=(family:AF_INET; port:7000);
var
sockfd:longint;
c,rc:char;
begin
(* Преобразовать и сохранить IP address сервера *)
server.addr := inet_addr ('127.0.0.1');
(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;
(* Подключить сокет к адресу сервера *)
if not connect (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова connect');
halt (1);
end;
(* Обмен данными с сервером *)
rc := #$a;
while true do
begin
if rc = #$a then
writeln ('Введите строчный символ');
c:=char(getchar);
send (sockfd, c, 1, 0);
if recv (sockfd, rc, 1, 0) > 0 then
write (rc)
else
begin
writeln ('Сервер не отвечает');
fdclose (sockfd);
halt (1);
end;
end;
end.
Упражнение 10.1. Запустите приведенную программу сервера и несколько клиентских процессов. Что произойдет после того, как все клиентские процессы завершат работу?
Упражнение 10.2. Измените код программ так, чтобы после того, как все клиентские процессы завершат свою работу, сервер также завершал работу после заданного промежутка времени, если не поступят новые запросы на соединение.
Упражнение 10.3. Измените код программ так, чтобы два взаимодействующих процесса выполнялись на одном и том же компьютере. В этом случае сокет должен иметь коммуникационный домен AF_UNIX.