Використання сокетів в Delphi. Частина перша: стандартні сокети (исходники), Різне, Програмування, статті

Стаття є першою в циклі з трьох статей, покликаних дати відповіді на подібні питання. Вона присвячена стандартним сокетів. Друга стаття буде присвячена сокетів Windows, а третя – внутрішньому устрою класів VCL, призначених для передачі даних за допомогою сокетів. Статті не претендують на вичерпне висвітлення проблеми (зокрема, будуть обговорюватися тільки протоколи TCP і UDP), однак вони повинні дати відомості, достатні для розуміння основних механізмів роботи сокетів і подальшого самостійного їх вивчення. У статтях багато додаткової інформації, яка не є необхідною безпосередньо для написання програми, але розширює кругозір в даній галузі знань. Строго кажучи, якщо писати тільки те, що необхідно для найпростішої організації зв’язку, кожна з трьох статей циклу вмістилася б на сторінці. Але в такому вигляді це було б корисно тільки ламера, яким аби здерти звідкись готове рішення і аби як вставити його в свою “програму”. А моя мета – написати щось корисне для тих, хто поки ще не знайомий близько з сокетами, але хоче в першу чергу зрозуміти, як вони влаштовані, а не отримати щось готовеньке. Таким людям, на мій погляд, буде корисно знати те, що знаходиться навколо, тому що це знання допомагає шукати рішення в нестандартних ситуаціях.

Термін “стандартні сокети”, який буде зустрічатися протягом всієї статті, досить умовний і потребує додаткового пояснення. Строго кажучи, стандартними сокетами називаються сокети Берклі (Berkley sockets), розроблені в університеті Берклі для системи Unix. Як це не парадоксально звучить, але сокети Берклі з’явилися до появи комп’ютерних мереж. Спочатку вони призначалися для взаємодії між процесами в системі і лише пізніше були пристосовані для TCP / IP. Робота з сокетами Берклі зроблена максимально схожою на роботу з файлами в Unix. Зокрема, для відправки та отримання даних використовуються ті ж функції, що і для файлового введення / виведення.


Сокети в Windows не повністю сумісні з сокетами Берклі (наприклад, для них передбачені спеціальні функції відправки та отримання даних, перевизначені деякі типи даних і т.п.). Але можливості роботи з сокетами в Windows можна розділити на дві частини: те, що вкладається в ідеологію сокетів Берклі, хоча і реалізовано трохи інакше, і те, що є специфічним для Windows. Ту частину реалізації сокетів Windows, яка за функціональністю відповідає сокетів Берклі, ми будемо називати стандартними сокетами, а сокетами Windows (Windows sockets) – специфічні для Windows розширення.


Угоди про імена

Перші бібліотеки сокетів писалися на мові С. У цій мові ідентифікатори чутливі до регістру символів, тобто, наприклад, SOCKET, Socket та socket – це різні ідентифікатори. Історично склалося, що імена вбудованих в С типів даних пишуться у нижньому регістрі, імена визначених у програмі типів, макровизначень і констант – у верхньому, імена функцій – в змішаному (великі літери виділяють початку слів, наприклад, GetWindowText). Розробники бібліотеки сокетів трохи відійшли від цих правил: імена всіх стандартних сокетних функцій пишуться в нижньому регістрі. На щастя, ми програмуємо не на С, а на Паскалі, нечутливим до регістру символів, тому будемо писати все ідентифікатори у вигляді, найбільш зручному для читання, тобто в змішаному регістрі.


Чутливість C до регістру символів створює деякі проблеми при переносі бібліотек, написаних на цій мові, в Delphi. Ці проблеми пов’язані з тим, що різні об’єкти можуть мати імена, що розрізняються тільки регістром символів. Зокрема, є тип SOCKET і функція socket. Зберегти ці імена в Delphi можливості немає. Щоб уникнути цю проблему, розробники Delphi при перенесенні бібліотек до імені типу додають букву “T”, причому незалежно від того, чи існують у цього типу однойменні функції чи ні. Так, тип SOCKET в С в Delphi називається TSocket. Імена функцій залишаються без змін.


Вище був згаданий термін “макровизначення”. Він може бути незрозумілий тим, хто не працював з мовами C і C + +, тому що в Delphi макровизначення відсутні. Нормальна послідовність трансляції програми в Delphi наступна: спочатку компілятор створює об’єктний код, в якому замість реальних адрес функцій, змінних і т.п. стоять посилання на них (на етапі компіляції ці адреси ще невідомі). Потім компонувальник розміщує об’єкти в пам’яті і замінює посилання реальними адресами. Так виходить готова до виконання програма. У C / C + + трансляція включає в себе ще один етап: перед компілятором текст програми модифікується препроцесором, а компілятор отримають вже змінений текст. Макровизначення, або просто макроси, – це директиви препроцесору, розпорядчі йому, як саме потрібно змінювати текст програми. Макрос задає підміну: скрізь, де в програмі зустрічається ім’я макросу, препроцесор змінює його на той текст, який заданий при визначенні цього макросу. Визначаються макроси за допомогою директиви препроцесору “# define”.


У простому випадку макроси використовуються для визначення констант. Наприклад, директива “# define SOMECONST 10” змушує препроцесор заміняти SOMECONST на 10. Для компілятора ця директива нічого не означає, ідентифікатора SOMECONST для нього не існує. Він отримає вже змінений препроцесором текст, в якому на місцях згадки SOMECONST буде стояти 10. Допускається також створювати параметризрвані макроси, які змінюють текст програми з більш складним правилам.


Макроси дозволяють у деяких випадках істотно скоротити програму і підвищити її читабельність. Тим не менш, вони вважаються застарілим засобом, тому що їх використання може призвести до істотних проблем (Обговорення цих проблем виходить за рамки цієї статті, якщо кому це цікаво – задавайте питання нижче, в обговоренні статті, і я відповім). В сучасних мовах від використання макросів відмовляються. Зокрема, в C + + макроси підтримуються в повному обсязі, але використовувати їх не рекомендується, тому що є безпечніші інструменти, вирішальні типові для макросів завдання. В C # і Java макроси відсутні. Тим не менш, в заголовних файлах для системних бібліотек Windows (в т.ч. і бібліотеки сокетів) макроси широко використовуються, тому що потрібно забезпечити сумісність з мовою C. При портаціі таких файлів в Delphi макроси без параметрів замінюються константами, а макроси з параметрами – функціями (іноді один макрос доводиться замінювати декількома функціями для різних типів даних).


Загальні відомості про сокетах

Сокетом (від англ. Socket – гніздо, розетка) називається спеціальний об’єкт, який створюється для відправки та отримання даних через мережу. Зазначимо, що під терміном “об’єкт” в даному випадку мається на увазі не об’єкт в термінах об’єктно-орієнтованого програмування, а деяка сутність, внутрішня структура якої прихована від нас, тому з цієї сутністю ми можемо оперувати тільки як з єдиним і неподільним (атомарним) об’єктом. Цей об’єкт створюється всередині бібліотеки сокетів, а програміст, який використовує цю бібліотеку, отримує унікальний номер (дескриптор) цього сокета. Конкретне значення цього дескриптора не несе для програміста ніякої корисної інформації і може бути використано тільки для того, щоб при виконанні функції з бібліотеки сокетів вказати, з яким сокетом потрібно виконати операцію.


Щоб дві програми могли спілкуватися один з одним через мережу, кожна з них повинна створити сокет. Кожен сокет володіє двома основними характеристиками: протоколом і адресою, до яких він прив’язаний. Протокол задається при створенні сокета і не може бути змінений згодом. Адреса сокету задається пізніше, але обов’язково до того, як через сокет підуть дані. В деяких випадках прив’язка сокета до адреси може бути неявній.


Формат адреси сокета визначається конкретним протоколом. Зокрема, для протоколів TCP і UDP адреса складається з IP-адреси мережевого інтерфейсу і номера порту.


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


Мережа може пов’язувати різні апаратні платформи, тому потрібне узгодження форматів даних, що передаються, зокрема – форматів цілих чисел. Двухбайтние цілі числа зберігаються в пам’яті у двох послідовно розташованих байтах. При цьому можливі два варіанти: у першому байті зберігається молодший байт числа, а в другому – старший, і навпаки. Спосіб зберігання визначається апаратною частиною платформи. Процесори Intel використовують перший варіант, тобто перший зберігається молодший байт, а інші процесори (наприклад, Motorola) – другий варіант. Те ж стосується і четирехбайтний чисел: процесори Intel зберігають їх, починаючи з молодшого байта, а деякі інші процесори – починаючи зі старшого. Мережевий формат подання таких чисел збігається з форматом процесора Motorola, тобто на платформах з процесором Intel необхідно переставляти байти при конвертації чисел в мережевий формат.


Бібліотека сокетів розроблялася для ОС Unix, в якій традиційно високо цінувалася переносимість між платформами, тому вона містить функції, що дозволяють не замислюватися про порядок байт в числах. Ці функції називаються NtoHS, NtoHL, HtoNS і HtoNL. Перша буква в назві цих функцій показує, в якому форматі дано вихідне число (N – Network – мережевий формат, H – Host – формат платформи), четверта буква – формат результату, остання буква – розрядність (S – Short – двухбайтное число, L – Long – четирехбайтное число). Наприклад, функція HtoNS приймає як параметр число типу u_short (Word) у форматі платформи і повертає те саме число в мережевому форматі. Реалізація цих функцій для кожної платформи своя: десь вони переставляють байти, десь вони повертають в точності те число, яке було їм передано. Використання цих функцій робить програми переносяться. Хоча для програміста на Delphi питання переносимості не настільки актуальні, доводиться використовувати ці функції хоча б тому, що байти переставляти треба, а ніякого більш зручного способу для цього не існує.


Мережеві протоколи. Семиуровневая модель OSI

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


Для встановлення взаємодії між комп’ютерами повинен бути узгоджений цілий ряд питань, починаючи від напруги в проводах і закінчуючи форматом пакетів. Реалізуються ці угоди на різних рівнях, тому логічніше мати не один протокол, що описує все і вся, а набір протоколів, кожен з яких охоплює лише питання одного рівня. Організація Open Software Interconnection (OSI) запропонувала розділити всі питання, що вимагають узгодження, на сім рівнів. Це розділення відомо як семирівнева модель OSI.


Сімейство протоколів, які реалізують різні рівні, називається стеком протоколів. Стеки протоколів не завжди точно слідують моделі OSI, деякі протоколи вирішують питання, пов’язані відразу з кількома рівнями.


Перший рівень в моделі OSI називається фізичним. На ньому узгоджуються фізичні, електричні та оптичні параметри мережі: напруга і форма імпульсів, що кодують 0 і 1, який штекер використовується і т.п.


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


Третій рівень – мережевий. На цьому рівні визначається, як адресуються комп’ютери. Більшість мереж використовують широкомовний спосіб передачі: пакет, переданий одним комп’ютером, отримують всі інші. Протокол мережевого рівня описує критерії, на підставі яких кожен комп’ютер може вибирати з мережі тільки ті пакети, які призначені йому, і ігнорувати всі інші. На цьому ж рівні визначається, як пакети проходять через роутер.


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


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


Шостий рівень називається рівнем уявлень. На цьому рівні визначається те, в якому форматі дані передаються по мережі. Під цим мається на увазі, в першу чергу, внутрішня структура пакета, а також спосіб представлення даних різних типів. Наприклад, для двох-і четирехбайтний цілих чисел повинен бути узгоджений порядок байт, для логічних величин – які значення відповідають True, які – False, для рядків – кодування і спосіб завдання кінця рядка і т.п.


Сьомий рівень називається рівнем додатків. Угоди цього рівня дозволяють працювати з ресурсами (файлами, принтерами і т.д.) віддаленого комп’ютера як з локальними, здійснювати віддалений виклик процедур і т.п.


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


Стек TCP / IP

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


В реальному житті не всі протоколи відповідають моделі OSI. Особливо це стосується старих протоколів. Існує таке поняття, як стек протоколів – набір протоколів різних рівнів, які сумісні один з одним. Ці рівні не завжди точно відповідають тим, які пропонує модель OSI, але певний розподіл завдань на рівні в них присутня. У даній роботі ми зосередимося на стеку протоколів, який називається TCP / IP (нерідко можна почути словосполучення “протокол TCP / IP” – це не зовсім коректно: TCP / IP не протокол, а стек протоколів). Назву цей стек отримав за назвою двох найвідоміших своїх протоколів – TCP і IP.


IP розшифровується як Internet Protocol. Ця назва іноді помилково перекладають як “протокол інтернету” або “протокол для інтернету”. Насправді коли розроблявся цей протокол, ніякого інтернету ще і в помині не було, тому правильний переклад – міжмережевий протокол. Історія появи цього протоколу пов’язана з особливостями роботи мережі Ethernet. Ця мережа будується за принципом шини, коли всі комп’ютери підключені, грубо кажучи, до одного проводу. Якщо хоча б два комп’ютери спробують одночасно передавати дані по загальній шині, виникне плутанина, тому всі шинні мережі будуються за принципом “один каже – всі слухають “. Очевидно, що потрібно якийсь захист від т.зв. колізій – ситуацій, коли два вузли одночасно намагаються передавати дані.


Різні мережі вирішують проблему колізій по-різному. У промислових мережах, наприклад, зазвичай використовується маркер – спеціальний індикатор, який показує, якому вузлу дозволено зараз передавати дані. Вузол, званий майстром, стежить за тим, щоб маркер вчасно передавався від одного вузла до іншого. Маркер виключає можливість колізій. Ethernet ж є однорангової мережею, в якій немає майстра, тому в ній використовується інший підхід: колізії допускаються, але існує механізм їх вирішення, полягає в тому, що, по-перше, вузол не починає передачу даних, якщо бачить, що інший вузол вже щось передає, а по-друге, якщо два вузли одночасно намагаються почати передачу, то обидва припиняють спробу і повторюють її через випадковий проміжок часу. У кого цей проміжок виявиться менше, той і захопить мережа (або за цей проміжок часу мережа буде захоплена кимось ще).


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


На канальному рівні існує адресація вузлів, заснована на т.зв. MAC-адресу мережевої карти (MAC – це скорочення Media Access Control). Ця адреса є унікальним номером картки, присвоєної їй виробником. Очевидно незручність такого способу адресації, тому що по MAC-адресу неможливо визначити положення комп’ютера в мережі, тобто куди направляти пакет. Крім того, при заміні мережевої карти змінюється адреса комп’ютера, що також не завжди зручно. Тому на мережевому рівні визначається власний спосіб адресації, не пов’язаний з апаратними особливостями вузла. Звідси випливає, що роутер повинен розуміти протокол мережевого рівня, щоб приймати рішення про передачу пакета з однієї мережі в іншу, а протокол, в свою чергу, повинен враховувати наявність роутерів в мережі та надавати їм необхідну інформацію. Протокол IP був одним з перших протоколів мережевого рівня, який вирішував таке завдання, і з його допомогою стала можливою передача пакетів між мережами. Тому він і отримав назву міжмережевого протоколу. Втім, назва прижилося: в деяких статтях MSDN “а мережевий рівень (network layer) називається міжмережевим рівнем (internet layer). У протоколі IP, зокрема, вводиться важливий параметр для кожного пакета: максимальне число роутерів, яке він може пройти, перш ніж потрапить до адресата. Це дозволяє захиститися від нескінченного блукання пакетів по мережі.


Тут слід зауважити, що мережа Ethernet пішла далеко вперед у порівнянні з моментом створення протоколу IP і тепер організована складніше, тому не слід думати, що в попередніх абзацах викладені всі принципи роботи цієї мережі (їх виклад виходить за рамки цієї статті). Тим не менш, протокол IP як і раніше використовується, а комп’ютери як і раніше бачать в мережі не тільки свої, але і чужі пакети. На цьому заснована робота т.зв. снифферов – програм, що дозволяють одному комп’ютеру читати пакети, що пересилаються між двома іншими комп’ютерами.


Для адресації комп’ютера протокол IP використовує унікальне четирехбайтное число, зване IP-адресою. Втім, більш поширена форма запису цього числа у вигляді чотирьох однобайтних значень. Система призначення цих адрес досить складна і покликана оптимізувати роботу роутерів, забезпечити проходження широкомовних пакетів тільки всередині певної частини мережі і т.п. Ми тут не будемо розбирати цю систему, тому що в правильно налаштованої мережі програмісту не потрібно знати ці тонкощі: досить знати, що кожен вузол має унікальний IP-адресу, для якого прийнята запис у вигляді чотирьох цифрових полів, розділених крапками, наприклад, 192.168.200.217. Також слід знати, що адреси з діапазону 127.0.0.1-127.255.255.255 задають т.зв. локальний вузол: через ці адреси можуть зв’язуватися програми, що працюють на одному комп’ютері. Таким чином, забезпечується прозорість місцезнаходження адресата. Крім того, один комп’ютер може мати кілька IP-адрес, які можуть використовуватися для одного і того ж або різних мережевих інтерфейсів.


Крім IP, в стеку TCP / IP існує ще декілька протоколів, що вирішують завдання мережевого рівня. Ці протоколи не є повноцінними протоколами і не можуть замінити IP. Вони використовуються тільки для вирішення деяких приватних задач. Це протоколи ICMP, IGMP і ARP.


Протокол ICMP (Internet Control Message Protocol – протокол міжмережевих керуючих повідомлень) забезпечує діагностику зв’язку на мережевому рівні. Багатьом знайома утиліта ping, що дозволяє перевірити зв’язок з віддаленим вузлом. В основі її роботи лежать спеціальні запити та відповіді, які визначаються в рамках протоколу ICMP. Крім того, цей же протокол визначає повідомлення, які отримує вузол, що відправив IP-пакет, якщо цей пакет з якихось причин не доставлений.


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


Протокол IGMP (Internet Group Management Protocol – протокол управління міжмережевими групами) призначений для управління групами вузлів, які мають один груповий IP-адресу. Відправлення пакета по такому адресою можна розглядати як щось середнє між адресною і широкомовної розсилкою, т.к. такий пакет буде отриманий відразу всіма вузлами, що входять в групу.


Протокол ARP (Address Resolution Protocol – протокол дозволу адрес) використовується для встановлення відповідності між IP-і MAC-адресами. Кожен вузол має таблицю відповідності. Вихідний пакет містить дві адреси вузла: MAC-адресу для канального рівня та IP-адреса для мережевого. Відправляючи пакет, вузол знаходить в своїй таблиці MAC-адресу, відповідний IP-адресою одержувача, і додає його до пакету. Якщо в таблиці таку адресу не знайдений, відправляється широкомовне повідомлення, формат якого визначається протоколом ARP. Отримавши таке повідомлення, вузол, чий IP-адреса відповідає шуканого, відправляє відповідь, в якому вказує свій MAC-адресу. Ця відповідь також є широкомовним, тому його отримують всі вузли, а не тільки що відправив запит, і всі вузли оновлюють свої таблиці відповідності.


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


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


Рівні сесії, уявлень та програми в стеку TCP / IP не розділені: протоколи HTTP, FTP, SMTP і т.д., що входять в цей стек, вирішують завдання всіх трьох рівнів. Ми тут не будемо розглядати ці протоколи, тому що при використанні сокетів вони в загальному випадку не потрібні: програміст сам визначає формат пакетів, що відправляються за допомогою TCP або UDP.


Новачки нерідко думають, що фраза “програма підтримує з’єднання через TCP / IP” повністю описує те, як можна зв’язатися з програмою і отримати дані. Насправді необхідно знати формат пакетів, які ця програма може приймати і відправляти, тобто повинні бути узгоджені протоколи рівня сесії і уявлень. Гнучкість сокетів дає програмісту можливість самостійно визначити цей формат, тобто, по суті справи, придумати і реалізувати власний протокол поверх TCP або UDP. І без опису цього протоколу організувати обмін даними з програмою неможливо.


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


Протокол UDP

Протокол UDP (User Datagram Protocol – протокол користувацьких дейтаграм) використовується рідше, ніж його “однокласник” TCP, але він простіше для розуміння, тому ми почнемо вивчення транспортних протоколів з нього. Коротко UDP можна описати як ненадійний протокол без з’єднання, заснований на дейтаграмах. Тепер розглянемо кожну з цих характеристик докладніше.


UDP не має ніяких додаткових засобів управління пакетами в порівнянні з IP. Це означає, що пакети, відправлені за допомогою UDP, можуть губитися, дублюватися і міняти порядок проходження. У мережі без роутерів нічого цього з пакетами майже ніколи не відбувається, і UDP може умовно вважатися надійним протоколом. Мережі з роутерами будуються, звичайно ж, таким чином, щоб подібні випадки відбувалися як можна рідше, але повністю виключити їх, тим не менше, не можна. Відбувається це через те, що передача даних може йти кількома шляхами через різні роутери. Наприклад, пакет може пропасти, якщо короткий шлях до віддаленого вузла тимчасово недоступний, а в довгому доводиться пройти більше роутерів, ніж це дозволено. Дублюватися пакети можуть, якщо вони помилково передаються двома шляхами, а порядок проходження може змінюватися, якщо пакет, посланий першим, йде по більш довгому шляху, ніж пакет, посланий другим.


Все вищесказане зовсім не означає, що на основі UDP не можна побудувати надійний обмін даними, просто турботу про це має взяти на себе сам додаток. Кожен вихідний пакет повинен містити порядковий номер, і у відповідь на нього має приходити спеціальний пакет – квитанція, яка повідомляє відправника, що пакет доставлений. При відсутності квитанції пакет висилається повторно (для цього необхідно ввести таймаут на отримання квитанції). Приймаюча сторона за номерами пакетів відновлює їх початковий порядок.


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


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


Щоб дані, передані різним сокетів, не перемішувалися, кожен сокет повинен отримати унікальний в межах вузла номер від 0 до 65535, званий номером порту. При відправці дейтаграми відправник вказує IP-адресу і порт одержувача, і система приймаючої сторони знаходить сокет, прив’язаний до зазначеного порту, і поміщає дані в його буфер. По суті справи, UDP є дуже простий надбудовою над IP, всі функції якої полягають в тому, що фізичний потік розділяється на декілька логічних за допомогою портів, і додається перевірка цілісності даних за допомогою контрольної суми (сам по собі протокол IP не гарантує відсутності спотворень даних при передачі).


Максимальний розмір однієї дейтаграми IP дорівнює 65535 байтам. З них не менше 20 байт займає заголовок IP. Заголовок UDP має розмір 8 байт. Таким чином, максимальний розмір однієї дейтаграми UDP становить 65507 байт.


Типова область застосування UDP – програми, для яких втрата пакетів некритична. Наприклад, деякі мережеві 3D-стрілялки в локальній мережі використовують UDP, тому що дуже часто посилають пакети, що інформують про дії гравця, і втрата одного пакета не призведе до суттєвих проблем: наступний пакет доставить необхідні дані. Достоїнствами UDP є простота встановлення зв’язку, можливість використання одного сокета для обміну даними з декількома адресами і відсутність необхідності відновлювати з’єднання після розриву зв’язку. У деяких задачах також дуже зручно те, що дейтаграми не змішуються, і одержувач завжди знає, які дані були відправлені однією дейтаграмою, а які – різними.


Ще одним достоїнством UDP є можливість відправки широкомовних дейтаграм. Для цього потрібно вказати широкомовна IP-адресу (звичайно це 255.255.255.255, але в деяких випадках можуть використовуватися адреси типу 192.168.100.225 для мовлення в межах мережі 192.168.100.ХХ і т.п.), і таку дейтаграму отримають всі сокети в локальній мережі, прив’язані до заданого порту. Цю можливість нерідко використовують програми, які заздалегідь не знають, з якими комп’ютерами вони повинні зв’язуватися. Вони посилають широкомовне повідомлення і зв’язуються з усіма вузлами, які розпізнали це повідомлення і надіслали на нього відповідну відповідь. За замовчуванням для широкомовних пакетів число роутерів, через які вони можуть пройти, встановлюється рівним нулю, тому такі пакети не виходять за межі підмережі.


Протокол TCP

Протокол TCP (Transmission Control Protocol – протокол управління передачею) є надійним потоковим протоколом із з’єднанням, тобто повною протилежністю UDP. Єдине, що у цих протоколів спільне – це спосіб адресації: в TCP кожному сокету також призначається унікальний номер порту. Унікальність номера порту потрібно тільки в межах протоколу: два сокета можуть використовувати однакові номери портів, якщо один з них працює через TCP, а інший – через UDP.


В TCP передбачені т.зв. добре відомі (well-known) порти, які зарезервовані для потреб системи і не повинні використовуватися програмами. Стандарт TCP визначає діапазон добре відомих портів від 0 до 255, в Windows і в деяких інших системах цей діапазон розширений до 0-1023. Частина портів UDP теж використовується для системних потреб, але зарезервованого діапазону в UDP немає. Крім того, деякі системні утиліти використовують порти за межами діапазону 0-1023. Повний список системних портів для TCP і UDP міститься в MSDN “е, в розділі Resource Kits / Windows 2000 Server Resource Kit / TCP / IP Core Networking/Appendixes/TCP and UDP Port Assignment.


Для відправки пакета за допомогою TCP відправнику необхідно спочатку встановити з’єднання з одержувачем. Після виконання цієї дії з’єднані таким чином сокети можуть використовуватися тільки для відправки повідомлень один одному. Якщо з’єднання розривається (самою програмою або через проблеми в мережі), ці сокети вже не можуть бути використані для встановлення нового з’єднання: вони повинні бути знищені, а замість них створені нові сокети.


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


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


Якщо через неполадки в мережі стався розрив зв’язку, при спробі відправити дані або прочитати їх клієнт отримає відмову, а з’єднання буде розірвано. Після цього клієнт повинен знищити сокет, створити новий і повторити підключення. Сервер також отримує помилку на сокеті, що обслуговує дане з’єднання, але істотно пізніше (ця затримка може досягати години). При виявленні помилки сервер просто знищує сокет і чекає нового підключення від клієнта. Можлива ситуація, коли клієнт вже підключився заново і для нього створено новий сокет, а старий сокет ще не закрито. Це не є суттєвою проблемою – на старому сокеті рано чи пізно буде отримана помилка, і він буде закритий. Тим не менш, сервер може враховувати таку ситуацію і знищувати старий сокет, не чекаючи, поки на ньому буде отримана помилка, якщо нове з’єднання з клієнтом вже встановлено. На вихідний сокет, що знаходиться в режимі очікування підключення, фізичні розриви зв’язку ніяк не впливають, після відновлення зв’язку жодних дій з ним проводити не потрібно.


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


Протокол TCP називається потоковим тому, що він збирає вхідні пакети в один потік. Зокрема, якщо в буфері сокета лежать 30 байт, прийняті по мережі, не існує можливості визначити, чи були ці 30 байт відправлені одним пакетом, 30-ю пакетами по 1 байту або ще як-небудь. Гарантується тільки те, що порядок байт в буфері збігається з тим порядком, в якому вони були відправлені. Приймаюча сторона також не обмежена в тому, як вона буде читати інформацію з буфера: все відразу або по частинах. Це істотно відрізняє TCP від ​​UDP, у якому дейтаграми не об’єднуються і не розбиваються на частини.


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


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


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


На відміну від UDP, при використанні TCP дані, які програма відправляє однією командою, можуть розбиватися на частини і відправлятися декількома IP-пакетами. Тому обмеження на довжину даних, що відправляються за один раз, в TCP відсутня (точніше, визначається доступними ресурсами системи). Кількість даних, що отримується відправником за одну операцію читання, обмежено розміром низкоуровнего буфера сокета і може бути різним у різних реалізаціях. Слід мати на увазі, що при переповненні буфера приймаючої сторони протокол TCP передбачає передачу відправляє стороні сигналу, по якому вона припиняє відправку, причому цей сигнал призупиняє всю передачу даних між цими двома комп’ютерами за допомогою TCP, тобто це може вплинути і на інші програми. Тому бажано не допускати таких ситуацій, коли у приймаючої сторони в буфері накопичується багато даних.


Створення сокета

До сих пір ми обговорювали тільки теоретичні аспекти використання сокетів. З цього розділу починається практична частина статті: будуть розглядатися конкретні функції, що дозволяють здійснювати ті чи інші операції з сокетами. Ці функції експортуються системної бібліотекою wsock32.dll (а також бібліотекою ws2_32.dll; взаємовідношення цих бібліотек буде обговорюватися в наступній статті циклу), для їх використання в Delphi в розділ uses потрібно додати стандартний модуль WinSock. Я не буду приводити тут вичерпного опису функцій, так як краще, ніж це зроблено в MSDN “е, мені не написати, але буду давати опис, досить повне для розуміння призначення функції, а також звертати увагу на деякі моменти, які в MSDN “е знайти важко. Тому я настійно рекомендую в доповнення до цієї статті уважно прочитати те, що написано в MSDN “е про кожну із згаданих мною функцій.


На самому початку статті говорилося, що тут ми будемо обговорювати тільки функції стандартної бібліотеки сокетів, а функції, специфічні для Windows, залишимо для наступної статті. Тим не менш, є три функції, відсутні в стандартній бібліотеці, знати які необхідно. Це функції WSAStartup, WSACleanup і WSAGetLastError (префікс WSA означає Windows Sockets API і використовується для іменування більшості функцій, що відносяться до Windows-розширенню бібліотеки сокетів).


Функція WSAStartup використовується для ініціалізації бібліотеки сокетів. Цю функцію необхідно викликати до виклику будь-якої іншої функції з цієї бібліотеки. Її прототип має вигляд:

function WSAStartup(wVersionRequired:Word;var WSData:TWSAData): Integer;

Параметр wVersionRequired задає необхідну версію бібліотеки сокетів. Молодший байт задає основну версію, старший – додаткову. Допустимі версії 1.0 ($ 0001), 1.1 ($ 0101), 2.0 ($ 0002) і 2.2 ($ 0202). Поки ми використовуємо стандартні сокети, принципової різниці між цими версіями немає, але версії 2.0 і вище краще не використовувати, тому що модуль WinSock не розрахований на їх підтримку. Питання взаємини бібліотек і версій будуть розглядатися в наступній статті даного циклу, а поки зупинимося на тому, що будемо завжди використовувати версію 1.1.


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


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


Функція WSACle anup завершує роботу з бібліотекою сокетів. Ця функція не має параметрів і повертає нуль у випадку успішного завершення або код помилки в іншому випадку.


Функцію WSAStartup досить викликати один раз, навіть в багатонитковою додатку. У цьому її відмінність від таких функцій, як, наприклад, CoInitialize, яка повинна бути викликана в кожної нитки, що використовує COM. Функцію можна викликати повторно – в цьому випадку її виклик не дає ніякого ефекту, але для завершення роботи з бібліотекою сокетів функція WSACleanup повинна бути викликана стільки ж разів, скільки було викликана WSAStartup.


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


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


Для створення сокета використовується стандартна функція Socket з наступним прототипом:

Function Socket(AF,SocketType,Protocol:Integer):TSocket;

Параметр AF задає сімейство адрес (address family). Цей параметр визначає, який спосіб адресації (тобто, по суті справи, який стек протоколів) буде використовуватися для даного сокета. При використанні TCP / IP цей параметр повинен бути рівний AF_Inet, для інших стеків також є відповідні константи, які можна подивитися у файлі WinSock.pas.


Параметр SocketType вказує на тип сокета і може приймати одне з двох значень: Sock_Stream (сокет використовується для потокових протоколів) і Sock_Dgram (сокет використовується для дейтаграмним протоколів).


Параметр Protocol дозволяє вказати, який саме протокол буде використовуватися сокетом. Цей параметр можна залишити рівним нулю – тоді буде обраний протокол за умовчанням, що відповідає заданим першими двома параметрами. Для стека TCP / IP потоковим протоколом за умовчанням є TCP, дейтаграмним – UDP. В деяких прикладах можна побачити, що значення третього параметра одно IPProto_IP. Значення цієї константи дорівнює 0, і її використання тільки підвищує читабельність коду, але призводить до того ж результату: буде обраний протокол за умовчанням. Якщо потрібно використовувати протокол, відмінний від протоколу за замовчуванням (наприклад, на базі IP існує протокол RDP – Reliable Datagram Protocol, надійний дейтаграмним протокол), слід вказати тут відповідну константу (для RDP це буде IPProto_RDP ). Можна також явно вказати на використання TCP або UDP за допомогою констант IPProto_TCP і IPProto_UDP відповідно.


Тип TSocket призначений для зберігання дескриптора сокета. Формально він збігається з 32-бітовим беззнакові цілим типом, але про це краще не згадувати, тому що будь-які операції над значеннями типу TSocket безглузді. Значення, що повертається функцією Socket, слід зберегти в змінній відповідного типу і потім використовувати для ідентифікації сокета при виклику інших функцій. Якщо з якихось причин створення сокета неможливо, функція поверне значення Invalid_Socket. Причину помилки можна дізнатися за допомогою функції WSAGetLastError.


Сокет, створений за допомогою функції Socket, не прив’язаний ні до якого адресою. Прив’язка здійснюється за допомогою функції Bind, що має наступний прототип:

function Bind(S:TSocket;var Addr:TSockAddr;NameLen:Integer):Integer;

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


Функція Bind призначена для сокетів, що реалізують різні протоколи з різних стеків, тому кодування адреси в ній зроблено досить універсальним. Втім, треба відзначити, що розробники модуля WinSock для Delphi вибрали не кращий спосіб переказу прототипу цієї функції на Паскаль, тому універсальність в значній мірі втрачена. В оригіналі прототип функції Bind має наступний вигляд:

int bind(SOCKET s,const struct sockaddr FAR* name,int namelen);

Видно, що другий параметр – це покажчик на структуру sockaddr. Проте компілятор C / C + + дозволяє при виконанні функції як параметр використовувати покажчик на будь-яку іншу структуру, якщо буде виконано явне приведення типів. Для кожного сімейства адрес використовується своя структура, і в якості фактичного параметра передається покажчик на цю структуру. Якби автори модуля WinSock описали друга параметр як параметр-значення типу покажчик, можна було б надходити точно так же. Однак вони описали цей параметр як параметр-змінну. В результаті на двійковому рівні нічого не змінилося: і там, і там в стек поміщається покажчик. Проте компілятор при виконанні функції Bind не допустить використання ніякої іншої структури, крім TSockAddr, а ця структура не універсальна і зручна, по суті справи, тільки при використанні стека TCP / IP. В інших випадках найкращим рішенням буде самостійно імпортувати функцію Bind з wsock32.dll з потрібним прототипом. При цьому доведеться імпортувати і деякі інші функції, що працюють з адресами. Втім, ми в даній статті обмежуємося тільки протоколами TCP і UDP, тому більше зупинятися на цьому питанні не будемо.


У стандартній бібліотеці сокетів (тобто в заголовних файлах для цієї бібліотеки, що входять в SDK) покладається, що адреса кодується структурою sockaddr довжиною 16 байт, причому перші два байти цієї структури кодують сімейство протоколів, а сенс інших залежить від цього сімейства. Зокрема, для стека TCP / IP сімейство протоколів задається константою PF_Inet. (Вище ми вже зустрічалися з терміном “сімейство адрес “і константою AF_Inet. У ранніх версіях бібліотеки сокетів сімейства протоколів і сімейства адрес були різними поняттями, але потім ці поняття злилися в одне, і константи AF_XXX і PF_XXX стали взаємозамінними.) Решта 14 байт структури sockaddr займає масив типу char (тип char в C / C + + відповідає одночасно двом типам Delphi: Char і ShortInt). В принципі, у стандартній бібліотеці сокетів передбачається, що структура, що задає адресу, завжди має довжину 16 байт, але про всяк випадок передбачений третій параметр функції Bind, який зберігає довжину структури. У Windows Sockets довжина структури може бути будь-який (це залежить від протоколу), так що цей параметр, в принципі, може згодиться.


Вище ми вже торкалися питання про те, що неструктуроване уявлення адреси у вигляді масиву з 14 байт буває незручно, і тому для кожного сімейства протоколів передбачена своя структура, що враховує особливості адреси. Зокрема, для протоколів стека TCP / IP використовується структура sockaddr_in, розмір якої також становить 16 байт. З них використовується тільки вісім: два для кодування сімейства протоколів, чотири для IP-адреси і два – для порту. Решта 8 байт не використовуються і повинні містити нулі.


Можна було б припустити, що типи TSockAddr і TSockAddrIn, описані в модулі WinSock, відповідають структурам sockaddr і sockaddr_in, проте це не так. Насправді ці типи описані наступним чином:

type SunB=packed record
s_b1,s_b2,s_b3,s_b4:u_char;
end;
SunW=packed record
s_w1,s_w2:u_short;
end;
in_addr=record
case Integer of
0:(S_un_b:SunB);
1:(S_un_w:SunW);
2:(S_addr:u_long);
end;
TInAddr=in_addr;
sockaddr_in=record
case Integer of
0:(sin_family:u_short;
sin_port:u_short;
sin_addr:TInAddr;
sin_zero:array[0..7] of Char);
1:(sa_family:u_short;
sa_data: array[0..13] of Char)
end;
TSockAddrIn=sockaddr_in;
TSockAddr=sockaddr_in;

Таким чином, типи TSockAddr і TSockAddrIn є синонімами типу sockaddr_in, але не того sockaddr_in, який є в стандартній бібліотеці сокетів, а типу sockaddr_in, описаного в модулі WinSock. А sockaddr_in з WinSock є варіантної записом, і в разі 0 відповідає типу sockaddr_in із стандартної бібліотеки сокетів, а в разі 1 – типу sockaddr з цієї ж бібліотеки. Ось така дещо заплутана ситуація, хоча на практиці все виглядає не так страшно.


Перейдемо, нарешті, до більш життєвому питання: якими значеннями потрібно заповнювати змінну типу TSockAddr, щоб при передачі її у функцію Bind сокет був прив’язаний до потрібною адресою. Так як ми обмежуємося розглядом протоколів TCP і UDP, нас не цікавить та частина варіантної записи sockaddr_in, яка відповідає випадку 1, тобто ми будемо розглядати тільки ті поля цієї структури, які мають префікс sin.


Поле sin_zero, очевидно, повинно містити масив нулів. Це те саме поле, яке не несе ніякого смислового навантаження і служить тільки для збільшення розміру структури до стандартних 16 байт. Поле sin_family повинно мати значення PF_Inet. В поле sin_port записується номер порту, до якого прив’язується сокет. Номер порту повинен бути записаний в мережевому форматі, тобто тут потрібно використовувати функцію HtoNS, щоб зі звичної нам записи номера порту отримати число в потрібному форматі. Номер порту можна залишити нульовим – тоді система вибере для сокета вільний порт з номером від 1024 до 5000.


IP-адреса для прив’язки сокета задається полем sin_addr, яке має тип TInAddr. Цей тип сам є варіантної записом, яка відображає три способи завдання IP-адреси: у вигляді 32-бітового числа, у вигляді двох 16-бітових чисел або у вигляді чотирьох 8-бітних чисел. На практиці найчастіше використовується формат у вигляді чотирьох 8-бітних чисел, рідше – у вигляді 32-бітового числа. Випадки використання формату з двох 16-бітних чисел мені невідомі.


Нехай у нас є змінна Addr типу TSockAddr, і нам потрібно в її поле sin_addr записати адресу 192.168.200.217. Це можна зробити наступним чином:

Addr.sin_addr.S_un_b.s_b1:=192;
Addr.sin_addr.S_un_b.s_b2:=168;
Addr.sin_addr.S_un_b.s_b3:=200;
Addr.sin_addr.S_un_b.s_b4:=217;

Існує альтернатива такому присвоєння чотирьох полів окремо – функція Inet_Addr. Ця функція в якості вхідного параметра приймає рядок, в якому записаний IP-адресу, і повертає цей IP-адреса в форматі 32-бітного числа. З використанням функції Inet_Addr вищенаведений код можна переписати так:

Addr.sin_addr.S_addr:=Inet_Addr(“192.168.200.217”);

Функція Inet_Addr виконує простий парсинг рядка і не перевіряє, чи існує така адреса насправді. Поля адреси можна задавати в десятковому, в вісімковому і в шістнадцятковому форматах. Вісімкове поле повинно починатися з нуля, шістнадцяткове – з “0x”. Наведений вище адресу можна записати у вигляді “0300.0250.0310.0331” (восьмеричний) або “0xC0.0xA8.0xC8.0xD9” (шістнадцятковий). Допускається також змішаний формат запису, в якому різні поля задані в різних системах числення. Функція Inet_Addr підтримує також менш поширені формати запису IP-адреси у вигляді трьох полів. Докладніше про це можна прочитати в MSDN “е.


У бібліотеці сокетів передбачена константа InAddr_Any, що дозволяє не вказувати явно адресу в програмі, а залишити його вибір на розсуд системи. Для цього треба полю sin_addr.S_addr присвоїти значення InAddr_Any. Якщо IP-адреса комп’ютера не призначений, при використанні цієї константи сокет буде прив’язаний до локальної адреси 127.0.0.1. Якщо комп’ютера призначений один IP-адресу, сокет буде прив’язаний до цього адресою. Якщо комп’ютеру призначено кілька IP-адрес, то буде обраний один з них, причому сама прив’язка при цьому відкладеться до встановлення з’єднання (в разі TCP) або до першої відправки даних через сокет (у разі UDP). Вибір конкретної адреси при цьому залежить від того, яку адресу має віддалена сторона.


Отже, резюмуємо вищесказане. Нехай у нас є сокет S, який треба прив’язати до адреси 192.168.200.217 і порту 3320. Для цього потрібно виконати наступний код:

Addr.sin_family:=PF_Inet;
Addr.sin_addr.S_addr:=Inet_Addr(“192.168.200.217”);
Addr.sin_port:=HtoNS(3320);
FillChar(Addr.sin_zero,SizeOf(Addr.sin_zero),0);
if Bind(S,Addr,SizeOf(Addr))=Socket_Error then
begin / / Якась помилка, аналізуємо за допомогою WSAGetLastError
end;

Процедура FillChar – це стандартна процедура Паскаля, що заповнює деяку область пам’яті заданим значенням. В даному випадку ми використовуємо її для заповнення нулями поля sin_zero. Для цієї ж мети можна було б використовувати функцію WinAPI ZeroMemory. У прикладах на C / C + + для цієї ж мети нерідко використовується функція memset.


Тепер розглянемо інший випадок: нехай вибір адреси і порту можна залишити на розсуд системи. Тоді код буде виглядати наступним чином:

Addr.sin_family:=PF_Inet;
Addr.sin_addr.S_addr:=InAddr_Any;
Addr.sin_port:=0;
FillChar(Addr.sin_zero,SizeOf(Addr.sin_zero),0);
if Bind(S,Addr,SizeOf(Addr))=Socket_Error then
begin / / Якась помилка, аналізуємо за допомогою WSAGetLastError
end;

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


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


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


Вище я вже говорив, що в системах з декількома мережевими картами прив’язка сокета до адреси в тому випадку, коли його вибір довірено системі, може здійснюватися не під час виконання функції Bind, а пізніше, коли системі стане зрозуміло, навіщо використовується цей сокет. Наприклад, коли TCP-клієнт здійснює підключення до сервера, система за адресою цього сервера визначає, через яку карту повинен йти обмін, і вибирає відповідну адресу. Те ж саме відбувається з UDP-клієнтом: коли він відправляє першу дейтаграму, система за адресою отримувача визначає, до якої карті слід прив’язати сокет. Тому клієнт і в даному випадку може залишити вибір адреси на розсуд системи. З серверами все трохи складніше. Система прив’язує сокет UDP-сервера до адреси, він очікує отримання пакета. В цей момент система не має ніякої інформації про те, з якими вузлами буде вестися обмін через даний сокет, і може вибрати не ту адресу, який потрібен. Тому сокети UDP-серверів, що працюють в подібних системах, повинні явно прив’язуватися до потрібною адресою. Сокети TCP-серверів, що знаходяться в режимі очікування і мають адресу InAddr_Any, допускають підключення до них з будь-якого мережевого інтерфейсу, який є в системі. Сокет, який створюється таким сервером при підключенні клієнта, буде автоматично прив’язаний до IP-адресою того мережевого інтерфейсу, через який здійснюється взаємодія з підключились клієнтом. Таким чином, сокети, створені для взаємодії з різними клієнтами, можуть виявитися прив’язаними до різних адресах.


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


Коли сокет більше не потрібен, необхідно звільнити пов’язані з ним ресурси. Це виконується в два етапи: спочатку сокет “вимикається”, а потім закривається.


Для виключення сокета використовується функція Shutdown, що має наступний прототип:

function Shutdown(S:TSocket;How:Integer):Integer;

Параметр S визначає сокет, який необхідно вимкнути, параметр How може приймати значення SD_Receive, SD_Send або SD_Both. Функція повертає нуль у випадку успішного виконання і Socket_Error в випадку помилки.


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


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


Параметр SD_Both дозволяє одночасно заборонити і прийом, і передачу даних через сокет.


Примітка: модуль WinSock до п’ятої версії Delphi включно містить помилку – в ньому не визначені константи SD_XXX. Щоб використовувати їх у своїй програмі, треба оголосити їх наступним чином:
const SD_Receive=0;
SD_Send=1;
SD_Both=2;

Для звільнення ресурсів, пов’язаних з сокетом, використовується функція CloseSocket. Ця функція звільняє пам’ять, виділену для буферів, і порт. Її єдиний параметр задає сокет, який потрібно закрити, а повертається значення – нуль або Socket_Error. Після виклику цієї функції відповідний дескриптор сокета перестає мати сенс, і використовувати його більше не можна.


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


Функція Shutdown потрібна в першу чергу для того, щоб заздалегідь повідомити партнерові по зв’язку про намір завершити зв’язок, причому це має сенс тільки для протоколів, що підтримують з’єднання. При використанні UDP функцію Shutdown викликати практично безглуздо, можна відразу викликати CloseSocket. При використанні TCP віддалена сторона отримує сигнал про вимикання партнера, але стандартна бібліотека сокетів не дозволяє програмі виявити його отримання (такі функції є в Windows Sockets, про що ми будемо говорити в наступній статті). Але цей сигнал може бути важливий для внутрішньосистемних функцій, що реалізують сокети. Windows-версія бібліотеки сокетів відноситься до відсутності даного сигналу досить ліберально, тому виклик Shutdown в тому випадку, коли і клієнт, і сервер працюють під управлінням Windows, не обов’язковий. Але реалізації TCP в інших системах не завжди настільки ж поблажливо ставляться до подібної недбалості. Результатом може стати довгий (до двох годин) “підвішений” стан сокета в тій системі, коли з ним і працювати вже не можна, та інформації про помилку програма не отримує. Тому при використанні TCP краще не нехтувати викликом Shutdown, щоб сокет на іншій стороні не мав проблем.


MSDN рекомендує наступний порядок закриття TCP-сокета. По-перше, сервер не повинен закривати свій сокет за власною ініціативою, він може це робити тільки після того, як був закритий пов’язаний з ним клієнтський сокет. Клієнт починає закриття сокета з виклику Shutdown з параметром SD_Send. Сервер після цього спочатку отримує всі дані, які залишалися в буфері сокета клієнта, а потім отримує від клієнта сигнал про завершення передачі. Тим не менш, сокет клієнта продовжує працювати на прийом, тому сервер при необхідності може на цьому етапі послати клієнту будь-які дані, якщо це необхідно. Потім сервер викликає Shutdown з параметром SD_Send, і відразу після цього – CloseSocket. Клієнт продовжує читати дані з вхідного буфера сокета до тих пір, поки не буде отриманий сигнал про завершення передачі сервером. Після цього клієнт також викликає CloseSocket. Така послідовність гарантує, що дані не будуть втрачені, але, як ми вже обговорювали вище, вона не може бути реалізована в рамках стандартних сокетів через неможливість отримати сигнал про завершення передачі, посланий віддаленої стороною. Тому слід використовувати спрощений спосіб завершення зв’язку: клієнт викликає Shutdown з параметром SD_Send або SD_Both, і відразу

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*