Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ОС_Шеховцов_1.docx
Скачиваний:
73
Добавлен:
09.11.2019
Размер:
14.73 Mб
Скачать

16.4.2. Створення сокетів

Перший етап, який необхідно виконати на клієнті та сервері, - створити деск­риптор сокета за допомогою системного виклику socket ():

#include <sys/socket.h>

int socket(int family, int type, int protocol):

де: family - комунікаційний домен (AFINET - домен Інтернету на основі IPv4, AFUNIX - домен UNIX);

type - тип сокета (S0CK_STREAM - потоковий сокет, S0CKDGRAM - дейтаграмний сокет, S0CKRAW - сокет прямого доступу (raw socket), призначений для роботи із протоколом мережного рівня, наприклад IP);

protocol - тип протоколу (звичайно 0; це означає, що протокол вибирають ав­томатично, виходячи з домену і типу сокета; наприклад, потокові сокети доме­ну Інтернет використовують TCP, а дейтаграмні - UDP).

Ось приклад використання socket ():

int sockfd = socket(AF_INET. SOCKJTREAM. 0);

Виклик socket О повертає цілочисловий дескриптор сокета або -1, якщо ста­лася помилка (при цьому, як і для інших викликів, змінна errno міститиме код по­милки). Цей сокет можна використовувати для різних цілей: організації очіку­вання з'єднань сервером, задання з'єднання клієнтом, отримання інформації про мережну конфігурацію хоста (в останньому випадку він може бути навіть не по­в'язаний з адресою).

Для системних викликів інтерфейсу сокетів Берклі далі за замовчуванням пе­редбачатиметься використання заголовного файла <sys/socket.h>.

16.4.3. Робота з потоковими сонетами

Тут розглядатимуться головні системні виклики, які використовують під час роз­робки серверів та клієнтів з використанням потокових сокетів на основі протоколу TCP (рис. 16.4). Це - основний вид сокетів, що найчастіше трапляється в реаль­них застосуваннях.

Зв'язування сокета з адресою

Сокет, дескриптор якого повернутий викликом socket О, не пов'язаний із кон­кретною адресою і недоступний для клієнтів. Для пов'язування сокета з адресою необхідно використати системний виклик bind():

int bind(int sockfd. const struct sockaddr *pmy_addr. socklen_t alen):

де: sockfd — дескриптор сокета, створений за допомогою socket();

pmyaddr — покажчик на заздалегідь заповнену структуру, що задає адресу со­кета (наприклад, sockaddrin);

alen - розмір структури, на яку вказує pmyaddr.

Цей виклик повертає -1, якщо виникла помилка.

Під час виконання bind О застосуванням-сервером необхідно задати наперед відомий порт, через який клієнти зв'язуватимуться з ним. Можна також вказати конкретну IP-адресу (при цьому допускатимуться лише з'єднання, для яких вона зазначена як адреса призначення), але найчастіше достатньо взяти довільну адре­су локального хоста, скориставшись константою INADDRANY:

struct sockaddMn my_addr = { 0 };

int listenfd – socket(...);

// ... задача my_addr.sin_family i my_addr.sin_port

my_addr.sin_addr.s_addr = INADDR_ANY;

bind(listenfd. (struct sockaddr *)&my_addr. sizeof(my_addr));

Найпоширенішою помилкою під час виклику bind() є помилка з кодом EADDRI-NUSE, яка свідчить про те, що цю комбінацію «IP-адреса — номер порту» вже вико­ристовує інший процес. Часто це означає повторну спробу запуску того самого сервера.

if (binddistenfd. ...)== -1 && errno == EADDRINUSE)

{ printfC'noMMnKa, адресу вже використовують\п"); exit(-l); }

Щоб досягти гнучкості у вирішенні цієї проблеми, рекомендують дозволяти користувачам налаштовувати номер порту (задавати його у конфігураційному файлі, командному рядку тощо).

Відкриття сокета для прослуховування

За замовчуванням сокет, створений викликом socketO є клієнтським активним сокетом, за допомогою якого передбачають з'єднання із сервером (особливості використання таких сокетів розглянемо в наступному розділі). Щоб перетворити такий сокет у серверний прослуховувальний (listening), призначений для при­ймання запитів на з'єднання від клієнтів, необхідно використати системний ви­клик listen()

int listenCint sockfd. int backlog):

де: sockfd - дескриптор сокета, пов'язаний з адресою за допомогою bindO;

backl og - задає довжину черги запитів на з'єднання, створеної для цього сокета.

Фактично внаслідок виклику listen О створяться дві черги запитів: перша з них містить запити, для яких процес з'єднання ще не завершений, друга — запи­ти, для яких з'єднання встановлене. Параметр backlog визначає сумарну довжину цих двох черг; для серверів, розрахованих на значне навантаження, рекоменду­ють задавати достатньо велике значення цього аргументу:

listentlistenfd. 1024):

Внаслідок виклику listen О сервер стає цілковито готовий до приймання за­питів на з'єднання.

Прийняття з'єднань

Спроба клієнта встановити з'єднання із сервером після виклику listenО має за­вершуватися успішно. Після створення нове з'єднання потрапляє у чергу вста­новлених з'єднань. Проте, для сервера воно залишається недоступним. Щоб от­римати доступ до з'єднання, на сервері його потрібно прийняти за допомогою системного виклику acceptO:

int acceptant sockfd. struct sockaddr *cliaddr. socklen_t *addrlen);

де: sockfd - дескриптор прослуховувального сокета;

cliaddr - покажчик на структуру, у яку буде занесена адреса клієнта, що за­просив з'єднання;

addrlen - покажчик на змінну, котра містить розмір структури cliaddr.

Головною особливістю цього виклику є повернене значення — дескриптор но­вого сокета з'єднання (connection socket), який створений внаслідок виконання цього виклику і призначений для обміну даними із клієнтом. Прослуховувальний сокет після виконання accepte) готовий приймати запити на нові з'єднання.

struct sockaddMn their_addr = { 0 };

int listenfd. connfd, sin_size = sizeof(struct sockaddrin);

// listenfd « sockett...). bindelistenfd. ...). 1 іsten(listenfd. ...)

connfd = aeeeptilistenfd. (struct sockaddr *)&their_addr. &sin_size);

// використання connfd для обміну даними із клієнтом

Виклик acceptO приймає з'єднання, що стоїть першим у черзі встановлених з'єднань. Якщо вона порожня, процес переходить у стан очікування, де перебува­тиме до появи нового запиту. Саме на цьому системному виклику очікують ітера­тивні та багатопотокові сервери, що використовують сокети. Такі сервери розгля­датимемо далі в цьому розділі.

Задання з'єднань на клієнті

Дотепер ми розглядали виклики, які мають бути використані на сервері. На клі­єнті ситуація виглядає інакше. Після створення сокета за допомогою socket ( ) не­обхідно встановити для нього з'єднання за допомогою системного виклику con­necte ), після чого можна відразу обмінюватися даними між клієнтом і сервером.

int connect(int sockfd. const_struct sockaddr *saddr. socklen_t alen); де: sockfd - сокет, створений за допомогою socketí);

saddr — покажчик на структуру, що задає адресу сервера (IP-адресу віддалено­го хоста, на якому запущено сервер і порт, який прослуховує сервер); alen - розмір структури saddr. Ось приклад створення з'єднання на клієнті:

struct sockaddrJn their_addr - { 0 }; sockfd - sockett...);

// заповнення their_addr.sin_family. their_addr.sin_port inet_aton("IP-aapeca-сервера". &(their_addr.sin_addr)); connectesockfd. (struct sockaddr *)&their_addr. sizeof(their_addr)); // обмін даними через sockfd

У разі виклику connecte ) починають створювати з'єднання. Повернення відбу­вається, якщо з'єднання встановлене або сталася помилка (при цьому поверне­ним значенням буде -1). Зазначимо, що, якщо connecte ) повернув помилку, кори­стуватися цим сокетом далі не можна, його необхідно закрити. Коли потрібно повторити спробу, створюють новий сокет за допомогою socket ( ).

Клієнтові зазвичай немає потреби викликати binde ) перед викликом connecte ) -ядро системи автоматично виділяє тимчасовий порт для клієнтського сокета.

Закриття з'єднань

Після використання всі з'єднання необхідно закривати. Для цього застосовують стандартний системний виклик close() :

close(sockfd);

Обмін даними між клієнтом і сервером

Після того як з'єднання було встановлене і відповідний сокет став доступний сер­веру (клієнт викликав connecte ), а сервер — accepte )), можна обмінюватися дани­ми між клієнтом і сервером. Для цього використовують стандартні системні ви­клики readO і writeO, а також спеціалізовані виклики recvO і sende ):

// прийняти nbytes або менше байтів із sockfd і зберегти їх у buf ssize_t reevtint sockfd. void *buf. size_t bytes_read. int flags): // відіслати nbytes або менше байтів із buf через sockfd ssize_t sendeint sockfd. const void *buf, size_t bytes_sent.ini flags):

Виклики recv() i sendO відрізняються від readO і writeO параметром flags, що задає додаткові характеристики передавання даних. Тут задаватимемо цей па­раметр, що дорівнює нулю:

int bytes_received - recv(sockfd, buf. sizeof(buf). 0): int bytes_sent = sendisockfd. buf. sizeof(buf). 0);

Важливою особливістю обміну даними між клієнтом і сервером є те, що sende ) і recv( ) можуть отримати або передати меншу кількість байтів, ніж було запитано за допомогою параметра nbytes, при цьому така ситуація не є помилкою (особли­во часто це трапляється для recvO). Для відсилання або отримання всіх даних у цьому разі необхідно використати відповідний системний виклик повторно:

// отримання даних обсягом sizeof(buf)

for (pos = 0: pos < sizeof(buf): pos += net_read)

net_read = recv(sockfd. &buf[pos], sizeoftbuf)-pos. 0); printfC'Bifl сервера: %s". buf);

У разі помилки ці виклики повертають -1. Серед кодів помилок важливими є ECONNRESET (віддалений процес завершився негайно, не закривши з'єднання) і ЕРІРЕ (для sendO це означає, що віддалений процес завершився за допомогою closeO, не прочитавши всіх даних із сокета; у цьому випадку також буде отрима­но сигнал SIGPIPE).

Виклик recvO, крім того, може повернути нуль. Це означає, що з'єднання було коректно закрите на іншій стороні (за допомогою виклику closeO). Так у коді сервера можна відстежувати закриття з'єднань клієнтами:

net_read = reevtsockfd. ...);

if (net_read -= 0) { printfC'3'єднання закрите\п"); }

Структура найпростішого ітеративного сервера

Розглянемо приклад розробки найпростішого луна-сервера, що негайно повертає клієнтові всі отримані від нього дані (в UNIX-системах така служба доступна за стандартним портом із номером 7).

Опишемо основний цикл сервера (інший код є стандартним - підготовка структур даних, виклик socketO, bindO і listenO).

У головному циклі спочатку необхідно прийняти з'єднання. Після цього в циклі зчитують дані із сокета з'єднання і відсилають назад клієнтові. Цей внут­рішній цикл триває доти, поки клієнт не закриє з'єднання або не буде повернено помилку. У кінці ітерації головного циклу сокет з'єднання закривають:

for(; :) {

sockfd = acceptelistenfd.

(struct sockaddr *)&their_addr, &sin_size); printfC'cepBep: з'єднання з адресою %s\n". i net_ntoa(thei r_addr.s i n_addr));

do {

bytes_read - reevtsockfd. buf. sizeof(buf). 0);

if (bytes_read > 0) sendtsockfd, buf. bytes_read. 0): } while (bytes_read > 0); // поки сокет не закритий close(sockfd);

} - ' ' :. '

closed і stenfd) ;

Зазначимо, що в даному прикладі сервер можна перервати тільки за допомо­гою сигналу (Ctrl+C, ki 11 ( ) тощо), при цьому за замовчуванням прослуховуваль-ний сокет закрито не буде. Коректне закриття сокета може бути зроблене в об­роблювачі сигналу або у функції завершення.

Недолік такого сервера очевидний — поки обробляються дані, нові клієнти не можуть створити з'єднання (не виконується accepte )). Далі розглянемо, як можна уникнути цієї проблеми.

Структура найпростішого клієнта

Приклад луна-клієнта, що відсилає серверу дані, введені із клавіатури, і відобра­жає все отримане у відповідь, наведено нижче.

Сокет і виклик connecte ) створюються стандартним способом. Обмін даними відбувається в нескінченному циклі (для виходу потрібно ввести рядок "вихід", після чого з'єднання буде коректно закрите). Зазначимо, що для простоти весь код повного отримання даних наведено тільки для recv() :

// sockfd = sockete...), connecttsockfd. ...)

for (; ;) {

fgetstbuf. sizeof(buf), stdin);

if (strcmptbuf, "вихід\п") — 0) break;

stdin_read = strlen(buf);

send(sockfd. buf. stdin_read. 0);

for (pos - 0; pos < stdin_read; pos += net_read)

netread = recv(sockfd. &buf[pos]. stdinread - pos. 0): printfC'eifl сервера отримано: %s", buf);

} -• i tfwbwaw», . Ш: ї9п

close(sockfd);

Структура найпростішого багатопотокового сервера

Розглянемо, як можна вирішити проблему ітеративного сервера відповідно до підходів, описаних у розділі 15. У цьому розділі зупинимося на розробці найпро­стішого багатопотокового сервера, а у розділі 16.4.4 — на введенні-виведенні з по­відомленням на базі виклику selecto.

Відмінності для багатопотокової реалізації фактично торкнуться тільки ос­новного циклу сервера. У ньому необхідно приймати з'єднання і створювати для його обробки окремий потік у неприєднаному стані (замість очікування завер­шення його виконання, потрібно очікувати нових з'єднань). Дескриптор сокета передають як параметр у функцію потоку.

for(: ;) {

connfd = accept(Iistenfd. ...); pthread_attr_i ni t(&att r);

pthread_attr_setdetachstate(&attr. PTHREADCREATEDETACHED): pthread_create(&tid. &attr. &process_request. (void *) connfd);

}

Функція потоку перетворює параметр до цілочислового типу, виконує обмін даними із клієнтом і закриває сокет з'єднання:

void *process_request(void *data) { int connfd = (int) data; // ... обмін даними із клієнтом через connfd close(connfd);