11. Інтернет-програмування
11.1. Généralités
11.1.1. Інтернет-протоколи
Тут ми подаємо вступ до протоколів зв’язку в Інтернеті, які також називають набором протоколів TCP/IP (Transfer Control Protocol / Internet Protocol) — за назвами двох основних протоколів. Читачеві може бути корисно мати загальне уявлення про функціонування мереж, а зокрема про протоколи TCP/IP, перш ніж приступати до розробки розподілених додатків. Наведений нижче текст є частковим перекладом уривка з документа «Lan Workplace for Dos — Administrator's Guide» від NOVELL, опублікованого на початку 90-х років.
Загальна концепція створення мережі гетерогенних комп’ютерів походить з досліджень, проведених DARPA (Агентство з перспективних оборонних досліджень) у США. DARPA розробила набір протоколів, відомий під назвою TCP/IP, який дозволяє гетерогенним машинам взаємодіяти між собою. Ці протоколи були протестовані в мережі під назвою ARPAnet, яка згодом перетворилася на мережу INTERNET. Протоколи TCP/IP визначають формати та правила передачі й прийому даних, незалежні від організації мереж та використовуваного обладнання.
Мережа, спроектована за допомогою DARPA та керована протоколами TCP/IP, є мережею з комутацією пакетів. Така мережа передає інформацію по мережі невеликими частинами, які називаються пакетами. Таким чином, якщо комп’ютер передає великий файл, його розбивають на невеликі частини, які надсилаються по мережі для подальшого відновлення у пункті призначення. TCP/IP визначає формат цих пакетів, а саме:
- джерело пакета
- призначення
- довжина
- тип
11.1.2. Модель OSI
Протоколи TCP/IP приблизно відповідають моделі відкритої мережі, яка називається OSI (Open Systems Interconnection Reference Model) і визначена ISO (Міжнародною організацією зі стандартизації). Ця модель описує ідеальну мережу, в якій взаємодію між машинами можна представити за допомогою семирівневої моделі:
![]() |
Кожен рівень отримує послуги від нижчого рівня та надає свої послуги вищому рівню. Припустимо, що два додатки, розташовані на різних машинах A та B, хочуть встановити зв’язок: вони роблять це на рівні Application. Їм не потрібно знати всіх деталей функціонування мережі: кожна програма передає інформацію, яку вона хоче надіслати, на нижній рівень — рівень Présentation. Отже, програмі потрібно знати лише правила взаємодії з рівнем Présentation.
Як тільки інформація потрапляє на рівень Présentation, вона за іншими правилами передається на рівень Session і так далі, доки інформація не потрапить на фізичний носій і не буде фізично передана на машину-приймач. Там вона пройде обробку, зворотну до тієї, яку вона пройшла на комп’ютері-відправнику.
На кожному рівні процес-відправник, відповідальний за надсилання інформації, надсилає її процесу-одержувачу на іншому комп’ютері, що належить до того самого рівня. Це відбувається за певними правилами, які називаються протоколом рівня. Отже, маємо таку кінцеву схему зв’язку:
![]() |
Роль різних рівнів така:
Забезпечує передачу бітів по фізичному носію. На цьому рівні знаходяться кінцеві пристрої обробки даних (E.T.T.D), такі як термінал або комп’ютер, а також пристрої завершення каналів передачі даних (E.T.C.D), такі як модулятор/демодулятор, мультиплексор, концентратор. На цьому рівні важливими є такі аспекти:
| |
Приховує фізичні особливості фізичного рівня. Виявляє та виправляє помилки передачі. | |
Керує маршрутом, яким має пройти інформація, що надсилається в мережі. Це називається routage: визначення маршруту, яким має пройти інформація, щоб дістатися до адресата. | |
Забезпечує взаємодію між двома додатками, тоді як попередні рівні дозволяли лише взаємодію між машинами. Однією зі служб, що надаються цим рівнем, може бути мультиплексування: транспортний рівень може використовувати одне й те саме мережеве з’єднання (від машини до машини) для передачі інформації, що належить кільком додаткам. | |
На цьому рівні знаходяться послуги, що дозволяють додатку відкривати та підтримувати робочу сесію на віддаленому комп’ютері. | |
Вона спрямована на уніфікацію представлення даних на різних машинах. Таким чином, дані, що надходять з машини A, будуть «оформлені» шаром Présentation машини A відповідно до стандартного формату перед відправкою в мережу. Потрапивши на рівень Présentation комп’ютера-одержувача B, який розпізнає їх завдяки стандартному формату, дані будуть «оформлені» іншим чином, щоб програма комп’ютера B могла їх розпізнати. | |
На цьому рівні знаходяться програми, які зазвичай використовуються користувачем, такі як електронна пошта або передача файлів. |
11.1.3. Модель TCP/IP
Модель OSI — це ідеальна модель, яка ще ніколи не була реалізована. Набір протоколів TCP/IP наближається до неї у такому вигляді:
![]() |
Фізичний рівень
У локальній мережі зазвичай використовується технологія Ethernet або Token-Ring. Тут ми розглянемо лише технологію Ethernet.
Ethernet
Ця назва походить від технології локальних мереж з комутацією пакетів, винайденої в компанії Xerox на початку 1970-х років і стандартизованої компаніями Xerox, Intel та Digital Equipment у 1978 році. Фізично мережа складається з коаксіального кабелю діаметром приблизно 1,27 см і довжиною не більше 500 м. Її можна розширити за допомогою répéteurs, причому між двома пристроями може бути не більше двох повторювачів. Кабель є пасивним: усі активні елементи знаходяться на пристроях, підключених до кабелю. Кожен пристрій підключається до кабелю за допомогою мережевої карти, що містить:
- передавач (transceiver), який виявляє наявність сигналів у кабелі та перетворює аналогові сигнали на цифрові і навпаки;
- з'єднувач, який приймає цифрові сигнали від передавача та передає їх на комп'ютер для обробки або навпаки.
Основні характеристики технології Ethernet такі:
- Пропускна здатність 10 мегабіт/секунду.
- Шинна топологія: усі пристрої підключені до одного кабелю
![]() |
- Мережа з розсилковим режимом — комп'ютер-відправник передає інформацію по кабелю разом з адресою комп'ютера-одержувача. Усі підключені комп'ютери отримують цю інформацію, але зберігає її лише той, якому вона призначена.
- Метод доступу полягає в наступному: передавач, який бажає здійснити передачу, прослуховує кабель — таким чином він виявляє наявність або відсутність несучої хвилі, наявність якої означатиме, що передача вже відбувається. Це технологія CSMA (Carrier Sense Multiple Access). За відсутності несучої хвилі передавач може вирішити передавати у свою чергу. Таке рішення можуть прийняти одразу кілька передавачів. Випромінювані сигнали змішуються: це називається колізією. Передавач виявляє цю ситуацію: одночасно з передачею по кабелю він прослуховує, що насправді проходить по ньому. Якщо він виявляє, що інформація, яка проходить по кабелю, не є тією, яку він сам передав, він робить висновок про наявність колізії та припиняє передачу. Інші передавачі, які здійснювали передачу, зроблять те саме. Кожен відновить передачу через випадковий проміжок часу, що залежить від конкретного передавача. Ця техніка називається CD (Collision Detect). Відповідно, метод доступу називається CSMA/CD.
- 48-бітна адресація. Кожна машина має адресу, яку тут називають фізичною адресою, яка записана на платі, що з’єднує її з кабелем. Цю адресу називають адресою Ethernet машини.
Мережевий рівень
На рівні цього рівня ми знаходимо протоколи IP, ICMP, ARP та RARP.
Передає пакети між двома вузлами мережі | |
ICMP забезпечує зв’язок між програмою протоколу IP на одній машині та програмою того ж протоколу на іншій машині. Отже, це протокол обміну повідомленнями всередині самого протоколу IP. | |
здійснює відповідність між інтернет-адресою машини та фізичною адресою машини | |
здійснює відповідність між фізичною адресою пристрою та його інтернет-адресою |
Транспортний та сесійний рівні
У цьому рівні містяться такі протоколи:
Забезпечує надійну передачу інформації між двома клієнтами | |
Забезпечує ненадійну передачу інформації між двома клієнтами |
Рівні «Додаток», «Представлення» та «Сесія»
Тут використовуються різні протоколи:
Емулятор терміналу, що дозволяє машині A підключатися до машини B як термінал | |
дозволяє передавати файли | |
дозволяє передавати файли | |
дозволяє обмінюватися повідомленнями між користувачами мережі | |
перетворює ім'я комп'ютера на його інтернет-адресу | |
створений компанією Sun MicroSystems, визначає стандартне представлення даних, незалежне від конкретних машин | |
також визначений компанією Sun; це протокол зв’язку між віддаленими додатками, незалежний від транспортного рівня. Цей протокол є важливим: він звільняє програміста від необхідності знати деталі транспортного рівня та робить додатки переносимими. Цей протокол базується на протоколі XDR | |
, який також був розроблений компанією Sun; цей протокол дозволяє одній машині «бачити» файлову систему іншої машини. Він базується на попередньому протоколі RPC |
11.1.4. Принцип роботи інтернет-протоколів
Додатки, розроблені в середовищі TCP/IP, зазвичай використовують кілька протоколів цього середовища. Прикладний додаток взаємодіє з найвищим рівнем протоколів. Цей рівень передає інформацію на нижній рівень і так далі, аж до фізичного носія. Там інформація фізично передається на приймальний пристрій, де вона знову проходить через ті самі рівні, цього разу у зворотному напрямку, аж доки не досягне програми-одержувача надісланої інформації. Наступна схема ілюструє шлях проходження інформації:
![]() |
Розглянемо приклад: додаток FTP, визначений на рівні шару Application, який забезпечує передачу файлів між машинами.
- Додаток передає послідовність байтів, що підлягає передачі, на рівень transport.
- Рівень transport розбиває цей послідовність байтів на segments та TCP і додає на початок кожного сегмента його номер. Сегменти передаються на мережевий рівень, що керується протоколом IP.
- Рівень IP створює пакет, що інкапсулює отриманий сегмент TCP. У заголовку цього пакета вона розміщує інтернет-адреси комп’ютерів-відправника та одержувача. Вона також визначає фізичну адресу комп’ютера-одержувача. Усе це передається на рівень передачі даних та фізичного зв’язку, тобто на мережеву карту, яка з’єднує комп’ютер із фізичною мережею.
- Там пакет IP, у свою чергу, інкапсулюється у фізичну рамку та надсилається адресату по кабелю.
- На комп’ютері-одержувачі рівень «Зв’язок даних та фізичний зв’язок» виконує зворотну операцію: він розпаковує пакет IP з фізичної рамки та передає його на рівень IP.
- Рівень IP перевіряє правильність пакета: він обчислює контрольну суму на основі отриманих бітів (checksum), яку має збігатися з контрольною сумою в заголовку пакета. Якщо це не так, пакет відкидається.
- Якщо пакет визнано правильним, рівень IP розпаковує сегмент TCP, що міститься в ньому, і передає його на вищий рівень transport.
- Рівень transport (у нашому прикладі — рівень TCP) перевіряє номер сегмента, щоб відновити правильний порядок сегментів.
- Вона також обчислює контрольну суму для сегмента TCP. Якщо вона визнається правильною, рівень TCP надсилає підтвердження про отримання на джерельну машину, інакше сегмент TCP відхиляється.
- Залишається лише шару TCP передати частину даних сегмента до програми-одержувача цих даних у верхньому шарі.
11.1.5. Проблеми адресації в Інтернеті
noeud у мережі може бути комп’ютером, інтелектуальним принтером, файловим сервером — фактично будь-чим, що може здійснювати зв’язок за допомогою протоколів TCP/IP. Кожен вузол має фізичну адресу, формат якої залежить від типу мережі. У мережі Ethernet фізична адреса кодується у 6 байтах. Адреса мережі X25 — це 14-значне число.
Інтернет-адреса вузла є логічною адресою: вона не залежить від апаратного забезпечення та використовуваної мережі. Це 4-байтова адреса, яка ідентифікує як локальну мережу, так і вузол цієї мережі. Інтернет-адреса зазвичай подається у вигляді 4 чисел — значень 4 байтів, розділених крапкою. Так, адреса комп’ютера Lagaffe на факультеті природничих наук в Анже записується як 193.49.144.1, а комп’ютера Liny — як 193.49.144.9. Звідси випливає, що інтернет-адреса локальної мережі становить 193.49.144.0. У цій мережі може бути до 254 вузлів.
Оскільки інтернет-адреси або адреси IP не залежать від мережі, комп’ютер із мережі A може спілкуватися з комп’ютером із мережі B, не зважаючи на тип мережі, у якій він знаходиться: достатньо, щоб він знав свою адресу IP. Протокол IP кожної мережі відповідає за перетворення адреси IP <--> фізичної адреси в обох напрямках.
Усі адреси IP мають бути різними. У Франції саме INRIA відповідає за присвоєння адрес IP. Фактично ця організація видає адресу для вашої локальної мережі, наприклад 193.49.144.0 для мережі факультету природничих наук в Анже. Адміністратор цієї мережі може потім розподіляти адреси IP від 193.49.144.1 до 193.49.144.254 на свій розсуд. Ця адреса зазвичай записується в спеціальний файл на кожному комп’ютері, підключеному до мережі.
11.1.5.1. Класи адрес IP
Адреса IP — це послідовність із 4 байтів, яка часто позначається як I1.I2.I3.I4 і фактично містить дві адреси:
- адресу мережі
- адресу вузла цієї мережі
Залежно від розміру цих двох полів адреси IP поділяються на 3 класи: класи A, B та C.
Клас A
Адреса IP: I1.I2.I3.I4 має вигляд R1.N1.N2.N3, де
R1 — це мережева адреса
N1.N2.N3 — адреса комп’ютера в цій мережі
Точніше кажучи, форма адреси класу A IP має такий вигляд:
Мережева адреса займає 7 бітів, а адреса вузла — 24 біти. Отже, може існувати 127 мереж класу A, кожна з яких може містити до 2²⁴ вузлів.
Клас B
Тут адреса IP : I1.I2.I3.I4 має вигляд R1.R2.N1.N2, де
R1.R2 — це адреса мережі
N1.N2 — адреса комп’ютера в цій мережі
Точніше кажучи, форма адреси класу B IP має такий вигляд:
Адреса мережі займає 2 байти (точніше — 14 бітів), так само як і адреса вузла. Отже, може існувати 2¹⁴ мереж класу B, кожна з яких містить до 2¹⁶ вузлів.
Клас C
У цьому класі адреса IP: I1.I2.I3.I4 має вигляд R1.R2.R3.N1, де
R1.R2.R3 — це адреса мережі
N1 — це адреса комп’ютера в цій мережі
Точніше кажучи, форма адреси класу C IP має такий вигляд:
![]() |
Мережева адреса займає 3 байти (мінус 3 біти), а адреса вузла — 1 байт. Отже, може існувати 221 мережа класу C, що містять до 256 вузлів.
Оскільки адреса комп’ютера Lagaffe на факультеті природничих наук в Анже становить 193.49.144.1, бачимо, що старший байт дорівнює 193, тобто у двійковій системі числення — 11000001. Звідси випливає, що мережа належить до класу C.
Зарезервовані адреси
- Деякі адреси, такі як IP, є адресами мереж, а не адресами вузлів у мережі. Це ті адреси, в яких адреса вузла встановлена на 0. Так, адреса 193.49.144.0 є адресою IP мережі Факультету наук міста Анже. Отже, жоден вузол мережі не може мати нульову адресу.
- Коли в адресі IP адреса вузла складається виключно з цифр 1, маємо адресу розсилки: ця адреса позначає всі вузли мережі.
- У мережі класу C, яка теоретично дозволяє мати 2⁸ = 256 вузлів, якщо виключити дві заборонені адреси, залишається лише 254 дозволених адреси.
11.1.5.2. Протоколи перетворення «Інтернет-адреса <--> фізична адреса»
Ми бачили, що під час передачі інформації від одного комп’ютера до іншого ці дані під час проходження через рівень IP інкапсулюються в пакети. Вони мають такий вигляд:
![]() |
Отже, пакет IP містить інтернет-адреси комп’ютерів-джерел та комп’ютерів-приймачів. Коли цей пакет передається на рівень, відповідальний за його відправлення у фізичну мережу, до нього додається додаткова інформація для формування фізичного кадру, який зрештою буде надіслано в мережу. Наприклад, формат кадру в мережі Ethernet є таким:
![]() |
У кінцевій рамці містяться фізичні адреси комп’ютерів-відправника та комп’ютера-одержувача. Як їх отримують?
Комп’ютер-відправник, знаючи адресу IP комп’ютера, з яким він хоче встановити зв’язок, отримує його фізичну адресу за допомогою спеціального протоколу, який називається ARP (Address Resolution Protocol).
- Вона надсилає пакет особливого типу, який називається пакетом ARP, що містить адресу IP комп’ютера, фізичну адресу якого потрібно знайти. Вона також подбала про те, щоб включити до нього свою власну адресу IP, а також свою фізичну адресу.
- Цей пакет надсилається до всіх вузлів мережі.
- Вони розпізнають особливий характер пакета. Вузол, який розпізнає у пакеті свою адресу IP, відповідає, надсилаючи відправнику пакета свою фізичну адресу. Як це можливо? Він знайшов у пакеті адреси IP та фізичну адресу відправника.
- Отже, відправник отримує фізичну адресу, яку шукав. Він зберігає її в пам’яті, щоб мати змогу використати її згодом, якщо потрібно буде надіслати інші пакети тому самому одержувачу.
Адреса IP комп’ютера зазвичай записана в одному з його файлів, який він може переглянути, щоб дізнатися її. Цю адресу можна змінити: достатньо відредагувати файл. Фізична адреса, у свою чергу, записана в пам’яті мережевої карти і не може бути змінена.
Коли адміністратор бажає організувати свою мережу по-іншому, йому може знадобитися змінити адреси IP усіх вузлів, а отже, редагувати різні файли конфігурації різних вузлів. Це може бути трудомістким процесом і спричиняти помилки, якщо комп’ютерів багато. Один із методів полягає в тому, щоб не призначати комп’ютерам адреси IP: у цьому випадку у файл, з якого комп’ютер мав би отримати свою адресу IP, вписується спеціальний код. Виявивши, що у неї немає адреси IP, машина запитує її за протоколом, який називається RARP (Reverse Address Resolution Protocol). Потім вона надсилає в мережу спеціальний пакет, який називається пакетом RARP, аналогічний попередньому пакету ARP, у який вона вказує свою фізичну адресу. Цей пакет надсилається до всіх вузлів, які потім розпізнають пакет RARP. Один із них, який називається сервером RARP, має файл, що містить відповідність «фізична адреса <--> адреса IP» для всіх вузлів. Тоді він відповідає відправнику пакета RARP, надіславши йому свою адресу IP. Отже, адміністратор, який бажає переналаштувати свою мережу, повинен лише відредагувати файл відповідності сервера RARP. Цей сервер зазвичай повинен мати фіксовану адресу IP, яку він повинен знати, не використовуючи сам протокол RARP.
11.1.6. Мережевий рівень, відомий як рівень IP в Інтернеті
Протокол IP (Інтернет-протокол) визначає форму, яку повинні мати пакети, та спосіб їх обробки під час передачі або прийому. Цей конкретний тип пакета називається датаграмою IP. Ми вже його розглядали:
![]() |
Важливо, що крім даних, які потрібно передати, датаграма IP містить інтернет-адреси комп’ютерів-відправника та комп’ютера-одержувача. Таким чином, комп’ютер-одержувач знає, хто надсилає йому повідомлення.
На відміну від мережевої рамки, довжина якої визначається фізичними характеристиками мережі, через яку вона проходить, довжина датаграми IP встановлюється програмним забезпеченням і тому буде однаковою в різних фізичних мережах. Ми бачили, що при переході від мережевого рівня до фізичного рівня датаграма IP була інкапсульована у фізичний кадр. Ми навели приклад фізичного кадру мережі Ethernet:
Фізичні кадри переміщуються від вузла до вузла до місця призначення, яке може не знаходитися в тій самій фізичній мережі, що й відправник. Отже, пакет IP може послідовно інкапсулюватися в різні фізичні кадри на вузлах, що з’єднують дві мережі різних типів. Також може статися так, що пакет IP виявиться занадто великим, щоб його можна було інкапсулювати у фізичний кадр. У такому разі програмне забезпечення IP вузла, де виникає ця проблема, розбиває пакет IP на fragments згідно з чіткими правилами, після чого кожен із них надсилається у фізичну мережу. Вони будуть знову зібрані лише у кінцевому пункті призначення.
11.1.6.1. Маршрутизація
Маршрутизація — це метод доставки пакетів IP до місця призначення. Існує два методи: пряма маршрутизація та непряма маршрутизація.
Пряма маршрутизація
Пряма маршрутизація — це передача пакета IP безпосередньо від відправника до одержувача в межах однієї мережі:
- Комп’ютер-відправник датаграми IP має адресу одержувача IP.
- Вона отримує фізичну адресу одержувача за допомогою протоколу ARP або зі своїх таблиць, якщо ця адреса вже була отримана.
- Вона надсилає пакет у мережу на цю фізичну адресу.
Непряма маршрутизація
Непряма маршрутизація означає передачу пакета IP до пункту призначення, що знаходиться в іншій мережі, ніж та, до якої належить відправник. У цьому випадку мережеві частини адрес IP комп’ютерів-джерел та комп’ютерів-приймачів відрізняються. Комп’ютер-джерело розпізнає цю особливість. Тоді вона надсилає пакет до спеціального вузла, який називається маршрутизатором (router) — вузла, що з’єднує локальну мережу з іншими мережами, і адресу якого (IP) вона знаходить у своїх таблицях; ця адреса була отримана спочатку або з файлу, або з постійної пам’яті, або ж через інформацію, що циркулює в мережі.
Маршрутизатор підключений до двох мереж і має адресу IP у межах цих двох мереж.
![]() |
У нашому прикладі вище:
. Мережа № 1 має інтернет-адресу 193.49.144.0, а мережа № 2 — адресу 193.49.145.0.
. У мережі № 1 маршрутизатор має адресу 193.49.144.6, а в мережі № 2 — адресу 193.49.145.3.
Завдання маршрутизатора полягає в тому, щоб перетворити пакет IP, який він отримує і який міститься у фізичній рамці, типовій для мережі № 1, у фізичну рамку, яка може передаватися в мережі № 2. Якщо адреса IP одержувача пакета знаходиться в мережі № 2, маршрутизатор надішле йому пакет безпосередньо; в іншому випадку він надішле його іншому маршрутизатору, який з'єднує мережу № 2 з мережею № 3, і так далі.
11.1.6.2. Повідомлення про помилки та контроль
Також на мережевому рівні, тобто на тому ж рівні, що й протокол IP, існує протокол ICMP (Internet Control Message Protocol). Він слугує для надсилання повідомлень про внутрішнє функціонування мережі: несправні вузли, затори на маршрутизаторі тощо... Повідомлення ICMP інкапсулюються в пакети IP і надсилаються по мережі. Рівні IP різних вузлів вживають відповідних заходів відповідно до отриманих повідомлень ICMP. Таким чином, сама програма ніколи не бачить цих проблем, властивих мережі.
Вузол використовуватиме інформацію ICMP для оновлення своїх таблиць маршрутизації.
11.1.7. Транспортний рівень: протоколи UDP та TCP
11.1.7.1. Протокол UDP: User Datagram Protocol
Протокол UDP забезпечує ненадійний обмін даними між двома точками, тобто правильна доставка пакета до місця призначення не гарантується. Додаток, за бажанням, може самостійно керувати цим процесом, наприклад, очікуючи після відправлення повідомлення підтвердження про отримання, перш ніж відправляти наступне.
Наразі на рівні мережі ми говорили про адреси IP комп’ютерів. Однак на одному комп’ютері можуть одночасно існувати різні процеси, які можуть взаємодіяти між собою. Тому під час надсилання повідомлення потрібно вказувати не лише адресу IP комп’ютера-одержувача, а й «ім’я» процесу-одержувача. Це ім’я насправді є номером, який називається номером порту. Деякі номери зарезервовані для стандартних програм: наприклад, порт 69 для програми tftp (trivial file transfer protocol).
Пакети, що обробляються протоколом UDP, також називаються датаграмами. Вони мають такий вигляд:
Ці датаграми інкапсулюються в пакети IP, а потім — у фізичні кадри.
11.1.7.2. Протокол TCP: Протокол керування передачею
Для безпечного обміну даними протоколу UDP недостатньо: розробник додатків повинен самостійно розробити протокол, що дозволить йому перевіряти правильність маршрутизації пакетів. Протокол TCP (Transfer Control Protocol) дозволяє уникнути цих проблем. Його характеристики такі:
- Процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом машини-відправника та портом машини-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання.
- Усі пакети, що надсилаються процесом-джерелом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені, чого не гарантував протокол UDP, оскільки пакети могли пролягати різними шляхами.
- Передана інформація має безперервний характер. Процес-відправник надсилає інформацію у своєму ритмі. Ця інформація не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо для відправлення. Вона зберігається у структурі, що називається сегментом TCP. Цей сегмент, щойно заповнений, буде переданий на рівень IP, де його інкапсулюють у пакет IP.
- Кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP на стороні одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає підтвердження про отримання відправнику.
- Коли останній отримує його, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням, чого не було можливо з протоколом UDP.
- Якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації.
- Віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-джерело продовжує надсилати інформацію. Це дозволяє, наприклад, вихідному протоколу TCP надсилати кілька сегментів, не чекаючи на підтвердження отримання. Якщо через певний час він виявляє, що не отримав підтвердження отримання певного сегмента № n, він відновить передачу сегментів з цієї точки.
11.1.8. Рівень додатків
Над протоколами UDP та TCP існують різні стандартні протоколи:
TELNET
Цей протокол дозволяє користувачеві з комп’ютера A в мережі підключитися до комп’ютера B (який часто називають хост-комп’ютером). TELNET емулює на комп’ютері A так званий універсальний термінал. Таким чином, користувач діє так, ніби має термінал, підключений до комп’ютера B. Telnet базується на протоколі TCP.
FTP: (Протокол передачі файлів)
Цей протокол дозволяє обмінюватися файлами між двома віддаленими машинами, а також виконувати операції з файлами, такі як, наприклад, створення каталогів. Він базується на протоколі TCP.
TFTP: (Тривіальний контроль передачі файлів)
Цей протокол є варіантом FTP. Він базується на протоколі UDP і є менш досконалим, ніж FTP.
DNS: (Система доменних імен)
Коли користувач бажає обмінюватися файлами з віддаленим комп’ютером, наприклад, за допомогою FTP, він повинен знати інтернет-адресу цього комп’ютера. Наприклад, щоб виконати FTP на комп’ютері Lagaffe в університеті Анже, потрібно запустити FTP наступним чином: FTP 193.49.144.1
Це вимагає наявності довідника, що встановлює відповідність між машиною та адресою IP. Ймовірно, у цьому довіднику машини позначатимуться символічними іменами, такими як:
машина DPX2/320 університету Анже
машина Sun університету Анже ISERPA
Очевидно, що було б зручніше позначати комп’ютер за іменем, а не за його адресою IP. Тоді постає проблема унікальності імені: між собою з’єднано мільйони комп’ютерів. Можна було б уявити, що імена присвоює централізована організація. Це, безсумнівно, було б досить громіздким. Насправді контроль за іменами розподілено між доменами. Кожна доменна зона управляється, як правило, невеликою організацією, яка має повну свободу у виборі імен комп’ютерів. Так, комп’ютери у Франції належать до доменної зони fr, якою керує паризький інститут Inria. Щоб ще більше спростити ситуацію, контроль розподіляють далі: всередині доменної зони fr створюються піддомени. Так, університет Анже належить до піддомену univ-Angers. Служба, що управляє цим доменом, має повну свободу у виборі імен комп’ютерів у мережі Університету Анже. Наразі цей домен не поділений на піддомени. Але у великому університеті, що має багато комп’ютерів у мережі, такий поділ може бути здійснений.
Комп’ютер DPX2/320 Університету Анже отримав ім’я Lagaffe, тоді як PC та 486DX50 отримали ім’я liny. Як посилатися на ці машини ззовні? Вказавши ієрархію доменів, до яких вони належать. Таким чином, повна назва машини Lagaffe буде:
Lagaffe.univ-Angers.fr
У межах доменів можна використовувати відносні імена. Отже, у межах домену fr та поза доменом univ-Angers на комп’ютер Lagaffe можна посилатися як
Lagaffe.univ-Angers
Нарешті, у межах домену univ-Angers на нього можна посилатися просто як
Lagaffe
Отже, програма може посилатися на комп’ютер за його іменем. Зрештою, все одно потрібно отримати інтернет-адресу цього комп’ютера. Як це здійснюється? Припустимо, що з комп’ютера A ми хочемо встановити зв’язок із комп’ютером B.
- якщо машина B належить до того самого домену, що й машина A, її адресу IP, ймовірно, можна буде знайти у файлі на машині A.
- інакше комп’ютер А знайде в іншому файлі або в тому самому, що й раніше, список декількох серверів імен із їхніми адресами IP. Сервер імен відповідає за встановлення відповідності між іменем комп’ютера та його адресою IP. Комп'ютер A надішле спеціальний запит до першого сервера імен зі свого списку, який називається запитом DNS і, отже, містить ім'я шуканого комп'ютера. Якщо запитуваний сервер має це ім'я у своїх таблицях, він надішле комп'ютеру A відповідну адресу IP. В іншому випадку сервер також знайде у своїх файлах список серверів імен, до яких він може звернутися. Тоді він це зробить. Таким чином, буде здійснено запит до певної кількості серверів імен — не хаотично, а таким чином, щоб мінімізувати кількість запитів. Якщо комп’ютер врешті-решт буде знайдено, відповідь надійде назад до комп’ютера A.
XDR: (eXternal Data Representation)
Створений компанією Sun MicroSystems, цей протокол визначає стандартне представлення даних, незалежне від конкретних машин.
RPC: (Виклик віддаленої процедури)
Цей протокол, також визначений компанією Sun, забезпечує зв’язок між віддаленими додатками, незалежно від транспортного рівня. Цей протокол є важливим: він звільняє програміста від необхідності знати деталі транспортного рівня та робить додатки переносимими. Цей протокол базується на протоколі XDR
NFS: Мережева файлова система
Цей протокол, також розроблений компанією Sun, дозволяє одній машині «бачити» файлову систему іншої машини. Він базується на попередньому протоколі RPC.
11.1.9. Висновок
У цьому вступі ми представили основні положення щодо інтернет-протоколів. Щоб глибше ознайомитися з цією темою, можна прочитати чудову книгу Дугласа Комера:
Назва TCP/IP: Архітектура, протоколи, додатки.
Автор Дуглас COMER
Видавництво InterEditions
11.2. Класи .NET управління адресами IP
Кожен пристрій в Інтернеті однозначно ідентифікується за адресою IP (Інтернет-протокол), яка може мати два види:
- IPv4: кодується на 32 бітах і представлена рядком у формі «I1.I2.I3.I4», де In — число від 1 до 254. Це найпоширеніші на сьогодні адреси IP.
- IPv6: кодується на 128 бітах і представлений рядком у вигляді «[I1.I2.I3.I4.I5.I6.I7.I8]», де In — це рядок із 4 шістнадцяткових цифр. У цьому документі ми не будемо використовувати адреси IPv6.
Машину також можна визначити за допомогою унікального імені. Це ім’я не є обов’язковим, оскільки програми в кінцевому підсумку завжди використовують адреси машин у форматі IP. Імена призначені для зручності користувачів. Таким чином, за допомогою браузера простіше звернутися до http://www.ibm.com (URL), ніж до URL або http://129.42.17.99, хоча обидва способи є можливими.
Комп’ютер може мати кілька адрес IP, якщо він фізично підключений до кількох мереж одночасно. У такому разі він має адресу IP у кожній мережі.
Адреса IP може бути представлена у .NET двома способами:
- у вигляді символьного рядка «I1.I2.I3.I4» або «[I1.I2.I3.I4.I5.I6.I7.I8]»
- у вигляді об’єкта типу IPAddress
Клас IPAddress
Серед методів M, властивостей P та констант C класу IPAddress є такі:
P | сімейство адреси IP. Тип AddressFamily є переліком. Два поширені значення: AddressFamily.InterNetwork: для адреси IPv4 AddressFamily.InterNetworkV6: для адреси IPv6 | |
C | адреса IP «0.0.0.0». Коли служба пов’язана з цією адресою, це означає, що вона приймає клієнтів за всіма адресами IP на машині, на якій вона працює. | |
C | адреса IP «127.0.0.1». Називається «адресою петлі». Коли служба пов’язана з цією адресою, це означає, що вона приймає лише клієнтів, які знаходяться на тій самій машині, що й вона. | |
C | адреса IP «255.255.255.255». Коли служба пов’язана з цією адресою, це означає, що вона не приймає жодних клієнтів. | |
M | намагається перетворити адресу IP ipString у формі «I1.I2.I3.I4» на об’єкт IPAddress address. Повертає true, якщо операція виконана успішно. | |
M | повертає true, якщо адреса IP дорівнює «127.0.0.1» | |
M | перетворює адресу IP у форму «I1.I2.I3.I4» або «[I1.I2.I3.I4.I5.I6.I7.I8]» |
Зв’язок між адресами IP <--> nomMachine забезпечується розподіленою службою Інтернету, яка називається DNS (Система доменних імен). Статичні методи класу Dns дозволяють встановити зв’язок між адресами IP <--> nomMachine:
повертає адресу IPHostEntry на основі адреси IP у вигляді рядка або на основі імені комп’ютера. Генерує виняток, якщо комп’ютер не вдається знайти. | |
повертає адресу IPHostEntry на основі адреси IP типу IPAddress. Генерує виняток, якщо машину не вдається знайти. | |
повертає ім’я машини, на якій виконується програма, що виконує цю інструкцію | |
повертає адреси IP машини, ідентифікованої за її ім’ям або однією з її адрес IP. |
Екземпляр IPHostEntry інкапсулює адреси IP, псевдоніми та ім’я машини. Тип IPHostEntry має такий вигляд:
P | таблиця адрес IP комп’ютера | |
P | псевдоніми DNS машини. Це імена, що відповідають різним адресам IP машини. | |
P | головне ім’я хоста комп’ютера |
Розглянемо наступну програму, яка виводить ім’я комп’ютера, на якому вона виконується, а потім у інтерактивному режимі надає відповідності між адресою IP та ім’ям комп’ютера:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// відображає ім’я локального комп’ютера
// потім у інтерактивному режимі надає інформацію про мережеві комп’ютери
// ідентифіковані за іменем або адресою IP
// локальний комп'ютер
Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
// інтерактивні запитання-відповіді
string machine;
IPHostEntry ipHostEntry;
while (true) {
// введення імені або адреси IP шуканого пристрою
Console.Write("Machine recherchée (rien pour arrêter) : ");
machine = Console.ReadLine().Trim().ToLower();
// закінчено?
if (machine == "") return;
// обробка виняткових ситуацій
try {
// пошук машини
ipHostEntry = Dns.GetHostEntry(machine);
// назва машини
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// адреси IP комп'ютера
Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
Console.Write(", {0}" , ipHostEntry.AddressList[i]);
}
Console.WriteLine();
// псевдоніми машини
if (ipHostEntry.Aliases.Length != 0) {
Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
Console.Write(", {0}" , ipHostEntry.Aliases[i]);
}
Console.WriteLine();
}
} catch {
// машина не існує
Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
}
}
}
}
}
Виконання програми дає такі результати:
11.3. Основи інтернет-програмування
11.3.1. Загальні відомості
Розглянемо взаємодію між двома віддаленими машинами A та B:
![]() |
Коли програма AppA на комп’ютері A хоче встановити зв’язок із програмою AppB на комп’ютері B в Інтернеті, вона повинна знати кілька речей:
- адреса IP або ім'я комп'ютера B
- номер порту, з яким працює програма AppB. Адже комп'ютер B може підтримувати безліч програм, що працюють в Інтернеті. Коли він отримує інформацію з мережі, він повинен знати, для якої програми призначена ця інформація. Доступ до мережі додатки на комп’ютері B отримують через «вікна», які також називаються портами зв’язку. Ця інформація міститься в пакеті, отриманому комп’ютером B, щоб його можна було доставити потрібному додатку.
- Протоколи зв’язку, які розуміє машина B. У нашому дослідженні ми використовуватимемо виключно протоколи TCP-IP.
- протокол діалогу, який підтримує додаток AppB. Адже машини A та B будуть «спілкуватися» між собою. Те, що вони скажуть, буде інкапсульовано в протоколи TCP-IP. Проте, коли на кінці ланцюга додаток AppB отримає інформацію, надіслану додатком AppA, він повинен бути здатний її інтерпретувати. Це аналогічно ситуації, коли дві особи А та Б спілкуються по телефону: їхній діалог передається через телефон. Слова кодуються телефоном А у вигляді сигналів, передаються по телефонних лініях, досягають телефону Б, де їх декодують. Тоді людина Б чує слова. Саме тут виникає поняття протоколу діалогу: якщо А розмовляє французькою, а Б не розуміє цю мову, А і Б не зможуть вести змістовний діалог.
Тому обидва додатки, що спілкуються, повинні домовитися про тип діалогу, який вони використовуватимуть. Наприклад, діалог із сервісом ftp відрізняється від діалогу із сервісом pop: ці два сервіси не приймають однакових команд. Вони мають різний протокол діалогу.
11.3.2. Характеристики протоколу TCP
Тут ми розглянемо лише мережеві комунікації, що використовують транспортний протокол TCP. Нагадаємо його характеристики:
- Процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом машини-відправника та портом машини-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання.
- Усі пакети, що надсилаються процесом-джерелом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені
- Інформація, що надсилається, має безперервний характер. Процес-відправник надсилає інформацію у своєму темпі. Вона не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо для надсилання. Інформація зберігається у структурі, що називається сегментом TCP. Цей сегмент, щойно заповнений, буде переданий на рівень IP, де його інкапсулюють у пакет IP.
- Кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає підтвердження про отримання відправнику.
- Коли відправник отримує це підтвердження, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням.
- Якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації.
- Віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-джерело продовжує надсилати інформацію. Це дозволяє, наприклад, протоколу-джерелу TCP надсилати кілька сегментів, не чекаючи на підтвердження отримання. Якщо через певний час він виявить, що не отримав підтвердження отримання певного сегмента № n, він відновить передачу сегментів з цієї точки.
11.3.3. Взаємодія «клієнт-сервер»
Часто комунікація в Інтернеті є асиметричною: машина А ініціює з’єднання, щоб звернутися до машини Б із запитом на послугу: вона вказує, що хоче встановити з’єднання зі службою SB1 на машині Б. Остання приймає або відхиляє запит. Якщо вона приймає, машина А може надсилати свої запити до служби SB1. Ці запити повинні відповідати протоколу взаємодії, який розуміє служба SB1. Таким чином між машиною А, яку називають клієнтською машиною, та машиною Б, яку називають серверною машиною, встановлюється діалог «запит-відповідь». Один із двох партнерів закриє з’єднання.
11.3.4. Архітектура клієнта
Архітектура мережевої програми, що звертається за послугами серверного додатка, буде такою:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
11.3.5. Архітектура сервера
Архітектура програми, що надає послуги, буде такою:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
Серверна програма по-різному обробляє початковий запит на з'єднання від клієнта та його подальші запити, спрямовані на отримання послуги. Програма сама не надає послугу. Якби вона це робила, то протягом часу надання послуги вона більше не прислухалася б до запитів на з'єднання, і клієнти не отримували б обслуговування. Тому вона діє інакше: щойно запит на з’єднання надходить на порт прослуховування та приймається, сервер створює завдання, відповідальне за надання послуги, запитуваної клієнтом. Ця послуга надається на іншому порту серверної машини, який називається портом обслуговування. Таким чином можна обслуговувати декількох клієнтів одночасно.
Завдання обслуговування матиме таку структуру:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
11.4. Дізнайтеся про протоколи зв’язку в Інтернеті
11.4.1. Вступ
Коли клієнт підключається до сервера, між ними встановлюється зв’язок. Характер цього зв’язку визначає так званий протокол зв’язку сервера. Серед найпоширеніших протоколів Інтернету можна виділити такі:
- HTTP: HyperText Transfer Protocol — протокол взаємодії з веб-сервером (сервер HTTP)
- SMTP: Simple Mail Transfer Protocol — протокол взаємодії з сервером відправлення електронної пошти (сервер SMTP)
- POP: Post Office Protocol — протокол взаємодії з сервером зберігання електронної пошти (сервер POP). Він призначений для отримання електронних листів, а не для їх надсилання.
- FTP: File Transfer Protocol — протокол взаємодії з сервером зберігання файлів (сервер FTP).
Усі ці протоколи мають особливість бути текстовими: клієнт і сервер обмінюються текстовими рядками. Якщо у нас є клієнт, здатний:
- встановити з'єднання з сервером TCP
- виводити на консоль текстові рядки, які надсилає йому сервер
- надсилати на сервер текстові рядки, які вводить користувач
то ми зможемо взаємодіяти з сервером TCP, що використовує протокол на основі текстових рядків, за умови, що знаємо правила цього протоколу.
Програма telnet, яка є на комп’ютерах під управлінням Unix або Windows, є саме таким клієнтом. На комп’ютерах під управлінням Windows також є утиліта під назвою putty, і саме її ми будемо використовувати в цьому прикладі. putty можна завантажити за адресою [http://www.putty.org/]. Це виконуваний файл (.exe), який можна запускати безпосередньо. Ми налаштуємо його наступним чином:
![]() |
- [1]: адреса IP TCP-сервера, до якого ми хочемо підключитися, або його ім’я
- [2]: порт прослуховування сервера TCP
- [3]: слід вибрати режим Raw, який позначає «сире» TCP-з’єднання.
- [4]: увімкнути режим Never, щоб запобігти закриттю вікна клієнта putty у разі, якщо сервер розірве з’єднання.
- [6,7]: кількість стовпців/рядків консолі
- [5]: максимальна кількість рядків, що зберігаються в пам’яті. Сервер HTTP може надсилати велику кількість рядків. Потрібно мати можливість прокручувати їх.
![]() |
- [8,9]: щоб зберегти попередні параметри, надайте назву конфігурації [8] та збережіть її [9].
- [11,12]: щоб відновити збережену конфігурацію, виберіть [11] і завантажте її як [12].
Ознайомимося з деякими протоколами TCP за допомогою цього налаштованого інструменту.
11.4.2. Протокол HTTP (HyperText Transfer Protocol)
Підключімо наш клієнт [1] до веб-сервера на машині istia.univ-angers.fr [2], порт 80 [3]:
![]() |
У консолі putty ми створюємо такий діалог HTTP:
- рядки 1–4 — це запит клієнта, введений з клавіатури
- рядки 5–19 — це відповідь сервера
- рядок 1: синтаксис GET UrlDocument HTTP/1.1 — ми запитуємо URL /, c.a.d. кореневий каталог веб-сайту [istia.univ-angers.fr].
- рядок 2: синтаксис Host: машина:порт
- рядок 3: синтаксис Connection: [mode de la connexion]. Режим [close] вказує серверу закрити з’єднання після надсилання відповіді. Режим [Keep-Alive] вимагає залишити з’єднання відкритим.
- рядок 4: порожній рядок. Рядки 1–3 називаються заголовками HTTP. Крім наведених тут, можуть бути й інші. Кінець заголовків HTTP позначається порожнім рядком.
- рядки 5–13: заголовки HTTP у відповіді сервера — також закінчуються порожнім рядком.
- рядки 14–19: документ, надісланий сервером, у даному випадку документ HTML
- рядок 5: синтаксис HTTP/1.1 код msg — код 200 вказує, що запитуваний документ було знайдено.
- рядок 6: дата та час сервера
- рядок 7: ідентифікація програмного забезпечення, що забезпечує роботу веб-сервісу — у даному випадку сервер Apache під управлінням Linux / Debian
- рядок 8: документ було динамічно згенеровано за допомогою PHP
- рядок 9: ідентифікаційний файл cookie клієнта — якщо клієнт хоче, щоб його впізнали під час наступного підключення, він повинен надіслати цей файл cookie у своїх заголовках HTTP.
- рядок 10: вказує, що після надання запитуваного документа сервер закриє з’єднання
- рядок 11: документ буде передано частинами (chunked), а не одним блоком.
- рядок 12: тип документа: у даному випадку це документ HTML
- рядок 13: порожній рядок, що позначає кінець заголовків HTTP сервера
- рядок 14: шістнадцяткове число, що вказує на кількість символів у першому блоці документа. Коли це число дорівнюватиме 0 (рядок 19), клієнт зрозуміє, що отримав весь документ.
- рядки 15–18: частина отриманого документа.
З’єднання було закрито, і клієнт putty неактивний. Повторно під’єднаємося [1] і очистимо екран від попередніх повідомлень [2,3]:
![]() |
Цього разу діалогове вікно виглядає так:
- рядок 1: було зроблено запит на документ, якого не існує
- рядок 5: сервер HTTP відповів кодом 404, що означає, що запитуваний документ не знайдено.
Якщо запитати цей документ за допомогою браузера Firefox:

Якщо ми запитуємо перегляд вихідного коду [Affichage/Code source]:
Ми отримуємо рядки 13–22, отримані нашим клієнтом putty. Перевага цього клієнта полягає в тому, що він додатково показує нам заголовки HTTP відповіді. Отримати їх також можна за допомогою Firefox.
11.4.3. Протокол SMTP (Simple Mail Transfer Protocol)
![]() |
Сервери SMTP зазвичай працюють на порту 25 [2]. Підключення здійснюється до сервера [1]. Тут, як правило, слід вибрати сервер
, що належить до того самого домену IP, що й комп’ютер, оскільки найчастіше сервери SMTP налаштовані так, щоб приймати лише запити від комп’ютерів, що належать до того самого домену, що й вони. Крім того, досить часто брандмауери або антивірусні програми на персональних комп’ютерах налаштовані так, щоб не приймати з’єднання на порт 25 від зовнішнього комп’ютера. У такому разі може знадобитися переналаштування [3] цього брандмауера або антивірусної програми.
Діалогове вікно SMTP у вікні клієнта putty виглядає наступним чином:
Нижче (D) — запит клієнта, (R) — відповідь сервера.
- рядок 1: (R) привітальне повідомлення сервера SMTP
- рядок 2: (D) команда HELO для привітання
- рядок 3: (R) відповідь сервера
- рядок 4: (D) адреса відправника, наприклад, mail from: someone@gmail.com
- рядок 5: (R) відповідь сервера
- рядок 6: (D) адреса одержувача, наприклад rcpt to: someoneelse@gmail.com
- рядок 7: (R) відповідь сервера
- рядок 8: (D) позначає початок повідомлення
- рядок 9: (R) відповідь сервера
- рядки 10–12: (D) повідомлення, що надсилається, закінчується рядком, який містить лише крапку.
- рядок 13: (R) відповідь сервера
- рядок 14: (D) клієнт повідомляє, що завершив роботу
- рядок 15: (R) відповідь сервера, який потім закриває з’єднання
11.4.4. Протокол POP (Post Office Protocol)
![]() |
Сервери POP зазвичай працюють на порту 110 [2]. Здійснюється підключення до сервера [1]. Діалог POP у вікні клієнта putty виглядає наступним чином:
- рядок 1: (R) привітальне повідомлення сервера POP
- рядок 2: (D) клієнт надає свій ідентифікатор POP, c.a.d — логін, під яким він читає свою пошту
- рядок 3: (R) відповідь сервера
- рядок 4: (D) пароль клієнта
- рядок 5: (R) відповідь сервера
- рядок 6: (D) клієнт запитує список своїх листів
- рядки 7–12: (R) список повідомлень у поштовій скриньці клієнта у форматі [N° du message taille en octets du message]
- рядок 13: (D) запитується повідомлення № 64
- рядки 14–25: (R) повідомлення № 64, де рядки 15–22 містять заголовки повідомлення, а рядки 23–24 — тіло повідомлення.
- рядок 26: (D) клієнт повідомляє, що завершив роботу
- рядок 27: (R) відповідь сервера, який потім закриває з’єднання.
11.4.5. Протокол FTP (File Transfer Protocol)
Протокол FTP є складнішим за ті, що були представлені раніше. Щоб ознайомитися з рядками тексту, якими обмінюються клієнт і сервер, можна скористатися таким інструментом, як FileZilla [http://www.filezilla.fr/].
![]() |
FileZilla — це клієнт FTP, що пропонує інтерфейс Windows для передачі файлів. Дії користувача в інтерфейсі Windows перетворюються на команди FTP, які реєструються в [1]. Це хороший спосіб ознайомитися з командами протоколу FTP.
11.5. Класи .NET у веб-програмуванні
11.5.1. Вибір відповідного класу
Фреймворк .NET пропонує різні класи для роботи з мережею:
![]() |
- Клас Socket працює найближче до мережі. Він дозволяє здійснювати детальне управління мережевим з’єднанням. Термін socket позначає розетку. Це поняття було розширено для позначення програмного мережевого сокета. Під час обміну даними TCP-IP між двома машинами A та B між собою взаємодіють два sockets. Додаток може працювати безпосередньо з sockets. Це стосується додатка A, наведеного вище. Сокет може бути сокетом типу client або serveur.
- Якщо потрібно працювати на рівні, менш детальному, ніж рівень класу Socket, можна використовувати класи
- TcpClient для створення клієнта TCP
- TcpListener для створення TCP-сервера
Ці два класи надають додатку, що їх використовує, спрощене уявлення про мережеву комунікацію, беручи на себе управління технічними деталями роботи з сокетами.
- .NET пропонує класи, специфічні для певних протоколів:
- клас SmtpClient для управління протоколом SMTP зв’язку із сервером SMTP для надсилання електронних листів
- клас WebClient для управління протоколами HTTP або FTP, що забезпечують зв’язок із веб-сервером.
Слід зауважити, що клас Socket сам по собі достатній для управління будь-яким зв’язком TCP/IP, але насамперед слід намагатися використовувати класи вищого рівня, щоб полегшити написання програми на базі TCP/IP.
11.5.2. Клас TcpClient
Клас TcpClient є класом, який у більшості випадків підходить для створення клієнта служби TCP. Серед його конструкторів C, методів M та властивостей P є такі:
C | створює TCP-з'єднання зі службою, що працює на вказаному порту (port) вказаної машини (hostname). Наприклад, new TcpClient("istia.univ-angers.fr",80) для підключення до порту 80 машини istia.univ-angers.fr | |
P | сокет, який використовує клієнт для зв’язку з сервером. | |
M | отримує потік для читання та запису на сервер. Саме цей потік забезпечує обмін даними між клієнтом і сервером. | |
M | закриває з’єднання. Сокет та потік NetworkStream також закриваються | |
P | true, якщо з'єднання було встановлено |
Клас NetworkStream представляє мережевий потік між клієнтом і сервером. Він походить від класу Stream. Багато клієнт-серверних додатків обмінюються рядками тексту, що закінчуються символами кінця рядка «\r\n». Тому доцільно використовувати об’єкти StreamReader та StreamWriter для читання та запису цих рядків у мережевий потік. Отже, якщо машина M1 встановила з’єднання з машиною M2 за допомогою об’єкта TcpClient client1 і вони обмінюються рядками тексту, вона зможе створити свої потоки читання та запису таким чином:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
Команда
означає, що потік запису від client1 не пройде через проміжний буфер, а надійде безпосередньо в мережу. Цей момент є важливим. Зазвичай, коли client1 надсилає рядок тексту своєму партнеру, він очікує на відповідь. Ця відповідь ніколи не надійде, якщо рядок насправді був зафіксований у буфері на машині M1 і ніколи не був надісланий на машину M2.
Щоб надіслати рядок тексту на машину M2, слід написати:
Щоб прочитати відповідь від M2, потрібно написати:
Тепер ми маємо все необхідне для написання базової архітектури інтернет-клієнта, який використовує такий базовий протокол зв’язку з сервером:
- клієнт надсилає запит, що міститься в одному рядку
- сервер надсилає відповідь, що міститься в одному рядку
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// встановлюється з'єднання зі службою
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// вихідний потік без буферизації
writer.AutoFlush = true;
// цикл «запит — відповідь»
while (true) {
// запит надходить з клавіатури
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// завершено?
if (demande.Trim().ToLower() == "bye")
break;
// запит надсилається на сервер
writer.WriteLine(demande);
// читаємо відповідь від сервера
réponse = reader.ReadLine();
// обробка відповіді
...
}
}
}
}
}
} catch (Exception e) {
// помилка
...
}
}
}
}
- рядок 11: встановлення з’єднання клієнтом — оператор using гарантує, що пов’язані з ним ресурси будуть звільнені після виходу з using.
- рядок 12: відкриття мережевого потоку в розділі using
- рядок 13: створення та використання потоку читання у клаузулі using
- рядок 14: створення та використання потоку запису в розділі using
- рядок 16: не буферизувати вихідний потік
- рядки 18–31: цикл «запит клієнта — відповідь сервера»
- рядок 26: клієнт надсилає свій запит на сервер
- рядок 28: клієнт очікує відповіді від сервера. Це блокуюча операція, подібна до читання з клавіатури. Очікування завершується надходженням рядка, що закінчується символом «\n», або закінченням потоку. Останнє відбудеться, якщо сервер закриє з’єднання, яке він відкрив із клієнтом.
11.5.3. Клас TcpListener
Клас TcpListener є класом, який у більшості випадків підходить для створення сервісу TCP. Серед його конструкторів C, методів M та властивостей P є такі:
C | створює службу TCP, яка буде очікувати (listen) запитів від клієнтів на порту, переданому як параметр (port), який називається портом прослуховування. Якщо машина підключена до декількох мереж IP, служба прослуховує кожну з цих мереж. | |
C | те саме, але прослуховування відбувається лише на вказаній IP-адресі. | |
M | запускає прослуховування запитів клієнтів | |
M | приймає запит клієнта. Після цього відкриває з ним нове з'єднання, яке називається сервісним з'єднанням. Порт, що використовується на стороні сервера, є випадковим і обирається системою. Його називають сервісним портом. AcceptTcpClient повертає в якості результату об'єкт TcpClient, пов'язаний на стороні сервера з сервісним з'єднанням. | |
M | припиняє очікування запитів від клієнтів | |
P | сокет прослуховування сервера |
Базова структура сервера TCP, який обмінюється даними зі своїми клієнтами за таким протоколом:
- клієнт надсилає запит, що міститься в одному рядку
- сервер надсилає відповідь, що міститься в одному рядку
може виглядати так:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace ... {
public class ... {
...
// створюється служба прослуховування
TcpListener ecoute = null;
try {
// створюється служба — вона буде слухати на всіх мережевих інтерфейсах комп'ютера
ecoute = new TcpListener(IPAddress.Any, port);
// запускаємо його
ecoute.Start();
// цикл роботи служби
TcpClient tcpClient = null;
// нескінченний цикл — зупиняється за допомогою Ctrl-C
while (true) {
// очікування клієнта
tcpClient = ecoute.AcceptTcpClient();
// сервіс обслуговується іншим завданням
ThreadPool.QueueUserWorkItem(Service, tcpClient);
// наступний клієнт
}
} catch (Exception ex) {
// повідомляється про помилку
...
} finally {
// завершення обслуговування
ecoute.Stop();
}
}
// -------------------------------------------------------
// надає послугу клієнту
public static void Service(Object infos) {
// отримано клієнта, якого потрібно обслужити
Client client = infos as Client;
// обробка з'єднання TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// небуферований вихідний потік
writer.AutoFlush = true;
// цикл читання запиту/запису відповіді
bool fini=false;
while (! fini) != null) {
// очікування запиту клієнта — операція, що блокує
demande=reader.ReadLine();
// підготовка відповіді
réponse=...;
// відправлення відповіді клієнту
writer.WriteLine(réponse);
// наступний запит
}
}
}
}
}
} catch (Exception e) {
// помилка
...
} finally {
// завершення роботи клієнта
...
}
}
}
}
- рядок 14: служба прослуховування створюється для заданого порту та заданої адреси IP. Тут слід пам’ятати, що комп’ютер має щонайменше дві адреси IP: адресу «127.0.0.1», яка є його локальною адресою, та адресу «I1.I2.I3.I4», яку він має в мережі, до якої підключений. Вона може мати й інші адреси IP, якщо підключена до кількох мереж IP. IPAddress.Any позначає всі адреси IP одного комп’ютера.
- рядок 16: запускається служба прослуховування. Раніше вона була створена, але ще не прослуховувала. Прослуховування означає очікування запитів від клієнтів.
- рядки 20–26: цикл очікування запиту клієнта / обслуговування клієнта повторюється для кожного нового клієнта
- рядок 22: запит клієнта прийнято. Метод AcceptTcpClient повертає екземпляр TcpClient, який називається сервісом:
- клієнт надіслав свій запит за допомогою власного екземпляра TcpClient на стороні клієнта, який ми назвемо TcpClientDemande
- сервер приймає цей запит за допомогою AcceptTcpClient. Цей метод створює екземпляр TcpClient на стороні сервера, який ми назвемо TcpClientService. У результаті відкривається TCP-з’єднання, на обох кінцях якого знаходяться екземпляри TcpClientDemande <--> TcpClientService.
- Подальший обмін даними між клієнтом і сервером відбувається через це з’єднання. Служба прослуховування більше не бере участі.
- рядок 24: щоб сервер міг обслуговувати декількох клієнтів одночасно, обслуговування здійснюється за допомогою потоків — по одному потоку на кожного клієнта.
- рядок 32: служба прослуховування закрита
- рядок 38: метод, що виконується потоком обслуговування клієнта. Він отримує як параметр екземпляр TcpClient, який уже підключений до клієнта, що обслуговується.
- рядки 38–71: тут міститься код, схожий на той, що був у базовому клієнті Tcp, який ми розглядали раніше.
11.6. Приклади клієнтів / серверів TCP
11.6.1. Сервер-ехо
Ми пропонуємо написати сервер-ехо, який запускатиметься з вікна DOS за допомогою команди:
ServeurEcho порт
Сервер працює на порту, переданому як параметр. Він просто повертає клієнту запит, який той йому надіслав. Програма виглядає так:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
// виклик: serveurEcho порт
// сервер відлуння
// повертає клієнту рядок, який той йому надіслав
namespace Chap9 {
public class ServeurEcho {
public const string syntaxe = "Syntaxe : [serveurEcho] port";
// головна програма
public static void Main(string[] args) {
// чи є аргумент?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// цей аргумент має бути цілим числом >0
int port = 0;
if (!int.TryParse(args[0], out port) || port<=0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
return;
}
// створюється служба прослуховування
TcpListener ecoute = null;
int numClient = 0; // наступний номер клієнта
try {
// створюємо службу — вона буде слухати на всіх мережевих інтерфейсах машини
ecoute = new TcpListener(IPAddress.Any, port);
// запускаємо його
ecoute.Start();
// моніторинг
Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
// потоки служби
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// цикл служби
TcpClient tcpClient = null;
// нескінченний цикл — зупиняється за допомогою Ctrl-C
while (true) {
// очікування клієнта
tcpClient = ecoute.AcceptTcpClient();
// послугу надає інший процес
ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// наступний клієнт
numClient++;
}
} catch (Exception ex) {
// повідомляється про помилку
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
} finally {
// завершення обслуговування
ecoute.Stop();
}
}
// -------------------------------------------------------
// обслуговує клієнта сервера відлуння
public static void Service(Object infos) {
// отримано клієнта, якому потрібно надати послугу
Client client = infos as Client;
// надає послугу клієнту
Console.WriteLine("Début de service au client {0}", client.NumClient);
// обробка з'єднання TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// небуферований вихідний потік
writer.AutoFlush = true;
// цикл читання запиту/запису відповіді
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// моніторинг консолі
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// відлуння запиту до клієнта
writer.WriteLine("[{0}]", demande);
// моніторинг консолі
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// служба зупиняється, коли клієнт надсилає «bye»
if (demande.Trim().ToLower() == "bye")
break;
}
}
}
}
}
} catch (Exception e) {
// помилка
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
// завершення роботи клієнта
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
}
// інформація про клієнта
internal class Client {
public TcpClient CanalTcp { get; set; } // з'єднання з клієнтом
public int NumClient { get; set; } // номер клієнта
}
}
Структура сервера-ехо відповідає базовій архітектурі TCP-серверів, описаній раніше. Ми прокоментуємо лише частину «обслуговування клієнта»:
- рядок 79: запит клієнта зчитується
- рядок 83: запит повертається клієнту в дужках
- рядок 79: служба зупиняється, коли клієнт закриває з'єднання
У вікні DOS ми використовуємо виконуваний файл проекту C#:
Потім запускаємо два клієнти putty, які підключаємо до порту 100 машини localhost:
![]() |
Вивід консолі сервера-відлуння виглядає так:
Спочатку клієнт 1, а потім клієнт 0 надсилають такі тексти:
![]() |
- [1]: клієнт № 1
- [2]: клієнт № 0
- [3]: консоль сервера відлуння
![]() |
- у [4]: клієнт 1 відключається за допомогою команди bye.
- в [5]: сервер це виявляє
Сервер можна зупинити за допомогою Ctrl-C. Клієнт № 0 тоді це виявляє [6].
11.6.2. Клієнт для сервера відлуння
Тепер ми напишемо клієнт для попереднього сервера. Він називатиметься так:
ClientEcho nomServeur порт
Він підключається до машини nomServeur на порту port, а потім надсилає серверу рядки тексту, які той відбиває у відповідь.
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
// підключається до сервера відлуння
// кожен рядок, введений з клавіатури, отримується у вигляді відлуння
class ClientEcho {
static void Main(string[] args) {
// синтаксис
const string syntaxe = "pg machine port";
// кількість аргументів
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// записуємо ім’я сервера
string serveur = args[0];
// порт повинен бути цілим числом >0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// можна працювати
string demande = null; // запит клієнта
string réponse = null; // відповідь сервера
try {
// встановлюємо з'єднання зі службою
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// небуферований вихідний потік
writer.AutoFlush = true;
// цикл «запит — відповідь»
while (true) {
// запит надходить з клавіатури
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// закінчено?
if (demande.Trim().ToLower() == "bye")
break;
// запит надсилається на сервер
writer.WriteLine(demande);
// зчитування відповіді з сервера
réponse = reader.ReadLine();
// обробляється відповідь
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
// помилка
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Структура цього клієнта відповідає загальній базовій архітектурі, запропонованій для клієнтів Tcp. Ось результати, отримані в такій конфігурації:
- сервер запущено на порту 100 у вікні DOS
- на тій самій машині запущено два клієнти у двох інших вікнах DOS
У вікні клієнта A (№ 0) відображається наступне:
У вікні клієнта B (№ 1):
У вікні сервера:
Клієнт А № 0 відключається:
Консоль сервера:
11.6.3. Універсальний клієнт TCP
Ми напишемо загальний TCP-клієнт, який запускатиметься наступним чином: ClientTcpGenerique серверний порт. Він працюватиме аналогічно до клієнта putty, але матиме консольний інтерфейс і не матиме опцій налаштування.
У попередньому додатку протокол взаємодії був відомим: клієнт надсилав один рядок, а сервер відповідав одним рядком. Кожна служба має свій особливий протокол, і також трапляються такі ситуації:
- клієнт повинен надіслати кілька рядків тексту, перш ніж отримати відповідь
- відповідь сервера може містити кілька рядків тексту
Тому цикл «відправлення одного рядка на сервер / отримання одного рядка від сервера» не завжди підходить. Для роботи з протоколами, складнішими за протокол «ехо», загальний TCP-клієнт матиме два потоки:
- головний потік читатиме рядки тексту, введені з клавіатури, і надсилатиме їх на сервер.
- додатковий потік працюватиме паралельно і буде присвячений зчитуванню рядків тексту, надісланих сервером. Як тільки він отримає такий рядок, він відобразить його на консолі. Потік зупиняється лише тоді, коли сервер закриває з’єднання. Отже, він працює безперервно.
Код виглядає так:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
// отримує як параметр характеристики служби у форматі: сервер порт
// підключається до сервісу
// надсилає на сервер кожен рядок, введений з клавіатури
// створює потік для безперервного зчитування рядків тексту, надісланих сервером
class ClientTcpGenerique {
static void Main(string[] args) {
// синтаксис
const string syntaxe = "pg serveur port";
// кількість аргументів
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// записуємо ім’я сервера
string serveur = args[0];
// порт повинен бути цілим числом >0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// здійснюється підключення до служби
TcpClient tcpClient = null;
try {
tcpClient = new TcpClient(serveur, port);
} catch (Exception ex) {
// помилка
Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
// кінець
return;
}
// запускаємо окремий потік для зчитування рядків тексту, надісланих сервером
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// читання команд з клавіатури відбувається в головному потоці
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string demande = null; // запит клієнта
try {
// використовуємо з'єднання з клієнтом
using (tcpClient) {
// створюється потік запису на сервер
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// потік виводу без буферизації
writer.AutoFlush = true;
// цикл запит — відповідь
while (true) {
demande = Console.ReadLine();
// завершено?
if (demande.Trim().ToLower() == "bye")
break;
// надсилання запиту на сервер
writer.WriteLine(demande);
}
}
}
}
} catch (Exception e) {
// помилка
Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
}
}
// потік читання клієнт <-- сервер
public static void Receive(object infos) {
// локальні дані
string réponse = null; // відповідь сервера
// створення вхідного потоку
try {
using (TcpClient tcpClient = infos as TcpClient) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// цикл безперервного читання рядків тексту з вхідного потоку
while ((réponse = reader.ReadLine()) != null) {
// виведення на консоль
Console.WriteLine("<-- {0}", réponse);
}
}
}
}
} catch (Exception ex) {
// помилка
Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// повідомлення про завершення потоку читання
Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
}
}
}
}
- рядок 34: клієнт підключається до сервера
- рядок 43: запускається потік для читання рядків тексту з сервера. Він повинен виконати метод Receive із рядка 73. Цьому методу передається екземпляр TcpClient, який було підключено до сервера.
- рядки 57–64: цикл введення команд з клавіатури / надсилання команд на сервер. Введення команд з клавіатури забезпечується головним потоком.
- рядки 75–98: метод Receive, що виконується потоком читання рядків тексту. Цей метод отримує як параметр екземпляр TcpClient, який було підключено до сервера.
- рядки 84–87: безперервний цикл читання рядків тексту, надісланих сервером. Він зупиняється лише тоді, коли сервер закриває відкрите з’єднання з клієнтом.
Ось кілька прикладів, що повторюють ті, які використовувалися з клієнтом putty у розділі 11.4. Клієнт виконується в консолі DOS.
Протокол HTTP
Просимо читача ще раз ознайомитися з поясненнями, наведеними в розділі 11.4.2. Ми коментуємо лише те, що стосується саме цього додатка:
- рядок 28: після відправлення рядка 27 сервер HTTP закрив з’єднання, що призвело до завершення потоку читання. Головний потік, який зчитує команди, введені з клавіатури, залишається активним. Команда в рядку 29, введена з клавіатури, зупиняє його.
Протокол SMTP
Читачеві пропонується ще раз ознайомитися з поясненнями, наведеними в розділі 11.4.3, та протестувати інші приклади, що використовуються з клієнтом putty.
11.6.4. : загальний TCP-сервер
Тепер ми розглянемо сервер
- , який виводить на екран команди, надіслані його клієнтами
- та надсилає їм у відповідь рядки тексту, введені користувачем з клавіатури. Отже, саме користувач виконує роль сервера.
Програму запускають у вікні DOS за допомогою команди: ServeurTcpGenerique portEcoute, де portEcoute — це порт, до якого повинні підключатися клієнти. Обслуговування клієнтів забезпечуватимуть два потоки:
- головний потік, який:
- оброблятиме клієнтів по черзі, а не паралельно;
- читатиме рядки, введені користувачем з клавіатури, та надсилатиме їх клієнту. Користувач за допомогою команди «bye» повідомить про завершення з’єднання з клієнтом. Оскільки консоль не може використовуватися для двох клієнтів одночасно, наш сервер обробляє лише одного клієнта за раз.
- допоміжний потік, який присвячений виключно зчитуванню рядків тексту, надісланих клієнтом
Сам сервер ніколи не зупиняється, за винятком випадків, коли користувач натискає комбінацію клавіш Ctrl-C.
Розглянемо кілька прикладів. Сервер запущено на порту 100, і для зв’язку з ним використовується загальний клієнт paragraphe11.6.3. Вікно клієнта виглядає так:
Рядки, що починаються з <--, — це ті, що надсилаються сервером клієнту, інші — від клієнта до сервера. Вікно сервера виглядає так:
Рядки, що починаються з <--, — це ті, що надсилаються від клієнта до сервера, інші — ті, що надсилаються від сервера до клієнта. Рядок 9 вказує, що потік читання запитів клієнта зупинився. Головний потік сервера все ще очікує на команди, введені з клавіатури, щоб надіслати їх клієнту. Тому потрібно ввести з клавіатури команду bye із рядка 10, щоб перейти до наступного клієнта. Сервер все ще активний, хоча клієнт 1 завершив роботу. Запускаємо другого клієнта для того самого сервера:
Вікно сервера тепер виглядає так:
Після 6-го рядка, наведеного вище, сервер перейшов у режим очікування нового клієнта. Його можна зупинити за допомогою Ctrl-C.
Тепер змоделюємо веб-сервер, запустивши наш універсальний сервер на порту 88:
Тепер відкриємо браузер і введемо адресу http://localhost:88/exemple.html. Браузер підключиться до порту 88 машини localhost, а потім запросить сторінку /exemple.html:
![]() |
Тепер поглянемо на вікно нашого сервера:
Ми бачимо заголовки HTTP, надіслані браузером. Це дозволяє нам виявити інші заголовки HTTP, крім тих, що ми вже зустрічали. Складімо відповідь для нашого клієнта. Користувач за клавіатурою тут є справжнім сервером і може скласти відповідь вручну. Згадаймо відповідь, яку надав веб-сервер у попередньому прикладі:
Спробуємо надати аналогічну відповідь, обмежившись суто необхідним мінімумом:
У нашій відповіді ми обмежилися заголовками HTTP у рядках 1–4. Ми не вказуємо розмір документа, який збираємося надіслати (Content-Length), а лише повідомляємо, що після його надсилання ми закриємо з’єднання (Connection: close). Цього достатньо для браузера. Побачивши, що з’єднання закрито, він зрозуміє, що відповідь сервера завершена, і відобразить сторінку HTML, яка була йому надіслана. Остання — це рядки 6–9. Потім користувач за допомогою клавіатури закриває з’єднання з клієнтом, вводячи команду bye (рядок 10). У відповідь на цю клавіатурну команду головний потік закриває з’єднання з клієнтом. Це викликає виняток у рядку 11. Потік, що зчитував текстові рядки від клієнта, був раптово перерваний через закриття з’єднання з клієнтом і видав виняток. Після рядка 12 сервер переходить у режим очікування нового клієнта.
Браузер клієнта тепер відображає таке:
![]() |
Якщо ввести вищевказане як Affichage/Source, щоб побачити, що отримав браузер, ми отримаємо [2], тобто саме те, що було надіслано з загального сервера.
Код загального сервера TCP виглядає так:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
public class ServeurTcpGenerique {
public const string syntaxe = "Syntaxe : ServeurGénérique Port";
// головна програма
public static void Main(string[] args) {
// чи є аргумент?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
Environment.Exit(1);
}
// цей аргумент має бути цілим числом >0
int port = 0;
if (!int.TryParse(args[0], out port) || port <= 0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
Environment.Exit(2);
}
// створюється служба прослуховування
TcpListener ecoute = null;
try {
// створюємо службу
ecoute = new TcpListener(IPAddress.Any, port);
// запускаємо його
ecoute.Start();
// моніторинг
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// очікування клієнта
Console.WriteLine("Attente du client suivant...");
TcpClient tcpClient = ecoute.AcceptTcpClient();
Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
// запускається окремий потік для зчитування рядків тексту, надісланих клієнтом
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// читання команд з клавіатури відбувається в головному потоці
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string réponse = null; // відповідь сервера
// обробка з'єднання з клієнтом
using (tcpClient) {
// створюється потік запису до клієнта
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// потік виводу без буферизації
writer.AutoFlush = true;
// цикл введення відповідей з клавіатури
while (true) {
réponse = Console.ReadLine();
// завершено?
if (réponse.Trim().ToLower() == "bye")
break;
// надсилається запит клієнту
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// повідомлення про помилку
Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// завершення очікування
ecoute.Stop();
}
}
// потік читання сервер <-- клієнт
public static void Receive(object infos) {
// локальні дані
string demande = null; // запит клієнта
string idClient=null; // ідентифікатор клієнта
// обробка з'єднання клієнта
try {
using (TcpClient tcpClient = infos as TcpClient) {
// ідентифікатор клієнта
idClient = tcpClient.Client.RemoteEndPoint.ToString();
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// цикл безперервного зчитування рядків тексту з вхідного потоку
while ((demande = reader.ReadLine()) != null) {
// вивід на консоль
Console.WriteLine("<-- {0}", demande);
}
}
}
}
} catch (Exception ex) {
// помилка
Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
} finally {
// повідомлення про завершення потоку читання
Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
}
}
}
}
- рядок 29: служба прослуховування створена, але не запущена. Вона прослуховує всі мережеві інтерфейси машини.
- рядок 31: служба прослуховування запущена
- рядок 34: нескінченний цикл очікування клієнтів. Користувач зупинить сервер за допомогою Ctrl-C.
- рядок 37: очікування клієнта — операція, що блокує виконання. Коли клієнт з’являється, екземпляр TcpClient, повернений методом AcceptTcpClient, представляє серверну сторону відкритого з’єднання з клієнтом.
- рядок 40: потік читання запитів клієнта доручається окремому потоку.
- рядок 45: використання з’єднання з клієнтом у блоці using, щоб гарантувати його закриття за будь-яких обставин.
- рядок 47: використання мережевого потоку в операторі using
- рядок 48: створення в клаузулі using потоку запису на мережевий потік
- рядок 50: потік запису не буде буферизуватися
- рядки 52–59: цикл введення з клавіатури команд, які потрібно надіслати клієнту
- рядок 69: завершення служби прослуховування. Ця інструкція тут ніколи не буде виконана, оскільки сервер зупиняється за допомогою Ctrl-C.
- рядок 78: метод Receive, який безперервно виводить на консоль рядки тексту, надіслані клієнтом. Тут ми бачимо те саме, що й у випадку з загальним клієнтом TCP.
11.6.5. -клієнт веб-браузера
У попередньому прикладі ми розглянули деякі заголовки HTTP, які надсилав браузер:
Ми напишемо веб-клієнт, якому як параметр передаватиметься URL і який відображатиме на екрані текст, надісланий сервером. Припустимо, що сервер підтримує протокол HTTP 1.1. З попередніх заголовків ми використовуватимемо лише такі:
- перший заголовок вказує на потрібний документ
- другий — сервер, до якого надсилається запит
- третій — що ми хочемо, щоб сервер закрив з’єднання після надання відповіді.
Якщо у рядку 1 вище замінити GET на HEAD, сервер надішле нам лише заголовки HTTP, а не документ, вказаний у рядку 1.
Наш веб-клієнт буде викликатися наступним чином: ClientWeb URL cmd, де URL — цеURL, а cmd — одним із двох ключових слів GET або HEAD, що вказує, чи потрібні лише заголовки (HEAD), чи також вміст сторінки (GET). Розглянемо перший приклад:
- у рядку 1 ми запитуємо лише заголовки HTTP (HEAD)
- рядки 2–9: відповідь сервера
Якщо ми використаємо GET замість HEAD у виклику веб-клієнта, ми отримаємо той самий результат, що й із HEAD, а також тіло запитуваного документа.
Код веб-клієнта такий:
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
class ClientWeb {
static void Main(string[] args) {
// синтаксис
const string syntaxe = "pg URI GET/HEAD";
// кількість аргументів
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// зафіксовано запит на URI
string stringURI = args[0];
string commande = args[1].ToUpper();
// перевірка дійсності URI
if(! stringURI.StartsWith("http://")){
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI неправильний
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// перевірка замовлення
if (commande != "GET" && commande != "HEAD") {
// неправильне замовлення
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// встановлюється з'єднання зі службою
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// вихідний потік не буферизується
writer.AutoFlush = true;
// запит на URL — надсилання заголовків HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// зчитування відповіді
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// відповідь виводиться на консоль
Console.WriteLine(réponse);
}
}
}
}
}
} catch (Exception e) {
// виводиться виняток
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Єдиною новинкою в цій програмі є використання класу Uri. Програма отримує URL (Uniform Resource Locator) або URI (Uniform Resource Identifier) у форматі http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Клас Uri дозволяє розкласти рядок URL на окремі елементи.
- рядки 26–33: об’єкт Uri створюється на основі рядка stringURI, отриманого як параметр. Якщо рядок URI, отриманий як параметр, не є дійсним URI (відсутність протоколу, сервера тощо), генерується виняток. Це дозволяє перевірити правильність отриманого параметра. Після створення об’єкта Uri ми отримуємо доступ до різних елементів цього URI. Отже, якщо об’єкт uri з попереднього коду було створено на основі рядка http://serveur:port/document?param1=val1¶m2=val2;..., ми отримаємо:
- uri.Host=serveur,
- uri.Port=port,
- uri.Path=document,
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. Веб-клієнт, що підтримує перенаправлення
Попередній веб-клієнт не підтримує можливе перенаправлення з URL, яке він сам ініціював. Ось приклад:
- рядок 2: код 302 Found вказує на перенаправлення. Адреса, на яку має перенаправитися браузер, міститься в тілі документа, у рядку 16.
Другий приклад:
- рядок 2: код 301 Moved Permanently вказує на перенаправлення. Адреса, на яку має перенаправитися браузер, вказана в рядку 6, у заголовку HTTP Location.
Третій приклад:
- рядок 2: код 302 Moved Temporarily вказує на перенаправлення. Адреса, на яку має перенаправитися браузер, вказана в рядку 5, у заголовку HTTP Location.
Четвертий приклад із сервером IIS, локальним на комп’ютері:
- рядок 2: код 302 «Object moved» вказує на перенаправлення. Адреса, на яку має перенаправитися браузер, вказана в рядку 5, у заголовку HTTP Location. Слід зауважити, що, на відміну від попередніх прикладів, адреса перенаправлення є відносною. Повна адреса насправді виглядає так: http://localhost/localstart.asp.
Ми пропонуємо обробляти перенаправлення, коли перший рядок заголовків HTTP містить ключове слово moved (без урахування регістру), а адреса перенаправлення вказана в заголовку HTTP Location.
Якщо взяти три останні приклади, отримаємо такі результати:
URL: http://www.bull.com
- рядок 11: перенаправлення відбувається на адресу з рядка 6
URL: http://www.gouv.fr
- рядок 11: відбувається перенаправлення на адресу з рядка 6
URL: http://localhost
- рядок 13: відбувається перенаправлення на адресу з рядка 6
- рядок 15: нам було відмовлено у доступі до сторінки http://localhost/localstart.asp.
Програма, що керує перенаправленням, має такий вигляд:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Chap9 {
class ClientWebAvecRedirection {
static void Main(string[] args) {
// синтаксис
const string syntaxe = "pg URI GET/HEAD";
// кількість аргументів
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// зазначено запит на URI
string stringURI = args[0];
string commande = args[1].ToUpper();
// перевірка дійсності URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI неправильний
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// перевірка команди
if (commande != "GET" && commande != "HEAD") {
// неправильний код
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
const int nbRedirsMax = 1; // допускається не більше одного перенаправлення
int nbRedirs = 0; // кількість поточних перенаправлень
// регулярний вираз для пошуку перенаправлення URL
Regex location = new Regex(@"^Location: (.+?)$");
try {
// може бути кілька запитів на URL, якщо є перенаправлення
while (nbRedirs <= nbRedirsMax) {
// управління перенаправленнями
bool redir = false;
bool locationFound = false;
string locationString = null;
// підключення до сервісу
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
// небуферований вихідний потік
writer.AutoFlush = true;
// запит на URL — надсилання заголовків HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// читається перший рядок відповіді
string premièreLigne = reader.ReadLine();
// відображення на екрані
Console.WriteLine(premièreLigne);
// перенаправлення?
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
// є перенаправлення
redir = true;
nbRedirs++;
}
// наступні заголовки HTTP, доки не буде знайдено порожній рядок, що сигналізує про кінець заголовків
string réponse = null;
while ((réponse = reader.ReadLine()) != "") {
// виводиться відповідь
Console.WriteLine(réponse);
// якщо є перенаправлення, шукаємо заголовок Location
if (redir && !locationFound) {
// порівняти поточний рядок із реляційним виразом location
Match résultat = location.Match(réponse);
if (résultat.Success) {
// якщо знайдено, фіксується URL перенаправлення
locationString = résultat.Groups[1].Value;
// фіксується, що знайдено
locationFound = true;
}
}
}
// заголовки HTTP вичерпано — записується порожній рядок
Console.WriteLine(réponse);
// потім переходимо до основної частини документа
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// чи завершено?
if (!locationFound || nbRedirs > nbRedirsMax)
break;
// необхідно виконати перенаправлення — формуємо новий URI
try {
if (locationString.StartsWith("http")) {
// повна http-адреса
uri = new Uri(locationString);
} else {
// відносна http-адреса щодо поточного URI
uri = new Uri(uri, locationString);
}
// журнал консолі
Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
} catch (Exception ex) {
// проблема з URI
Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
}
}
} catch (Exception e) {
// виводиться виняток
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Порівняно з попередньою версією, зміни такі:
- рядок 46: регулярний вираз для отримання адреси перенаправлення в заголовку HTTP Location: адреса.
- рядок 49: код, який раніше виконувався для одного URI, тепер може виконуватися послідовно для декількох URI.
- рядок 66: зчитується перший рядок заголовків HTTP, надісланих сервером. Саме він містить ключове слово moved, якщо запитуваний документ було переміщено.
- рядки 71–75: перевіряється, чи містить перший рядок ключове слово moved. Якщо так, його фіксують.
- рядки 79–93: зчитування інших заголовків HTTP доти, доки не зустрінеться порожній рядок, що сигналізує про їхній кінець. Якщо перший рядок повідомляв про перенаправлення, то зупиняємося на заголовку HTTP Location: адреса, щоб зберегти адресу перенаправлення в locationString.
- рядки 98–100: решта відповіді сервера HTTP виводиться на консоль.
- рядки 105–106: запитуваний URI повністю оброблено та виведено на екран. Якщо перенаправлення не потрібно або якщо перевищено кількість дозволених перенаправлень, програма завершується.
- рядки 108–122: якщо є перенаправлення, обчислюється новий URI для запиту. Тут потрібно виконати невеликі обчислення залежно від того, чи знайдена адреса перенаправлення була абсолютною (рядок 111) чи відносною (рядок 114).
11.7. Класи .NET, спеціалізовані на конкретному інтернет-протоколі
У попередніх прикладах веб-клієнта протокол HTTP оброблявся за допомогою клієнта TCP. Тому нам доводилося самостійно обробляти конкретний протокол зв’язку, що використовувався. Ми могли б аналогічним чином створити клієнт SMTP або POP. Фреймворк .NET пропонує спеціалізовані класи для протоколів HTTP та SMTP. Ці класи знають протокол зв’язку між клієнтом і сервером і позбавляють розробника необхідності керувати ними. Тепер ми їх представимо.
11.7.1. classeWebClient
Існує клас WebClient, здатний взаємодіяти з веб-сервером. Розглянемо приклад веб-клієнта з розділу 11.6.5, який тут реалізовано за допомогою класу WebClient.
using System;
using System.IO;
using System.Net;
namespace Chap9 {
public class Program {
public static void Main(string[] args) {
// синтаксис: [prog] URI
const string syntaxe = "pg URI";
// кількість аргументів
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// зафіксовано запит на URI
string stringURI = args[0];
// перевірка дійсності URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI неправильний
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
try {
// створення веб-клієнта
using (WebClient client = new WebClient()) {
// додавання заголовка HTTP
client.Headers.Add("user-agent", "st");
using (Stream stream = client.OpenRead(uri)) {
using (StreamReader reader = new StreamReader(stream)) {
// відображення відповіді веб-сервера
Console.WriteLine(reader.ReadToEnd());
// відображення заголовків відповіді сервера
Console.WriteLine("---------------------");
foreach (string clé in client.ResponseHeaders.Keys) {
Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
}
Console.WriteLine("---------------------");
}
}
}
} catch (WebException e1) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
} catch (Exception e2) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
}
}
}
}
- рядок 35: веб-клієнт створено, але ще не налаштовано
- рядок 37: до запиту HTTP, який буде зроблено, додається заголовок HTTP. Ми побачимо, що інші заголовки будуть надіслані за замовчуванням.
- рядок 38: веб-клієнт запитує URI, вказаний користувачем, і зчитує надісланий документ. [WebClient].OpenRead(URI) встановлює з’єднання з Uri і зчитує відповідь. У цьому і полягає суть класу. Він відповідає за взаємодію з веб-сервером. Результат методу OpenRead має тип Stream і представляє запитуваний документ. Заголовки HTTP, надіслані сервером і розміщені перед документом у відповіді, не входять до його складу.
- рядок 39: використовується StreamReader, а в рядку 41 — його метод ReadToEnd для зчитування всієї відповіді.
- рядки 44–46: виводяться заголовки HTTP з відповіді сервера. [WebClient].ResponseHeaders представляє колекцію значень, ключами якої є імена заголовків HTTP, а значеннями — символьні рядки, пов’язані з цими заголовками.
- рядок 51: винятки, що виникають під час обміну даними між клієнтом і сервером, мають тип WebException.
Розглянемо кілька прикладів.
Запускаємо загальний сервер TCP, створений у розділі 6.4.6:
Запускаємо попередній веб-клієнт наступним чином:
Запитаний URI відповідає універсальному серверу. Тоді він відображає заголовки HTTP, які йому надіслав веб-клієнт:
Таким чином, бачимо:
- що веб-клієнт за замовчуванням надсилає 3 заголовки HTTP (рядки 3, 5, 6)
- рядок 4: заголовок, який ми створили самостійно (рядок 37 коду)
- що веб-клієнт за замовчуванням використовує метод GET (рядок 3). Існують й інші методи, серед яких POST та HEAD.
Тепер спробуємо отримати неіснуючий ресурс:
- рядок 2: сталося виключення типу WebException, оскільки сервер відповів кодом 404 Not Found, щоб вказати, що запитуваний ресурс не існує.
Наостанок зробимо запит на існуючий ресурс:
Файл istia.univ-angers.txt, створений цією командою, має такий вигляд:
- рядок 1: запитуваний документ HTML.
- рядки 3–10: заголовки відповіді HTTP у порядку, який не обов’язково відповідає тому, в якому вони були надіслані.
Клас WebClient має методи, що дозволяють отримувати документ (методи DownLoad) або надсилати його (методи UpLoad):
для завантаження ресурсу у вигляді масиву байтів (наприклад, зображення) | |
для завантаження ресурсу та збереження його у локальному файлі | |
для завантаження ресурсу та отримання його у вигляді символьного рядка (наприклад, HTML-файлу) | |
аналог OpenRead, але для надсилання даних на сервер | |
аналог DownLoadData, але для відправки даних на сервер | |
аналог DownLoadFile, але для відправки даних на сервер | |
аналог DownLoadString, але на сервер | |
для надсилання на сервер даних команди POST та отримання результатів у вигляді масиву байтів. Команда POST запитує документ, одночасно передаючи серверу інформацію, необхідну для визначення фактичного документа, який слід надіслати. Ця інформація надсилається серверу як документ, звідки й походить назва методу UpLoad. Вона надсилається після порожнього рядка заголовків HTTP у форматі param1=valeur1¶m2=valeur2&...:
Цей самий документ можна отримати за допомогою методу GET:
Різниця між цими двома методами полягає в тому, що браузер, який відображає запитуваний URI, покаже /document у випадку POST та /document?param1=valeur1¶m2=valeur2&... у випадку GET. |
11.7.2. Класи WebRequest / WebResponse
Іноді клас WebClient виявляється недостатньо гнучким для виконання необхідних завдань. Повернемося до прикладу веб-клієнта з перенаправленням, розглянутого в розділі 11.6.6. Нам потрібно надіслати заголовок HTTP:
Ми бачили, що заголовки HTTP, які веб-клієнт надсилає за замовчуванням, були такими:
Ми також побачили, що до попередніх заголовків можна додати заголовки HTTP за допомогою колекції [WebClient].Headers. Лише рядок 1 не є заголовком, що належить до колекції Headers, оскільки він не має форми «ключ: значення». Я не знайшов способу змінити GET на HEAD у рядку 1, виходячи з класу WebClient (можливо, я неправильно шукав?). Коли клас WebClient досяг своїх меж, можна перейти до класів WebRequest / WebResponse:
- WebRequest: представляє весь запит веб-клієнта.
- WebResponse: представляє всю відповідь веб-сервера
Ми вже зазначали, що клас WebClient обробляє схеми http:, https:, ftp:, file:. Запити та відповіді цих різних протоколів мають різну форму. Тому необхідно оперувати саме конкретними типами цих елементів, а не їхніми загальними типами WebRequest та WebResponse. Тому ми будемо використовувати такі класи:
- HttpWebRequest, HttpWebResponse для клієнта HTTP
- FtpWebRequest, FtpWebResponse для клієнта FTP
Тепер за допомогою класів HttpWebRequest та HttpWebresponse розглянемо приклад веб-клієнта з перенаправленням, який було розглянуто в розділі 11.6.6. Код виглядає наступним чином:
using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
namespace Chap9 {
class WebRequestResponse {
static void Main(string[] args) {
// синтаксис
const string syntaxe = "pg URI GET/HEAD";
// кількість аргументів
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// зазначається запитуване URI
string stringURI = args[0];
string commande = args[1].ToUpper();
// перевірка дійсності URI
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI неправильний
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// перевірка замовлення
if (commande != "GET" && commande != "HEAD") {
// неправильне замовлення
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// запит налаштовується
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// виконується
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// результат
Console.WriteLine("---------------------");
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
// заголовки HTTP
Console.WriteLine("---------------------");
foreach (string clé in httpWebResponse.Headers.Keys) {
Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
}
Console.WriteLine("---------------------");
// документ
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
// відображаємо відповідь у консолі
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// отримано відповідь
HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
} catch (Exception e2) {
// виводиться виняток
Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
}
}
}
}
- рядок 40: об’єкт типу WebRequest створюється за допомогою статичного методу WebRequest.Create(Uri uri), де uri — це URI документа, який потрібно завантажити. Оскільки відомо, що протокол URI — це HTTP, тип результату змінюється на HttpWebRequest, щоб отримати доступ до специфічних елементів протоколу HTTP.
- рядок 41: ми встановлюємо метод GET / POST / HEAD у першому рядку заголовків HTTP. У даному випадку це буде GET або HEAD.
- рядок 42: у приватній корпоративній мережі комп'ютери підприємства часто ізолюються від Інтернету з міркувань безпеки. Для цього приватна мережа використовує інтернет-адреси, які маршрутизатори Інтернету не маршрутизують. Приватна мережа підключена до Інтернету за допомогою спеціальних комп’ютерів, які називаються проксі-серверами і підключені одночасно як до приватної мережі підприємства, так і до Інтернету. Це приклад комп’ютерів із кількома адресами, наприклад IP. Комп’ютер у приватній мережі не може самостійно встановити з’єднання з сервером в Інтернеті, наприклад, із веб-сервером. Він повинен попросити проксі-сервер зробити це за нього. На одному проксі-сервері proxy можуть розміщуватися сервери proxy для різних протоколів. Термін «проксі-сервер HTTP» використовується для позначення служби, яка відповідає за надсилання запитів HTTP від імені комп’ютерів приватної мережі. Якщо такий проксі-сервер HTTP існує, його потрібно вказати в полі [WebRequest].proxy. Наприклад, слід вказати:
якщо проксі-сервер HTTP працює на порту 3128 комп’ютера pproxy.istia.uang. У полі [WebRequest].proxy вказується null, якщо комп’ютер має прямий доступ до Інтернету і не потребує проксі-сервера.
- рядок 44: метод GetResponse() запитує документ, ідентифікований за його URI, і повертає об’єкт WebRequestResponse, який тут перетворюється на об’єкт HttpWebResponse. Цей об’єкт представляє відповідь сервера на запит документа.
- рядок 47:
- [HttpWebResponse].ResponseUri: це URI сервера, який надіслав документ. У разі перенаправлення цей URI може відрізнятися від URI сервера, до якого спочатку надходив запит. Слід зауважити, що код не обробляє перенаправлення. Воно обробляється автоматично методом GetResponse. Це ще одна перевага класів високого рівня порівняно з базовими класами протоколу TCP.
- [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription представляють перший рядок відповіді, наприклад: HTTP/1.1 200 OK. StatusCode дорівнює 200, а StatusDescription дорівнює OK.
- рядок 50: [HttpWebResponse].Headers — це набір заголовків HTTP відповіді.
- рядок 55: [HttpWebResponse].GetResponseStream — це потік, що дозволяє отримати документ, який міститься у відповіді.
- рядок 61: може виникнути виняток типу WebException
- рядок 63: [WebException].Response — це відповідь, яка спричинила виникнення винятку.
Ось приклад виконання:
- рядки 1 і 3: сервер, який надіслав відповідь, не є тим самим, до якого було надіслано запит. Отже, відбулося перенаправлення.
- рядки 5–11: заголовки HTTP, надіслані сервером
11.7.3. Застосування: проксі-клієнт веб-сервера перекладу
Тепер ми покажемо, як попередні класи дозволяють нам використовувати веб-ресурси.
11.7.3.1. L'application
У мережі існують сайти перекладу. Тут ми використовуватимемо сайт http://trans.voila.fr/traduction_voila.php:
![]() | Текст, що підлягає перекладу, вводиться в [1], напрямок перекладу обирається в [2]. Запит на переклад надсилається через [3], а результат отримується в [4]. |
Ми напишемо клієнтську програму для Windows, яка буде взаємодіяти з вищезазначеним додатком. Вона не буде виконувати ніяких додаткових функцій, крім тих, що виконує додаток на сайті [trans.voila.fr]. Її інтерфейс буде таким:
![]() |
11.7.3.2. Архітектура програми
Додаток матиме таку двошарову архітектуру:
![]() |
11.7.3.3. Проєкт Visual Studio
Проєкт Visual Studio матиме такий вигляд:
![]() |
- У [1] рішення складається з двох проектів,
- [2]: один для шару [dao] та об’єктів, що ним використовуються,
- [3] — інший для інтерфейсу Windows
11.7.3.4. Проєкт [dao]
Проєкт [dao] складається з таких елементів:
- IServiceTraduction.cs: інтерфейс, представлений на рівні [ui]
- ServiceTraduction: реалізація цього інтерфейсу
- WebTraductionsException: виняток, специфічний для даного додатка
Інтерфейс IServiceTraduction має такий вигляд:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// використані мови
IDictionary<string, string> LanguesTraduites { get; }
// переклад
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- рядок 6: властивість LanguesTraduites повертає словник мов, що підтримуються сервером перекладу. Цей словник містить записи у форматі ["fe","Français-Anglais"], де значення позначає напрямок перекладу (у даному випадку з французької на англійську), а ключ «fe» — це код, який використовується сервером перекладу trans.voila.fr.
- рядок 8: метод Traduire є методом перекладу:
- texte — це текст, що підлягає перекладу
- deQuoiVersQuoi — один із ключів словника перекладаних мов
- метод виконує переклад тексту
ServiceTraduction — це клас реалізації інтерфейсу IServiceTraduction. Ми детально розглянемо його в наступному розділі.
WebTraductionsException — це наступний клас винятків:
using System;
namespace entites {
public class WebTraductionsException : Exception {
// код помилки
public int Code { get; set; }
// виробники
public WebTraductionsException() {
}
public WebTraductionsException(string message)
: base(message) {
}
public WebTraductionsException(string message, Exception e)
: base(message, e) {
}
}
}
- рядок 7: код помилки
11.7.3.5. Веб-клієнт [ServiceTraduction]
Повернемося до архітектури нашого додатка:
![]() |
Клас [ServiceTraduction], який ми маємо написати, є клієнтом веб-сервісу перекладу [trans.voila.fr]. Щоб його написати, нам потрібно зрозуміти,
- чого очікує сервер перекладу від свого клієнта
- що він повертає у відповідь своєму клієнту
Розглянемо на прикладі діалог «клієнт — сервер», який відбувається під час перекладу. Повернемося до прикладу, наведеного у вступі до додатка:
![]() | Текст для перекладу вводиться у [1], напрямок перекладу обирається у [2]. Запит на переклад надсилається через [3], а результат отримується у [4]. |
Щоб отримати переклад [4], браузер надіслав такий запит GET (відображений у його адресному рядку):
Його досить просто зрозуміти:
- http://trans.voila.fr/traduction_voila.php — це URL-адреса сервісу перекладу
- isText=1, судячи з усього, означає, що мова йде про текст
- translationDirection позначає напрямок перекладу, у даному випадку Français-Anglais
- stext — це текст, що підлягає перекладу, у так званому URL-кодованому вигляді. Справа в тому, що деякі символи не можуть з’являтися в URL-адресі. Наприклад, пробіл, який тут було закодовано символом «+». Фреймворк .Net надає статичний метод System.Web.HttpUtility.UrlEncode для виконання цього кодування.
Звідси випливає, що для звернення до сервера перекладу наш клас [ServiceTraduction] зможе використовувати рядок
, де маркери {0} та {1} будуть замінені відповідно на напрямок перекладу та текст, що підлягає перекладу.
Як дізнатися, які варіанти перекладу приймає сервер? На знімку екрана вище мови перекладу вказані у випадаючому списку. Якщо в браузері переглянути (Вигляд / Джерело) HTML-код сторінки, для цього випадаючого списку можна побачити таке:
Це не дуже «чистий» HTML-код, оскільки кожен тег <option> зазвичай має закриватися тегом </option>. Проте атрибути value надають нам список кодів перекладу, які потрібно надіслати на сервер. У словнику LanguesTraduites для інтерфейсу IServiceTraduction ключами будуть вищезазначені атрибути value, а значеннями — тексти, що відображаються у випадаючому списку.
Тепер давайте подивимося (Вигляд / Джерело), де на HTML-сторінці знаходиться переклад, повернутий сервером перекладу:
Переклад знаходиться посередині HTML-сторінки, що повертається. Як його знайти? Можна використати регулярний вираз із послідовністю <div class="txtTrad">...</div>, оскільки тег <div class="txtTrad"> присутній лише в цьому місці HTML-сторінки. Регулярний вираз на C#, що дозволяє витягти перекладений текст, має такий вигляд:
Тепер ми маємо все необхідне для написання класу реалізації ServiceTraduction для інтерфейсу IServiceTraduction:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;
namespace dao {
public class ServiceTraduction : IServiceTraduction {
// автоматичні параметри конфігурації служби
public IDictionary<string, string> LanguesTraduites { get; set; }
public string UrlServeurTraduction { get; set; }
public string ProxyHttp { get; set; }
public String RegexTraduction { get; set; }
// переклад
public string Traduire(string texte, string deQuoiVersQuoi) {
// чи можливий запитуваний переклад?
if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
}
// текст для перекладу
string texteATraduire = HttpUtility.UrlEncode(texte);
// URI, який потрібно отримати
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
// регулярний вираз для пошуку перекладу у відповіді
Regex patternTraduction = new Regex(RegexTraduction);
// виняток
WebTraductionsException exception = null;
// переклад
string traduction = null;
try {
// налаштування запиту
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = "GET";
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
// виконуємо його
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// документ
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
bool traductionTrouvée = false;
string ligne = null;
while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
// пошук перекладу в поточному рядку
MatchCollection résultats = patternTraduction.Matches(ligne);
// переклад знайдено?
if (résultats.Count != 0) {
traduction = résultats[0].Groups[1].Value.Trim();
traductionTrouvée = true;
}
}
// переклад знайдено?
if (!traductionTrouvée) {
exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
}
}
}
} catch (Exception e) {
exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
}
// виняток?
if (exception != null) {
throw exception;
} else {
return traduction;
}
}
}
}
- рядок 12: властивість LanguesTraduites інтерфейсу IServiceTraduction — ініціалізована ззовні
- рядок 13: властивість UrlServeurTraduction — це URL-адреса, яку потрібно надіслати на сервер перекладу: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, де маркер {0} слід замінити на напрямок перекладу, а маркер {1} — на текст, що перекладається — ініціалізується ззовні
- рядок 14: властивість ProxyHttp — це проксі-сервер HTTP, який може використовуватися, наприклад: pproxy.istia.uang:3128 — ініціалізується ззовні
- рядок 15: властивість RegexTraduction — це регулярний вираз, що дозволяє витягувати переклад із HTML-потоку, який повертає сервер перекладу, наприклад @"<div class=""txtTrad"">(.*?)</div>" — ініціалізується ззовні
- ці чотири властивості в нашому додатку будуть ініціалізовані Spring.
- рядки 20–22: перевіряється, чи запитуваний напрямок перекладу дійсно існує у словнику перекладених мов. Якщо це не так, генерується виняток.
- рядок 24: текст, що підлягає перекладу, кодується, щоб його можна було включити до URL-адреси
- рядок 26: формується URI сервісу перекладу. Якщо властивість UrlServeurTraduction є рядком http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, то маркер {0} замінюється змістом перекладу, а маркер {1} — текстом, що підлягає перекладу.
- рядок 28: формується шаблон пошуку перекладу у HTML-відповіді сервера перекладу.
- рядки 33, 60: операція запиту до сервера перекладу відбувається в блоці try / catch
- рядок 35: створюється об’єкт HttpWebRequest, який буде використовуватися для запиту до сервера перекладу, із URI запитуваного документа.
- рядок 36: метод запиту — GET. Цю інструкцію можна було б опустити, оскільки GET, ймовірно, є методом за замовчуванням для об’єкта HttpWebRequest.
- рядок 37: встановлюється властивість Proxy об’єкта HttpWebRequest.
- рядок 39: надсилається запит до сервера перекладу та отримується відповідь типу HttpWebResponse.
- рядки 41–42: використовується StreamReader для зчитування кожного рядка HTML-відповіді сервера.
- рядки 45–53: у кожному рядку відповіді шукається переклад. Коли його знайдено, читання HTML-відповіді припиняється, і закриваються всі відкриті потоки.
- рядки 55–57: якщо у відповіді HTML не знайдено перекладу, готується виняток типу WebTraductionsException, щоб про це повідомити.
- рядки 60–62: якщо під час обміну даними між клієнтом і сервером сталося виключення, його інкапсулюють у виключення типу WebTraductionsException, щоб про це повідомити.
- рядки 64–68: якщо було зареєстровано виняток, його запускають; інакше повертається знайдений переклад.
У нашому прикладі передбачається, що проксі-сервер Http не вимагає автентифікації. Якби це було не так, ми б написали щось на зразок:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
Тут ми використали WebRequest / WebResponse замість WebClient, оскільки нам не потрібно обробляти весь HTML-відповідь сервера перекладу. Як тільки переклад знайдено в цій відповіді, решта рядків відповіді нам більше не потрібні. Клас WebClient не дозволяє цього зробити.
Ось програма для тестування класу ServiceTraduction:
using System;
using System.Collections.Generic;
using dao;
using entites;
namespace ui {
class Program {
static void Main(string[] args) {
try {
// створення служби перекладу
ServiceTraduction serviceTraduction = new ServiceTraduction();
// регулярний вираз для пошуку перекладу
serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
// URL-адреса сервера перекладу
serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
// словник перекладених мов
Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
languesTraduites["fe"]= "Français-Anglais";
languesTraduites["fs"]= "Français-Espagnol";
languesTraduites["ef"]= "Anglais-Français";
serviceTraduction.LanguesTraduites = languesTraduites;
// проксі
//serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
// переклад
string texte = "ce chien est perdu";
string deQuoiVersQuoi = "fe";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "l'été sera chaud";
deQuoiVersQuoi = "fs";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "my tailor is rich";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "xx";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
} catch (WebTraductionsException e) {
// помилка
Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
}
}
}
}
Отримано такі результати:
Проєкт [dao] з цього рішення компілюється у DLL та HttpTraductions.dll:
![]() |
11.7.3.6. Графічний інтерфейс додатка
Повернемося до архітектури нашого додатка:
![]() |
Тепер ми пишемо рівень [ui]. Він є предметом проекту [ui] у розробці:
![]() |
Папка [lib] [3] містить деякі з DLL, на які посилається проект [4]:
- ті, що необхідні для Spring: Spring.Core, Common.Logging, antlr.runtime
- файл для шару [dao]: HttpTraductions
Файл [App.config] містить конфігурацію Spring:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Traductions sur le web</description>
<!-- сервіс перекладу -->
<object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
<property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<property name="LanguesTraduites">
<dictionary key-type="string" value-type="string">
<entry key="fe" value="Français-Anglais"/>
<entry key="ef" value="Anglais-Français"/>
...
<entry key="ei" value="Anglais-Italien"/>
<entry key="ie" value="Italien-Anglais"/>
</dictionary>
</property>
</object>
</objects>
</spring>
</configuration>
- рядок 15: об’єкти, які має інстанціювати Spring. Буде лише один — той, що в рядку 18, який інстанціює сервіс перекладу за допомогою класу ServiceTraduction, знайденого в DLL HttpTraductions.
- рядок 19: властивість UrlServeurTraduction класу ServiceTraduction. Виникає проблема з символом & в URL-адресі. Цей символ має певне значення у файлі XML. Тому його потрібно захистити. Те саме стосується й інших символів, які ми зустрінемо далі у файлі. Їх потрібно замінити послідовністю [&code;]: & — на [&], < — на [<], > — на [>], " — на ["].
- рядок 21: властивість ProxyHttp класу ServiceTraduction. Неініціалізованою залишається властивість null. Невизначення цієї властивості означає, що проксі Http відсутній.
- рядок 23: властивість RegexTraduction класу ServiceTraduction. У регулярному виразі символи [< > "] довелося замінити на їхні захищені еквіваленти.
- рядки 24–33: властивість LanguesTraduites класу ServiceTraduction.
Програма [Program.cs] виконується під час запуску додатка. Її код такий:
using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
namespace ui {
static class Program {
/// <summary>
/// Основна точка входу для додатка.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- Код розробника
// інстанціювання служби перекладу
IApplicationContext ctx = null;
Exception ex = null;
ServiceTraduction serviceTraduction = null;
try {
// контекст Spring
ctx = ContextRegistry.GetContext();
// запит на посилання на службу перекладу
serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
} catch (Exception e1) {
// збереження винятку
ex = e1;
}
// форма для відображення
Form form = null;
// Чи стався виняток?
if (ex != null) {
// так — створюється повідомлення про помилку для відображення
StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
Exception e = ex;
while (e != null) {
msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
e = e.InnerException;
}
// створення вікна помилки, до якого передається повідомлення про помилку для відображення
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
// це буде вікно, яке потрібно відобразити
form = form2;
} else {
// все пройшло успішно
// створення графічного інтерфейсу [Form1], якому передається посилання на службу перекладу
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
// це буде вікно, яке потрібно відобразити
form = form1;
}
// відображення вікна
Application.Run(form);
}
}
}
Цей код уже використовувався в додатку «Податки», версія 6, у розділі 7.6.2.
- Служба перекладу створюється Spring у рядку 27. Якщо створення пройшло успішно, буде відображено форму [Form1] (рядки 52–55), інакше — форму помилки [Form2] (рядки 36–48).
Форма [Form2] використовується в додатку «Податки» версії 6 і була описана в параграфі 7.6.4.
Форма [Form1] має такий вигляд:
![]() |
№ | тип | назва | роль |
1 | TextBox | textBoxTexteATraduire | поле для введення тексту, що перекладається MultiLine=true |
2 | ComboBox | comboBoxLangues | список напрямків перекладу |
3 | Кнопка | buttonTraduire | щоб замовити переклад тексту [1] у напрямку [2] |
4 | TextBox | textBoxTraduction | переклад тексту [1] |
Код форми [Form1] такий:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
namespace ui {
public partial class Form1 : Form {
// сервіс перекладу
public ServiceTraduction ServiceTraduction { get; set; }
// словник мов
Dictionary<string, string> languesInversées = new Dictionary<string, string>();
// конструктор
public Form1() {
InitializeComponent();
}
// початкове завантаження форми
private void Form1_Load(object sender, EventArgs e) {
// побудова зворотного словника мов
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// мови
string langues = ServiceTraduction.LanguesTraduites[code];
// додавання (мови, код) до зворотного словника
languesInversées[langues] = code;
}
// заповнення комбінованого списку в алфавітному порядку мов
string[] languesCombo = languesInversées.Keys.ToArray();
Array.Sort<string>(languesCombo);
foreach (string langue in languesCombo) {
comboBoxLangues.Items.Add(langue);
}
// вибір першої мови
if (comboBoxLangues.Items.Count != 0) {
comboBoxLangues.SelectedIndex = 0;
}
}
private void buttonTraduire_Click(object sender, EventArgs e) {
// щось перекласти?
string texte = textBoxTexteATraduire.Text.Trim();
if (texte == "") return;
// переклад
try {
textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
} catch (Exception ex) {
textBoxTraduction.Text = ex.Message;
}
}
}
}
- рядок 10: посилання на службу перекладу. Ця загальнодоступна властивість була ініціалізована [Program.cs], рядок 53. Отже, під час виконання методів Form1_Load (рядок 20) або buttonTraduire_Click (рядок 40) це поле вже ініціалізовано.
- рядок 12: словник перекладених мов із записами типу ["Français-Anglais","fe"], c.a.d. Інверсія словника LanguesTraduites, що повертається службою перекладу.
- рядок 20: метод Form1_Load виконується під час завантаження форми.
- рядки 22–27: використовуються словники serviceTraduction.LanguesTraduites та ["fe","Français-Anglais"] для побудови словників languesInversées та ["Français-Anglais", "fe"].
- рядок 29: languesCombo — це масив ключів словників languesInversées та c.a.d. Масив елементів ["Français-Anglais"]
- рядок 30: цей масив відсортовано, щоб у списку випадаючого меню переклади відображалися в алфавітному порядку
- рядки 31–33: комбінований список мов заповнено.
- рядок 40: метод, що виконується, коли користувач натискає на кнопку [Traduire]
- рядок 46: для запиту перекладу достатньо викликати метод serviceTraduction.Traduire. Перший параметр — це текст, що перекладається, другий — код напрямку перекладу. Цей код визначається у словнику languesInversées на основі елемента, вибраного у комбінованому списку мов.
- рядок 48: якщо виникає виняток, він відображається замість перекладу.
11.7.3.7. Conclusion
Ця програма продемонструвала, що веб-клієнти фреймворку .NET дають нам змогу використовувати веб-ресурси. Техніка в кожному випадку є схожою:
- визначити URI, до якого слід звернутися. Цей URI здебільшого є параметризованим.
- запитати його
- знайти у відповіді сервера те, що ми шукаємо, за допомогою регулярних виразів
Ця техніка є нестабільною. Адже з часом URI, до якого надсилається запит, або регулярний вираз, що дозволяє знайти очікуваний результат, можуть змінюватися. Тому доцільно розмістити ці дві інформації у файлі конфігурації. Але цього може виявитися недостатньо. У наступному розділі ми побачимо, що в Інтернеті існують більш стабільні ресурси: веб-сервіси.
11.7.4. Клієнт SMTP (Simple Mail Transport Protocol) із класом SmtpClient
Клієнт SMTP є клієнтом сервера SMTP, сервера відправлення пошти. Клас .NET SmtpClient повністю охоплює потреби такого клієнта. Розробнику не потрібно знати подробиці протоколу SMTP. Ми знаємо цей протокол. Він був представлений у розділі 11.4.3.
Ми демонструємо клас SmtpClient у рамках простої програми для Windows, яка дозволяє надсилати електронні листи з вкладеннями. Програма підключатиметься до порту 25 сервера SMTP. Нагадаємо, що на більшості Windows-систем брандмауери або антивірусні програми блокують з’єднання з портом 25. Тому для тестування програми необхідно вимкнути цей захист:
![]() |
SMTP-клієнт матиме однорівневу архітектуру:
![]() |
Проєкт Visual Studio виглядає наступним чином:
![]() |
Графічний інтерфейс [SendMailForm.cs] додатка виглядає так:
![]() |
№ | тип | назва | роль |
1 | TextBox | textBoxServeur | назва сервера SMTP, до якого потрібно підключитися |
2 | NumericUpDown | numericUpDownPort | порт, до якого слід підключитися |
3 | TextBox | textBoxExpediteur | адреса відправника повідомлення |
4 | TextBox | textBoxTo | адреси одержувачів у форматі: адреса1, адреса2, ... |
5 | TextBox | textBoxCc | адреси одержувачів у копії (CC=Carbon Copy) у форматі: адреса1, адреса2, ... |
6 | TextBox | textBoxBcc | адреси одержувачів у прихованій копії (BCC=Blind Carbon Copy) у форматі: адреса1, адреса2, ... Усі адреси з цих трьох полів введення отримають одне й те саме повідомлення з однаковими вкладеннями. Одержувачі повідомлення зможуть дізнатися адреси, що містилися в полях 4 і 5, але не ті, що в полі 6. Отже, Bcc — це спосіб додати когось у копію, не повідомляючи про це іншим одержувачам повідомлення. |
7 | Кнопка | buttonAjouter | щоб додати вкладення до листа |
8 | ListBox | listBoxPiecesJointes | перелік документів, які слід додати до листа |
9 | TextBox | textBoxSujet | тема листа |
10 | TextBox | textBoxMessage | Текст повідомлення. MultiLine=true |
11 | Button | buttonEnvoyer | для відправлення повідомлення та можливих вкладень |
12 | TextBox | textBoxRésultat | відображає підсумок надісланого повідомлення або повідомлення про помилку, якщо виникла проблема |
13 | Кнопка | buttonEffacer | для очищення [12] |
OpenfileDialog | openFileDialog1 | невізуальна перевірка, що дозволяє вибрати вкладення у локальній файловій системі |
У наведеному вище прикладі у [12] відображається такий звіт:
Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc :
Bcc :
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,
Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
Cordialement,
ST
Код форми [SendMailForm.cs] такий:
using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;
namespace Chap9 {
public partial class SendMailForm : Form {
public SendMailForm() {
InitializeComponent();
}
// додавання вкладення
private void buttonAjouter_Click(object sender, EventArgs e) {
// налаштування діалогового вікна openfileDialog1
openFileDialog1.InitialDirectory = Application.ExecutablePath;
openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
openFileDialog1.FilterIndex = 0;
openFileDialog1.FileName = "";
// відображення діалогового вікна та отримання його результату
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
// отримання імені файлу
listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
}
}
private void textBoxServeur_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void setStatutEnvoyer() {
buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
}
// видалити вкладення
private void buttonRetirer_Click(object sender, EventArgs e) {
// вкладення вибрано?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// видалити його
listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
// оновити кнопку «Видалити»
buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
}
}
private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
// вкладення вибрано?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// оновити кнопку «Видалити»
buttonRetirer.Enabled = true;
}
}
// відправлення повідомлення з вкладеннями
private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
}
private void textBoxTo_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void textBoxSujet_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void buttonEffacer_Click(object sender, EventArgs e) {
textBoxResultat.Text = "";
}
}
}
Ми не будемо коментувати цей код, оскільки він не містить нових елементів. Щоб зрозуміти метод buttonAjouter_Click у рядку 14, читачеві пропонується перечитати параграф 7.5.1.
Метод buttonEnvoyer_Click у рядку 55, який відправляє листа, має такий вигляд:
private void buttonEnvoyer_Click(object sender, EventArgs e) {
try {
// пісочний годинник
Cursor = Cursors.WaitCursor;
// SMTP-клієнт
SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
// повідомлення
MailMessage message = new MailMessage();
// відправник
message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
message.From = message.Sender;
// адресати
Regex marqueur = new Regex("\\s*,\\s*");
string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
foreach (string destinataire in destinataires) {
if (destinataire.Trim() != "") {
message.To.Add(new MailAddress(destinataire));
}
}
// CC
string[] copies = marqueur.Split(textBoxCc.Text.Trim());
foreach (string copie in copies) {
if (copie.Trim() != "") {
message.CC.Add(new MailAddress(copie));
}
}
// BCC
string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
foreach (string blindCopie in blindCopies) {
if (blindCopie.Trim() != "") {
message.Bcc.Add(new MailAddress(blindCopie));
}
}
// тема
message.Subject = textBoxSujet.Text.Trim();
// текст повідомлення
message.Body = textBoxMessage.Text;
// вкладення
foreach (string attachement in listBoxPiecesJointes.Items) {
message.Attachments.Add(new Attachment(attachement));
}
// відправлення повідомлення
smtpClient.Send(message);
// ОК — відображається підсумок
StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
textBoxSujet.Clear();
msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
textBoxTo.Clear();
msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
textBoxCc.Clear();
msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
textBoxBcc.Clear();
msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
foreach (string attachement in listBoxPiecesJointes.Items) {
msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
}
msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
listBoxPiecesJointes.Items.Clear();
textBoxResultat.Text = msg.ToString();
} catch (Exception ex) {
// відображається помилка
textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
}
// звичайний курсор
Cursor = Cursors.Arrow;
}
- рядок 6: створюється клієнт SMTP. Йому потрібні два параметри: ім’я сервера SMTP та порт, на якому він працює
- рядок 8: створюється повідомлення типу MailMessage. Саме воно буде інкапсулювати весь текст повідомлення, що надсилається.
- рядок 10: вказано електронну адресу Sender відправника. Електронна адреса — це екземпляр типу MailAddress, побудований на основі символьного рядка «xx@yy.zz». Цей рядок повинен мати форму, що відповідає вимогам до електронної адреси, інакше генерується виняток. У цьому випадку він відобразиться у полі textBoxResultat (рядок 63) у не дуже зручному для сприйняття вигляді.
- рядки 13–19: електронні адреси одержувачів розміщуються у списку «To» повідомлення. Ці адреси отримуються з поля textBoxTo. Регулярний вираз у рядку 13 дозволяє витягти окремі адреси, розділені комою.
- рядки 21–26: той самий процес повторюється для заповнення поля CC повідомлення адресами з поля «Копія» (textBoxCc).
- рядки 28–33: повторюємо той самий процес, щоб заповнити поле «Bcc» повідомлення адресами зі сліпої копії поля textBoxBcc.
- рядок 35: поле Subject повідомлення заповнюється темою з поля textBoxSujet.
- рядок 37: поле Body повідомлення ініціалізується текстом повідомлення textBoxMessage.
- рядки 39–41: до повідомлення додаються вкладення. Кожне вкладення додається у вигляді об’єкта Attachment до поля Attachments повідомлення. Об’єкт Attachment створюється на основі повного шляху до файлу, що додається, у локальній файловій системі.
- рядок 43: повідомлення надсилається за допомогою методу Send клієнта Smtp.
- рядки 45–60: запис підсумкової інформації про відправлення у поле textBoxResultat та скидання форми.
- рядок 63: відображення можливої помилки
11.8. Загальний асинхронний TCP-клієнт
11.8.1. Огляд
У всіх прикладах цього розділу зв’язок між клієнтом і сервером відбувався у блокуючому режимі, який також називають синхронним:
- коли клієнт підключається до сервера, він чекає на відповідь сервера на цей запит, перш ніж продовжувати.
- коли клієнт зчитує рядок тексту, надісланий сервером, він залишається заблокованим, доки сервер не надішле цей рядок.
- на стороні сервера службові потоки, що забезпечують обслуговування клієнта, працюють так само, як описано вище.
У графічних інтерфейсах часто необхідно не блокувати користувача під час тривалих операцій. Часто наводять приклад завантаження великого файлу. Під час цього завантаження користувач повинен мати можливість продовжувати взаємодіяти з графічним інтерфейсом.
Тут ми пропонуємо переписати загальний TCP-клієнт із розділу 11.6.3, внесли в нього такі зміни:
- інтерфейс буде графічним
- засобом зв’язку із сервером буде об’єкт Socket
- режим зв’язку буде асинхронним:
- клієнт ініціюватиме з’єднання з сервером, але не залишатиметься заблокованим, чекаючи на його встановлення
- клієнт ініціює відправку даних на сервер, але не буде залишатися в стані очікування до завершення відправки
- клієнт ініціюватиме прийом даних від сервера, але не залишатиметься в стані очікування до завершення цього процесу.
Нагадаємо, на якому рівні знаходиться об’єкт Socket у TCP-комунікації «клієнт-сервер»:
![]() |
Клас Socket — це той, що працює найближче до мережі. Він дозволяє точно керувати мережевим з’єднанням. Термін socket означає розетку. Це поняття було розширено для позначення програмного мережевого сокета. У процесі зв’язку TCP-IP між двома машинами A та B між собою взаємодіють два sockets. Додаток може працювати безпосередньо з sockets. Це стосується додатка A, наведеного вище. Сокет може бути сокетом типу client або serveur.
11.8.2. Графічний інтерфейс асинхронного TCP-клієнта
Додаток Visual Studio виглядає наступним чином:
![]() |
[ClientTcpAsynchrone.cs] — це графічний інтерфейс. Він виглядає так:
![]() |
№ | тип | назва | роль |
1 | TextBox | textBoxNomServeur | ім'я TCP-сервера, до якого потрібно підключитися |
2 | NumericUpDown | numericUpDownPortServeur | порт, до якого потрібно підключитися |
3 | RadioButton | radioButtonLF radioButtonRCLF | щоб вказати символ кінця рядка, який повинен використовувати клієнт: LF "\n" або RCLF "\r\n" |
4 | Кнопка | buttonConnexion | для підключення до порту [2] сервера [1]. Напис на кнопці — [Connecter], коли клієнт не підключений до сервера, і [Déconnecter] — коли він підключений. |
5 | TextBox | textBoxMsgToServeur | повідомлення, яке надсилається на сервер після встановлення з’єднання. Коли користувач натискає клавішу [Entrée], повідомлення надсилається з обраним у [3] символом кінця рядка |
6 | ListBox | listBoxEvts | список, у якому відображаються основні події з’єднання «клієнт/сервер»: підключення, відключення, закриття потоку, помилки зв’язку |
7 | ListBox | listBoxDialogue | список, у якому відображаються повідомлення діалогу «клієнт/сервер» |
8 | Кнопка | buttonRazEvts | для очищення списку [6] |
4 | Кнопка | buttonRazDialogue | для очищення списку [7] |
Принципи роботи цього інтерфейсу такі:
- користувач підключає свій графічний TCP-клієнт до TCP-сервісу за допомогою [1, 2, 3, 4].
- асинхронний потік безперервно приймає всі дані, надіслані сервером TCP, і відображає їх у списку [7]. Цей потік не пов'язаний з іншими операціями інтерфейсу.
- Користувач може надсилати повідомлення на сервер у зручному для себе темпі за допомогою [5]. Кожне повідомлення надсилається асинхронним потоком. На відміну від потоку прийому, який ніколи не зупиняється, потік відправлення завершується одразу після надсилання повідомлення. Для наступного повідомлення буде використано новий асинхронний потік.
- Зв’язок між клієнтом і сервером завершується, коли один із учасників закриває з’єднання. Користувач може зробити це за допомогою кнопки [4], яка після встановлення з’єднання має назву [Déconnecter].
Ось знімок екрана з прикладом виконання:
![]() |
- у [1]: підключення до служби POP
- в [2]: відображення подій, що відбулися під час з’єднання
- у [3]: повідомлення, надіслане сервером POP після завершення підключення
- у [4]: кнопка [Connecter] перетворилася на кнопку [Déconnecter]
![]() |
- в [1] було надіслано команду quit на сервер POP. Сервер відповів +OK goodbye та закрив з’єднання
- у [2] було виявлено це закриття з боку сервера. Після цього клієнт закрив з’єднання зі свого боку.
- У [3] кнопка [Déconnecter] знову стала кнопкою [Connecter]
11.8.3. Асинхронне з'єднання з сервером
Натискання кнопки [Connecter] викликає виконання наступного методу:
private void buttonConnexion_Click(object sender, EventArgs e) {
// увійти чи вийти?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- рядок 3: кнопка може мати назву [Connecter] або [Déconnecter].
Метод підключення такий:
using System.Net.Sockets;
...
namespace Chap9 {
public partial class ClientTcp : Form {
const int tailleBuffer = 1024;
private Socket client = null;
private byte[] data = new byte[tailleBuffer];
private string réponse = null;
private string finLigne = "\r\n";
// делегати
public delegate void writeLog(string log);
public ClientTcp() {
InitializeComponent();
}
....................................
private void connexion() {
// перевірка даних
string nomServeur = textBoxNomServeur.Text.Trim();
if (nomServeur == "") {
logEvent("indiquez le nom du serveur");
return;
}
// відстеження
logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
try {
// створення сокета
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// асинхронне підключення
client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
} catch (Exception ex) {
logEvent(String.Format("erreur de connexion : {0}", ex.Message));
return;
}
}
// з'єднання встановлено
private void connecté(IAsyncResult résultat) {
// отримано сокет клієнта
Socket client = résultat.AsyncState as Socket;
...
}
// відстеження процесу
private void logEvent(string msg) {
....
}
}
}
- рядок 1: клас Socket належить до простору імен System.Net.Sockets.
Деякі дані мають бути спільними для кількох методів форми. Це такі дані:
- рядок 7: client — це сокет для зв’язку з сервером
- рядки 6 і 8: клієнт отримуватиме свої повідомлення у масиві байтів data.
- рядок 9: «response» — це відповідь, надіслана сервером.
- рядок 10: finLigne — це маркер кінця рядка, який використовує клієнт TCP; за замовчуванням він ініціалізується як RCLF, але користувач може змінити його за допомогою перемикачів [3].
Процедура connexion у рядку 19 здійснює підключення до сервера TCP:
- рядки 21–25: перевіряється, чи ім’я сервера не є порожнім. Якщо це не так, подія реєструється в listBoxEvts за допомогою методу logEvent у рядку 49.
- рядок 27: повідомляється, що з’єднання відбудеться
- рядок 30: створюється об’єкт Socket, необхідний для зв’язку TCP/IP. Конструктор приймає три параметри:
- AddressFamily addressFamily: сімейство адрес IP клієнта та сервера, у даному випадку адреси IPv4 (AddressFamily.InterNetwork)
- SocketType socketType: тип сокета. Тип SocketType.Stream підходить для з’єднань TCP/IP
- ProtocolType protocolType: тип використовуваного інтернет-протоколу, у даному випадку протокол TCP
- рядок 32: з'єднання встановлюється асинхронно. З'єднання ініціюється, але виконання продовжується, не чекаючи його завершення. Метод [Socket].BeginConnect приймає чотири параметри:
- IPAddress ipAddress: IP-адреса комп’ютера, на якому виконується служба, до якої потрібно підключитися
- Int32 port: порт служби
- AsyncCallBack asyncCallBack: AsyncCallBack є типом делегату:
Метод asyncCallBack, що передається як третій параметр методу BeginConnect, повинен бути методом, який приймає тип IAsyncCallBack і не повертає жодного результату. Саме цей метод буде викликано після встановлення з’єднання. Тут ми передаємо як третій параметр метод connecté із рядка 41.
- (продовження)
- Об’єкт state: об’єкт, який потрібно передати методу asyncCallBack. Цей метод отримує (див. делегат вище) параметр ar типу IAsyncResult. Об’єкт state можна буде отримати в ar.AsyncState (рядок 43). Тут ми передаємо як четвертий параметр сокет клієнта.
- рядок 38: метод завершено. Користувач знову може взаємодіяти з графічним інтерфейсом. З’єднання відбувається у фоновому режимі, паралельно з обробкою подій графічного інтерфейсу. Також паралельно, після завершення з’єднання — незалежно від того, чи воно завершилося успішно чи ні — буде викликано метод connecté із рядка 41.
Код методу connecté такий:
// з'єднання встановлено
private void connecté(IAsyncResult résultat) {
// отримано сокет клієнта
Socket client = résultat.AsyncState as Socket;
try {
// завершуємо асинхронну операцію
client.EndConnect(résultat);
// відстеження
logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
// форма
buttonConnexion.Text = "Déconnecter";
// асинхронне зчитування даних із сервера
réponse = "";
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
} catch (SocketException e) {
logEvent(String.Format("erreur de connexion : {0}", e.Message));
return;
}
}
// отримання даних
private void lecture(IAsyncResult résultat) {
// отримання сокета клієнта
Socket client = résultat.AsyncState as Socket;
...
}
- рядок 4: сокет клієнта отримується з параметра résultat, отриманого методом. Нагадаємо, що цей об’єкт передається як 4-й параметр методу BeginConnect.
- рядок 7: спроба підключення завершується методом EndConnect, якому слід передати параметр résultat, отриманий методом.
- рядок 9: подія заноситься до списку подій
- рядок 11: кнопка [Connecter] перетворюється на кнопку [Déconnecter], щоб користувач міг ініціювати відключення.
- рядок 13: ініціалізується відповідь сервера. Вона буде оновлюватися за допомогою повторних викликів асинхронного методу BeginReceive.
- рядок 14: перший виклик асинхронного методу BeginReceive. Він викликається з такими параметрами:
- byte[] buffer: буфер, у який будуть записані дані, що будуть отримані — тут буфер має ім’я data
- int offset: з якої позиції буфера слід розміщувати дані, що будуть отримані — тут зміщення дорівнює 0, c.a.d, тобто дані розміщуються починаючи з першого байта буфера.
- int size: розмір буфера в байтах — тут розмір становить tailleBuffer.
- SocketFlags socketFlags: конфігурація сокета — тут конфігурація відсутня
- AsyncCallBack asyncCallBack: метод, який слід викликати після завершення прийому. Це відбудеться або тому, що буфер отримав дані, або тому, що з’єднання було закрито. Тут методом-зворотним викликом є метод lecture із рядка 22.
- Об’єкт state: об’єкт, який передається методу зворотного виклику asyncCallBack. Тут знову передається сокет клієнта.
Слід зауважити, що все це відбувається без будь-яких дій з боку користувача, окрім початкового запиту на з’єднання за допомогою кнопки [Connecter]. У кінці методу connecté у фоновому режимі виконується інший метод: метод lecture, який ми зараз розглядаємо.
// отримання даних
private void lecture(IAsyncResult résultat) {
// отримання сокета клієнта
Socket client = résultat.AsyncState as Socket;
int nbOctetsReçus = 0;
bool erreur = false;
try {
// кількість отриманих байтів
nbOctetsReçus = client.EndReceive(résultat);
if (nbOctetsReçus == 0) {
// сервер більше не відповідає
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// виникла проблема з отриманням
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// завершено?
if (nbOctetsReçus == 0 || erreur) {
// за необхідності відключаємо клієнта
déconnexion();
// відображається кінець відповіді
afficherRéponseServeur(réponse, true);
// кінець читання
return;
}
// отримуємо отримані дані
string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
// додаємо їх до вже отриманих даних
réponse += données;
// відображається відповідь
afficherRéponseServeur(réponse, false);
// продовжується читання
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
}
- рядок 2: метод lecture запускається у фоновому режимі, коли буфер data отримав дані або коли з'єднання було закрито сервером.
- рядок 9: асинхронний запит на читання завершується методом EndReceive. І тут цей метод слід викликати з параметром, отриманим від функції зворотного виклику. Метод EndReceive повертає кількість байтів, отриманих у буфері читання.
- рядок 10: якщо кількість байтів дорівнює нулю, це означає, що з’єднання було закрито сервером.
- рядок 12: подія записується до списку подій
- рядок 14: обробляється можливе виключення
- рядки 16–17: записуємо подію до списку подій та фіксуємо помилку
- рядок 20: перевіряємо, чи потрібно закрити з’єднання
- рядок 22: закриваємо з'єднання на стороні клієнта за допомогою методу déconnexion, який ми розглянемо пізніше.
- рядок 24: відповідь сервера, c.a.d. Глобальна змінна réponse відображається у списку діалогу listBoxDialogue за допомогою приватного методу afficherRéponseServeur.
- рядок 26: кінець асинхронного методу lecture
- рядок 29: отримані байти поміщаються в рядок у форматі UTF8.
- рядок 31: вони додаються до відповіді, що формується
- рядок 33: відповідь відображається у списку listBoxDialogue.
- рядок 35: знову починається очікування даних від сервера
Зрештою, асинхронний метод lecture ніколи не зупиняється. Він безперервно зчитує дані з сервера та відображає їх у списку listBoxDialogue. Він зупиняється лише тоді, коли з’єднання закривається або сервером, або самим користувачем.
11.8.4. Відключення від сервера
Натискання кнопки [Déconnecter] спричиняє виконання наступного методу:
private void buttonConnexion_Click(object sender, EventArgs e) {
// підключення чи відключення?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- рядок 3: кнопка може мати напис [Connecter] або [Déconnecter].
Метод déconnexion забезпечує відключення клієнта:
private void déconnexion() {
// закриття сокета
if (client != null && client.Connected) {
try {
// відстеження
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
// відключення
client.Shutdown(SocketShutdown.Both);
client.Close();
// форма
buttonConnexion.Text = "Connecter";
} catch (Exception ex) {
// відстеження
logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
}
}
}
- рядок 3: якщо клієнт існує і підключений
- рядок 6: відключення оголошується в listBoxEvts. Властивість client.RemoteEndPoint повертає пару (IP-адреса, порт) іншого кінця з’єднання, у даному випадку сервера (c.a.d).
- рядок 8: потік даних сокета закривається за допомогою методу ShutDown. Потік даних сокета є двонаправленим: сокет надсилає та отримує дані. Параметром методу ShutDown може бути: ShutDown.Receive для закриття потоку прийому, Shutdonw.Send для закриття потоку передачі або ShutDown.Both для закриття обох потоків.
- рядок 9: звільняються ресурси, пов’язані з сокетом
- рядок 11: кнопка [Déconnecter] стає кнопкою [Connecter]
- рядки 12–15: обробка можливого винятку
11.8.5. Асинхронна передача даних на сервер
Коли користувач підтверджує повідомлення у полі textBoxMsgToServeur, виконується наступний метод:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// клавіша [Entrée] ?
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- рядки 3–5: якщо користувач натиснув клавішу [Entrée] і сокет клієнта підключений, то повідомлення з поля textBoxMsgToServeur надсилається за допомогою методу envoyerMessage.
Метод envoyerMessage виглядає наступним чином:
private void envoyerMessage() {
// асинхронне надсилання повідомлення
// повідомлення
byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
// воно надіслано
client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
// діалог
logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
// очищення повідомлення
textBoxMsgToServeur.Clear();
}
- рядок 4: до повідомлення додається маркер кінця рядка клієнта, і воно поміщається в масив байтів message.
- рядок 6: запущено асинхронну передачу за допомогою методу BeginSend. Параметри методу BeginSend ідентичні параметрам методу BeginReceive. Після завершення асинхронної передачі повідомлення буде викликано метод écriture.
- рядок 8: надіслане повідомлення додається до списку listBoxDialogue для відстеження діалогу «клієнт-сервер»
- рядок 10: надіслане повідомлення видаляється з графічного інтерфейсу
Метод зворотного виклику écriture має такий вигляд:
private void écriture(IAsyncResult résultat) {
// результат відправлення повідомлення
Socket client = résultat.AsyncState as Socket;
try {
client.EndSend(résultat);
} catch (Exception e) {
// виникли проблеми з відправленням
logEvent(String.Format("erreur d'émission : {0}", e.Message));
}
}
- рядок 4: метод зворотного виклику écriture отримує параметр результату типу IAsyncResult.
- рядок 3: у параметрі résultat отримується сокет клієнта. Цей сокет був 5-м параметром методу BeginSend.
- рядок 5: завершується асинхронна операція відправлення.
Ми не чекаємо завершення передачі повідомлення, щоб повернути контроль користувачеві. Таким чином, користувач може надіслати друге повідомлення, навіть якщо передача першого ще не завершилася.
11.8.6. Відображення подій та діалогу «клієнт/сервер»
Події відображаються за допомогою методу logEvents:
// відстеження процесу
private void logEvent(string msg) {
listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
}
private void logEventCallBack(string msg) {
// відображення повідомлення
msg = msg.Replace(finLigne, " ");
listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
- рядок 2: метод logEvents отримує як параметр повідомлення, яке потрібно додати до списку listBoxEvts.
- рядок 3: не можна безпосередньо використовувати компонент listBoxEvents. Справа в тому, що метод logEvents викликається двома типами потоків:
- головний потік, який володіє графічним інтерфейсом, наприклад, коли він повідомляє про те, що триває спроба підключення
- допоміжний потік, що забезпечує асинхронну операцію. Цей тип потоку не є власником компонентів, і його доступ до компонента C повинен контролюватися операцією C.Invoke. Ця операція повідомляє елементу управління C, що потік хоче виконати над ним операцію. Метод Invoke приймає два параметри:
- функцію зворотного виклику типу delegate. Ця функція зворотного виклику буде виконана потоком, який володіє графічним інтерфейсом, а не потоком, що виконує метод C.Invoke.
- об’єкт, який буде передано до функції зворотного виклику.
У даному випадку першим параметром, що передається методу Invoke, є екземпляр наступного делегата:
public delegate void writeLog(string log);
Делегат writeLog має параметр типу string і не повертає жодного результату. Цим параметром буде повідомлення, яке потрібно записати в listBoxEvts.
У рядку 3 першим параметром, переданим методу Invoke, є метод logEventCallBack з рядка 6. Він точно відповідає сигнатурі делегату writeLog. Другим параметром, що передається методу Invoke, є повідомлення, яке буде передано як параметр методу logEventCallBack.
Операція Invoke є синхронною. Виконання вторинного потоку блокується доти, доки потік-власник елемента управління не виконає метод зворотного виклику.
- рядок 6: метод зворотного виклику, що виконується потоком графічного інтерфейсу, отримує повідомлення, яке потрібно відобразити в елементі управління listBoxEvts.
- рядок 9: подія записується в журнал на першому місці списку, щоб найсвіжіші події знаходилися у верхній частині списку.
Повідомлення діалогу «клієнт/сервер» відображаються за допомогою методу logDialogue:
// відстеження діалогу
private void logDialogue(string msg) {
listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
}
private void logDialogueCallBack(string msg) {
// відображення повідомлення
msg = msg.Replace(finLigne, " ");
listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
Принцип роботи такий самий, як і в методі logEvent.
Повідомлення, отримані клієнтом, відображаються за допомогою методу afficherRéponseServeur:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
Перший параметр — це повідомлення, яке потрібно відобразити. Це повідомлення може складатися з декількох рядків. Справа в тому, що клієнт зчитує дані з сервера блоками розміром tailleBuffer (1024) байт. У цих 1024 байтах можуть міститися різні рядки, які розпізнаються за символом кінця рядка «\n». Останній рядок може бути неповним, оскільки його маркер кінця рядка міститься в наступних 1024 байтах. Метод знаходить у повідомленні рядки, що закінчуються символом «\n», а потім звертається до logDialogue із запитом на їх відображення. Другий параметр методу вказує, чи слід відобразити останній знайдений рядок, чи залишити його в буфері для доповнення наступним повідомленням. Код є досить складним і не представляє тут інтересу. Тому він не коментуватиметься.
11.8.7. Висновок
Цей самий приклад можна було б реалізувати за допомогою синхронних операцій. У даному випадку асинхронний характер графічного інтерфейсу не дає користувачеві особливих переваг. Проте, якщо користувач підключається, а потім виявляє, що сервер «більше не відповідає», він має можливість відключитися завдяки тому, що графічний інтерфейс продовжує реагувати на події під час виконання асинхронних операцій. Цей досить складний приклад дав нам змогу представити нові поняття:
- використання сокетів
- використання асинхронних методів. Те, що ми розглянули, є частиною стандарту. Існують й інші асинхронні методи, які працюють за тим самим принципом.
- оновлення елементів управління графічного інтерфейсу за допомогою допоміжних потоків.
Асинхронна комунікація TCP/IP має для сервера більш суттєві переваги, ніж ті, що продемонстровано в попередньому прикладі. Відомо, що сервер обслуговує своїх клієнтів за допомогою допоміжних потоків. Якщо його пул потоків містить N потоків, це означає, що він може обслуговувати лише N клієнтів одночасно. Якщо всі N потоків виконують блокуючу (синхронну) операцію, то для нового клієнта більше немає вільних потоків, доки одна з блокуючих операцій не завершиться і не звільнить потік. Якщо в потоках виконувати асинхронні, а не синхронні операції, потік ніколи не блокується і може бути швидко перенаправлений для обслуговування нових клієнтів.
11.9. Приклад програми, версія 8: Сервер розрахунку податків
11.9.1. Архітектура нової версії
Ми повертаємося до програми розрахунку податків, яку вже розглядали в різних варіантах. Нагадаємо її останню версію — версію 7 з параграфа 9.8.
![]() |
Дані зберігалися в базі даних, а рівень [ui] був графічним інтерфейсом:
![]() |
Ми повернемося до цієї архітектури та розподілимо її між двома машинами:
![]() |
- машина [serveur] розміщуватиме шари [metier] та [dao] версії 7. Буде створено TCP/IP-шар [serveur] [1], щоб надати можливість інтернет-користувачам звертатися до сервісу розрахунку податку.
- Сервер [client] буде розміщувати рівень [ui] версії 7. Буде створено рівень TCP/IP [client] [2], щоб рівень [ui] міг звертатися до служби розрахунку податків.
Архітектура тут суттєво змінюється. Версія 7 була однокористувацьким додатком для Windows. Версія 8 стає клієнт-серверним інтернет-додатком. Сервер зможе обслуговувати декількох клієнтів одночасно.
Спочатку ми напишемо частину програми [serveur].
11.9.2. Сервер розрахунку податків
11.9.2.1. Проєкт Visual Studio
![]() |
Проєкт Visual Studio матиме такий вигляд:
![]() |
- у [1] — проект. У ньому містяться такі елементи:
- [ServeurImpot.cs]: сервер TCP/IP для розрахунку податку у вигляді консольного додатка.
- [dbimpots.sdf]: база даних SQL Server Compact версії 7, описана в розділі 9.8.5.
- [App.config]: файл конфігурації програми.
- У [2] папка [lib] містить необхідні для проєкту файли DLL:
- [ImpotsV7-dao]: шар [dao] версії 7
- [ImpotsV7-metier]: шар [metier] версії 7
- [antlr.runtime, CommonLogging, Spring.Core] для Spring
- у [3], посилання на проект
11.9.2.2. Конфігурація додатка
Файл [App.config] використовується Spring. Його вміст такий:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- рядки 16–20: конфігурація шару [dao], пов’язаного з базою SQL Server compact
- рядки 21–23: конфігурація шару [metier].
Це файл конфігурації, що використовується в шарі [ui] версії 7. Він був представлений у розділі 9.8.4.
11.9.2.3. Функціонування сервера
Під час запуску сервера серверна програма створює екземпляри шарів [metier] та [dao], а потім відображає консольний інтерфейс адміністрування:
Консоль адміністрування підтримує такі команди:
для запуску служби на заданому порту | |
для зупинки служби. Потім її можна запустити знову на тому самому або іншому порту. | |
для увімкнення відображення діалогу «клієнт-сервер» на консолі | |
для вимкнення відбиття | |
для відображення стану служби (активний/неактивний) | |
для виходу з програми |
Запустимо сервер:
Тепер запустимо асинхронний графічний TCP-клієнт, розглянутий раніше в розділі 11.8.

Клієнт підключений. Він може надсилати такі команди на сервер розрахунку податків:
щоб отримати список дозволених команд | |
для розрахунку податку для особи, яка має nbEnfants дітей та заробітну плату в розмірі salaireAnnuel євро. marié дорівнює o, якщо особа одружена, n — у протилежному випадку. | |
для завершення з’єднання з сервером |
Ось приклад діалогу:
![]() |
На стороні сервера консоль відображає таке:
Увімкнемо відлуння та розпочнемо новий діалог із графічного клієнта:
![]() |
Консоль адміністрування відображає наступне:
- рядок 1: увімкнено відлуння діалогу «клієнт/сервер»
- рядок 2: надійшов запит від клієнта
- рядок 3: він надіслав команду [aide]
- рядки 4–7: відповідь сервера, що складається з 4 рядків.
Зупинимо службу:
- рядок 1: надсилається запит на зупинку служби (а не самої програми)
- рядок 2: виняток, спричинений тим, що сервер, який застряг у режимі очікування клієнта, був раптово перерваний через закриття служби прослуховування.
- рядок 3: тепер службу можна перезапустити за допомогою start port або зупинити за допомогою quit.
До зупинки служби прослуховування клієнт обслуговувався через інше з’єднання. Це з’єднання не закривається при закритті сокета прослуховування. Клієнт може продовжувати надсилати команди: службовий потік, який був пов’язаний з ним до закриття служби прослуховування, продовжує відповідати йому:

11.9.3. Код TCP-сервера для розрахунку податку
![]() |
1 ![]() |
Код сервера [ServeurImpot.cs] такий:
...
namespace Chap9 {
public class ServeurImpot {
// дані, спільні для потоків і методів
private static IImpotMetier metier = null;
private static int port;
private static TcpListener service;
private static bool actif = false;
private static bool echo = false;
// головна програма
public static void Main(string[] args) {
// інстанціювання шарів [metier] та [dao]
IApplicationContext ctx = null;
metier = null;
try {
// контекст Spring
ctx = ContextRegistry.GetContext();
// запит на посилання на рівень [metier]
metier = (IImpotMetier)ctx.GetObject("metier");
// конфігурація пулу потоків
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// читає команди адміністрування сервера, введені з клавіатури, у нескінченному циклі
string commande = null;
string[] champs = null;
while (true) {
// запрошення
Console.Write("Serveur de calcul d'impôt >");
// читання команди
commande = Console.ReadLine().Trim().ToLower();
champs = Regex.Split(commande, @"\s+");
// виконання команди
switch (champs[0]) {
case "start":
// активний?
if (actif) {
//помилка
Console.WriteLine("Le serveur est déjà actif");
} else {
// перевірка порту
if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
Console.WriteLine("Syntaxe : start port. Port incorrect");
} else {
// запуск служби прослуховування
ThreadPool.QueueUserWorkItem(doEcoute, null);
}
}
break;
case "echo":
// echo start / stop
if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
Console.WriteLine("Syntaxe : echo start / stop");
} else {
echo = champs[1] == "start";
}
break;
case "stop":
// завершення роботи служби
if (actif) {
service.Stop();
actif = false;
}
break;
case "status":
// стан сервера
if (actif) {
Console.WriteLine("Le service est lancé sur le port {0}", port);
} else {
Console.WriteLine("Le service n'est pas lancé}");
}
break;
case "quit":
// вихід із програми
Console.WriteLine("Fin du service");
Environment.Exit(0);
break;
default:
// неправильна команда
Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
break;
}
}
} catch (Exception e1) {
// вивід винятку
Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
return;
}
}
private static void doEcoute(Object data) {
...
}
....
}
}
- рядки 18–21: шари [metier] та [dao] інстанціюються Spring, налаштованим за допомогою [App.config]. Після цього ініціалізується глобальна змінна metier у рядку 6.
- рядки 24–25: налаштовується пул потоків додатка з мінімальною та максимальною кількістю 10 потоків.
- рядки 30–86: цикл обробки команд адміністрування сервісу (start, stop, quit, echo, status).
- рядок 32: запит сервера для кожної нової команди
- рядок 34: зчитування команди адміністратора
- рядок 35: команда розбивається на поля для подальшого аналізу
- рядки 38–52: команда start port, призначена для запуску служби прослуховування
- рядок 40: якщо служба вже активна, нічого робити не потрібно
- рядок 45: перевіряється наявність і правильність порту. Якщо так, встановлюється глобальна змінна port з рядка 7.
- рядок 49: служба прослуховування буде управлятися додатковим потоком, щоб головний потік міг продовжувати виконувати команди з консолі. Якщо метод doEcoute успішно встановить з’єднання, ініціалізуються глобальні змінні service у рядку 8 та actif у рядку 9.
- рядки 53–60: команда echo start / stop, яка вмикає / вимикає відображення діалогу «клієнт–сервер» на консолі
- рядок 58: встановлено значення глобальної змінної echo з рядка 7
- рядки 61–67: команда stop, яка зупиняє службу прослуховування.
- рядок 64: зупинка служби прослуховування
- рядки 68–75: команда status, яка відображає стан служби (активний/неактивний)
- рядки 76–80: команда quit, яка зупиняє все.
Потік, відповідальний за обробку запитів клієнтів, виконує наступний метод doEcoute:
private static void doEcoute(Object data) {
// потік, що обробляє запити клієнтів
try {
// створення служби
service = new TcpListener(IPAddress.Any, port);
// запуск служби
service.Start();
// сервер активний
actif = true;
// моніторинг
Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
// цикл обслуговування клієнтів
TcpClient tcpClient = null;
// номер клієнта
int numClient = 0;
// нескінченний цикл
while (true) {
// очікування клієнта
tcpClient = service.AcceptTcpClient();
// обслуговування здійснюється іншим завданням
ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// наступний клієнт
numClient++;
}
} catch (Exception ex) {
// повідомляється про помилку
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
}
}
// інформація про клієнта
internal class Client {
public TcpClient CanalTcp { get; set; } // зв'язок із клієнтом
public int NumClient { get; set; } // номер клієнта
}
Тут ми маємо код, схожий на код сервера-ехо, розглянутого в розділі 11.6.1. Ми коментуємо лише те, що відрізняється:
- рядок 7: запущено службу підтримки
- лінія 9: відзначається, що служба тепер активна
Лінія 21: клієнти обслуговуються сервісними потоками, що виконують наступний метод doService:
private static void doService(Object infos) {
// вибирається клієнт, якого потрібно обслужити
Client client = infos as Client;
// надання послуги клієнту
Console.WriteLine("Début du service au client {0}", client.NumClient);
// обробка зв’язку TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// небуферований вихідний потік
writer.AutoFlush = true;
// надсилання привітального повідомлення клієнту
writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
// цикл читання запиту/запису відповіді
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// моніторинг консолі
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// аналіз запиту
demande = demande.Trim().ToLower();
// порожній запит?
if (demande.Length == 0) {
// помилковий запит
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// розбиття запиту на поля
string[] champs = Regex.Split(demande, @"\s+");
// аналіз
switch (champs[0].ToLower()) {
case "aide":
writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
break;
case "impot":
// розраховується податок
writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
break;
case "aurevoir":
serviceFini = true;
writeClient(writer, client.NumClient, "Au revoir...");
break;
default:
writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
break;
}
}
}
}
}
}
} catch (Exception e) {
// помилка
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
private static void writeClient(StreamWriter writer, int numClient, string message) {
// вивести повідомлення в консоль?
if (echo) {
Console.WriteLine("---> Client {0} : {1}", numClient, message);
}
// відправлення повідомлення клієнту
writer.WriteLine(message);
}
Знову ж таки, ми маємо тут код, схожий на той, що використовується в сервері відлуння, розглянутому в розділі 11.6.1. Ми коментуємо лише те, що відрізняється:
- рядок 15: після підключення клієнта сервер надсилає йому привітальне повідомлення.
- рядки 19–52: цикл зчитування команд клієнта. Цикл зупиняється, коли клієнт надсилає команду «aurevoir».
- рядок 27: випадок порожньої команди
- рядок 34: запит розбивається на поля для аналізу
- рядок 37: команда aide: клієнт запитує список дозволених команд
- рядок 40: запит impot: клієнт запитує розрахунок податку. У відповідь надсилається повідомлення, повернуте методом calculImpot, яке ми детально розглянемо незабаром.
- рядок 44: запит aurevoir: клієнт повідомляє, що завершив операцію.
- рядок 45: готуємося вийти з циклу читання запитів клієнта (рядки 19–52)
- рядок 46: клієнту надсилається повідомлення з прощанням
- рядок 48: некоректна команда. Клієнту надсилається повідомлення про помилку.
Обробка запиту impot здійснюється за допомогою наступного методу calculImpot:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
// запит на розрахунок для одружених (Так/Ні) nbEnfants salaireAnnuel
// потрібно 4 поля
if (champs.Length != 4) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// поля [1]
string marié = champs[1];
if (marié != "o" && marié != "n") {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// поля [2]
int nbEnfants;
if (!int.TryParse(champs[2], out nbEnfants)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// поля [3]
int salaireAnnuel;
if (!int.TryParse(champs[3], out salaireAnnuel)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// все гаразд — розраховуємо податок
int impot = 0;
try {
impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
return impot.ToString();
} catch (Exception ex) {
return ex.Message;
}
}
- рядок 1: метод отримує як третій параметр масив полів замовлення impot. Якщо воно сформовано правильно, воно має вигляд «дохід одружений nbEnfants salaireAnnuel». Метод повертає у якості результату відповідь, яку слід надіслати клієнту.
- рядок 4: перевіряється, чи має команда 4 поля
- рядок 8: перевіряється, чи поле marié є дійсним
- рядок 14: перевіряється, чи поле nbEnfants є дійсним
- рядок 19: перевіряється, чи поле salaireAnnuel є дійсним
- рядок 25: податок обчислюється за допомогою методу CalculerImpot шару [metier]. Нагадуємо, що цей шар інкапсульований у DLL.
- рядок 26: якщо шар [metier] повернув результат, він передається клієнту.
- рядок 28: якщо рівень [metier] викликав виняток, повідомлення про нього передається клієнту.
11.9.4. Графічний клієнт сервера Tcp для розрахунку податків
11.9.4.1. Проєкт у Visual Studio
![]() |
Проєкт Visual Studio для графічного клієнта матиме такий вигляд:
![]() |
- у [1] — обидва проекти рішення, по одному для кожного з двох рівнів додатка
- у [2] — TCP-клієнт, який виконує роль шару [metier] для шару [ui]. Тому ми будемо використовувати обидва терміни.
- у [3] — рівень [ui] версії 7, за винятком однієї деталі, про яку ми поговоримо
11.9.4.2. Шар [metier]
Інтерфейс IImpotMetier не змінився. Це все ще інтерфейс версії 7:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
Реалізацією цього інтерфейсу є наступний клас [ImpotMetierTcp]:
using System.Net.Sockets;
using System.IO;
namespace Metier {
public class ImpotMetierTcp : IImpotMetier {
// інформація [serveur]
private string Serveur { get; set; }
private int Port { get; set; }
// розрахунок податку
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// підключення до сервісу
using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// небуферований вихідний потік
writer.AutoFlush = true;
// пропускаємо привітальне повідомлення
reader.ReadLine();
// запит
writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
// відповідь
return int.Parse(reader.ReadLine());
}
}
}
}
}
}
}
- рядок 7: ім’я або IP-адреса TCP-сервера для розрахунку податку
- рядок 8: порт, на якому цей сервер приймає запити
- ці дві властивості будуть ініціалізовані Spring під час інстанціювання класу [ImpotMetierTcp].
- рядок 11: метод розрахунку податку. Під час його виконання властивості Serveur та Port вже ініціалізовані. У коді простежується класичний підхід до роботи TCP-клієнта
- рядок 13: відкрито з’єднання з сервером
- рядки 14–16: отримується (рядок 14) мережевий потік, пов’язаний із цим з’єднанням, з якого виокремлюються потік читання (рядок 15) та потік запису (рядок 16).
- рядок 18: потік запису має бути без буферизації
- рядок 20: тут слід пам’ятати, що під час відкриття з’єднання сервер надсилає клієнту перший рядок — привітальне повідомлення «Ласкаво просимо на сервер розрахунку податку». Це повідомлення зчитується та ігнорується.
- рядок 22: на сервер надсилається команда типу: impot o 2 60000, щоб попросити його розрахувати податок для одруженої особи, яка має 2 дітей та річний дохід у розмірі 60000 євро.
- рядок 24: сервер відповідає сумою податку у форматі «4282» або ж повідомленням про помилку, якщо запит був сформований неправильно (у цьому випадку цього не станеться) або якщо під час розрахунку податку виникла проблема. У даному випадку останній випадок не обробляється, але, безперечно, було б «чистіше» це зробити. Адже якщо прочитаний рядок є повідомленням про помилку, буде згенеровано виняток, оскільки перетворення у ціле число завершиться невдачею. Виняток, який отримає графічний інтерфейс, буде помилкою перетворення, тоді як первинний виняток має зовсім інший характер. Читачеві пропонується вдосконалити цей код.
- рядки 25–28: звільнення всіх ресурсів, використаних за допомогою оператора «using».
Шар [metier] компілюється в DLL ImpotsV8-metier.dll:

11.9.4.3. Рівень [ui]
![]() |
Шар [ui] [1,3] — це той самий шар, що розглядається у версії 7 у розділі 9.8.4, за винятком трьох деталей:
- конфігурація шару [metier] у [App.config] відрізняється, оскільки змінилася його реалізація
- графічний інтерфейс [Form1.cs] було змінено для відображення можливого винятку
- шар [metier] знаходиться в DLL [ImpotsV8-metier.dll].
Файл [App.config] має такий вигляд:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
<property name="Serveur" value="localhost"/>
<property name="Port" value="27"/>
</object>
</objects>
</spring>
</configuration>
- рядок 16: створення екземпляра шару [metier] за допомогою класу Metier.ImpotMetierTcp із DLL ImpotsV8-metier.dll
- рядки 17–18: ініціалізуються властивості «Сервер» та «Порт» класу Metier.ImpotMetierTcp. Сервер буде розташований на машині localhost і працюватиме на порту 27.
Графічний інтерфейс, що відображається користувачеві, виглядає наступним чином:
![]() |
- у [1] додано поле TextBox для відображення можливого винятку. Цього поля не було в попередній версії.
Окрім цієї деталі, код форми є тим самим, що вже розглядався в розділі 6.4.3. Читачеві пропонується звернутися до нього. У [2] наведено приклад виконання, отриманий із сервером, запущеним таким чином:
Знімок екрана [2] клієнта відповідає рядкам клієнта 9, наведеним вище.
11.9.5. Висновок
Знову ж таки, нам вдалося повторно використати існуючий код без змін (шари [metier], [dao] на сервері) або з мінімальними змінами (шар [ui] на стороні клієнта). Це стало можливим завдяки нашому систематичному використанню інтерфейсів та їх інстанціюванню за допомогою Spring. Якби у версії 7 ми розмістили бізнес-код безпосередньо в обробниках подій графічного інтерфейсу, цей бізнес-код не можна було б повторно використовувати. Це є головним недоліком одношарових архітектур.
Нарешті, слід зазначити, що рівень [ui] не має жодного уявлення про те, що суму податку для нього обчислює віддалений сервер.























































