Протоколи та сокети tcp ip. Сокети. Відлуння TCP і UDP, що використовує функцію select

Сокети

Сокет- це один кінець двостороннього каналу зв'язку між двома програмами, що працюють у мережі. З'єднуючи разом два сокети, можна передавати дані між різними процесами(локальними чи віддаленими). Реалізація сокетів забезпечує інкапсуляцію протоколів мережного та транспортного рівнів.

Спочатку сокети були розроблені для UNIX в університеті Каліфорнії в Берклі. У UNIX забезпечує зв'язок метод введення-виведення слідує алгоритму open/read/write/close. Перш ніж використовувати ресурс, його потрібно відкрити, задавши відповідні дозволи та інші параметри. Як тільки ресурс відкритий, з нього можна зчитувати або записувати дані. Після використання ресурсу користувач повинен викликати метод Close(), щоб подати сигнал операційній системі завершення його роботи з цим ресурсом.

Коли до операційної системи UNIX було додано кошти міжпроцесної взаємодії (Inter-Process Communication, IPC)та мережного обміну, був запозичений звичний шаблон введення-виведення. Усі ресурси, відкриті зв'язку, в UNIX і Windows ідентифікуються дескрипторами. Ці дескриптори, або описувачі (handles), можуть вказувати на файл, пам'ять або інший канал зв'язку, а фактично вказують на внутрішню структуру даних, використовувану операційною системою. Сокет, будучи таким самим ресурсом, теж представляється дескриптором. Отже, для сокетів життя дескриптора можна поділити на три фази: відкрити (створити) сокет, отримати із сокету або відправити сокету і зрештою закрити сокет.

Інтерфейс IPC для взаємодії між різними процесами побудований поверх методів введення-виведення. Вони полегшують для сокетів відправлення та отримання даних. Кожен цільовий об'єкт задається адресою сокета, отже, цю адресу можна вказати в клієнті, щоб встановити з'єднання з метою.

Типи сокетів

Існують два основних типи сокетів - потокові сокети та дейтаграмні.

Поточні сокети (stream socket)

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

Потоковий сокет гарантує виправлення помилок, обробляє доставку та зберігає послідовність даних. На нього можна покластися в доставці впорядкованих даних. Потоковий сокет також підходить для передачі великих обсягів даних, оскільки накладні витрати, пов'язані з встановленням окремого з'єднання для кожного повідомлення, що надсилається, може виявитися неприйнятним для невеликих обсягів даних. Поточні сокети досягають цього рівня якості за рахунок використання протоколу Transmission Control Protocol (TCP). TCP забезпечує надходження даних на інший бік у потрібній послідовності і без помилок.

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

Однак, окремі повідомленняможуть подрібнюватися на пакети, і способу визначити межі записів не існує. При використанні TCP цей протокол бере на себе розбиття даних, що передаються на пакети відповідного розміру, відправку їх в мережу і складання їх на іншій стороні. Додаток знає лише, що він відправляє на рівень TCPпевна кількість байтів та інша сторона отримує ці байти. У свою чергу, TCP ефективно розбиває ці дані на пакети відповідного розміру, отримує ці пакети на іншій стороні, виділяє з них дані і об'єднує їх разом.

Потоки базуються на явних з'єднаннях: сокет А запитує з'єднання з сокетом, а сокет або погоджується із запитом на встановлення з'єднання, або відкидає його.

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

Сервер електронної пошти представляє приклад програми, яка має доставляти зміст у правильному порядку, без дублювання та перепусток. Поточний сокет розраховує, що TCP забезпечить доставку повідомлень за призначенням.

Дейтаграмні сокети (datagram socket)

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

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

Використання дейтаграмних сокетів вимагає, щоб передачі даних від клієнта до сервера займалися. User Datagram Protocol (UDP). У цьому протоколі розмір повідомлень накладаються деякі обмеження, і на відміну потокових сокетів, вміють надійно відправляти повідомлення серверу-адресату, дейтаграммные сокети надійність не забезпечують. Якщо дані загубилися в мережі, сервер не повідомить про помилки.

Крім двох розглянутих типів існує узагальнена форма сокетів, яку називають необроблюваними або сирими.

Сирі сокети (raw socket)

Головна мета використання сирих сокетів полягає у обході механізму, з допомогою якого комп'ютер обробляє TCP/IP. Це досягається забезпеченням спеціальної реалізації стека TCP/IP, що заміщає механізм, наданий стеком TCP/IP в ядрі - пакет безпосередньо передається додатку і, отже, обробляється набагато ефективніше, ніж під час проходження через головний стек протоколів клієнта.

За визначенням, сирий сокет - це сокет, який приймає пакети, обходить рівні TCP і UDP у стеку TCP/IP і надсилає їх додатку.

У разі використання таких сокетів пакет не проходить через фільтр TCP/IP, тобто. ніяк не обробляється, і постає у своїй сирій формі. У такому разі обов'язок правильно обробити всі дані та виконати такі дії, як видалення заголовків та розбір полів, лягає на одержуючий додаток - все одно, що включити додаток невеликий стек TCP/IP.

Однак нечасто може бути потрібна програма, що працює з сирими сокетами. Якщо ви не пишете системне програмне забезпечення або програму, аналогічну до аналізатора пакетів, вникати в такі деталі не доведеться. Сирі сокети переважно використовуються розробки спеціалізованих низькорівневих протокольних додатків. Наприклад, такі різноманітні утиліти TCP/IP, як trace route, ping чи arp використовують сирі сокети.

Робота із сирими сокетами потребує солідного знання базових протоколів TCP/UDP/IP.

Порти

Порт визначено, щоб вирішити завдання одночасної взаємодії з кількома програмами. Фактично з його допомогою розширюється поняття IP-адреси. Комп'ютер, на якому одночасно виконується кілька програм, отримуючи пакет з мережі, може ідентифікувати цільовий процес, користуючись унікальним номеромпорту, визначеним під час встановлення з'єднання.

Сокет складається з IP-адреси машини та номера порту, що використовується програмою TCP. Оскільки IP-адреса унікальна в Інтернеті, а номери портів унікальні на окремій машині, номери сокетів також унікальні у всьому Інтернеті. Ця характеристика дозволяє процесу спілкуватися через мережу з іншим процесом лише на підставі номера сокету.

За певними службами номери портів зарезервовані - широко відомі номери портів, наприклад порт 21, що використовується в FTP. Ваша програма може користуватися будь-яким номером порту, який не був зарезервований і поки не зайнятий. Агентство Internet Assigned Numbers Authority (IANA)веде список широко відомих номерів портів.

Зазвичай додаток клієнт-сервер, що використовує сокети, складається з двох різних додатків - клієнта, що ініціює з'єднання з метою (сервером), та сервера, що очікує з'єднання від клієнта.

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

Якщо події розвиваються вдало, за умови, що сервер запущений перш, ніж клієнт спробував з ним з'єднатися, сервер погоджується на з'єднання. Давши згоду, серверний додаток створює новий сокет для взаємодії саме з клієнтом, що встановив з'єднання:

Тепер клієнт і сервер можуть взаємодіяти між собою, зчитуючи повідомлення кожен із свого сокету і, відповідно, записуючи повідомлення.

Робота із сокетами в.NET

Підтримку сокетів в .NET забезпечують класи у просторі імен System.Net.Sockets- Почнемо з їх короткого опису.

Класи для роботи із сокетами
Клас Опис
MulticastOption Клас MulticastOption встановлює значення IP-адреси для приєднання до IP-групи або виходу з неї.
NetworkStream Клас NetworkStream реалізує базовий клас потоку, з якого дані відправляються та в якому вони виходять. Це абстракція високого рівня, що є з'єднання з каналом зв'язку TCP/IP.
TcpClient Клас TcpClient будується на класі Socket, щоб забезпечити обслуговування TCP на вищому рівні. TcpClient надає кілька методів для надсилання та отримання даних через мережу.
TcpListener Цей клас також збудовано на низькорівневому класі Socket. Його основне призначення – серверні програми. Він очікує на вхідні запити на з'єднання від клієнтів і повідомляє додаток про будь-які з'єднання.
UdpClient UDP - це протокол, який не організує з'єднання, отже, для реалізації UDP-обслуговування в.NET потрібна інша функціональність.
SocketException Це виняток породжується, як у сокеті виникає помилка.
Socket Останній клас у просторі імен System.Net.Sockets – це сам клас Socket. Він забезпечує базову функціональність програми сокета.

Клас Socket

Клас Socket грає важливу рольу мережному програмуванні, забезпечуючи функціонування як клієнта, і сервера. Головним чином, виклики методів цього класу виконують необхідні перевірки, пов'язані з безпекою, зокрема перевіряють дозволи системи безпеки, після чого вони переправляються до аналогів цих методів у Windows Sockets API.

Перш ніж звертатися до прикладу використання класу Socket, розглянемо деякі важливі властивості та методи цього класу:

Властивості та методи класу Socket
Властивість чи метод Опис
AddressFamily Дає сімейство адрес сокету - значення з переліку Socket.AddressFamily.
Available Повертає обсяг доступних для читання даних.
Blocking Дає або встановлює значення, що показує, чи знаходиться сокет у режимі блокування.
Connected Повертає значення, що інформує, чи з'єднаний сокет з віддаленим хостом.
LocalEndPoint Дає локальну кінцеву точку.
ProtocolType Дає тип протоколу сокету.
RemoteEndPoint Дає кінцеву точку сокету.
SocketType Дає тип сокету.
Accept() Створює новий сокет для обробки запиту на з'єднання.
Bind() Зв'язує сокет з локальною кінцевою точкою для очікування запитів на з'єднання.
Close() Змушує сокет закритися.
Connect() Встановлює з'єднання з віддаленим хостом.
GetSocketOption() Повертає значення SocketOption.
IOControl() Встановлює для сокету низькорівневі режими роботи. Цей метод забезпечує низькорівневий доступ до класу Socket, що лежить в основі.
Listen() Поміщає сокет у режим прослуховування (очікування). Цей метод призначений лише для серверних програм.
Receive() Отримує дані з'єднаного сокету.
Poll() Визначає статус сокету.
Select() Перевіряє статус одного чи кількох сокетів.
Send() Надсилає дані з'єднаному сокету.
SetSocketOption() Встановлює опцію сокету.
Shutdown() Забороняє операції надсилання та отримання даних на сокеті.

35 відповідей

Резюме

Сокет TCP є екземпляром кінцевої точки, що визначається IP-адресою та портом у контексті певного TCP-з'єднання або стану прослуховування.

Порт – це ідентифікатор віртуалізації, що визначає кінцеву точку служби (на відміну від кінцевої точки екземпляра служби або її ідентифікатора сеансу).

Сокет TCP не є з'єднанням, Це кінцева точка певного з'єднання.

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

Для цієї комбінації адрес і портів може бути лише один роз'єм-слухач.

Експозиція

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

В самому широкому значенні, порт є точкою входу чи виходу. Французьке слово porte, хоч і не використовується в мережевому контексті, буквально означає двері або шлюз, що ще раз підкреслює той факт, що порти є транспортними кінцевими точками, чи надсилаєте ви дані або великі сталеві контейнери.

З метою цієї дискусії я обмежу розгляд контексту мереж TCP-IP. Модель OSI дуже добре, але ніколи не була повністю реалізована, а тим паче широко розгорнута в умовах високої напруги з високим трафіком.

Комбінація IP-адреси та порту суворо відома як кінцева точка і іноді називається сокетом. Це використання пов'язане з RFC793, оригінальною специфікацією TCP.

TCP-з'єднання визначається двома кінцевими точками як сокетами.

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

Призначення портів - це розрізняти кілька кінцевих точок на заданій адресі мережі. Можна сказати, що порт є віртуалізованою кінцевою точкою. Ця віртуалізація уможливлює кілька паралельних підключеньодному мережному інтерфейсі.

Це пара сокетів (4-кортеж, що складається з IP-адреси клієнта, номер порту клієнта, IP-адреса сервера, і номер порту сервера), який вказує дві кінцеві точки, які однозначно ідентифікує кожне TCP-з'єднання в Інтернеті. (TCP-IP Illustrated Volume 1, W. Richard Stevens)

У більшості мов, заснованих на C, TCP-з'єднання встановлюються та обробляються з використанням методів в екземплярі класу Socket. Хоча поширено працювати більш високому рівні абстракції, зазвичай це екземпляр класу NetworkStream, він зазвичай надає посилання об'єкт сокета. До кодера цей об'єкт сокета, здається, представляє з'єднання, оскільки з'єднання створюється і управляється з допомогою методів об'єкта сокета.

У C#, щоб встановити TCP-з'єднання (до існуючого слухача), ви спочатку створюєте TcpClient. Якщо ви не вкажете кінцеву точку для конструктора TcpClient, вона використовує значення за промовчанням – так чи інакше визначається локальна кінцева точка. Потім ви викликаєте Connect метод на створеному екземплярі. Цей метод вимагає параметра, який описує іншу кінцеву точку.

Все це трохи заплутує та змушує вас повірити, що сокет – це з'єднання, яке є блокуванням. Я працював під цим непорозумінням, поки Річард Дорман не запитав.

Проробивши багато читання та мислення, я тепер переконаний, що було б набагато розумніше мати клас TcpConnection з конструктором, який приймає два аргументи: LocalEndpoint та RemoteEndpoint. Ймовірно, ви могли б підтримати один аргумент RemoteEndpoint, коли допустимі значення за промовчанням для локальної кінцевої точки. Це неоднозначно для багатоядерних комп'ютерів, але неоднозначність може бути вирішена за допомогою таблиці маршрутизації шляхом вибору інтерфейсу з найкоротшим маршрутом до віддаленої кінцевої точки.

Ясність буде підвищено й інших відносинах. Сокет не ідентифікується комбінацією IP-адреси та порту:

[...] TCP демультиплексує вхідні сегменти, використовуючи всі чотири значення, які містять локальні та зовнішні адреси: IP-адреса одержувача, номер порту призначення, IP-адреса джерела та номер порту джерела. TCP неспроможна визначити, який процес отримує вхідний сегмент, лише дивлячись порт призначення. Крім того, єдина з [різних] кінцевих точок у [даному номері порту], яка прийматиме вхідні запити на з'єднання, є однією в стані прослуховування. (p255, TCP-IP Illustrated Volume 1, W. Richard Stevens)

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

Хаграваль не вірить мені (див. коментарі), тому тут справжній зразок. Я підключив веб-браузер до http://dilbert.com, а потім запустив netstat -an-p tcp. Останні шість рядків виведення містять два приклади того, що адреси та порту недостатньо, щоб однозначно ідентифікувати сокет. Існує два різних з'єднання між 192.168.1.3 (моя робоча станція) та 54.252.92.236:80

TCP 192.168.1.3:63240 54.252.94.236:80 SYN_SENT TCP 192.168.1.3:63241 54.252.94.236:80 SYN_SENT TCP 192.167.1.2.2.2:2. SENT TCP 192.168.1.3:63243 207.38.110.62:80 SYN_SENT TCP 192.168 .1.3:64161 65.54.225.168:443 ESTABLISHED

Оскільки сокет є кінцевою точкою з'єднання, є два сокети з комбінацією адрес/портів 207.38.110.62:80 і ще два з комбінацією адрес/портів 54.252.94.236:80.

Я думаю, що неправильне розуміння Хаграваля виникає з мого дуже обережного використання слова "ідентифікує". Я маю на увазі "цілком, однозначно і однозначно ідентифікувати". У наведеному вище прикладі є дві кінцеві точки з комбінацією адрес/порт 54.252.94.236:80. Якщо у вас є адреса та порт, у вас недостатньо інформації, щоб поділити ці роз'єми. Недостатньо інформації для ідентифікації сокету.

Додавання

В абзаці другому розділу 2.7 RFC793 говориться:

З'єднання повністю визначається парою сокетів на кінцях. локальна розетка може брати участь у багатьох з'єднаннях з різними зарубіжними розетками.

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

Посилання

    TCP-IP Ілюстрований том 1 Протоколи, W. Richard Stevens, 1994 Addison Wesley

    Сокетявляє собою одне з'єднання між двома мережними програмами. Ці дві програми номінально працюють на різних комп'ютерах, але сокети також можуть використовуватися для міжпроцесної взаємодії на одному комп'ютері. Програми можуть створювати кілька сокетів для зв'язку між собою. Сокети є двоспрямованими, що означає, що обидві сторони з'єднання здатні надсилати та отримувати дані. Тому сокет може бути створений теоретично на будь-якому рівні моделі OSI із 2 вгору. Програмісти часто використовують сокети в мережевому програмуванні, хоч і побічно. Бібліотеки програмування, такі як Winsock, приховують багато низькорівневих деталей програмування сокетів. Розетки широко використовуються з початку 1980-х років.

    Портє кінцевою точкою або "канал" для мережного зв'язку. Номери портів дозволяють різним програмам на одному комп'ютері використовувати мережні ресурси, не заважаючи один одному. Номери портів найбільше часто зустрічаються в мережевому програмуванні, особливо в програмуванні сокетів. Іноді номери портів стають видимими для випадкового користувача. Наприклад, деякі веб-сайти, які людина відвідує в Інтернеті, використовують наступну URL-адресу:

    З деякою аналогією

    Хоча для сокетіввже було дано багато технічних деталей... Я хотів би додати свою відповідь, про всяк випадок, якщо хтось все ще не може відчути різницю між ip, портом та сокетами

    Розглянемо сервер S,

    і скажемо, людині X, Y, Zпотрібна служба (скажімо, служба чату) з цього сервера S

    IP-адреса говоритьхто?той сервер чату "S", з яким X, Y, Z хоче зв'язатися

    добре, ви отримали "хто сервер"

    але припустимо, що сервер S також надає деякі інші послуги іншим людям, скажімо, S надає послуги зберігання для осіб A, B, C

    порт каже ---> Котрий?обслуговування, який вам (X, Y, Z) необхідний, тобто. сервіс чату, а не сервіс зберігання

    добре.., ви змушуєте сервер знати, що вам потрібен чат-сервіс, а не сховище

    вам три роки, і сервер може захотіти ідентифікувати всі три по-різному

    приходить розетка

    тепер сокет кажеякий?конкретне з'єднання

    тобто, скажімо,

    розетка 1 для людини X

    розетка 2 для людини Y

    та розетка 3 для людини Z

    Я сподіваюся, що це допомагає комусь, хто був все ще збентежений:)

    По-перше, я думаю, ми повинні почати з невеликого розуміння того, що складає отримання пакета від A до B.

    Загальним визначенням для мережі є використання моделі OSI, яка поділяє мережу на кілька шарів відповідно до призначення. Є кілька важливих, які ми розглянемо тут:

    • Рівень каналу передачі. Цей рівень відповідає за отримання пакетів даних від одного мережного пристрою до іншого і знаходиться трохи вище за рівень, що фактично передає. Він говорить про MAC-адреси і знає, як знайти хости на основі їх MAC (апаратної) адреси, але не більше.
    • Мережевий рівень - це рівень, який дозволяє переносити дані по машинам і фізичним кордонам, таким як фізичні пристрої. Мережевий рівень повинен по суті підтримувати додатковий механізм, заснований на адресі, яка пов'язана з фізичною адресою; введіть IP-адресу (IPv4). IP-адреса може отримати ваш пакет від A до B через Інтернет, але нічого не знає про те, як проходити індивідуальні перельоти. Це обробляється рівнем вище відповідно до інформації про маршрутизацію.
    • Транспортний рівень. Цей рівень відповідає за визначення способу отримання інформації від A до B та будь-яких обмежень, перевірок чи помилок у цій поведінці. Наприклад, TCP додає додаткову інформаціюпакет, так що можна вивести, якщо пакети були втрачені.

    TCP містить, окрім іншого, концепцію ports. Це фактично різні кінцеві точки даних на тій самій IP-адресі, до якої може прив'язуватися Internet Socket (AF_INET).

    Коротка коротка відповідь.

    A портможе бути описаний як внутрішня адресавсередині хоста, що ідентифікує програму чи процес.

    A socketможна описати як програмний інтерфейсдозволяє програмі спілкуватися з іншими програмами або процесами, в Інтернеті або локально.

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

    Щоб отримати послугу, вам потрібен сервісний номер. Цей сервісний номер називається портом. Просто, як той.

    Наприклад, HTTP як служба працює на порту 80.

    Тепер багато людей можуть запросити послугу, і з'єднання з клієнт-сервером встановлено. Буде багато зв'язків. Кожне з'єднання представляє клієнта. Щоб підтримувати кожне з'єднання, сервер створює сокет для кожного з'єднання для підтримки клієнта.

    Здається, що є багато відповідей, які порівнюють сокет із з'єднанням між двома ПК. Я думаю, що це абсолютно не так. Сокет завжди був кінцевою точкою на 1 ПК, який може бути або не підключений - звичайно, ми всі використовували приймач або UDP-сокети * в якийсь момент. Важлива частина полягає в тому, що він адресний та активний. Надсилання повідомлення у файл 1.1.1.1:1234 навряд чи працюватиме, оскільки для цієї кінцевої точки немає сокету.

    Сокети специфічні для протоколу - тому реалізація унікальності полягає в тому, що TCP / і UDP / використовує * (ipaddress: порт), відрізняється від, наприклад, IPX (Мережа, Node та... гейм, сокет - але інший сокет, ніж під загальним терміном "сокет".Номери сокетів IPX еквівалентні IP-портам). Але всі вони пропонують унікальну кінцеву точку, що адресується.

    Оскільки IP став домінуючим протоколом, порт (у мережевих термінах) став сингулярним із номером порту UDP або TCP, який є частиною адреси сокету.

    • UDP не має відношення до підключення – це означає, що віртуальна схема між двома кінцевими точками ніколи не створюється. Однак як кінцева точка ми як і раніше ставимося до UDP-сокетів . Функції APIдають зрозуміти, що обидва просто різними типамисокетів. SOCK_DGRAM - це UDP (просто надсилання повідомлення), а SOCK_STREAM - TCP (створення віртуальної схеми).

      Технічно, заголовок IP містить IP-адресу, а протокол поверх IP (UDP або TCP) містить номер порту. Це дозволяє використовувати інші протоколи (наприклад, ICMP, які не мають номерів портів, але мають інформацію про IP-адресу).

      Це терміни з двох різних доменів: "порт" - це концепція мереж TCP/IP, "сокет" - це API (програмування). "Сокет" створюється (в коді), беручи порт, ім'я хоста або мережевий адаптері об'єднуючи їх у структуру даних, яку ви можете використовувати для надсилання або отримання даних.

      З'єднання TCP-IP - це двонаправлені шляхи, що з'єднують одну адресу: комбінація портів з іншою адресою: комбінація портів. Тому, коли ви відкриваєте з'єднання з локальною машиною на порт на віддаленому сервері (наприклад, www.google.com:80), ви також зв'язуєте новий номер порту на вашому комп'ютері з підключенням, щоб сервер міг відправляти речі назад до вас ( наприклад, 127.0.0.1: 65234). Корисно використовувати netstat для перегляду ваших з'єднань з комп'ютером:

      > netstat -nWp tcp (on OS X) Active Internet connections Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 192.168.0.6.49871 17.172.232.57.5223 ESTABLIS

      Сокет - це особливий тип дескриптора файлу, який використовується процесом для запиту мережевих сервісів операційної системи. Адреса сокету - це потрійна: (protocol, local-address, local-process), де локальний процесідентифікується номером порту.

      У наборі TCP/IP, наприклад:

      (tcp, 193.44.234.3, 12345)

      Розмова - це лінія зв'язку між двома процесами, що таким чином зображує зв'язок між двома. Асоціацією є 5-кортеж, який повністю визначає два процеси, що містять з'єднання: (protocol, local-address, local-process, foreign-address, foreign-process)

      У наборі TCP/IP, наприклад:

      (tcp, 193.44.234.3, 1500, 193.44.234.5, 21)

      може бути припустимою асоціацією.

      Напівасоціація: (протокол, локальна адреса, локальний процес)

      (protocol, foreign-address, foreign-process)

      які визначають кожну половину з'єднання.

      Напівзв'язок також називається сокет або транспортна адреса. Тобто, сокет є кінцевою точкою для зв'язку, який може бути названий і адресований в мережі. Інтерфейс сокету одна із кількох інтерфейсів прикладного програмування (API) для протоколів зв'язку. Розроблений як універсальний інтерфейс комунікаційний програмування, він вперше був впроваджений системою UNIX 4.2BSD. Хоча він був стандартизований, він став фактичним промисловим стандартом.

      Порт був легкою частиною, це просто унікальний ідентифікатор сокету. Сокет - це те, що процеси можуть використовувати для встановлення з'єднань та спілкування один з одним. У Tall Jeff була чудова телефонна аналогія, яка не була ідеальною, тому я вирішив її виправити:

      Додаток складається з кількох процесів, які обмінюються даними по мережі (пара клієнт-сервер). Ці процеси надсилають та отримують повідомлення в мережу та з неї через програмний інтерфейс. сокет. Враховуючи аналогію, представлену у книзі "Комп'ютерна мережа: підхід зверху донизу". Існує будинок, який хоче спілкуватися з іншим будинком. Тут будинок аналогічний процесу та двері до розетки. Процес відправлення передбачає, що з іншого боку дверей є інфраструктура, яка передаватиме дані до пункту призначення. Як тільки повідомлення надійде з іншого боку, воно проходить через двері приймача (гніздо) до будинку (процесу). Ця ілюстрація з тієї ж книги може допомогти вам:

      Сокети є частиною транспортного рівня, що забезпечує логічний зв'язок із додатками. Це означає, що з точки зору застосування обидва вузли безпосередньо пов'язані один з одним, хоча між ними існує безліч маршрутизаторів та/або перемикачів. Таким чином, сокет не є самою сполукою, це кінцева точка з'єднання. Протоколи транспортного рівня реалізуються лише хостах, а чи не на проміжних маршрутизаторах.
      Портинадають засоби внутрішньої адресації машини. Основна мета - дозволити кільком процесам відправляти та отримувати дані по мережі без втручання в інші процеси (їх дані). Всі сокети мають номер порту. Коли сегмент надходить на хост, рівень транспорту досліджує номер порту призначення сегмента. Потім він переводить сегмент у відповідний сокет. Це завдання доставки даних у сегменті транспортного рівня у правильний сокет називається демпплексуванням. Потім дані сегмента передаються до процесу, приєднаного до сокету.

      Сокет - це структура вашого програмного забезпечення. Це більш-менш файл; він має операції, такі як читання та запис. Не фізична річ; це спосіб для вашого програмного забезпечення посилатися на фізичні речі.

      Порт - річ, подібна до пристрою. Кожен хост має одну або кілька мереж (фізично); хост має адресу у кожній мережі. Кожна адреса може мати тисячі портів.

      Лише один сокет може використовувати порт за адресою. Розетка розподіляє порт приблизно так само, як виділення пристрою для введення/виведення файлової системи. Як тільки порт буде виділено, жодний інший сокет не зможе підключитися до цього порту. Порт буде звільнено, коли сокет буде закрито.

      Сокет - це одна кінцева точка двосторонньої лінії зв'язку між двома програмами, запущеними у мережі. Сокет прив'язаний до номера порту, так що рівень TCP може ідентифікувати програму, для якої дані призначені для надсилання.

      Відносна термінологія TCP/IP, яку я припускаю, має на увазі це питання. В умовах нефахівця:

      PORT – це номер телефону певного будинку у певному поштовому індексі. Поштовий код міста можна розглядати як IP-адресу міста та всіх будинків у цьому місті.

      SOCKET, з іншого боку, більше схожий на встановлений телефонний дзвінокміж телефонами кількох будинків, що розмовляють один з одним. Ці дзвінки можна встановити між будинками в одному місті або двома будинками в різних містах. Це той тимчасовий шлях між двома телефонами, що говорять один з одним, що є SOCKET.

      Порт та сокет можна порівняти з відділенням банку.

      Номер будівлі "Банку" аналогічний IP-адресі. Банк має різні розділи, такі як:

      1. Відділ ощадного рахунку
      2. Відділ персональних кредитів
      3. Відділ іпотечного кредитування
      4. Відділ розгляду скарг

      Таким чином, 1 (відділ ощадного рахунку), 2 (відділ персональних кредитів), 3 (відділ житлових кредитів) та 4 (відділ розгляду скарг) є портами.

      Тепер дозвольте нам сказати, що ви йдете, щоб відкрити ощадний рахунок, ви йдете в банк (IP-адреса), потім ви йдете в "відділ ощадного рахунку" (порт № 1), потім ви зустрічаєте одного зі співробітників, які працюють у "відділ" ощадного рахунку" ". Давайте зателефонуємо йому SAVINGACCOUNT_EMPLOYEE1 для відкриття рахунку.

      SAVINGACCOUNT_EMPLOYEE1 – ваш дескриптор сокету, тому може бути від SAVINGACCOUNT_EMPLOYEE1 до SAVINGACCOUNT_EMPLOYEEN. Це все дескриптори сокетів.

      Аналогічно інші відділи матимуть роботу під їх керівництвом, і вони аналогічні сокету.

      Сокет є кінцевою точкою зв'язку. Сокет не має прямого відношення до сімейства протоколів TCP/IP, його можна використовувати з будь-яким протоколом, що підтримується вашою системою. API сокету C очікує, що ви спочатку отримаєте порожній об'єкт сокету із системи, який потім можна буде прив'язати до локальної адреси сокету (щоб безпосередньо отримувати вхідний трафікдля протоколів без встановлення з'єднання або приймати вхідні запити на з'єднання для протоколів, орієнтованих на встановлення з'єднання або що ви можете підключитися до адреси віддаленого сокету (для будь-якого типу протоколу). Ви можете навіть зробити те й інше, якщо хочете контролювати обидва: локальну адресу сокета, до якої прив'язаний сокет, і адресу віддаленого сокета, до якого підключений сокет. Для протоколів без встановлення з'єднання підключення сокету навіть необов'язково, але якщо ви цього не зробите, вам також доведеться передавати адресу призначення з кожним пакетом, який ви хочете відправити через сокет, як інакше, як сокет знав, куди відправити ці дані для? Перевага полягає в тому, що ви можете використовувати один сокет для надсилання пакетів на різні адреси сокетів. Після того, як ви налаштували сокет і, можливо, навіть підключили його, вважайте двонаправленим каналом зв'язку. Ви можете використовувати його для передачі даних до будь-якого пункту призначення, а інший пункт призначення може використовувати його для передачі даних вам. Те, що ви пишете в сокет, вирушає, а те, що було отримано, доступне для читання.

      З іншого боку, порти - це те, що є лише у певних протоколів стека протоколів TCP/IP. TCP та UDP пакети мають порти. Порт – це просто число. Комбінація порту джерела та порту призначення визначає канал зв'язку між двома хостами. Наприклад, у вас може бути сервер, який повинен бути і простим сервером HTTP, і простим сервером FTP. Якщо зараз надходить пакет для адреси цього сервера, як він дізнається, чи це пакет для HTTP або FTP-сервера? Що ж, він знатиме, оскільки HTTP-сервер працюватиме на порту 80, а FTP-сервер - на порту 21, тому, якщо пакет надходить з портом призначення 80, він призначений для HTTP-сервера, а не для FTP-сервера . Також пакет має порт джерела, оскільки без такого порту джерела сервер може мати лише одне підключення одного IP-адресу за раз. Порт джерела дозволяє серверу розрізняти ідентичні з'єднання: всі вони мають один і той же порт призначення, наприклад, порт 80, ту саму IP-адресу призначення, завжди ту саму адресу сервера і ту саму IP-адресу джерела, оскільки всі вони виходять від одного і того ж клієнта, але так як вони мають різні вихідні порти, сервер може відрізнити їх один від одного. І коли сервер відправляє назад відповіді, він робить це з портом, з якого надійшов запит, таким чином клієнт також може розрізняти різні відповіді, які він отримує.

      Один порт може мати один або кілька роз'ємів, підключених до іншої зовнішньої IP-адреси, наприклад, кількох розеток.

      TCP 192.168.100.2:9001 155.94.246.179:39255 ESTABLISHED 1312 TCP 192.168.100.2:9001 171.25.193.9:61831 ESTABLISHED 1: 78.62.199.226:37912 ESTABLISHED 1312 TCP 192.168.100.2:9001 188.193.64.150:40900 ESTABLISHED 1312 TCP 192.168.100.2:9001 198.23.194.149:43970 ESTABLISHED 1312 TCP 192.168.100.2:9001 198.49.73.21 3

      Socket – це абстракція, що надається ядром користувачам додаткамдля введення/виведення даних. Тип сокету визначається протоколом його обробки, IPC-зв'язком тощо. Тому, якщо хтось створює сокет TCP, він може робити маніпуляції, такі як читання даних в сокет і запис даних на нього за допомогою простих методів та обробки протоколу нижнього рівня, таких як перетворення TCP та пересилання пакетів на нижчі мережеві протоколи виконується конкретною реалізацією сокету в ядрі. Перевага полягає в тому, що користувачеві не потрібно турбуватися про те, як поводитися з конкретними конкретними протоколами, а також читати і записувати дані в сокет, як звичайний буфер. Те саме вірно у випадку IPC, користувач просто зчитує і записує дані в сокет, а ядро ​​обробляє всі деталі нижнього рівня в залежності від типу соз даного сокету.

    Socket vs Socket частина 2, або скажемо "ні" протоколу TCP - Архів WASM.RU

    У першій частині, присвяченій основам використання сокетів MS Windows в асемблерних програмах, ми говорили про те, що таке сокети, як вони створюються і які параметри при цьому еадаються. Тоді ж побіжно було сказано про протокол UDP, який не орієнтований на з'єднання, який не гарантує доставку пакетів, а також черговість їх надходження до пункту призначення. У навчальному прикладі використовувався тоді наш улюблений протокол TCP. І все було в нас добре, але наприкінці залишилася низка невирішених питань, зокрема, як організувати взаємний обмін між кількома комп'ютерами в мережі, як передати щось відразу багатьом комп'ютерам тощо.

    Взагалі кажучи, прочитання першої частини для розуміння нинішньої зовсім не обов'язково, хоча по ходу справи я постійно на неї посилатися. Такі справи. Ха-ха...

    Отже, ставимо завдання: є локальна мережа, скажімо, з десятка комп'ютерів, потрібно організувати обмін повідомленнями між двома будь-якими з них, і (за бажанням) між одним та всіма іншими.

    Чую, чую хор підказок, що мовляв, використовуй вбудовані можливості Windows, типу:

    net send 192.168.0.4 Женя шле тобі привіт!

    net send Node4 Чекаю на відповідь!

    На це є лише два заперечення. Перше, чи мало, що може наша операційна система чи інші готові програми, адже ми хочемо навчитися писати свої програми, чи не так? А друге, не факт, що повідомлення йде від людини до людини. Загалом оператор може нічого і не знати... А то й не повинен нічого знати...

    Для мене найголовнішим у постановці цього завдання було забезпечити можливість передачі будь-чого всім комп'ютерам мережі відразу. Уявіть, що ми написали програму... Хто сказав - троян? Ні ні та ні! Жодних троянів. Просто маленьку (дуже) бухгалтерську програму, наприклад. Яка таки через деякий час оселилася на багатьох комп'ютерах нашої локальної мережі. І ось приходить призначений час, настав час звести сальдо з бульдо, підвести, так би мовити, підсумок за квартал... Треба зробити все швидко і бажано одночасно. Як це зробити в рамках того матеріалу, що ми вивчили у першій частині, залишалося незрозумілим.

    Відповідь, як завжди, надає WindowsAPI. Шукаємо та знаходимо. Функція sendto() – надсилає дані за вказаною адресою. А в чому тоді її відмінність від вже вивченої в першій частині функції send() ? Виявляється, що sendto() може здійснювати широкомовну передачу за спеціальним IP-адресою. Але, увага, це працює лише для сокетів типу SOCK_DGRAM! А сокети, при відкритті яких як параметр типу сокету використовувалося значення SOCK_DGRAM працюють через протокол UDP, а чи не TCP! Звідси стає ясно значення підзаголовка цієї статті… Звичайно, це лише літературний прийом, жоден протокол не кращий і не гірший за інший, просто вони… різні, от і все. Хоча обидва – це протоколи транспортного рівня, які “забезпечують передачу даних між прикладними процесами”. Обидва звертаються до протоколу мережного рівня, такого, як IP передачі (прийому) даних. Через який далі вони (дані) потрапляють фізичний рівень, тобто. у середу передачі... А що там за середовище, хто його знає. Може це мідний кабель, а може й не середовище зовсім, а четвер, і не мідний кабель, а ефір…

    Схема взаємодії мережевих протоколів.

    UDPU ser D atagram P rotocol

    TCP – T ransmission C ontrol P rotocol

    ICMP – I nternet C ontrol M essage P rotocol (протоколобменакеруючими повідомленнями)

    ARP –A ddress R esolution P rotocol (протокол визначення адрес)

    Загалом якщо малюнок нічим Вам не допоміг, не біда. Важливо зрозуміти, що TCP – протокол транспортного рівня, що забезпечує надійну транспортування даних між прикладними процесами, шляхом установки логічного з'єднання (Виділено мною). А UDP – ні. І ще. Десь там, на прикладному рівні, в одному з порожніх прямокутників і буде наш додаток.

    На цьому закінчимо вступну частину та перейдемо до розгляду того, як цим користуватися із самого початку.

    Для демонстрації всього матеріалу, як завжди, використовується навчальний приклад, який можна завантажити< >. Пропускаємо загальну для всіх Windows додатківчастину та описуємо тільки те, що стосується роботи сокетів. Спочатку потрібно ініціалізувати Windows Sockets DLL за допомогою функції WSAStartup() , яка поверне нуль у разі успішного виконання, або, в іншому випадку, один із кодів помилки. Потім при ініціалізації головного вікна програми відкриваємо сокет для прийому повідомлень:

      invoke socket, AF_INET, \

      SOCK_DGRAM, \; задає тип сокету – протокол UDP!

      0; тип протоколу

      If eax != INVALID_SOCKET ; якщо немає помилки

      mov hSocket, eax; запам'ятати дескриптор

    Після цього, як завжди, треба вказати Windows надсилати повідомлення заданому вікну від відкритого нами сокету:

      invoke WSAAsyncSelect, hSocket, hWnd, WM_SOCKET, FD_READ

    де hSocket- дескриптор сокету
    hWnd- дескриптор вікна, процедурі якого надсилатимуться повідомлення
    WM_SOCKET- Повідомлення, нами ж визначене в секції.
    FD_READ- маска, що задає цікаві для нас події, в даному випадкуце готовність даних від сокету для читання.

    Чую, чую здивований хор з відчаєм у голосі: обіцяли прихований додаток, а тут головне вікно і таке інше… Справа в тому, що без нього не обійтися, т.к. операційна система посилає всі повідомлення нашому додатку через процедуру його вікна. Вихід простий. При необхідності зробіть прихованим це найголовніше вікно програми. Як? Наприклад, закоментуйте рядок:

      invoke ShowWindow, hwnd, SW_SHOWNORMAL

    або, що правильніше, використовуйте:

      invoke ShowWindow, hwnd, SW_HIDE

    Після цього наш додаток також запускатиметься, створюватиметься головне вікно, йому від Windows буде надіслано повідомлення WM_CREATE з усіма витікаючими… Тільки його вікна не буде видно ні на робочому столі, ні на панелі завдань. Якщо це те, чого ви хотіли, я радий. У будь-якому випадку, продовжуємо...

    Для цього перетворимо номер порту на мережевий порядокбайт за допомогою спеціальної функції АPI:

      invoke htons, Port

      mov sin.sin_port, ax

      mov sin.sin_family, AF_INET

      mov sin.sin_addr, INADDR_ANY

    Невеликий ліричний відступ, необов'язковий для розуміння змісту цієї статті .

    Номери портів для наших сокетів були обговорені наприкінці першої частини. Важко дати рекомендації щодо того, якими вони мають бути. Єдине, що можна сказати, це те, якими вони не можуть бути. Нерозумно намагатися використовувати номери портів, визначені для найпоширеніших служб, таких як:

    через протокол TCP: 20, 21 - ftp; 23 – telnet; 25 - smtp; 80 - http; 139 - NetBIOS session service;

    через протокол UDP: 53 - DNS; 137, 138 - NetBIOS; 161 - SNMP;

    Звичайно, у складі API є спеціальна функція getservbyport() , яка за заданим номером порту повертає ім'я відповідного йому сервісу. Вірніше, сама функція повертає покажчик на структуру, всередині якої є покажчик на це ім'я.

    Викликати її можна так:

      invoke htons, Port; перетворимо номер порту на мережевий порядок байт

      invoke getservbyport, ax, 0;

    Зверніть увагу на те, що повідомляє Win32 Programmer'sReference з приводу getservbyport:

    “...повертає покажчик на структуру, яка розподілена на Windows Sockets. Програма ніколи не повинна намагатися змінювати цю структуру або будь-який її компонент. Крім того, лише одна копія цієї структури розподілена дляпотоку, так що програма має скопіювати будь-яку інформацію, яка йому потрібна, перед будь-яким іншим викликом функції Windows Sockets”.

    А ось і сама структура:

    1. s_name DWORD?; покажчик на рядок з ім'ям сервісу

      s_aliases DWORD?;

      s_port WORD?; номер порту

      s_proto DWORD?;

    Є в API, так би мовити, і "парна" функція: getservbyname(), яка на ім'я сервісу повертає інформацію про номер порту, що використовується.

    На жаль, практичної користі з цих функцій нам отримати не вдасться. Тож знайте, що вони є і забудьте про них...

      invoke bind, hSocket, addr sin, sizeof sin

      If eax == SOCKET_ERROR; якщо помилка

      invoke MessageBox, NULL, addr …

    На цьому підготовчу роботу зі створення та настроювання приймаючого сокету з використанням датаграм можна вважати закінченою. Немає необхідності встановлювати сокет у стан слухання порту функцією invoke listenЯк ми це робили для сокету типу SOCK_STREAM в першій частині. Тепер у процедурі головного вікна нашої програми ми можемо додати код, який буде виконуватись при надходженні повідомлення WM_SOCKET від сокету:

      ; якщо отримано повідомлення від сокету (hSocket)

      Elseif uMsg == WM_SOCKET

    1. If ax == FD_READ;

    2. If ax == NULL; відсутня помилка

      ; прийняти дані (64 байти) від сокету до буферу BytRecu

      invoke recv, hSocket, addr BytRecu, 64, 0;

    Тепер про те, як відкрити сокет для надсилання повідомлень. Ось усі необхідні дії програми:

      invoke socket, AF_INET, SOCK_DGRAM, 0

        invoke htons, Port

        mov sin_to.sin_port, ax

        mov sin_to.sin_family, AF_INET

        invoke inet_addr, addr AdresIP

        mov sin_to.sin_addr, eax

      Коли справа доходить до передачі даних, достатньо зробити наступне:

        invoke sendto, hSocket1, addr BytSend1, 64, 0, \

        addr sin_to, sizeof sin_to

      Значення параметрів під час виклику цієї функції API:

      hSocket1- дескриптор раніше відкритого сокету
      addrBytSend1- адреса буфера, що містить дані про передачу
      64 - розмір даних у буфері, в байтах
      0 - індикатор ..., у прикладі MSDN це просто 0
      addrsin_to- покажчик на структуру, яка містить адресу призначення
      sizeofsin_to- Розмір цієї структури в байтах.

      Якщо під час виконання функції sendto() не виникло помилок, то вона повертає число переданих байт, інакше на виході маємо eaxзначення SOCKET_ERROR.

      Тепер саме час поговорити про ту саму широкомовну адресу, про яку згадувалося спочатку. У структурі ми попередньо заповнили поле з IP-адресою призначення, вказуючи, куди, власне, надсилати дані. Якщо це адреса 127.0.0.1 – природно, нікуди далі власного комп'ютеранаші дані не підуть. У літературі чітко сказано, що пакет, надісланий у мережу з адресою 127.x.x.x, не передаватиметься ні по якій мережі. Більше того, маршрутизатор або шлюз ніколи не повинен поширювати інформацію про маршрути мережі з номером 127 - ця адреса не є адресою мережі. Щоб відправити "передачу" відразу всім комп'ютерам локальної мережі потрібно використовувати адресу, сформовану з нашого власного IP - адреси, але має одиниці в молодшому октеті, що-небудь типу 192.168.0.255.

      Ось, власне, і все. У момент закриття програми необхідно закрити сокети та звільнити ресурси Sockets DLL, робиться це просто:

        invoke closesocket, hSocket

        invoke closesocket, hSocket1

        invoke WSACleanup

      Для мультипотокових додатків після WSACleanupзавершуються операції із сокетами для всіх потоків.

      Найважчим у цій статті було для мене вирішити, як найкраще проілюструвати використання Windows Sockets API. Один підхід ви, напевно, вже побачили, коли в єдиному додаткуодночасно використовувався і сокет прийому, і сокет передачі повідомлень. Не менш привабливим здається і інший спосіб, коли код для одного та іншого чітко розділений, аж те, що існує в різних додатках. Зрештою, я реалізував ще й цей спосіб, який може виявитися для розуміння початківцями дещо простіше. У другому<архиве

      Без цьогофункція send()видасть SOCKET_ERROR!

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

      Взагалі кажучи, централізована обробка повідомлень головним вікном – це, як здається, більш простий розуміння метод, який, тим щонайменше, практично може додати клопоту. Якщо програма використовує більше одного сокету в той же час, потрібно зберігати список дескрипторів сокетів. При появі повідомлення від сокетів, головна віконна процедура у списку шукає інформацію, пов'язану з цим дескриптором сокету і надсилає повідомлення про зміну стану далі призначену для цього процедуру. Яка вже у той чи інший спосіб реагує, щось там робить... Цей підхід змушує обробку мережевих завдань інтегрувати в ядро ​​програми, що ускладнює створення бібліотек мережевих функцій. Щоразу, коли використовуються ці мережні функції, до головного віконного обробника програми потрібно додавати додатковий код.

      У другому методі обробки повідомлень для їх отримання програма створює приховане вікно. Воно служить відділення головної віконної процедури докладання від обробки мережевих повідомлень. Цей підхід може спростити головну програму та полегшити використання наявного мережного коду в інших програмах. Негативною стороною такого підходу є надмірне використання Windows-user пам'яті, т.к. для кожного створеного вікна резервується великий його обсяг.

      Який спосіб вибрати – вирішуйте самі. І ще одне. На час експериментів, можливо, доведеться вимкнути ваш персональний firewall. Так, наприклад, Outpost Pro 2.1.275 в режимі навчання реагував на спробу передачі в сокет, але коли передача вручну дозволялася, дані все одно не доходили. Ось вам і UDP. Хоча річ може бути і не в цьому. Проблем з моїм ZoneAlarmPro 5.0.590 у такій ситуації не було.

      P.S.Закінчуючи другу частину статті випадково натрапив у мережі на вихідники трояна нашою улюбленою мовою MASM. Все компілюється і запускається, але але, клієнт не хоче коннектитися з сервером, та ще під Windows 2000 sp4 іноді вилітає з помилкою, мовляв, додаток буде закрито і таке інше… Особисто мені в цьому трояні подобається, що програма не просто там веде лог натискань , або "видирає" файл з паролями і відсилає його по електронці, а має широкий набір керованих дистанційно функцій, оригінально реалізованих. Якщо вдасться привести все це господарство до тями, то, можливо, скоро з'явиться і третина, присвячена опису конкретної реалізації… Для тих, хто уважно прочитав обидві статті і розібрався з роботою функцій сокет API, там немає нічого складного. Начебто ... До речі, сам автор пише в readme, що написав його (троян) в освітніх цілях. Ну ну. Цим і скористаємось.

      DirectOr

    Останнє оновлення: 31.10.2015

    Протокол UDP не вимагає встановлення постійного підключення, і, можливо, багатьом здасться легше працювати з UDP, ніж TCP. Більшість принципів під час роботи з UDP самі, як і з TCP.

    Спочатку створюється сокет:

    Socket socket = New Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

    Якщо сокет повинен отримувати повідомлення, то треба прив'язати його до локальної адреси та одного з портів за допомогою методу Bind:

    IPEndPoint localIP = новий IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555); socket.Bind(localIP);

    Після цього можна надсилати та отримувати повідомлення. Для отримання повідомлень використовується метод ReceiveFrom():

    Byte data=new byte; // буфер для одержуваних даних // адресу, з якого прийшли дані EndPoint remoteIp = new IPEndPoint (IPAddress.Any, 0); int bytes = socket.ReceiveFrom(data, ref remoteIp);

    В якості параметра метод передається масив байтів, в який треба вважати дані, і віддалена точка, з якої приходять ці дані. Метод повертає кількість лічених байтів.

    Для надсилання даних використовується метод SendTo():

    String message = Console.ReadLine(); byte data = Encoding.Unicode.GetBytes(message); EndPoint remotePoint = новий IPEndPoint(IPAddress.Parse("127.0.0.1"), remotePort); listeningSocket.SendTo(data, remotePoint);

    У метод передається масив даних, що надсилаються, а також адресу, за якою ці дані треба відправити.

    Створимо програму UDP-клієнта:

    Using System; використовуючи System.Text; використовуючи System.Threading.Tasks; using System.Net; використовуючи System.Net.Sockets; namespace SocketUdpClient ( class Program ( static int localPort; // порт прийому повідомлень static int remotePort; // порт для відправки повідомлень static Socket listeningSocket; static void Main(string args) ( Console.Write("Введіть порт для прийому повідомлень: ") ; localPort = Int32.Parse(Console.ReadLine()); Console.Write("Введіть порт для надсилання повідомлень: "); remotePort = Int32.Parse(Console.ReadLine()); Console.WriteLine("Для відправлення повідомлень введіть повідомлення і натисніть Enter"); Console.WriteLine(); try ( listeningSocket = новий Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); відправка повідомлень на різні порти while (true) (string message = Console.ReadLine(); byte data = Encoding.Unicode.GetBytes(message); listeningSocket.SendTo(data, remotePoint); ) ) catch (Exception ex) ( Console.WriteLine(ex.Message); ) finally ( Close(); ) ) // потік для прийому підключень private static void Listen() ( try ( //Прослуховуємо за адресою IPEndPoint localIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), localPort); listeningSocket .Bind(localIP);while(true) ( ​​// отримуємо повідомлення StringBuilder builder = new StringBuilder(); int bytes = 0; // кількість отриманих байтів byte data = new byte; // буфер для одержуваних даних //адреса, з якого прийшли дані EndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0); do ( bytes = listeningSocket.ReceiveFrom(data, ref remoteIp); (listeningSocket.Available > 0);// отримуємо дані про підключення IPEndPoint remoteFullIp = remoteIp as IPEndPoint; // виводимо повідомлення Console.WriteLine("(0):(1) - (2)", remoteFullIp.Address.ToString() , remoteFullIp.Port, builder.ToString()); ) ) catch (Exception ex) ( Console.WriteLine(ex.Message); ) finally ( Close(); ) ) // закриття сокету (listeningSocket! = null) (listeningSocket.Shutdown(SocketShutdown.Both); listeningSocket.Close(); listeningSocket = null; ) ) ) )

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

    Після введення портів запускається завдання прослуховування вхідних повідомлень. На відміну від tcp-сервера, тут не треба викликати методи Listen і Accept. У нескінченному циклі ми можемо безпосередньо отримати отримання дані за допомогою методу ReceiveFrom() , який блокує викликаючий потік, поки не прийде чергова порція даних.

    Цей метод повертає через ref-параметр віддалену точку, з якої отримані дані:

    IPEndPoint remoteFullIp = remoteIp as IPEndPoint;

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

    У головному потоці відбувається надсилання повідомлень за допомогою методу SendTo()

    Таким чином, програма відразу здійснює функції і сервера, і клієнта.

    Тепер запустимо дві копії програми та введемо різні дані для портів. Перший клієнт:

    Введіть порт для отримання повідомлень: 4004 Введіть порт для відправки повідомлень: 4005 Для відправки повідомлень введіть повідомлення і натисніть Enter 127.0.0.1:4005 - привет порт 4004 добрий день, порт 4005

    Другий клієнт:

    Введіть порт для отримання повідомлень: 4005 Введіть порт для відправки повідомлень: 4004 Для відправки повідомлень введіть повідомлення і натисніть Enter привіт порт 4004 127.0.0.1:4004 - добрий день, порт 4005 127.0.0.1:4004

    Програми, що використовують TCP і UDP, фундаментально відрізняються один від одного, тому що UDP є ненадійним протоколом дейтаграм, не орієнтованим на встановлення з'єднання, і цим не схожий на орієнтований на встановлення з'єднання і надійну передачу потоку байтів TCP. Проте є випадки, коли є сенс використовувати UDP замість TCP. Подібні випадки ми розглядаємо у розділі 22.4. Деякі популярні програмипобудовані з використанням UDP, наприклад DNS ( Domain Name System - система доменних імен), NFS (мережева файлова система - Network File System) та SNMP (Simple Network Management Protocol - простий протокол управління мережею).

    На рис. 8.1 показано виклики функцій для типової схеми клієнт-сервер UDP. Клієнт не встановлює з'єднання із сервером. Натомість клієнт лише відправляє серверу дейтаграму, використовуючи функцію sendto (вона описується в наступному розділі), якою потрібно задати адресу одержувача (сервера) як аргумент. Аналогічно сервер не встановлює з'єднання з клієнтом. Натомість сервер лише викликає функцію recvfrom , яка чекає, коли надійдуть дані від будь-якого клієнта. Функція recvfrom повертає адресу клієнта (для даного протоколу) разом із дейтаграмою, і таким чином сервер може надіслати відповідь саме тому клієнту, який надіслав дейтаграму.

    Мал. 8.1. Опції сокету для моделі клієнт-сервер UDP

    Малюнок 8.1 ілюструє тимчасову діаграму типового сценарію обміну UDP-дейтаграм між клієнтом і сервером. Ми можемо порівняти цей приклад із типовим обміном по протоколу TCP, зображеним на рис. 4.1.

    У цьому розділі ми опишемо нові функції, які застосовуються з сокетами UDP, - recvfrom і sendto, і переробимо нашу модель клієнт-сервер для застосування UDP. Крім того, ми розглянемо використання функції connect з UDP-сокетом і концепцію асинхронних помилок.

    8.2. Функції recvfrom та sendto

    Ці дві функції аналогічні стандартним функціям read і write, але потребують трьох додаткових аргументів.

    ssize_t recvfrom(int sockfd, void * buff, size_t nbytes, int flags,

    struct sockaddr * from , socklen_t * addrlen);

    ssize_t sendto(int sockfd, const void * buff, size_t nbytes, int flags,

    const struct sockaddr * to, socklen_t addrlen);

    Обидві функції повертають кількість записаних або прочитаних байтів у разі успішного виконання -1 у разі помилки

    Перші три аргументи, sockfd , buff і nbytes , ідентичні першим трьом аргументам функцій read і write: дескриптор, покажчик на буфер, з якого виконується читання чи який відбувається запис, і число байтів для читання чи записи.

    Ми розповімо про аргумент flags у розділі 14, де розглядаємо функції recv , send , recvmsg і sendmsg , оскільки у нашому простому прикладівони непотрібні. Поки ми завжди будемо встановлювати аргумент flags на нуль.

    Аргумент to для функції sendto - це структура адреси сокета, що містить адресу протоколу (наприклад, IP-адресу та номер порту) адресата. Розмір цієї структури адреси сокета задається аргументом addrlen. Функція recvform заповнює структуру адреси сокета, яку вказує аргумент from, записуючи у ній протокольний адресу відправника дейтаграммы. Число байтів, що зберігаються в структурі адреси сокету, також повертається процесу, що викликає, в цілому числі, на яке вказує аргумент addrlen . Зверніть увагу, що останній аргумент функції sendto є цілим числом, тоді як останній аргумент функції recvfrom - це покажчик на ціле значення (аргумент типу «значення-результат»).

    Останні два аргументи функції recvfrom аналогічні двом останнім аргументам функції accept: вміст структури адреси сокета після завершення повідомляє, хто відправив дейтаграму (у разі UDP) або хто ініціював з'єднання (у випадку TCP). Останні два аргументи функції sendto аналогічні двом останнім аргументам функції connect: ми заповнюємо структуру адреси сокета протокольною адресою одержувача дейтаграми (у разі UDP) або адресою вузла, з яким буде встановлюватися з'єднання (у разі TCP).

    Обидві функції повертають як функцію довжину даних, які були прочитані або записані. При типовому використанні функції recvfrom з протоколом дейтаграм повертається - це обсяг даних користувача в отриманій дейтаграмі.

    Дейтаграма може мати нульову довжину. У випадку UDP повертається дейтаграма IP, що містить заголовок IP (зазвичай 20 байт для IPv4 або 40 байт для IPv6), 8-байтовий заголовок UDP і ніяких даних. Це також означає, що нульове значення, що повертається з функції recvfrom, цілком прийнятне для протоколу дейтаграм: воно не є ознакою того, що співрозмовник закрив з'єднання, як це відбувається при поверненні нульового значенняіз функції read на сокеті TCP. Оскільки протокол UDP не орієнтований на встановлення з'єднання, то в ньому не існує такої події, як закриття з'єднання.

    Якщо аргумент від функції recvfrom є порожнім покажчиком, то відповідний аргумент довжини (addrlen) також має бути порожнім покажчиком, і це означає, що нас не цікавить адреса відправника даних.

    І функція recvfrom , і функція sendto можуть використовуватися з TCP, хоча зазвичай у цьому немає необхідності.

    8.3. Відлуння UDP: функція main

    Тепер ми переробимо нашу просту модель клієнт-сервер із розділу 5, використовуючи UDP. Діаграма викликів функцій у програмах наших клієнта та сервера UDP показана на рис. 8.1. На рис. 8.2 представлені використовувані функції. У лістингу 8.1 показано функцію сервера main.

    Мал. 8.2. Проста модель клієнт-сервер, яка використовує UDP

    Лістинг 8.1. Відлуння UDP

    //udpcliserv/udpserv01.с

    1 #include "unp.h"

    3 intmain(int argc, char **argv)

    6 struct sockaddr_in servaddr, cliaddr;

    7 sockfd = Socket (AF_INET, SOCK_DGRAM, 0);

    8 bzero(&servaddr, sizeof(servaddr));

    9 servaddr.sin_family = AF_INET;

    10 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    12 Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));

    13 dg_echo(sodkfd, (SA*)&cliaddr, sizeof(cliaddr));

    Створення сокету UDP, зв'язування із заздалегідь відомим портом за допомогою функції bind

    7-12 Ми створюємо сокет UDP, задаючи як другий аргумент функції socket значення SOCK_DGRAM (сокет дейтаграм у протоколі IPv4). Як і в прикладі сервера TCP, IPv4 адреса для функції bind задається як INADDR_ANY , а заздалегідь відомий номер порту сервера - це константа SERV_PORT з заголовка unp.h .

    13 Потім викликається функція dg_echo для обробки запиту клієнта сервером.

    8.4. Відлуння UDP: функція dg_echo

    У лістингу 8.2 показано функцію dg_echo.

    Лістинг 8.2. Функція dg_echo: відображення рядків на сокеті дейтаграм

    1 #include "unp.h"

    3 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    6 socklen_t len;

    7 char mesg;

    10 n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    11 Sendto(sockfd, mesg, n, 0, pcliaddr, len);

    Читання дейтаграми, відображення відправнику

    8-12 Ця функція є простим циклом, в якому чергова дейтаграма, що приходить на порт сервера, читається функцією recvfrom і за допомогою функції sendto відправляється назад.

    Незважаючи на простоту цієї функції, потрібно врахувати низку важливих деталей. По перше, Ця функція ніколи не завершується. Оскільки UDP - це протокол, не орієнтований встановлення з'єднання, у ньому немає ніяких аналогів ознаки кінця файла, що у TCP.

    По-друге, ця функція дозволяє створити послідовний сервер, а не паралельний, який ми отримували у разі TCP. Оскільки немає виклику функції fork, один процес сервера виконує обробку всіх клієнтів. Загалом більшість серверів TCP є паралельними, більшість серверів UDP - послідовними.

    Для сокету лише на рівні UDP відбувається неявна буферизація дейтаграм як черги. Дійсно, кожен сокет UDP має буфер прийому, і кожна дейтаграма, яка приходить на цей сокет, міститься в його буфер прийому. Коли процес викликає функцію recvfrom, чергова дейтаграма з буфера повертається процесу в порядку FIFO (First In, First Out - першим прийшов, першим обслужений). Таким чином, якщо безліч дейтаграм приходить на сокет до того, як процес може прочитати дані, вже встановлені в чергу для сокету, то дейтаграми, що приходять, просто додаються в буфер прийому сокету. Але цей буфер має обмежений розмір. Ми обговорювали цей розмір та способи його збільшення за допомогою параметра сокета SO_RCVBUF у розділі 7.5.

    На рис. 8.3 показано узагальнення нашої моделі TCP клієнт-сервер з розділу 5, коли два клієнти встановлюють з'єднання із сервером.

    Мал. 8.3. Узагальнення моделі TCP клієнт-сервер із двома клієнтами

    Тут є два приєднані сокети, і кожен із приєднаних сокетів на вузлі сервера має свій власний буфер прийому. На рис. 8.4 показаний випадок, коли два клієнти надсилають дейтаграми серверу UDP.

    Мал. 8.4. Узагальнення моделі UDP клієнт-сервер із двома клієнтами

    Існує тільки один процес сервера, і у нього є один сокет, на який сервер отримує всі дейтаграми, що приходять і з якого відправляє всі відповіді. У цього сокету є буфер прийому, в який поміщаються всі дейтаграми, що приходять.

    Функція main у лістингу 8.1 залежить від протоколу (вона створює сокет сімейства AF_INET , та був виділяє і ініціалізує структуру адреси сокету IPv4), але функція dg_echo від протоколу залежить. Причина, через яку функція dg_echo не залежить від протоколу, полягає в тому, що викликальний процес(У нашому випадку функція main) повинен розмістити в пам'яті структуру адреси сокета коректного розміру, і покажчик на цю структуру разом з її розміром передаються аргументами функції dg_echo . Функція dg_echo ніколи не заглиблюється в цю структуру: вона просто передає покажчик на неї функціям recvfrom і sendto. Функція recvfrom заповнює цю структуру, вписуючи в неї IP-адресу і номер порту клієнта, і оскільки той самий покажчик (pcliaddr) потім передається функції sendto як адреса одержувача, таким чином дейтаграма відображається назад клієнту, який відправив дейтаграму.

    8.5. Ехо-клієнт UDP: функція main

    Функція main клієнта UDP показана у лістингу 8.3.

    Лістинг 8.3. Відлуння-клієнт UDP

    //udpcliserv/udpcli01.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    6 struct sockaddr_in servaddr;

    7 if (argc! = 2)

    8 err_quit("usage: udpcli ");

    9 bzero(&servaddr, sizeof(servaddr));

    10 servaddr.sin_family = AF_INET;

    11 servaddr.sin_port = htons(SERV_PORT);

    12 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

    13 sockfd = Socket (AF_INET, SOCK_DGRAM, 0);

    14 dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));

    Заповнення структури адреси сокета адресою сервера

    9-12 Структура адреси сокету IPv4 заповнюється IP-адресою та номером порту сервера. Ця структура буде передана функції dg_cli. Вона визначає, куди відправляти дейтаграми.

    13-14 Створюється сокет UDP і викликається функція dg_cli.

    8.6. Відлуння-клієнт UDP: функція dg_cli

    У лістингу 8.4 показано функцію dg_cli , яка виконує більшу частину роботи за клієнта.

    Лістинг 8.4. Функція dg_cli: цикл обробки клієнта

    1 #include "unp.h"

    7 while (Fgets(sendline, MAXLINE, fp)! = NULL) (

    8 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

    9 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

    10 recvline[n] = 0; /* Завершальний нуль */

    11 Fputs(recvline, stdout);

    7-12 У циклі обробки на стороні клієнта є чотири кроки: читання рядка з стандартного потокувведення за допомогою функції fgets, відправлення рядка серверу за допомогою функції sendto, читання відображеної відповіді сервера за допомогою функції recvfrom і поміщення відображеного рядка до стандартного потоку виведення за допомогою функції fputs.

    Наш клієнт не запитував у ядра присвоювання порту, що динамічно призначається, своєму сокету (тоді як для клієнта TCP це мало місце при виклику функції connect). У разі сокету UDP при першому виклику функції sendto ядро ​​вибирає порт, що динамічно призначається, якщо з цим сокетом ще не був пов'язаний ніякий локальний порт. Як і у випадку TCP, клієнт може викликати функцію bind очевидно, але це робиться рідко.

    Зверніть увагу, що при виклику функції recvfrom як п'ятий і шостий аргументи задаються порожні покажчики. Таким чином, ми повідомляємо ядру, що ми не зацікавлені в тому, щоб знати, хто надіслав відповідь. Існує ризик, що будь-який процес, що знаходиться як на тому ж вузлі, так і на будь-якому іншому, може відправити на IP-адресу та порт клієнта дейтаграму, яка буде прочитана клієнтом, який передбачає, що це відповідь сервера. Цю ситуацію ми розглянемо у розділі 8.8.

    Як і у разі функції сервера dg_echo , функція клієнта dg_cli не залежить від протоколу, але функція main клієнта залежить від протоколу. Функція main розміщує у пам'яті та ініціалізує структуру адреси сокету, що відноситься до певного типупротоколу, а потім передає функції dg_cli покажчик на структуру разом із її розміром.

    8.7. Втрачені дейтаграми

    Клієнт та сервер UDP у нашому прикладі є ненадійними. Якщо дейтаграма клієнта втрачена (припустимо, вона проігнорована деяким маршрутизатором між клієнтом і сервером), клієнт назавжди заблокується у своєму виклику функції recvfrom всередині функції dg_cli , очікуючи від відповіді, який ніколи не прийде. Аналогічно, якщо дейтаграма клієнта приходить до сервера, але відповідь сервера втрачена, клієнт назавжди заблокується у своєму виклику функції recvfrom . Єдиний спосібзапобігти цій ситуації - помістити тайм-аут в клієнтський виклик функції recvfrom. Ми розглянемо це у розділі 14.2.

    Просте приміщення тайм-ауту у виклик функції recvfrom - ще не повне рішення. Наприклад, якщо заданий часочікування закінчилося, а відповідь не отримана, ми не можемо сказати точно, в чому справа - або наша дейтаграма не дійшла до сервера, або відповідь сервера не прийшла назад. Якби запит клієнта містив вимогу типу «перевести певну кількість грошей з рахунку А на рахунок Б» (на відміну від нашого простого луна-сервера), то тоді між втратою запиту і втратою відповіді існувала б велика різниця. Докладніше про додавання надійності до моделі клієнт-сервер UDP ми розповімо в розділі 22.5.

    8.8. Перевірка отриманої відповіді

    Наприкінці розділу 8.6 ми згадали, що будь-який процес, який знає номер порту клієнта, що динамічно призначається, може відправляти дейтаграми нашому клієнту, і вони будуть перемішані з нормальними відповідями сервера. Все, що ми можемо зробити, - це змінити виклик функції recvfrom , представлений у лістингу 8.4, так, щоб вона повертала IP-адресу і порт відправника відповіді, і ігнорувати будь-які дейтаграми, що не надходять від того сервера, якому ми відправляємо дейтаграму. Однак тут є кілька пасток, як ми побачимо далі.

    Спочатку ми змінюємо функцію клієнта main (див. листинг 8.3) до роботи зі стандартним ехо-сервером (див. табл. 2.1). Ми просто замінюємо привласнення

    servaddr.sin_port = htons(SERV_PORT);

    привласненням

    servaddr.sin_port = htons(7);

    Тепер ми можемо використовувати з нашим клієнтом будь-який вузол, на якому працює стандартний луна-сервер.

    Потім ми переписуємо функцію dg_cli , щоб вона розміщувала в пам'яті іншу структуру адреси сокета для зберігання структури, що повертається функцією recvfrom . Ми показуємо її у лістингу 8.5.

    Лістинг 8.5. Версія функції dg_cli, що перевіряє адресу сокету, що повертається

    //udpcliserv/dgcliaddr.c

    1 #include "unp.h"

    3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    6 char sendline, recvline;

    7 socklen_t len;

    8 struct sockaddr *preply_addr;

    9 preply_addr = Malloc (servlen);

    10 while (Fgets (sendline, MAXLINE, fp)! = NULL) (

    11 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

    12 len = servlen;

    13 n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);

    14 if (len! = servlen | | memcmp (pservaddr, preply_addr, len)! = 0) (

    15 printf("reply from %s (ignored)\n",

    18 recvline[n] = 0; /* Завершальний нуль */

    19 Fputs(recvline, stdout);

    Розміщення іншої структури адреси сокета у пам'яті

    9 Ми розміщуємо в пам'яті іншу структуру адреси сокету за допомогою функції malloc. Зверніть увагу, що функція dg_cli все ще не залежить від протоколу. Оскільки нам не важливо, з яким типом структури адреси сокету ми маємо справу, ми використовуємо у виклику функції malloc лише її розмір.

    Порівняння адрес, що повертаються

    12-13 У виклику функції recvfrom ми повідомляємо ядру, що потрібно повернути адресу відправника дейтаграми. Спочатку ми порівнюємо довжину, що повертається функцією recvfrom в аргументі типу значення-результат, а потім порівнюємо самі структури адреси сокету за допомогою функції memcmp .

    Нова версія нашого клієнта працює чудово, якщо сервер знаходиться на вузлі з однією єдиною IP-адресою. Але ця програма може не спрацювати, якщо сервер має кілька мережних інтерфейсів (multihomed server). Запускаємо цю програму, звертаючись до вузла freebsd4, у якого є два інтерфейси і дві IP-адреси:

    macosx % host freebsd4

    freebsd4.unpbook.com has address 172.24.37.94

    freebsd4.unpbook.com has address 135.197.17.100

    macosx % udpcli02 135.197.17.100

    reply from 172.24.37.94:7 (ignored)

    За рис. 1.7 видно, що ми задали IP-адресу з іншої підмережі. Зазвичай це припустимо. Більшість реалізацій IP приймають IP-дейтаграму, що приходить, призначену для будь-якого з IP-адрес вузла, незалежно від інтерфейсу, на який вона приходить . Документ RFC 1122 називає це моделлю системи з гнучкою прив'язкою (weak end system model). Якщо система повинна реалізувати те, що в цьому документі називається моделлю системи з жорсткою прив'язкою (strong end system model), вона приймає дейтаграму, що приходить, тільки якщо дейтаграма приходить на той інтерфейс, якому вона адресована.

    IP-адреса, що повертається функцією recvfrom (IP-адреса відправника дейтаграми UDP), не є IP-адресою, на яку ми посилали дейтаграму. Коли сервер надсилає свою відповідь, IP-адреса одержувача – це адреса 172.24.37.94. Функція маршрутизації всередині ядра на вузлі freebsd4 вибирає адресу 172.24.37.94 як вихідний інтерфейс. Оскільки сервер не зв'язав IP-адресу зі своїм сокетом (сервер зв'язав зі своїм сокетом універсальну адресу, яку ми можемо перевірити, запустивши програму netstat на вузлі freebsd4), ядро ​​вибирає адресу відправника дейтаграми IP. Цією адресою стає первинна IP-адреса вихідного інтерфейсу. Якщо ми відправляємо дейтаграму не на первинну IP-адресу інтерфейсу (тобто альтернативне ім'я, псевдонім), то наша перевірка, показана в лістингу 8.5, також виявиться невдалою.

    Одним із рішень буде перевірка клієнтом доменного імені відповідного вузла замість його IP-адреси. Для цього ім'я сервера шукається в DNS (див. розділ 11) на основі IP-адреси, що повертається функцією recvfrom . Інше рішення - зробити так, щоб сервер UDP створив по одному сокету для кожної IP-адреси, налаштованої на вузлі, зв'язав за допомогою функції bind цю IP-адресу з сокетом, викликав функцію select для кожного з усіх цих сокетів (очікуючи, коли який- або з них стане готовим для читання), а потім відповів із сокету, готового для читання. Оскільки сокет, що використовується для відповіді, пов'язаний з IP-адресою, яка була адресою одержувача запиту клієнта (інакше дейтаграма не була б доставлена ​​на сокет), ми можемо бути впевнені, що адреси відправника відповіді та одержувача запиту збігаються. Ми показуємо ці приклади у розділі 22.6.

    ПРИМІТКА

    У системі Solaris з кількома мережевими інтерфейсами IP-адреса відправника відповіді сервера - це IP-адреса одержувача клієнтського запиту. Сценарій, описаний у цьому розділі, відноситься до реалізації, що походять від Берклі, які вибирають IP-адресу відправника, ґрунтуючись на вихідному інтерфейсі.

    8.9. Запуск клієнта без запуску сервера

    Наступний сценарій, який ми розглянемо - це запуск клієнта без запуску сервера. Якщо ми зробимо так і введемо один рядок на стороні клієнта, нічого не відбуватиметься. Клієнт назавжди блокується у своєму виклику функції recvfrom , чекаючи на відповідь сервера, який ніколи не прийде. Але в даному прикладі це не має значення, оскільки зараз ми прагнемо глибше зрозуміти протоколи та з'ясувати, що відбувається з нашим мережевим додатком.

    Спочатку ми запускаємо програму tcpdump на вузлі macosx, а потім - клієнт на тому ж вузлі, задавши як вузл сервера freebsd4. Потім ми вводимо один рядок, але цей рядок не відображається сервером.

    macosx % udpcli01 172.24.37.94

    hello, worldми вводимо цей рядок,

    але нічого не отримуємо у відповідь

    У лістингу 8.6 показаний висновок програми tcpdump.

    Лістинг 8.6. Виведення програми tcpdump, коли процес сервера не запускається на вузлі сервера

    01 0.0 arp who-has freebsd4 tell macosx

    02 0.003576 (0.0036) arp reply freebsd4 is-at 0:40:5:42:d6:de

    03 0.003601 (0.0000) macosx.51139 > freebsd4.9877: udp 13

    04 0.009781 (0.0062) freebsd4 >

    В першу чергу ми помічаємо, що запит та відповідь ARP отримані до того, як вузол клієнта зміг надіслати дейтаграму UDP вузлу сервера. (Ми залишили цей обмін у виведенні програми, щоб ще раз наголосити, що до відправки IP-дейтаграми завжди слід надсилання запиту та отримання відповіді за протоколом ARP.)

    У рядку 3 ми бачимо, що дейтаграма клієнта відправлена, але вузол сервера відповідає у рядку 4 повідомленням ICMP про недоступність порту. (Довжина 13 включає 12 символів плюс символ нового рядка.) Однак ця помилка ICMP не повертається клієнтському процесу з причин, які ми коротко перерахуємо трохи нижче. Натомість клієнт назавжди блокується у виклику функції recvfrom у лістингу 8.4. Ми також відзначаємо, що в ICMPv6 є помилка «Порт недоступний», аналогічна помилці ICMPv4 (див. табл. А.5 та А.6), тому результати, представлені тут, аналогічні результатам IPv6.

    Ця помилка ICMP є асинхронною помилкою. Помилка була викликана функцією sendto, але функція sendto завершилася нормально. Згадайте з розділу 2.9, що нормальне повернення з операції виводу UDP означає лише те, що дейтаграма була додана до черги виводу канального рівня. Помилка ICMP не повертається, поки не пройде певна кількість часу (4 мс для лістингу 8.6), тому вона називається асинхронною.

    Основне правило полягає в тому, що асинхронні помилки не повертаються для UDP-сокету, якщо сокет не був приєднаний. Ми показуємо, як викликати функцію connect для UDP, в розділі 8.11. Не всі розуміють, чому було ухвалено це рішення, коли сокети були вперше реалізовані. (Мірки про реалізації обговорюються на с. 748-749.) Розглянемо клієнт UDP, який послідовно відправляє три дейтаграми трьом різним серверам (тобто на три різні IP-адреси) через один сокет UDP. Клієнт входить у цикл, що викликає функцію recvfrom для читання відповідей. Дві дейтаграми доставляються коректно (тобто сервер було запущено двох із трьох вузлів), але третьому вузлі був запущений сервер, і третій вузол відповідає повідомленням ICMP про недоступності порту. Це повідомлення про помилку ICMP містить IP-заголовок та UDP-заголовок дейтаграми, що спричинила помилку. (Повідомлення про помилки ICMPv4 і ICMPv6 завжди містять заголовок IP і весь заголовок UDP або частину заголовка TCP, щоб дати можливість одержувачу повідомлення визначити, який сокет викликав помилку. Це показано на мал. отримувача дейтаграми, що викликала помилку, щоб точно визначити, яка з трьох дейтаграм викликала помилку. Але як ядро ​​може повідомити цю інформацію процесу? Єдине, що може повернути функцію recvfrom, - це значення змінної errno. Але функція recvfrom не може повернути в помилку IP-адресу та номер порту одержувача UDP-дейтаграми. Отже, було ухвалено рішення, що ці асинхронні помилки повертаються процесу, тільки якщо процес приєднав сокет UDP лише до одного певного співрозмовника.

    ПРИМІТКА

    Linux повертає більшість помилок ICMP про відсутність порту навіть для неприєднаного сокету, якщо не включено параметр сокета SO_DSBCOMPAT. Повертаються всі помилки про відсутність одержувача, показані в табл. А.5, крім помилок з кодами 0, 1, 4, 5, 11 і 12.

    Ми повернемося до проблеми асинхронних помилок із сокетами UDP у розділі 28.7 та покажемо простий спосіб отримання цих помилок на неприєднаному сокеті за допомогою нашого власного демона.

    8.10. Підсумковий приклад клієнт-сервера UDP

    На рис. 8.5 великими чорними точками показано чотири значення, які мають бути задані або вибрані, коли клієнт надсилає дейтаграму UDP.

    Мал. 8.5. Узагальнення моделі клієнт-сервер UDP з погляду клієнта

    Клієнт повинен встановити IP-адресу сервера та номер порту для виклику функції sendto . Зазвичай клієнтська IP-адреса та номер порту автоматично вибираються ядром, хоча ми зазначали, що клієнт може викликати функцію bind . Ми також зазначали, що якщо ці два значення вибираються для клієнта ядром, то порт, що динамічно призначається, клієнта вибирається один раз - при першому виклику функції sendto , і більше ніколи не змінюється. Однак IP-адреса клієнта може змінюватися для кожної дейтаграми UDP, яку відправляє клієнт, якщо припустити, що клієнт не пов'язує із сокетом певну IP-адресу за допомогою функції bind . Причину пояснює рис. 8.5: якщо вузол клієнта має кілька мережевих інтерфейсів, клієнт може перемикатися між ними (на рис. 8.5 одна адреса відноситься до канального рівня, зображеного зліва, інша - до зображеного праворуч). У найгіршому варіанті цього сценарію IP-адреса клієнта, вибраний ядром на основі вихідного канального рівня, змінюватиметься для кожної дейтаграми.

    Що станеться, якщо клієнт за допомогою функції bind зв'яже IP-адресу зі своїм сокетом, але ядро ​​вирішить, що вихідна дейтаграма має бути відправлена ​​з якогось іншого канального рівня? У цьому випадку дейтаграма IP міститиме IP-адресу відправника, відмінну від IP-адреси вихідного канального рівня (див. вправу 8.6).

    На рис. 8.6 представлені самі чотири значення, але з погляду сервера.

    Мал. 8.6. Узагальнення моделі клієнт-сервер UDP з погляду сервера

    Сервер може дізнатися принаймні чотири параметри для кожної отриманої дейтаграми: IP-адреса відправника, IP-адреса одержувача, номер порту відправника та номер порту одержувача. Виклики, що повертають ці відомості серверам TCP та UDP, наведено у табл. 8.1.

    Таблиця 8.1. Інформація, доступна серверу з дейтаграми IP, що приходить

    У сервері TCP завжди є простий доступ до всіх чотирьох фрагментів інформації для приєднаного сокету, і ці чотири значення залишаються постійними протягом всього часу з'єднання. Однак у разі з'єднання UDP IP-адресу одержувача можна отримати лише за допомогою установки параметра сокету IP_RECVDSTADDR для IPv4 або IPV6_PKTINFO для IPv6 та наступного виклику функції recvmsg замість функції recvfrom . Оскільки протокол UDP не орієнтований на встановлення з'єднання, IP-адреса одержувача може змінюватися для кожної дейтаграми, що надсилається серверу. Сервер UDP може також отримувати дейтаграми, призначені для однієї з широкомовних адрес вузла або для багатоадресної адреси передачі, що ми обговорюємо в розділах 20 і 21. Ми покажемо, як визначити адресу одержувача дейтаграми UDP, в розділі 20.2, після того як опишемо функцію recvmsg .

    8.11. Функція connect для UDP
    ПРИМІТКА
    ПРИМІТКА
    ПРИМІТКА

    Таблиця 8.2

    ПРИМІТКА

    Мал. 8.7. Приєднаний сокет UDP

    Мал. 8.8

    Багаторазовий виклик функції connect для сокету UDP

    Процес із приєднаним сокетом UDP може знову викликати функцію connect Для цього сокету, щоб:

    в– задати нову IP-адресу та порт;

    в-від'єднати сокет.

    Перший випадок, завдання нового співрозмовника для приєднаного сокету UDP, відрізняється від використання функції connect з TCP: для сокету TCP функція connect може бути викликана тільки один раз.

    Щоб від'єднати сокет UDP, ми викликаємо функцію connect , але присвоюємо елементу сімейства структури адреси сокету (sin_family для IPv4 або sin6_family для IPv6) значення AF_UNSPEC . Це може призвести до помилки EAFNOSUPPORT, але це нормально. Саме процес виклику функції connect на вже приєднаному сокеті UDP дозволяє від'єднати сокет.

    ПРИМІТКА

    У посібнику BSD щодо функції connect традиційно говорилося: «Сокети дейтаграмм можуть розривати зв'язок, з'єднуючись з недійсними адресами, як-от порожні адреси». На жаль, в жодному посібнику не сказано, що є «порожньою адресою», і не згадується, що в результаті повертається помилка (що нормально). Стандарт POSIX явно вказує, що сімейство адрес має бути встановлене в AF_UNSPEC, але потім повідомляє, що цей виклик функції connect може повернути, а може не повернути помилку EAFNOSUPPORT.

    Продуктивність

    Коли програма викликає функцію sendto на неприєднаному сокеті UDP, ядра реалізацій, що походять від Берклі, тимчасово з'єднуються з сокетом, відправляють дейтаграму і потім від'єднуються від сокету. Таким чином, виклик функції sendto для послідовного відправлення двох дейтаграм на неприєднаний сокет включає наступні шість кроків, що виконуються ядром:

    в-приєднання сокету;

    в-виведення першої дейтаграми;

    в-від'єднання сокету;

    в-приєднання сокету;

    в-виведення другої дейтаграми;

    в-від'єднання сокету.

    ПРИМІТКА

    Інший момент, який потрібно враховувати, – кількість пошуків у таблиці маршрутизації. Перше тимчасове з'єднання здійснює пошук у таблиці маршрутизації IP-адреси одержувача та зберігає (кешує) цю інформацію. Друге тимчасове з'єднання зазначає, що адреса одержувача збігається з кешованою адресою з таблиці маршрутизації (ми вважаємо, що обом функціям sendto заданий той самий одержувач), і йому не потрібно знову проводити пошук у таблиці маршрутизації .

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

    в-приєднання сокету;

    в-виведення першої дейтаграми;

    в-виведення другої дейтаграми.

    У цьому випадку ядро ​​копіює структуру адреси сокета, що містить IP-адресу одержувача та порт, лише один раз, а при подвійному виклику функції sendto копіювання виконується двічі. Наголошується, що на тимчасове приєднання від'єднаного сокету UDP припадає приблизно третина вартості кожної передачі UDP.

    8.12. Функція dg_cli (продовження)

    Повернемося до функції dg_cli, показаної в лістингу 8.4, і перепишемо її, щоб вона викликала функцію connect. У лістингу 8.7 показано нова функція.

    Лістинг 8.7. Функція dg_cli, що викликає функцію connect

    //udpcliserv/dgcliconnect.c

    1 #include "unp.h"

    3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    6 char sendline, recvline;

    7 Connect(sockfd, (SA*)pservaddr, servlen);

    8 while (Fgets(sendline, MAXLINE, fp)! = NULL) (

    9 Write(sockfd, sendline, strlen(sendline));

    10 n = Read(sockfd, recvline, MAXLINE);

    11 recvline[n] = 0; /* Завершальний нуль */

    12 Fputs(recvline, stdout);

    Зміни в порівнянні з попередньою версією - це додавання виклику функції connect і заміна викликів функцій sendto і recvfrom викликами функцій write і read. Функція dg_cli залишається залежною від протоколу, оскільки вона вникає у структуру адреси сокета, передану функції connect . Наша функція main клієнта, показана у лістингу 8.3, залишається такою самою.

    Якщо ми запустимо програму на вузлі macosx , задавши IP-адресу вузла freebsd4 (яка не запускає наш сервер на порті 9877), ми отримаємо наступний висновок:

    macosx % udpcli04 172.24.37.94

    hello, world

    read error: Connection refused

    Перше, що ми помічаємо - ми не отримуємо помилку, коли запускаємо процес клієнта. Помилка відбувається лише після того, як ми надсилаємо серверу першу дейтаграму. Саме надсилання цієї дейтаграми викликає помилку ICMP від ​​вузла сервера. Але коли клієнт TCP викликає функцію connect , задаючи вузол сервера, на якому не запущено процес сервера, функція connect повертає помилку, оскільки виклик функції connect викликає відправлення першого пакета триетапного рукостискання TCP, і цей пакет викликає отримання сегмента RST від співрозмовника (див. розділ 4.3).

    У лістингу 8.8 показаний висновок програми tcpdump.

    Лістинг 8.8. Виведення програми tcpdump під час запуску функції dg_cli

    macosx % tcpdump

    01 0.0 macosx.51139 > freebsd4 9877:udp 13

    02 0.006180 (0.0062) freebsd4 > macosx: icmp: freebsd4 udp port 9877 unreachable

    У табл. A.5 ми також бачимо, що ICMP ядро, що виникла, зіставляє помилку ECONNREFUSED , яка відповідає виведенню рядка повідомлення Connection refused (У з'єднанні відмовлено) функцією err_sys .

    ПРИМІТКА

    На жаль, не всі ядра повертають повідомлення ICMPприєднаного сокету UDP, як ми показали у цьому розділі. Зазвичай ядра реалізацій, що походять від Берклі, повертають цю помилку, а ядра System V - не повертають. Наприклад, якщо ми запустимо той самий клієнт на вузлі Solaris 2.4 і за допомогою функції connect з'єднаємося з вузлом, на якому не запущено наш сервер, то за допомогою програми tcpdump ми зможемо переконатися, що помилка ICMP про недоступність порту повертається вузлом сервера, але викликана клієнтом функція read ніколи не завершується. Ця ситуація була виправлена ​​у Solaris 2.5. UnixWare не повертає помилку, а AIX, Digital Unix, HP-UX і Linux повертають помилку.

    8.13. Відсутність керування потоком в UDP

    Лістинг 8.9

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    8 char sendline;

    Лістинг 8.10

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    3 static int count;

    7 socklen_t len;

    8 char mesg;

    11 len = clilen;

    17 recvfrom_int(int signo)

    Лістинг 8.11. Виведення на вузлі сервера

    freebsd% netstat -s -p udp

    71208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    0 not for hashed pcb

    137685 datagrams output

    freebsd% udpserv06запускаємо наш сервер

    клієнт посилає дейтаграми

    ^C

    freebsd% netstat -s -р udp

    73208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams стягується до ніг.

    0 not for hashed pcb

    137685 datagrams output

    aix % udpserv06

    ^?

    received 2000 datagrams

    Приймальний буфер сокету UDP

    Число дейтаграм UDP, встановлених у чергу UDP, для сокету обмежено розміром його приймального буфера. Ми можемо змінити його за допомогою параметра сокета SO_RCVBUF, як ми показали у розділі 7.5. У FreeBSD за замовчуванням розмір приймального буфера UDP дорівнює 42 080 байт, що допускає можливість зберігання тільки 30 з наших 1400-байтових дейтаграм. Якщо ми збільшимо розмір приймального буфера сокету, можемо розраховувати, що сервер отримає додаткові дейтаграммы. У лістингу 8.12 представлена ​​змінена функція dg_echo з лістингу 8.10, яка збільшує розмір приймального буфера сокету до 240 Кбайт. Якщо ми запустимо цей сервер у системі Sun, а клієнт - у системі RS/6000, то лічильник отриманих дейтаграм матиме значення 103. Оскільки це лише трохи краще, ніж у попередньому прикладі з розміром буфера, заданим за умовчанням, ясно, що ми поки що не отримали вирішення проблеми.

    Лістинг 8.12. Функція dg_echo, що збільшує розмір приймального буфера сокету

    //udpcliserv/dgecholоор2.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    8 socklen_t len;

    9 char mesg;

    10 Signal(SIGINT, recvfrom_int);

    11 n = 240*1024;

    12 Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

    14 len = clilen;

    15 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    20 recvfrom_int(int signo)

    22 printf("\nreceived %d datagrams\n", count);

    ПРИМІТКА

    Чому ми встановлюємо розмір буфера прийому сокету рівним 240Г-1024 байт у лістингу 8.12? Максимальний розмір приймального буфера сокету в BSD/OS 2.1 за умовчанням дорівнює 262 144 байта (256Г-1024), але через спосіб розміщення буфера в пам'яті (описаного в розділі 2) він насправді обмежений до 246 723 байт. Багато більше ранні системи, засновані на 4.3BSD, обмежували розмір буфера прийому сокету приблизно 52 000 байт.

    8.14. Визначення вихідного інтерфейсу для UDP

    За допомогою приєднаного сокету UDP також можна задавати вихідний інтерфейс, який буде використаний для відправки дейтаграм до певного одержувача. Це пояснюється побічним ефектом функції connect , застосованої до сокету UDP: ядро ​​вибирає локальну IP-адресу (передбачається, що процес ще не викликав функцію bind для явного завдання). Локальна адреса вибирається в процесі пошуку адреси одержувача в таблиці маршрутизації, причому береться основна IP-адреса інтерфейсу, з якої згідно таблиці будуть відправлятися дейтаграми.

    У лістингу 8.13 показана проста програма UDP, яка за допомогою функції connect з'єднується із заданою IP-адресою і потім викликає функцію getsockname , виводячи локальну IP-адресу та порт.

    Лістинг 8.13. Програма UDP, що використовує функцію connect для визначення вихідного інтерфейсу

    //udpcliserv/udpcli09.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    6 socklen_t len;

    7 struct sockaddr_in cliaddr, servaddr;

    8 if (argc! = 2)

    9 err_quit("usage: udpcli ");

    10 sockfd = Socket (AF_INET, SOCK_DGRAM, 0);

    11 bzero(&servaddr, sizeof(servaddr));

    12 servaddr.sin_family = AF_INET;

    13 servaddr.sin_port = htons(SERV_PORT);

    14 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

    15 Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));

    16 len = sizeof(cliaddr);

    17 Getsockname(sockfd, (SA*)&cliaddr, &len);

    18 printf("local address %s\n", Sock_ntop((SA*)&cliaddr, len));

    Якщо ми запустимо програму на вузлі freebsd з кількома мережевими інтерфейсами, то отримаємо наступний висновок:

    freebsd% udpcli09 206.168.112.96

    local address 12.106.32.254:52329

    freebsd% udpcli09 192.168.42.2

    local address 192.168.42.1:52330

    freebsd% udpcli09 127.0.0.1

    local address 127.0.0.1:52331

    За рис. 1.7 видно, що коли ми запускаємо програму перші двічі, аргументом командного рядкає IP-адреса в різних мережах Ethernet. Ядро надає локальну IP-адресу первинній адресі інтерфейсу у відповідній мережі Ethernet. При виклику функції connect на сокеті UDP нічого не відправляється на цей вузол - це повністю локальна операція, яка зберігає IP-адресу та порт співрозмовника. Ми також бачимо, що виклик функції connect на неприєднаному сокеті UDP також надає сокету порт, що динамічно призначається.

    ПРИМІТКА

    На жаль, ця технологія діє не в усіх реалізаціях, що стосується ядер, що походять від SVR4. Наприклад, це не працює в Solaris 2.5, але працює в AIX, Digital Unix, Linux, MacOS X та Solaris 2.6.

    8.15. Відлуння TCP і UDP, що використовує функцію select

    Тепер ми об'єднаємо наш паралельний луна-сервер TCP з глави 5 і наш послідовний луна-сервер UDP з даного розділу в один сервер, який використовує функцію select для мультиплексування сокетів TCP і UDP. У лістингу 8.14 представлено першу частину цього сервера.

    Лістинг 8.14. Перша частина луна-сервера, що обробляє сокети TCP і UDP за допомогою функції select

    //udpcliserv/udpservselect01.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    5 int listenfd, connfd, udpfd, nready, maxfdp1;

    6 char mesg;

    7 pid_t childpid;

    10 socklen_t len;

    11 const int on = 1;

    12 struct sockaddr_in cliaddr, servaddr;

    13 void sig_chld(int);

    14 /* створення сокету TCP, що прослуховується */

    15 listenfd = Socket (AF_INET, SOCK_STREAM, 0);

    16 bzero(&servaddr, sizeof(servaddr));

    17 servaddr.sin_family = AF_INET;

    18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    19 servaddr.sin_port = htons(SERV_PORT);

    20 Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    21 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));

    22 Listen(listenfd, LISTENQ);

    23 /* створення сокету UDP */

    24 udpfd = Socket (AF_INET, SOCK_DGRAM, 0);

    25 bzero(&servaddr, sizeof(servaddr));

    26 servaddr.sin_family = AF_INET;

    27 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    28 servaddr.sin_port = htons(SERV_PORT);

    29 Bind(udpfd, (SA*)&servaddr, sizeof(servaddr));

    Створення сокету TCP, що прослуховується

    14-22 Створюється сокет TCP, що прослуховується, який зв'язується з заздалегідь відомим портом сервера. Ми встановлюємо параметр сокета SO_REUSEADDR, якщо на цьому порті існують з'єднання.

    Створення сокету UDP

    23-29 Також створюється сокет UDP і зв'язується з тим самим портом. Навіть якщо той самий порт використовується для сокетів TCP і UDP, немає необхідності встановлювати параметр сокета SO_REUSEADDR перед цим викликом функції bind , оскільки порти TCPне залежить від портів UDP.

    У лістингу 8.15 показано другу частину нашого сервера.

    Лістинг 8.15. Друга половина луна-сервера, що обробляє TCP і UDP за допомогою функції select

    udpcliserv/udpservselect01.c

    30 Signal (SIGCHLD, sig_chld); /* потрібно викликати waitpid() */

    31 FD_ZERO(&rset);

    32 maxfdp1 = max(listenfd, udpfd) + 1;

    34 FD_SET(listenfd, &rset);

    35 FD_SET(udpfd, &rset);

    36 if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL))

    37 if (errno == EINTR)

    38 continue; /* назад у for() */

    40 err_sys("select error");

    42 if (FD_ISSET(listenfd, &rset)) (

    43 len = sizeof(cliaddr);

    44 connfd = Accept(listenfd, (SA*)&cliaddr, &len);

    45 if ((childpid = Fork()) == 0) ( /* дочірній процес */

    46 Close(listenfd); /* закривається сокет, що прослуховується */

    47 str_echo(connfd); /* опрацювання запиту */

    50 Close(connfd); /* Батько закриває приєднаний сокет */

    52 if (FD_ISSET(udpfd, &rset)) (

    53 len = sizeof(cliaddr);

    54 n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA*)&cliaddr, &len);

    55 Sendto(udpfd, mesg, n, 0, (SA*)&cliaddr, len);

    Встановлення оброблювача сигналу SIGCHLD

    30 Для сигналу SIGCHLD встановлюється обробник, оскільки з'єднання TCP будуть оброблятися дочірнім процесом. Цей обробник сигналу ми показали у лістингу 5.8.

    Підготовка до виклику функції select

    31-32 Ми ініціалізуємо набір дескрипторів для функції select і обчислюємо максимальний з двох дескрипторів, на готовність якого чекатимемо.

    Виклик функції select

    34-41 Ми викликаємо функцію select , очікуючи тільки готовності до читання сокету TCP, що прослуховується, або сокету UDP. Оскільки наш обробник сигналу sig_chld може перервати виклик функції select обробляємо помилку EINTR.

    Обробка нового клієнтського з'єднання

    42-51 За допомогою функції accept ми приймаємо нове клієнтське з'єднання, а коли сокет TCP, що прослуховується, готовий для читання, за допомогою функції fork породжуємо дочірній процес і викликаємо нашу функцію str_echo в дочірньому процесі. Це та сама послідовність дій, яку ми виконували у розділі 5.

    Обробка дейтаграми, що приходить.

    52-57 Якщо сокет UDP готовий до читання, дейтаграма прийшла. Ми читаємо її за допомогою функції recvfrom і відправляємо назад клієнту за допомогою функції sendto.

    8.16. Резюме

    Перетворити наші ехо-клієнт та луна-сервер так, щоб використовувати UDP замість TCP, виявилося нескладно. Але при цьому ми втратили безліч можливостей, що надаються протоколом TCP: визначення втрачених пакетів та повторна передача, перевірка, чи надходять пакети від коректного співрозмовника, і т.д. Ми повернемось до цієї теми в розділі 22.5 і побачимо, як можна покращити надійність програми UDP.

    Сокети UDP можуть генерувати асинхронні помилки, тобто помилки, про які повідомляється через деякий час після того, як пакет було відправлено. Сокети TCP завжди повідомляють додаток про них, але у разі UDP для отримання цих помилок сокет має бути приєднаним.

    У UDP відсутня можливість керування потоком, що дуже легко продемонструвати. Зазвичай це не створює проблем, оскільки багато програм UDP побудовані з використанням моделі «запит-відповідь» і не призначені для передачі великої кількостіданих.

    Є ще ряд моментів, які потрібно враховувати при написанні програм UDP, але ми розглянемо їх у розділі 22 після опису функцій інтерфейсів, широкомовної та багатоадресної передачі.

    Вправи

    1. Допустимо, у нас є дві програми, одна використовує TCP, а інша - UDP. У приймальному буфері сокету TCP перебуває 4096 байт даних, а приймальному буфері для сокету UDP - дві дейтаграми по 2048 байт. Додаток TCP викликає функцію read з третім аргументом 4096, а програма UDP викликає функцію recvfrom з третім аргументом 4096. Чи є між цими викликами якась різниця?

    2. Що станеться у лістингу 8.2, якщо ми замінимо останній аргумент функції sendto (який ми позначили len) аргументом clilen?

    3. Відкомпілюйте та запустіть сервер UDP із лістингів 8.1 та 8.4, а потім – клієнт із лістингів 8.3 та 8.4. Переконайтеся, що клієнт та сервер працюють разом.

    4. Запустіть програму ping в одному вікні, задавши параметр -i 60 (надсилання одного пакета кожні 60 секунд; деякі системи використовують ключ I замість i), параметр -v (виведення всіх отриманих повідомлень про помилки ICMP) і задаючи адресу кільця на себе ( зазвичай 127.0.0.1). Ми будемо використовувати цю програму, щоб побачити помилку ICMP відсутності порту, що повертається вузлом сервера. Потім запустіть наш клієнт із попередньої вправи в іншому вікні, задавши IP-адресу деякого вузла, на якому не запущено сервер. Що відбувається?

    5. Розглядаючи рис. 8.3 ми сказали, що кожен приєднаний сокет TCP має свій власний буфер прийому. Як ви думаєте, чи є у сокету, що прослуховується, свій власний буфер прийому?

    6. Використовуйте програму sock (див. розділ В.3) та такий засіб, як, наприклад, tcpdump (див. розділ В.5), щоб перевірити твердження з розділу 8.10: якщо клієнт за допомогою функції bind пов'язує IP-адресу зі своїм сокетом, але відправляє дейтаграму, що виходить від іншого інтерфейсу, то результуюча дейтаграма містить IP-адресу, яка була пов'язана з сокетом, навіть якщо вона не відповідає вихідному інтерфейсу.

    7. Відкомпілюйте програми з розділу 8.13 та запустіть клієнт та сервер на різних вузлах. Поміщайте printf у клієнт щоразу, коли дейтаграма записується у сокет. Чи змінює це відсоток отриманих пакетів? Чому? Викликайте printf із сервера щоразу, коли дейтаграма читається із сокету. Чи змінює це відсоток отриманих пакетів? Чому?

    8. Яка найбільша довжинами можемо передати функції sendto для сокету UDP/IPv4, тобто яке найбільша кількістьданих, які можуть розміститися в дейтаграмі UDP/IPv4? Що змінюється у разі UDP/IPv6?

    Змініть листинг 8.4, щоб надіслати одну дейтаграму UDP максимального розміру, вважати її назад і вивести число байтів, що повертаються функцією recvfrom .

    9. Змініть листинг 8.15 таким чином, щоб він відповідав RFC 1122: для сокету UDP слід використовувати параметр IP_RECVDSTADDR .

    Наприкінці розділу 8.9 ми згадали, що асинхронні помилки не повертаються на сокет UDP, якщо сокет не був приєднаний. Насправді ми можемо викликати функцію connect для UDP-сокету (див. розділ 4.3). Але це не приведе ні до чого схожого на з'єднання TCP: тут не існує триетапного рукостискання. Ядро просто перевіряє, чи немає відомостей про явну недоступність адресата, після чого записує IP-адресу і номер порту співрозмовника, які містяться в структурі адреси сокета, що передається функції connect, і негайно повертає управління процесом, що викликає.

    ПРИМІТКА

    Перевантаження функції connect цієї нової можливості для сокетів UDP може внести плутанину. Якщо використовується угода про те, що sockname - це адреса локального протоколу, a peername - адреса віддаленого протоколу, то краще б ця функція називалася setpeername. Аналогічно, функції bind більше підійшло б назву setsockname.

    З огляду на це необхідно розуміти різницю між двома видами сокетів UDP.

    в– Неприєднаний (unconnected) сокет UDP – це сокет UDP, що створюється за умовчанням.

    в– Приєднаний (connected) сокет UDP – результат виклику функції connect для сокету UDP.

    Приєднаний сокет UDP має три відмінності від неприєднаного сокету, який створюється за замовчуванням.

    1. Ми більше не можемо задавати IP-адресу одержувача та порт для операції виведення. Тобто ми використовуємо замість функції sendto функцію write або send. Все, що записується в приєднаний сокет UDP, автоматично відправляється на адресу (наприклад, IP-адресу та порт), заданий функцією connect.

    ПРИМІТКА

    Аналогічно TCP, ми можемо викликати функцію sendto для приєднаного сокету UDP, але не можемо задати адресу одержувача. П'ятий аргумент функції sendto (покажчик на структуру адреси сокета) має бути порожнім покажчиком, а шостий аргумент (розмір структури адреси сокета) має бути нульовим. У стандарті POSIX визначено, що коли п'ятий аргумент є порожнім покажчиком, шостий аргумент ігнорується.

    2. Замість функції recvfrom ми використовуємо функцію read чи recv . Єдині дейтаграми, що повертаються ядром для операції введення через приєднаний сокет UDP, - це дейтаграми, що надходять з адреси, заданої функції connect . Дейтаграми, призначені для адреси локального протоколу приєднаного сокету UDP (наприклад, IP-адреса та порт), але надходять з адреси протоколу, відмінного від того, до якого сокет був приєднаний за допомогою функції connect, не передаються приєднаному сокету. Це обмежує приєднаний сокет UDP, дозволяючи йому обмінюватися дейтаграмами з одним і лише одним співрозмовником.

    ПРИМІТКА

    Точніше, обмін дейтаграмами відбувається лише з однією IP-адресою, а чи не з одним співрозмовником, оскільки це може бути IP-адреса багатоадресної передачі, що представляє, таким чином, групу співрозмовників.

    3. Асинхронні помилки повертаються процесу лише при операціях із приєднаним сокетом UDP. В результаті, як ми вже казали, неприєднаний сокет UDP не отримує жодних асинхронних помилок.

    У табл. 8.2 зводяться воєдино властивості, перелічені в першому пункті, стосовно 4.4BSD.

    Таблиця 8.2. Сокети TCP та UDP: чи може бути задана адреса протоколу одержувача

    ПРИМІТКА

    POSIX визначає, що операція виведення, яка не задає адресу одержувача на неприєднаному сокеті UDP, повинна повертати помилку ENOTCONN, а не EDESTADDRREQ.

    Solaris 2.5 допускає функцію sendto, яка визначає адресу одержувача для приєднаного сокету UDP. POSIX визначає, що у такій ситуації має повертатися помилка EISCONN.

    На рис. 8.7 узагальнюється інформація про приєднаний сокет UDP.

    Мал. 8.7. Приєднаний сокет UDP

    Програма викликає функцію connect , задаючи IP-адресу та номер порту співрозмовника. Потім воно використовує функції read і write обмінюватись даними зі співрозмовником.

    Дейтаграми, що надходять з будь-якої іншої IP-адреси або порту (який ми позначаємо як «???» на рис. 8.7), не передаються на приєднаний сокет, оскільки або IP-адреса, або UDP-порт відправника не збігаються з адресою протоколу, з яким сокет з'єднується за допомогою функції connect. Ці дейтаграми можуть бути доставлені на інший сокет UDP на вузлі. Якщо немає іншого збігаючого сокету для дейтаграми, UDP проігнорує її і згенерує ICMP-повідомлення про недоступність порту.

    Узагальнюючи сказане вище, ми можемо стверджувати, що клієнт або сервер UDP може викликати функцію connect , тільки якщо цей процес використовує сокет UDP для зв'язку лише з одним співрозмовником. Зазвичай саме клієнт UDP викликає функцію connect, але існують програми, в яких сервер UDP пов'язується з одним клієнтом на тривалий час (наприклад, TFTP), і в цьому випадку клієнт, і сервер викликають функцію connect.

    Ще один приклад довгострокової взаємодії – це DNS (рис. 8.8).

    Мал. 8.8. Приклад клієнтів та серверів DNS та функції connect

    Клієнт DNS може бути налаштований для використання одного або більше серверів, зазвичай за допомогою переліку IP-адрес серверів у файлі /etc/resolv.conf . Якщо в цьому файлі вказано лише один сервер (на малюнку цей клієнт зображений у крайньому ліворуч прямокутнику), клієнт може викликати функцію connect, але якщо перелічено безліч серверів (другий праворуч прямокутник на малюнку), клієнт не може викликати функцію connect . Зазвичай сервер DNSобробляє також будь-які запити клієнта, отже, сервери не можуть викликати функцію connect .

    Тепер ми перевіримо, як впливає роботу програми відсутність будь-якого управління потоком в UDP. Спочатку ми змінимо нашу функцію dg_cli так, щоб вона надсилала фіксоване число дейтаграм. Вона більше не читатиме зі стандартного потоку введення. У лістингу 8.9 показано нову версію функції. Ця функція надсилає серверу 2000 дейтаграм UDP по 1400 байт кожна.

    Лістинг 8.9. Функція dg_cli, що відсилає фіксовану кількість дейтаграм серверу

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    2 #define NDG 2000 /* кількість дейтаграм для відправки */

    3 #define DGLEN 1400 /* довжина кожної дейтаграми */

    5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    8 char sendline;

    10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

    Потім ми змінюємо сервер так, щоб він отримував дейтаграми та вважав кількість отриманих дейтаграм. Сервер більше не відображає дейтаграм назад клієнту. У лістингу 8.10 показана нова функція dg_echo. Коли ми завершуємо процес сервера, натиснувши клавішу переривання на терміналі (що призводить до відправки сигналу SIGINT процесу), сервер виводить число отриманих дейтаграм і завершується.

    Лістинг 8.10. Функція dg_echo, що вважає отримані дейтаграми

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    7 socklen_t len;

    8 char mesg;

    9 Signal (SIGINT, recvfrom_int);

    11 len = clilen;

    12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    17 recvfrom_int(int signo)

    19 printf("\nreceived %d datagrams\n", count);

    Тепер ми запускаємо сервер на вузлі freebsd, який є повільним комп'ютером SPARCStation. Клієнт ми запускаємо у значно швидшій системі RS/6000 з операційною системою aix. Вони з'єднані між собою безпосередньо каналом Ethernet на 100 Мбіт/с. Крім того, ми запускаємо програму netstat -s на вузлі сервера і до, і після запуску клієнта і сервера, оскільки статистика, що виводиться, покаже, скільки дейтаграм ми втратили. У лістингу 8.11 показано виведення сервера.

    Лістинг 8.11. Виведення на вузлі сервера

    freebsd% netstat -s -p udp

    71208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams стягується до ніг.

    1971 г. схилився до full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    freebsd% udpserv06запускаємо наш сервер

    клієнт посилає дейтаграми

    ^Cдля закінчення роботи клієнта вводимо наш символ переривання

    freebsd% netstat -s -р udp

    73208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams стягується до ніг.

    3941 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    Клієнт відправив 2000 дейтаграм, але додаток-сервер отримав лише 30 із них, що означає рівень втрат 98%. Ні сервер, ні клієнт не отримують повідомлення, що ці дейтаграми втрачені. Як ми й казали, UDP не має можливості керування потоком – він ненадійний. Як ми показали, для відправника UDP не важко переповнити буфер одержувача.

    Якщо ми подивимося висновок програми netstat , то побачимо, що загальна кількість дейтаграм, отриманих вузлом сервера (не додатком-сервером) дорівнює 2000 (73 208 – 71 208). Лічильник dropped due to full socket buffers (відкинуто через переповнення буферів сокету) показує, скільки дейтаграм було отримано UDP і проігноровано тому, що приймальний буфер приймаючого сокета був повний . Це значення дорівнює 1970 (3941 – 1971), що з додаванні до висновку лічильника дейтаграм, отриманих додатком (30), дає 2000 дейтаграм, отриманих вузлом. На жаль, лічильник дейтаграм, відкинутих через заповнений буфер, у програмі netstat поширюється на всю систему. Не існує способу визначити, на які програми (наприклад, порти UDP) це впливає.

    Число дейтаграм, одержаних сервером у цьому прикладі, недетерміновано. Воно залежить від багатьох факторів, таких як навантаження мережі, завантаженість вузла клієнта та вузла сервера.

    Якщо ми запустимо той самий клієнт і той самий сервер, але цього разу клієнт на повільній системі Sun, а сервер на швидкій системі RS/6000, ніякі дейтаграми не губляться.

    aix % udpserv06

    ^? після закінчення роботи клієнта вводимо наш символ переривання

    received 2000 datagrams

    Наприкінці розділу 8.9 ми згадали, що асинхронні помилки не повертаються на сокет UDP, якщо сокет не був приєднаний. Насправді ми можемо викликати функцію connect для UDP-сокету (див. розділ 4.3). Але це не приведе ні до чого схожого на з'єднання TCP: тут не існує триетапного рукостискання. Ядро просто перевіряє, чи немає відомостей про явну недоступність адресата, після чого записує IP-адресу і номер порту співрозмовника, які містяться в структурі адреси сокета, що передається функції connect, і негайно повертає управління процесом, що викликає.

    ПРИМІТКА

    Перевантаження функції connect цієї нової можливості для сокетів UDP може внести плутанину. Якщо використовується угода про те, що sockname - це адреса локального протоколу, a peername - адреса віддаленого протоколу, то краще б ця функція називалася setpeername. Аналогічно, функції bind більше підійшло б назву setsockname.

    З огляду на це необхідно розуміти різницю між двома видами сокетів UDP.

    в– Неприєднаний (unconnected) сокет UDP – це сокет UDP, що створюється за умовчанням.

    в– Приєднаний (connected) сокет UDP – результат виклику функції connect для сокету UDP.

    Приєднаний сокет UDP має три відмінності від неприєднаного сокету, який створюється за замовчуванням.

    1. Ми більше не можемо задавати IP-адресу одержувача та порт для операції виведення. Тобто ми використовуємо замість функції sendto функцію write або send. Все, що записується в приєднаний сокет UDP, автоматично відправляється на адресу (наприклад, IP-адресу та порт), задану функцією connect .

    ПРИМІТКА

    Аналогічно TCP, ми можемо викликати функцію sendto для приєднаного сокету UDP, але не можемо задати адресу одержувача. П'ятий аргумент функції sendto (покажчик на структуру адреси сокета) має бути порожнім покажчиком, а шостий аргумент (розмір структури адреси сокета) має бути нульовим. У стандарті POSIX визначено, що коли п'ятий аргумент є порожнім покажчиком, шостий аргумент ігнорується.

    2. Замість функції recvfrom ми використовуємо функцію read чи recv . Єдині дейтаграми, що повертаються ядром для операції введення через приєднаний сокет UDP, - це дейтаграми, що надходять з адреси, заданої функції connect . Дейтаграми, призначені для адреси локального протоколу приєднаного сокету UDP (наприклад, IP-адреса та порт), але надходять з адреси протоколу, відмінного від того, до якого сокет був приєднаний за допомогою функції connect, не передаються приєднаному сокету. Це обмежує приєднаний сокет UDP, дозволяючи йому обмінюватися дейтаграмами з одним і лише одним співрозмовником.

    ПРИМІТКА

    Точніше, обмін дейтаграмами відбувається лише з однією IP-адресою, а чи не з одним співрозмовником, оскільки це може бути IP-адреса багатоадресної передачі, що представляє, таким чином, групу співрозмовників.

    3. Асинхронні помилки повертаються процесу лише при операціях із приєднаним сокетом UDP. В результаті, як ми вже казали, неприєднаний сокет UDP не отримує жодних асинхронних помилок.

    У табл. 8.2 зводяться воєдино властивості, перелічені в першому пункті, стосовно 4.4BSD.

    Таблиця 8.2. Сокети TCP та UDP: чи може бути задана адреса протоколу одержувача

    ПРИМІТКА

    POSIX визначає, що операція виведення, яка не задає адресу одержувача на неприєднаному сокеті UDP, повинна повертати помилку ENOTCONN, а не EDESTADDRREQ.

    Solaris 2.5 допускає функцію sendto, яка визначає адресу одержувача для приєднаного сокету UDP. POSIX визначає, що у такій ситуації має повертатися помилка EISCONN.

    На рис. 8.7 узагальнюється інформація про приєднаний сокет UDP.

    Мал. 8.7. Приєднаний сокет UDP

    Програма викликає функцію connect , задаючи IP-адресу та номер порту співрозмовника. Потім воно використовує функції read і write обмінюватись даними зі співрозмовником.

    Дейтаграми, що надходять з будь-якої іншої IP-адреси або порту (який ми позначаємо як «???» на рис. 8.7), не передаються на приєднаний сокет, оскільки або IP-адреса, або UDP-порт відправника не збігаються з адресою протоколу, з яким сокет з'єднується за допомогою функції connect. Ці дейтаграми можуть бути доставлені на інший сокет UDP на вузлі. Якщо немає іншого збігаючого сокету для дейтаграми, UDP проігнорує її і згенерує ICMP-повідомлення про недоступність порту.

    Узагальнюючи сказане вище, ми можемо стверджувати, що клієнт або сервер UDP може викликати функцію connect , тільки якщо цей процес використовує сокет UDP для зв'язку лише з одним співрозмовником. Зазвичай саме клієнт UDP викликає функцію connect, але існують програми, в яких сервер UDP пов'язується з одним клієнтом на тривалий час (наприклад, TFTP), і в цьому випадку клієнт, і сервер викликають функцію connect.

    Ще один приклад довгострокової взаємодії – це DNS (рис. 8.8).

    Мал. 8.8. Приклад клієнтів та серверів DNS та функції connect

    Клієнт DNS може бути налаштований для використання одного або більше серверів, зазвичай за допомогою переліку IP-адрес серверів у файлі /etc/resolv.conf . Якщо в цьому файлі вказано лише один сервер (на малюнку цей клієнт зображений у крайньому ліворуч прямокутнику), клієнт може викликати функцію connect, але якщо перелічено безліч серверів (другий праворуч прямокутник на малюнку), клієнт не може викликати функцію connect . Зазвичай сервер DNS обробляє також будь-які запити клієнта, отже, сервери не можуть викликати функцію connect .

    Тепер ми перевіримо, як впливає роботу програми відсутність будь-якого управління потоком в UDP. Спочатку ми змінимо нашу функцію dg_cli так, щоб вона надсилала фіксоване число дейтаграм. Вона більше не читатиме зі стандартного потоку введення. У лістингу 8.9 показано нову версію функції. Ця функція надсилає серверу 2000 дейтаграм UDP по 1400 байт кожна.

    Лістинг 8.9. Функція dg_cli, що відсилає фіксовану кількість дейтаграм серверу

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    2 #define NDG 2000 /* кількість дейтаграм для відправки */

    3 #define DGLEN 1400 /* довжина кожної дейтаграми */

    5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    8 char sendline;

    10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

    Потім ми змінюємо сервер так, щоб він отримував дейтаграми та вважав кількість отриманих дейтаграм. Сервер більше не відображає дейтаграм назад клієнту. У лістингу 8.10 показана нова функція dg_echo. Коли ми завершуємо процес сервера, натиснувши клавішу переривання на терміналі (що призводить до відправки сигналу SIGINT процесу), сервер виводить число отриманих дейтаграм і завершується.

    Лістинг 8.10. Функція dg_echo, що вважає отримані дейтаграми

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    7 socklen_t len;

    8 char mesg;

    9 Signal (SIGINT, recvfrom_int);

    11 len = clilen;

    12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    17 recvfrom_int(int signo)

    19 printf("\nreceived %d datagrams\n", count);

    Тепер ми запускаємо сервер на вузлі freebsd, який є повільним комп'ютером SPARCStation. Клієнт ми запускаємо у значно швидшій системі RS/6000 з операційною системою aix. Вони з'єднані між собою безпосередньо каналом Ethernet на 100 Мбіт/с. Крім того, ми запускаємо програму netstat -s на вузлі сервера і до, і після запуску клієнта і сервера, оскільки статистика, що виводиться, покаже, скільки дейтаграм ми втратили. У лістингу 8.11 показано виведення сервера.

    Лістинг 8.11. Виведення на вузлі сервера

    freebsd% netstat -s -p udp

    71208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams стягується до ніг.

    1971 г. схилився до full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    freebsd% udpserv06запускаємо наш сервер

    клієнт посилає дейтаграми

    ^Cдля закінчення роботи клієнта вводимо наш символ переривання

    freebsd% netstat -s -р udp

    73208 datagrams отриманий

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 без checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams стягується до ніг.

    3941 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    Клієнт відправив 2000 дейтаграм, але додаток-сервер отримав лише 30 із них, що означає рівень втрат 98%. Ні сервер, ні клієнт не отримують повідомлення, що ці дейтаграми втрачені. Як ми й казали, UDP не має можливості керування потоком – він ненадійний. Як ми показали, для відправника UDP не важко переповнити буфер одержувача.

    Якщо ми подивимося висновок програми netstat , то побачимо, що загальна кількість дейтаграм, отриманих вузлом сервера (не додатком-сервером) дорівнює 2000 (73 208 – 71 208). Лічильник dropped due to full socket buffers (відкинуто через переповнення буферів сокету) показує, скільки дейтаграм було отримано UDP і проігноровано тому, що приймальний буфер приймаючого сокета був повний . Це значення дорівнює 1970 (3941 – 1971), що з додаванні до висновку лічильника дейтаграм, отриманих додатком (30), дає 2000 дейтаграм, отриманих вузлом. На жаль, лічильник дейтаграм, відкинутих через заповнений буфер, у програмі netstat поширюється на всю систему. Не існує способу визначити, на які програми (наприклад, порти UDP) це впливає.

    Число дейтаграм, одержаних сервером у цьому прикладі, недетерміновано. Воно залежить від багатьох факторів, таких як навантаження мережі, завантаженість вузла клієнта та вузла сервера.

    Якщо ми запустимо той самий клієнт і той самий сервер, але цього разу клієнт на повільній системі Sun, а сервер на швидкій системі RS/6000, ніякі дейтаграми не губляться.

    aix % udpserv06

    ^? після закінчення роботи клієнта вводимо наш символ переривання

    received 2000 datagrams