int main(int argc, char **argv)
{
int c, s, bytes;
char buf[BUF_SIZE]; /* буфер для входящего файла */
struct hostent *h; /* информация о сервере */
struct sockaddr_in channel; /* содержит IP-адрес */
if (argc != 3) {printf("Для запуска введите: client имя_сервера имя_файла"); exit(-1);}
h = gethostbyname(argv[1]); /* поиск IP-адреса хоста */
if (!h) {printf("gethostbyname не удалось найти %s", argv[1]); exit(-1;}
s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s <0) {printf("сбой вызова сокета"); exit(-1);}
memset(&channel, 0, sizeof(channel));
channel.sin_family= AF_INET;
memcpy(&channel.sin_addr.s_addr, h->h_addr, h->h_length);
channel.sin_port= htons(SERVER_PORT);
c = connect(s, (struct sockaddr *) &channel, sizeof(channel));
if (c < 0) {printf("сбой соединения"); exit(-1);}
/* Соединение установлено. Отправляется имя файла с нулевым байтом на конце */
write(s, argv[2], strlen(argv[2])+1);
/* Получить файл, записать на стандартное устройство вывода */
while (1) {
bytes = read(s, buf, BUF_SIZE); /* Читать из сокета */
if (bytes <= 0) exit(0); /* Проверка конца файла */
write(1, buf, bytes); /* Записать на стандартное устройство вывода */
}
}
Илл. 6.6. Клиентская программа для использования сокетов. Серверная программа представлена на следующей странице
#include <sys/types.h> /* Серверная программа */
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define SERVER_PORT 8080 /* По договоренности между клиентом и сервером */
#define BUF_SIZE 4096 /* Размер передаваемых блоков */
#define QUEUE_SIZE 10
int main(int argc, char *argv[])
{ int s, b, l, fd, sa, bytes, on = 1;
char buf[BUF_SIZE]; /* буфер для исходящего файла */
struct sockaddr_in channel; /* содержит IP-адрес */
/* Создать структуру адреса для привязки к сокету */
memset(&channel, 0, sizeof(channel)); /* Обнуление channel */
channel.sin_family = AF_INET;
channel.sin_addr.s_addr = htonl(INADDR_ANY);
channel.sin_port = htons(SERVER_PORT);
/* Пассивный режим. Ожидание соединения */
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); /* Создать сокет */
if (s <0) {printf("сбой вызова сокета"); exit(-1);}
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
b = bind(s, (struct sockaddr *) &channel, sizeof(channel));
if (b < 0) {printf("сбой связывания"); exit(-1);}
l = listen(s, QUEUE_SIZE); /* Определение размера очереди */
if (l < 0) {printf("сбой ожидания"); exit(-1);}
/* Теперь сокет установлен и привязан. Ожидание и обработка соединения. */
while (1) {
sa = accept(s, 0, 0); /* Блокировка в ожидании запроса соединения */
if (sa < 0) {printf("сбой доступа"); exit(-1);}
read(sa, buf, BUF_SIZE); /* Считать имя файла из сокета */
/* Получить и возвратить файл. */
fd = open(buf, O_RDONLY); /* Открыть файл для обратной отправки */
if (fd < 0) {printf("сбой открытия файла");
while (1) {
bytes = read(fd, buf, BUF_SIZE); /* Читать из файла */
if (bytes <= 0) break; /* Проверка конца файла */
write(sa, buf, bytes); /* Записать байты в сокет */
}
close(fd); /* Закрыть файл */
close(sa); /* Разорвать соединение */
}
}
Сначала рассмотрим ту часть программы, которая описывает сервер. Она начинается с включения некоторых стандартных заголовков, последние три из которых содержат основные структуры данных и определения, относящиеся к интернету. Затем SERVER_PORT определяется как 8080. Значение выбрано случайным образом. Любое число от 1024 до 65 535 также подойдет, если только оно не используется другим процессом; порты с номерами 1023 и ниже зарезервированы для привилегированных пользователей.
В последующих двух строках определяются две необходимые серверу константы. Первая из них задает размер блока данных для передачи файлов (в байтах). Вторая определяет максимальное количество незавершенных соединений, после установки которых новые соединения будут отвергаться.
После объявления локальных переменных начинается сама программа сервера. Вначале она инициирует структуру данных, которая будет содержать IP-адрес сервера. Эта структура вскоре будет привязана к серверному сокету. Вызов memset полностью обнуляет структуру данных. Последующие три присваивания заполняют три поля этой структуры. Последнее содержит порт сервера. Функции htonl и htons преобразуют значения в стандартный формат, что позволяет программе нормально выполняться на устройствах с представлением числовых разрядов little-endian (например, Intel x86) и big-endian (например, SPARC).
После этого сервер создает и проверяет сокет на ошибки (определяется по s < 0). В окончательной версии программы сообщение об ошибке может быть чуть более понятным. Вызов setsockopt нужен для того, чтобы порт мог использоваться несколько раз, а сервер — бесконечно, обрабатывая запрос за запросом. Теперь IP-адрес привязывается к сокету и выполняется проверка успешного завершения вызова bind. Конечным этапом инициализации является вызов listen. Он свидетельствует о готовности сервера к приему входящих вызовов и сообщает системе о том, что нужно ставить в очередь до QUEUE_SIZE вызовов, пока сервер обрабатывает текущий вызов. При заполнении очереди прибытие новых запросов спокойно игнорируется.
В этот момент сервер входит в основной цикл программы и уже из него не выходит. Этот цикл можно остановить только извне. Вызов accept блокирует сервер на то время, пока клиент пытается установить соединение. В случае успеха accept возвращает дескриптор сокета, который можно использовать для чтения и записи, аналогично тому, как файловые дескрипторы применяются для чтения и записи в каналы. Однако, в отличие от однонаправленных каналов, сокеты двунаправлены, поэтому для чтения (и записи) данных из соединения можно использовать sa (принятый сокет). Файловые дескрипторы канала могут применяться для чтения или записи, но не одновременно.
После установления соединения сервер считывает имя файла. Если оно еще недоступно, сервер блокируется, ожидая его. Получив имя файла, сервер открывает файл и входит в цикл, который читает блоки данных из файла и записывает их в сокет. Это продолжается до тех пор, пока не будут скопированы все запрошенные данные. Затем файл закрывается, соединение разрывается и начинается ожидание нового вызова. Данный цикл повторяется бесконечно.
Теперь рассмотрим часть кода, описывающую клиента. Чтобы понять, как работает программа, сначала необходимо разобраться, как она запускается. Если она называется client, ее типичный вызов будет выглядеть так:
client flits.cs.vu.nl /usr/tom/filename >f
Этот вызов сработает, только если сервер расположен по адресу flits.cs.vu.nl, файл /usr/tom/filename существует и у сервера есть доступ по чтению для этого файла. Если вызов произведен успешно, файл передается по интернету и записывается в f, после чего клиентская программа заканчивает свою работу. Поскольку серверная программа продолжает работать, клиент может быть запущен снова с новыми запросами на получение файлов.
Клиентская программа начинается с подключения файлов и объявлений. Прежде всего проверяется корректность числа аргументов (где argc = 3 означает, что была вызвана программа с указанием ее имени и двух аргументов). Обратите внимание на то, что argv[1] содержит имя сервера (например, flits.cs.vu.nl) и переводится в IP-адрес с помощью функции gethostbyname. Для поиска имени эта функция использует DNS. Межсетевые экраны мы изучим отдельно в главе 7.
Затем создается и инициализируется сокет, после чего клиент пытается установить TCP-соединение с сервером посредством connect. Если сервер включен, работает на указанном компьютере, соединен с SERVER_PORT и либо простаивает, либо имеет достаточно места в очереди listen (очереди ожидания), то соединение с клиентом будет рано или поздно установлено. По данному соединению клиент передает имя файла, записывая его в сокет.