Лабораторная работа № 8
Клиент-серверные взаимодействия посредством сокетов в режиме TCP-соединения
Цель работы
Практическое освоение механизма сокетов. Построение TCP-соединений для межпроцессного взаимодействия программ клиента и сервера в модели "клиент-сервер".
Содержание работы
  1. Ознакомиться с заданием к лабораторной работе.
  2. Ознакомиться с понятиями сокета, моделей взаимодействия между процессами на основе сокетов, структурами данных и адресацией, используемых при работе с сокетами, основными шагами при организации взаимодействия процессов в сети в режиме TCP-соединения.
  3. Для указанного варианта составить на языке Си две программы: программу сервера и программу клиента, реализующие требуемые действия.
  4. Выбрать и изучить набор системных вызовов, обеспечивающих решение задачи.
  5. Отладить и оттестировать составленную программу, используя инструментарий ОС UNIX.
  6. Защитить лабораторную работу, ответив на контрольные вопросы.
Методические указания к лабораторной работе
Существует две модели взаимодействия между процессами в сети: модель соединений с протоколом TCP (Transmission Control Protocol), и модель дейтаграмм с протоколом UDP (User Datagram Protocol). В данной лабораторной работе используется первая из названных моделей.
Далее приводятся основные шаги и необходимые системные вызовы для выполнения основных этапов при работе с сокетами в режиме TCP-соединения.
1. Адресация и создание сокета
Совокупная информация об адресе, порте программы-адресата (абонента), модели соединения, протоколе взаимодействия составляет так называемый сокет (конечная абонентская точка), формально представляющий собой структуру данных. Существует несколько видов сокетов:
Создается сокет при помощи системного вызова socket()
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
При программировании TCP-соединения должны быть созданы сокеты (системный вызов socket()) и в программе сервера, и в программе клиента, при этом в обеих программах сокеты связываются с адресом машины, на которую будет установлена программа сервера. Но, если в программе сервера для определения IP-адреса в структуре сокета может быть использована переменная INADDR_ANY, то в программе клиента для занесения в структуру сокета IP-адреса машины сервера необходимо использовать системный вызов inet_addr().
Сетевые вызовы inet_addr() и inet_ntoa() выполняют преобразования IP-адреса из формата текстовой строки "x.y.z.t" в структуру типа in_addr и обратно.
#include <arpa/inet.h>
in_addr_t inet_addr (const char *ip_address);
char * inet_ntoa(const struct in_addr in);
Для того чтобы процесс мог ссылаться на адрес своего компьютера, в файле <netinet/in.h> определена переменная INADDR_ANY, содержащая локальный адрес компьютера в формате in_addr_t.
2. Связывание
Системный вызов bind() связывает сетевой адрес компьютера с идентификатором сокета.
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr *address, size_t add_len);
В случае успешного завершения вызова bind() он возвращает значение 0. В случае ошибки, например, если сокет для этого адреса уже существует, вызов bind() возвращает значение -1. Переменная errno будет иметь при этом значение EADDRINUSE.
Операция связывания выполняется только в программе сервера.
3. Включение приема TCP-соединений
После выполнения связывания с адресом и перед тем, как какой-либо клиент сможет подключиться к созданному сокету, сервер должен включить прием соединений посредством системного вызова listen():
#include <sys/socket.h>
int listen (int sockfd, int queue_size);
Данная операция выполняется только в программе сервера.
4. Прием запроса на установку TCP-соединения
Когда сервер получает от клиента запрос на соединение, он создаёт новый сокет для работы с новым соединением. Первый же сокет используется только для установки соединения. Дополнительный сокет для работы с соединением создаётся при помощи вызова accept(), принимающего очередное соединение:
#include <sys/types.h>
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *address, size_t *add_len);
Возвращаемое значение соответствует идентификатору нового сокета, который будет использоваться для связи. До тех пор, пока от клиента не поступил запрос на соединение, процесс, выдавший системный вызов accept() переводится в состояние ожидания.
Данная операция выполняется только в программе сервера.
5. Подключение клиента
Для выполнения запроса на подключение к серверному процессу клиент использует системный вызов connect():
#include <sys/types.h>
#include <sys/socket.h>
int connect (int csockfd, const struct sockaddr *address, size_t add_len);
В случае успешного завершения вызова connect() он возвращает значение 0. В случае ошибки, системный вызов connect() возвращает значение -1, а переменная errno идентифицирует ошибку.
Данная операция выполняется только в программе клиента.
6. Пересылка данных
Для сокетов типа SOCK_STREAM дескрипторы сокетов, полученные сервером посредством вызова accept() и клиентом с помощью вызова socked(), могут использоваться для чтения или записи. Для этого могут использоваться обычные вызовы read() и write(), либо специальные системные вызовы send() и recv(), позволяющие задавать дополнительные параметры пересылки данных по сети. Синхронизация данных при работе с сокетом аналогична передаче данных через программный канал.
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv (int sockfd, void *buffer, size_t length, int flags);
ssize_t send (int sockfd, const void *buffer, size_t length, int flags);
В случае успешного чтения/записи системные вызовы send() и recv() возвращают число прочитанных/отосланных байт, или -1 в случае ошибки; в случае разорванной связи (клиент разорвал TCP-соединение) вызов recv() (или read()) возвращают нулевое значение; если процесс пытается записать данные через разорванное TCP-соединение посредством write() или send(), то он получает сигнал SIGPIPE, который можно обработать, если предусмотрена обработка данного сигнала.
В случае flags=0 вызовы send() и recv() полностью аналогичны системным вызовам read() и write().
Возможные комбинациями констант параметра flags системного вызова send():
Возможные комбинациями констант параметра flags системного вызова recv():
Данные операции выполняются и в программе сервера и в программе клиента.
7. Закрытие TCP-соединения
Закрываются сокеты так же, как и обычные дескрипторы файлового ввода/вывода, - при помощи системного вызова close(). Для сокета SOCK_STREAM ядро гарантирует, что все записанные в сокет данные будут переданы принимающему процессу.
Данные операции выполняются и в программе сервера, и в программе клиента.
Варианты заданий
  1. Эмуляция DNS сервера. Клиент подсоединяется к серверу, IP которого хранится в файле dns.url, и делает ему запрос на подключение к серверу "Имя сервера". DNS-сервер имеет список, хранящийся в файле о соответствии имен серверов и IP-адресов. Если в списке нет "имени сервера", запрошенного клиентом, то сервер DNS подключается последовательно к другим серверам, хранящимся в файле dns.url, и т.д. Если сервер не найден, клиенту возвращается соответствующее сообщение.
  2. Организовать чат. К серверу подключаются клиенты. При подключении клиента сервер спрашивает имя, под которым клиент будет известен в соединении. Сервер хранит IP-адреса подключаемых клиентов и их имена. Все сообщения каждого клиента рассылаются остальным в виде "имя_клиента - сообщение". Сообщения рассылаются сервером всем клиентам также при вхождении в связь нового клиента, и выходе какого-либо клиента.
  3. Организовать взаимодействие типа клиент-сервер. Клиенты подключаются к первому серверу и передают запрос на получение определенного файла. Если этого файла нет, сервер подключается ко второму серверу и ищет файл там. Затем либо найденный файл пересылается клиенту, либо высылается сообщение, что такого файла нет.
  4. Организовать взаимодействие типа клиент-сервер. Клиент отсылает строку серверу. Сервер отсылает данную строку на другие сервера, список которых хранится в файле, а там уже осуществляется поиск файлов содержащих данную строку. Результаты поиска отсылаются клиенту.
  5. Организовать взаимодействие типа клиент-сервер. Сервер при подключении к нему нового клиента высылает список IP-адресов уже подключенных клиентов. А остальным клиентам рассылается сообщение в виде IP-адреса о том, что подключился такой-то клиент.
  6. Организовать взаимодействие типа клиент-сервер. К серверу одновременно может подключиться только один клиент. Остальные клиенты заносятся в очередь, и им высылается сообщение об ожидании освобождения сервера.
  7. Организовать взаимодействие типа клиент-сервер. Клиент при входе в связь с сервером должен ввести пароль. Разрешено сделать три попытки. Если пароль не верен, сервер должен блокировать IP-адрес клиента на 5 минут.
  8. Сервер, моделирующий работу примитивной СУБД, хранит единственную таблицу в оперативной памяти. Клиенты подключаются к серверу, чтобы получить подробную информацию об хранящихся в таблице объектах. Для этого клиенты пересылают серверу строку, являющуюся уникальным ключом, который однозначно характеризует какой-либо объект, хранящийся в таблице. Сервер ищет в таблице объект с таким ключом и возвращает клиенту полную информацию об объекте, либо сообщает об отсутствии искомого объекта.
  9. Организовать взаимодействие типа клиент-сервер. Клиент делает запрос серверу на выполнение какой-либо команды. Сервер выполняет эту команду и возвращает результаты клиенту.
  10. Организовать взаимодействие типа клиент-сервер. Клиент делает запрос серверу о передаче файлов с определенным расширением из указанной директории. Сервер сканирует указанную директорию и отправляет клиенту список файлов, удовлетворяющих запросу.
Контрольные вопросы
  1. Какова структура IP-адреса?
  2. Как поместить и извлечь IP-адрес из структуры сокета?
  3. В чем разница между моделями TCP-соединения и дейтаграмм?
  4. Каковы основные шаги межпроцессного взаимодействия в модели TCP-соединения?
  5. Каковы основные шаги межпроцессного взаимодействия в модели дейтаграмм?
  6. Как занести в структуру сокета IP-адрес своего компьютера?
  7. Каким образом извлечь информацию о клиенте после установки TCP-соединения?
  8. Какова реакция системных вызовов посылки и приема сообщений в модели TCP-соединения при разрыве связи?