Skip to content

8. Програмування TCP-IP

8.1. Загальні відомості

8.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 визначає формат цих пакетів, а саме:

  • джерело пакета
  • призначення
  • довжина
  • тип

8.1.2. Модель OSI

Протоколи TCP/IP приблизно відповідають моделі відкритої мережі, яка називається OSI (Open Systems Interconnection Reference Model) і визначена ISO (Міжнародною організацією зі стандартизації). Ця модель описує ідеальну мережу, в якій взаємодію між машинами можна представити у вигляді семирівневої моделі:

Image

Кожен рівень отримує послуги від нижчого рівня та надає свої послуги вищому рівню. Припустимо, що два додатки, розташовані на різних машинах A та B, хочуть встановити зв’язок: вони роблять це на рівні Application. Їм не потрібно знати всіх деталей функціонування мережі: кожна програма передає інформацію, яку вона хоче надіслати, на нижній рівень — рівень Présentation. Отже, програмі потрібно знати лише правила взаємодії з рівнем Présentation.

Після того як інформація потрапляє в шар Présentation, вона, згідно з іншими правилами, передається до шару Session і так далі, доки інформація не потрапить на фізичний носій і не буде фізично передана на машину-приймач. Там вона пройде обробку, зворотну до тієї, яку вона пройшла на комп’ютері-відправнику.

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

Image

Роль різних рівнів така:

Physique
Забезпечує передачу бітів по фізичному носію. На цьому рівні знаходяться кінцеві пристрої обробки даних (E.T.T.D), такі як термінал або комп’ютер, а також пристрої завершення каналів передачі даних (E.T.C.D), такі як модулятор/демодулятор, мультиплексор, концентратор. На цьому рівні слід звернути увагу на:
. вибір способу кодування інформації (аналоговий або цифровий)
. вибір режиму передачі (синхронний або асинхронний).
Liaison de données
Приховує фізичні особливості фізичного рівня. Виявляє та виправляє помилки передачі.
Réseau
Керує маршрутом, яким має пройти інформація, що надсилається в мережі. Це називається routage: визначення маршруту, яким має пройти інформація, щоб дістатися до адресата.
Transport
Забезпечує взаємодію між двома додатками, тоді як попередні рівні дозволяли лише взаємодію між машинами. Однією зі служб, що надаються цим рівнем, може бути мультиплексування: транспортний рівень може використовувати одне й те саме мережеве з’єднання (від машини до машини) для передачі інформації, що належить кільком додаткам.
Session
На цьому рівні знаходяться послуги, що дозволяють додатку відкривати та підтримувати робочу сесію на віддаленому комп’ютері.
Présentation
Вона спрямована на уніфікацію представлення даних на різних машинах. Таким чином, дані, що надходять з машини A, будуть «оформлені» шаром Présentation машини A відповідно до стандартного формату перед відправкою в мережу. Потрапивши на рівень Présentation комп’ютера-одержувача B, який розпізнає їх завдяки стандартному формату, дані будуть «оформлені» іншим чином, щоб програма комп’ютера B могла їх розпізнати.
Application
На цьому рівні знаходяться програми, які зазвичай використовуються користувачем, такі як електронна пошта або передача файлів.

8.1.3. Модель TCP/IP

Модель OSI — це ідеальна модель, яка ще ніколи не була реалізована. Набір протоколів TCP/IP наближається до неї у такому вигляді:

Image

Фізичний рівень

У локальній мережі зазвичай використовується технологія Ethernet або Token-Ring. Тут ми розглянемо лише технологію Ethernet.

Ethernet

Ця назва походить від технології локальних мереж з комутацією пакетів, винайденої в компанії Xerox на початку 1970-х років і стандартизованої компаніями Xerox, Intel та Digital Equipment у 1978 році. Фізично мережа складається з коаксіального кабелю діаметром приблизно 1,27 см і довжиною не більше 500 м. Її можна розширити за допомогою répéteurs, причому між двома пристроями може бути не більше двох повторювачів. Кабель є пасивним: усі активні елементи знаходяться на пристроях, підключених до кабелю. Кожен пристрій підключається до кабелю за допомогою мережевої карти, що містить:

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

Основні характеристики технології Ethernet такі:

  • Пропускна здатність 10 мегабіт/секунду.
  • Шинна топологія: усі пристрої підключені до одного кабелю

Image

  • Мережа з розсилковим доступом — комп'ютер-відправник передає інформацію по кабелю, вказуючи адресу комп'ютера-одержувача. Усі підключені комп'ютери отримують цю інформацію, але зберігає її лише той, якому вона призначена.
  • Метод доступу такий: передавач, який бажає здійснити передачу, прослуховує кабель — він виявляє наявність або відсутність несучої хвилі, наявність якої означала б, що передача вже відбувається. Це техніка CSMA (Carrier Sense Multiple Access). За відсутності несучої хвилі передавач може вирішити передавати у свою чергу. Таке рішення можуть прийняти одразу кілька передавачів. Випромінювані сигнали змішуються: кажуть, що відбувається колізія. Передавач виявляє цю ситуацію: одночасно з передачею по кабелю він прослуховує, що насправді проходить по ньому. Якщо він виявляє, що інформація, яка проходить по кабелю, не є тією, яку він сам передав, він робить висновок про наявність колізії та припиняє передачу. Інші передавачі, які здійснювали передачу, зроблять те саме. Кожен відновить передачу через випадковий проміжок часу, що залежить від конкретного передавача. Ця техніка називається CD (Collision Detect). Відповідно, метод доступу називається CSMA/CD.
  • 48-бітна адресація. Кожна машина має адресу, яку тут називають фізичною адресою, яка вказана на платі, що з’єднує її з кабелем. Цю адресу називають адресою Ethernet машини.

Мережевий рівень

На цьому рівні ми знаходимо протоколи IP, ICMP, ARP та RARP.

IP (Інтернет-протокол)
Передає пакети між двома вузлами мережі
ICMP
(Протокол керування повідомленнями Інтернету)
ICMP забезпечує зв’язок між програмою протоколу IP на одній машині та програмою на іншій машині. Отже, це протокол обміну повідомленнями всередині самого протоколу IP.
ARP
(Протокол розпізнавання адрес)
здійснює відповідність між інтернет-адресою комп’ютера та фізичною адресою комп’ютера
RARP
(Протокол зворотного розпізнавання адрес)
здійснює відповідність між фізичною адресою пристрою та його інтернет-адресою

Транспортний та сесійний рівні

У цьому рівні знаходяться такі протоколи:

TCP (Протокол управління передачею)
Забезпечує надійну передачу інформації між двома клієнтами
UDP (Протокол користувацьких датаграм)
Забезпечує ненадійну передачу інформації між двома клієнтами

Рівні «Додаток», «Представлення» та «Сесія»

Тут можна знайти різні протоколи:

TELNET
Емулятор терміналу, що дозволяє машині A підключатися до машини B як термінал
FTP (File Transfer Protocol)
дозволяє передавати файли
TFTP (Trivial File Transfer Protocol)
дозволяє передавати файли
SMTP (Simple Mail Transfer protocol)
дозволяє обмінюватися повідомленнями між користувачами мережі
DNS (Domain Name System)
перетворює ім’я комп’ютера на його інтернет-адресу
XDR (eXternal Data Representation)
створений компанією Sun MicroSystems, визначає стандартне представлення даних, незалежне від конкретних машин
RPC(Remote Procedures Call)
також визначений компанією Sun; це протокол зв’язку між віддаленими додатками, незалежний від транспортного рівня. Цей протокол є важливим: він звільняє програміста від необхідності знати деталі транспортного рівня та робить додатки переносимими. Цей протокол базується на протоколі XDR
NFS (Network File System)
, який також був розроблений компанією Sun; цей протокол дозволяє одній машині «бачити» файлову систему іншої машини. Він базується на попередньому протоколі RPC

8.1.4. Принцип роботи інтернет-протоколів

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

Image

Розглянемо приклад: додаток FTP, визначений на рівні шару Application, який забезпечує передачу файлів між машинами.

  • Додаток передає послідовність байтів, що підлягає передачі, на рівень transport.
  • Рівень transport розбиває цей послідовність байтів на segments та TCP і додає на початок кожного сегмента його номер. Сегменти передаються на мережевий рівень, що керується протоколом IP.
  • Рівень IP створює пакет, що інкапсулює отриманий сегмент TCP. У заголовку цього пакета вона розміщує інтернет-адреси комп’ютерів-джерел та комп’ютерів-приймачів. Вона також визначає фізичну адресу комп’ютера-приймача. Усе це передається на рівень передачі даних та фізичного зв’язку, тобто на мережеву карту, яка з’єднує комп’ютер із фізичною мережею.
  • Там пакет IP, у свою чергу, інкапсулюється у фізичну рамку та надсилається адресату по кабелю.
  • На комп’ютері-одержувачі рівень «Зв’язок даних та фізичний зв’язок» виконує зворотну операцію: він розпаковує пакет IP з фізичної рамки та передає його на рівень IP.
  • Рівень IP перевіряє правильність пакета: він обчислює контрольну суму на основі отриманих бітів (checksum), яку повинен знайти в заголовку пакета. Якщо це не так, пакет відкидається.
  • Якщо пакет визнано правильним, рівень IP розпаковує сегмент TCP, що міститься в ньому, і передає його на вищий рівень transport.
  • Рівень transport (у нашому прикладі — рівень TCP) перевіряє номер сегмента, щоб відтворити правильний порядок сегментів.
  • Вона також обчислює контрольну суму для сегмента TCP. Якщо вона визнається правильною, рівень TCP надсилає підтвердження про отримання на джерельний пристрій, інакше сегмент TCP відхиляється.
  • Залишається лише шару TCP передати частину даних сегмента до програми-одержувача цих даних у верхньому шарі.

8.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 повинні бути всі різними. Їх розподілом займаються офіційні органи. Фактично ці органи присвоюють адреси локальним мережам, наприклад 193.49.144.0 для мережі факультету природничих наук в Анже. Адміністратор цієї мережі може потім розподіляти адреси IP від 193.49.144.1 до 193.49.144.254 на свій розсуд. Ця адреса зазвичай записується в спеціальний файл на кожному комп’ютері, підключеному до мережі.

8.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 має такий вигляд:

Image

Мережева адреса займає 7 бітів, а адреса вузла — 24 біти. Отже, може існувати 127 мереж класу A, кожна з яких може містити до 2²⁴ вузлів.

Клас B

У цьому випадку адреса IP : I1.I2.I3.I4 має вигляд R1.R2.N1.N2, де

R1.R2 — це адреса мережі

N1.N2 — адреса комп’ютера в цій мережі

Точніше кажучи, форма адреси класу B IP має такий вигляд:

Image

Адреса мережі займає 2 байти (точніше — 14 бітів), так само як і адреса вузла. Отже, може існувати 2¹⁴ мереж класу B, кожна з яких містить до 2¹⁶ вузлів.

Клас C

У цьому класі адреса IP: I1.I2.I3.I4 має вигляд R1.R2.R3.N1, де

R1.R2.R3 — це адреса мережі

N1 — це адреса комп’ютера в цій мережі

Точніше кажучи, форма адреси класу C IP має такий вигляд:

Image

Мережева адреса займає 3 байти (мінус 3 біти), а адреса вузла — 1 байт. Отже, може існувати 221 мережа класу C, що містять до 256 вузлів.

Оскільки адреса комп’ютера Lagaffe на факультеті природничих наук в Анже становить 193.49.144.1, бачимо, що старший байт дорівнює 193, тобто у двійковій системі числення — 11000001. Звідси випливає, що мережа належить до класу C.

Зарезервовані адреси

. Деякі адреси, такі як IP, є адресами мереж, а не адресами вузлів у мережі. Це ті адреси, в яких адреса вузла встановлена на 0. Так, адреса 193.49.144.0 є адресою IP мережі Факультету наук міста Анже. Отже, жоден вузол мережі не може мати адресу, що дорівнює нулю.

. Якщо в адресі IP адреса вузла складається виключно з цифр 1, то це адреса розсилки: така адреса позначає всі вузли мережі.

. У мережі класу C, яка теоретично дозволяє 28=256 вузлів, якщо виключити дві заборонені адреси, залишається лише 254 дозволених адреси.

8.1.5.2. Протоколи перетворення «Інтернет-адреса <--> Фізична адреса»

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

Image

Отже, пакет IP містить інтернет-адреси комп’ютерів-джерел та комп’ютерів-приймачів. Коли цей пакет передається на рівень, відповідальний за його відправлення у фізичну мережу, до нього додається додаткова інформація для формування фізичного кадру, який зрештою буде надіслано в мережу. Наприклад, формат кадру в мережі Ethernet є таким:

Image

У кінцевій рамці містяться фізичні адреси комп’ютерів-відправника та комп’ютера-одержувача. Як їх отримують?

Комп’ютер-відправник, знаючи адресу 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.

8.1.6. Мережевий рівень, так званий рівень IP, Інтернету

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

Image

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

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

Image

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

8.1.6.1. Маршрутизація

Маршрутизація — це метод доставки пакетів IP до місця призначення. Існує два методи: пряма маршрутизація та непряма маршрутизація.

Пряма маршрутизація

Пряма маршрутизація — це передача пакета IP безпосередньо від відправника до одержувача в межах однієї мережі:

  • Комп’ютер-відправник датаграми IP має адресу одержувача IP.
  • Вона отримує фізичну адресу одержувача за допомогою протоколу ARP або зі своїх таблиць, якщо ця адреса вже була отримана.
  • Вона надсилає пакет у мережу на цю фізичну адресу.

Непряма маршрутизація

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

Маршрутизатор підключений до двох мереж і має адресу IP у межах цих двох мереж.

Image

У нашому прикладі вище:

  • Мережа № 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, і так далі.

8.1.6.2. Повідомлення про помилки та контроль

Також на мережевому рівні, тобто на тому ж рівні, що й протокол IP, існує протокол ICMP (Internet Control Message Protocol). Він слугує для надсилання повідомлень про внутрішнє функціонування мережі: несправні вузли, затори на маршрутизаторі тощо... Повідомлення ICMP інкапсулюються в пакети IP і надсилаються по мережі. Рівні IP різних вузлів вживають відповідних заходів відповідно до отриманих повідомлень ICMP. Таким чином, сама програма ніколи не бачить цих проблем, властивих мережі.

Вузол використовуватиме інформацію ICMP для оновлення своїх таблиць маршрутизації.

8.1.7. Транспортний рівень: протоколи UDP та TCP

8.1.7.1. Протокол UDP: User Datagram Protocol

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

Наразі на рівні мережі ми говорили про адреси IP комп’ютерів. Однак на одному комп’ютері можуть одночасно існувати різні процеси, які можуть взаємодіяти між собою. Тому під час надсилання повідомлення потрібно вказувати не лише адресу IP комп’ютера-одержувача, а й «ім’я» процесу-одержувача. Це ім’я насправді є номером, який називається номером порту. Деякі номери зарезервовані для стандартних програм: наприклад, порт 69 для програми tftp (trivial file transfer protocol).

Пакети, що обробляються протоколом UDP, також називаються датаграмами. Вони мають такий вигляд:

Image

Ці датаграми інкапсулюються в пакети IP, а потім — у фізичні кадри.

8.1.7.2. Протокол TCP: Протокол керування передачею

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

Протокол TCP (Протокол керування передачею) дозволяє уникнути цих проблем. Його характеристики такі:

  • Процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом комп’ютера-відправника та портом комп’ютера-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання.
  • Усі пакети, що надсилаються процесом-джерелом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені, чого не гарантував протокол UDP, оскільки пакети могли пролягати різними шляхами.
  • Передана інформація має безперервний характер. Процес-відправник надсилає інформацію у своєму ритмі. Ця інформація не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо для відправлення. Вона зберігається у структурі, що називається сегментом TCP. Цей сегмент, щойно заповнений, буде переданий на рівень IP, де його інкапсулюють у пакет IP.
  • Кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP на стороні одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає підтвердження про отримання відправнику.
  • Коли відправник отримує це підтвердження, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням, чого не було можливо за допомогою протоколу UDP.
  • Якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації.
  • Віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-джерело продовжує надсилати інформацію. Це дозволяє, наприклад, вихідному протоколу TCP надсилати кілька сегментів, не чекаючи на підтвердження отримання. Якщо через певний час він виявить, що не отримав підтвердження отримання певного сегмента № n, він відновить передачу сегментів з цього моменту.

8.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. В іншому випадку сервер також знайде у своїх файлах список серверів імен, до яких він може звернутися. Тоді він це зробить. Таким чином, буде здійснено запит до певної кількості серверів імен — не хаотично, а таким чином, щоб мінімізувати кількість запитів. Якщо комп’ютер врешті-решт буде знайдено, відповідь надійде назад до комп’ютера А.

XDR: (eXternal Data Representation)

Створений компанією Sun MicroSystems, цей протокол визначає стандартне представлення даних, незалежне від конкретних машин.

RPC: (Виклик віддаленої процедури)

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

NFS: Мережева файлова система

Цей протокол, також розроблений компанією Sun, дозволяє одній машині «бачити» файлову систему іншої машини. Він базується на попередньому протоколі RPC.

8.1.9. Висновок

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

Назва: TCP/IP: Архітектура, протоколи, застосування.

Автор: Дуглас COMER

Видавництво: InterEditions

8.2. Управління мережевими адресами в Java

8.2.1. Визначення

Кожен комп’ютер в Інтернеті ідентифікується за допомогою унікальної адреси або імені. Ці два елементи в Java керуються класом InetAddress, деякі методи якого наведено нижче:

byte [] getAddress()
повертає 4 байти адреси IP поточного екземпляра InetAddress
String getHostAddress()
повертає адресу IP поточного екземпляра InetAddress
String getHostName()
повертає інтернет-ім'я поточного екземпляра InetAddress
String toString()
вказує ідентифікатор адреси IP/інтернет-ім'я поточного екземпляра InetAddress
InetAddress getByName(String Host)
створює екземпляр InetAddress для машини, вказаної параметром Host. Генерує виняток, якщо Host невідомий. Host може бути інтернет-ім'ям машини або її адресою IP у формі I1.I2.I3.I4
InetAddress getLocalHost()
створює екземпляр InetAddress комп’ютера, на якому виконується програма, що містить цю інструкцію.

8.2.2. Декілька прикладів

8.2.2.1. Ідентифікація локального комп’ютера


import java.net.*;

public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// кінець блоку try
  }// кінець main
}// кінець класу

Результати виконання такі:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

Кожна машина має внутрішню адресу IP, яка дорівнює 127.0.0.1. Коли програма використовує цю мережеву адресу, вона використовує ту машину, на якій працює. Перевага цієї адреси полягає в тому, що вона не вимагає наявності мережевої карти. Отже, можна тестувати мережеві програми, не підключаючись до мережі. Інший спосіб позначити локальний комп’ютер — це використання імені localhost.

8.2.2.2. Ідентифікація будь-якого комп’ютера


import java.net.*;

public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // отримуємо аргумент
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // спроба отримати адресу машини
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// кінець try
  }// кінець функції main
}// кінець класу

За допомогою виклику java getbyname отримуємо такі результати:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

При виклику java getbyname shiva.istia.univ-angers.fr отримуємо:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

При виклику Java-методу getbyname www.ibm.com отримуємо:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. Повідомлення TCP-IP

8.3.1. Загальні відомості

Image

Коли програма AppA на комп'ютері A хоче встановити зв'язок із програмою AppB на комп'ютері B в Інтернеті, їй потрібно знати кілька речей:

  • адресу IP або ім’я комп’ютера B
  • номер порту, з яким працює програма AppB. Адже комп’ютер B може підтримувати безліч програм, що працюють в Інтернеті. Коли він отримує інформацію з мережі, він повинен знати, для якої саме програми призначена ця інформація. Доступ до мережі додатки на комп’ютері B отримують через «вікна», які також називаються портами зв’язку. Ця інформація міститься в пакеті, отриманому комп’ютером B, щоб його можна було доставити потрібному додатку.
  • Протоколи зв’язку, які розуміє машина B. У нашому дослідженні ми використовуватимемо виключно протоколи TCP-IP.
  • протокол діалогу, який підтримує програма AppB. Адже машини A та B будуть «спілкуватися» між собою. Те, що вони скажуть, буде інкапсульовано в протоколи TCP-IP. Проте, коли на кінці ланцюга додаток AppB отримає інформацію, надіслану додатком AppA, він повинен бути здатний її інтерпретувати. Це аналогічно ситуації, коли дві особи А і В спілкуються по телефону: їхній діалог передається через телефон. Мова кодується у вигляді сигналів телефоном А, передається по телефонних лініях, досягає телефону В, де її декодують. Тоді людина Б чує слова. Саме тут виникає поняття протоколу діалогу: якщо А розмовляє французькою, а Б не розуміє цієї мови, А і Б не зможуть вести змістовний діалог.

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

8.3.2. Характеристики протоколу TCP

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

  • Процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом машини-відправника та портом машини-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання.
  • Усі пакети, що надсилаються процесом-джерелом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені
  • Інформація, що надсилається, має безперервний характер. Процес-відправник надсилає інформацію у своєму темпі. Вона не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо для надсилання. Інформація зберігається у структурі, що називається сегментом TCP. Цей сегмент, як тільки буде заповнений, передається на рівень IP, де він буде інкапсульований у пакет IP.
  • Кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP на стороні одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає відправнику підтвердження про отримання.
  • Коли відправник отримує це підтвердження, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням.
  • Якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації.
  • Віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-джерело продовжує надсилати інформацію. Це дозволяє, наприклад, вихідному протоколу TCP надсилати кілька сегментів, не чекаючи на підтвердження отримання. Якщо через певний час він виявить, що не отримав підтвердження отримання певного сегмента № n, він відновить передачу сегментів з цього моменту.

8.3.3. Взаємодія «клієнт-сервер»

Часто комунікація в Інтернеті є асиметричною: машина А ініціює з’єднання, щоб звернутися до машини Б із запитом на послугу: вона вказує, що хоче встановити з’єднання зі службою SB1 на машині Б. Остання приймає або відхиляє запит. Якщо вона приймає, машина А може надсилати свої запити до служби SB1. Ці запити повинні відповідати протоколу взаємодії, який розуміє служба SB1. Таким чином між машиною А, яку називають клієнтською машиною, та машиною Б, яку називають серверною машиною, встановлюється діалог «запит-відповідь». Один із двох партнерів закриє з’єднання.

8.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

8.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

8.3.6. Клас Socket

8.3.6.1. Définition

Основним інструментом, який використовують програми для зв'язку в Інтернеті, є socket. Це англійське слово означає «розетка». У даному контексті його значення розширено до «мережевого сокета». Щоб програма могла надсилати та отримувати інформацію через Інтернет, їй потрібна мережева розетка, тобто socket. Цей інструмент був спочатку створений у версіях Unix Беркліського університету. Згодом його було адаптовано для всіх систем Unix, а також для середовища Windows. Він також існує на віртуальних машинах Java у двох формах: клас Socket для клієнтських програм та клас ServerSocket для серверних програм. Тут ми розглянемо деякі конструктори та методи класу Socket:

public Socket(String host, int port)
відкриває віддалене з'єднання з портом port комп'ютера host
public int getLocalPort()
повертає номер локального порту, який використовує сокет
  
public int getPort()
повертає номер віддаленого порту, до якого підключений сокет
  
public InetAddress getLocalAdress()
повертає локальну адресу InetAddress, до якої прив’язаний сокет
  
public InetAddress getInetAdress()
повертає віддалену адресу InetAddress, до якої підключений сокет
  
public InputStream getInputStream()
створює вхідний потік, що дозволяє зчитувати дані, надіслані віддаленим партнером
  
public OutputStream getOutputStream()
повертає вихідний потік, що дозволяє надсилати дані віддаленому партнеру
  
public void shutdownInput()
закриває вхідний потік сокета
  
public void shutdownOutput()
закриває вихідний потік сокета
  
public void close()
закриває сокет та його потоки вводу-виводу
  
public String toString()
повертає рядок, що «представляє» сокет
 

8.3.6.2. Відкриття з'єднання з сервером

Ми бачили, що для того, щоб машина A встановила з’єднання зі службою на машині B, їй потрібні дві відомості:

  • адреса IP або ім’я машини B
  • номер порту, на якому працює потрібна служба

Конструктор

    public Socket(String  host, int  port);

створює сокет і підключає його до машини host на порту port. Цей конструктор генерує виняток у різних випадках:

  • неправильна адреса
  • неправильний порт
  • запит відхилено

Нам потрібно обробити це виключення:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // підключення не вдалося — обробляємо помилку
        ….
    }

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

public int getLocalPort();

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

public int getPort();

8.3.6.3. Передача інформації через мережу

Потік запису на сокет, а отже, і в мережу, можна отримати за допомогою методу:

public OutputStream getOutputStream();

Усе, що буде надіслано цим потоком, буде отримано на сервісному порту сервера. Багато програм мають діалог у вигляді текстових рядків, що закінчуються переходом на новий рядок. Тому метод println є дуже зручним у таких випадках. Тоді вихідний потік OutputStream перетворюється на потік PrintWriter, який має метод println. Запис може спричинити виняток.

8.3.6.4. Читання інформації з мережі

Отримати потік для читання інформації, що надходить на сокет, можна за допомогою методу:

public InputStream getInputStream();

Усе, що буде прочитано в цьому потоці, надходить із службового порту сервера. Для додатків, діалог у яких відбувається у вигляді текстових рядків, що закінчуються переходом на новий рядок, доцільно використовувати метод readLine. Для цього вхідний потік InputStream перетворюється на потік BufferedReader, який має метод readLine(). Під час зчитування може виникнути виняток.

8.3.6.5. Закриття з’єднання

Це здійснюється за допомогою методу:

public void close();

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

8.3.6.6. Архітектура клієнта

Тепер ми маємо всі елементи, щоб описати базову архітектуру інтернет-клієнта:


    Socket  sClient=null;
    try{
            // встановлюємо з'єднання зі службою, що працює на порту P машини M
        sClient=new Socket(M,P);

        // створюються потоки вводу-виводу клієнтського сокета
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);

        // цикл «запит — відповідь»
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // підготовка запиту
            demande=…
            // відправляємо його
            out.println(demande);
            // читання відповіді
            réponse=in.readLine();
            // обробка відповіді

        }
        // завершено
        sClient.close();
    } catch(Exception e){
        // обробка винятку
        ….
    }

Ми не намагалися обробляти різні типи винятків, що генеруються конструктором Socket або методами readline, getInputStream, getOutputStream, close, щоб не ускладнювати приклад. Все було об'єднано в один виняток.

8.3.7. Клас ServerSocket

8.3.7.1. Définition

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

public ServerSocket(int port)
створює сокет для прослуховування на порту port
public ServerSocket(int port, int count)
те саме, але встановлює в count розмір черги, c.a.d — максимальну кількість клієнтських з’єднань, що ставляться в чергу, якщо сервер зайнятий у момент надходження клієнтського з’єднання.
public int getLocalPort()
повертає номер порту прослуховування, який використовує сокет
public InetAddress getInetAdress()
повертає локальну адресу InetAddress, до якої прив’язаний сокет
public Socket accept()
переводить сервер у режим очікування з'єднання (блокуюча операція). При надходженні клієнтського з'єднання повертає сокет, через який буде надаватися послуга клієнту.
public void close()
закриває сокет та його потоки вводу-виводу
public String toString()
повертає рядок символів, що «представляє» сокет
public void close()
закриває сокет служби та звільняє пов’язані з ним ресурси

8.3.7.2. Відкриття сервісу

Це здійснюється за допомогою двох конструкторів:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

port — це порт, на якому служба очікує з'єднання: саме на нього клієнти надсилають свої запити на підключення. count — це максимальний розмір черги служби (за замовчуванням — 50), у якій зберігаються запити клієнтів на підключення, на які сервер ще не відповів. Коли черга заповнена, запити на підключення, що надходять, відхиляються. Обидва оператори генерують виняток.

8.3.7.3. Прийняття запиту на підключення

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

    public Socket accept();

Цей метод повертає екземпляр Socket: це сокет служби, через який буде надаватися послуга, найчастіше іншим завданням. Метод може генерувати виняток.

8.3.7.4. Читання/запис через сокет служби

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

8.3.7.5. Ідентифікація клієнта

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

    public InetAddress getInetAddress()

класу Socket. Після цього ми отримаємо доступ до адреси IP та імені клієнта.

8.3.7.6. Закриття сервісу

Це здійснюється за допомогою методу

    public void close();

класу ServerSocket. Це звільняє зайняті ресурси, зокрема порт прослуховування. Метод може генерувати виняток.

8.3.7.7. Базова архітектура сервера

Виходячи з вищесказаного, можна описати базову структуру сервера:


SocketServer sEcoute=null;
try{
    // запуск служби
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);

    // обробка запитів на підключення
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // очікування та прийняття запиту
        sService=sEcoute.accept();

        // послуга надається іншим завданням, якому передається сокет служби
        new Service(sService).start();

        // повертаємося до очікування запитів на з'єднання
    }
    // все закінчено — служба закривається
    sEcoute.close();
} catch (Exception e){
    // обробляється виняток

}

Клас Service — це thread, який може мати такий вигляд:


public class Service extends Thread{

    Socket sService;        // сокет служби

    // конструктор
    public Service(Socket S){
        sService=S;
    }

// виконання
public void run(){
    try{
        // створюються потоки вводу-виводу
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);

    // цикл «запит — відповідь»
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // зчитування запиту
        demande=in.readLine();

        // обробка запиту 


        // готуємо відповідь
        réponse=…

        // відправляємо її
        out.println(réponse);
    }
    // процес завершено
    sService.close();
    } catch(Exception e){
    // обробляється виняток
    ….
    }// try
} // виконання

8.4. Додатки

8.4.1. Сервер відлуння

Ми пропонуємо написати сервер ехо, який запускатиметься з вікна DOS за допомогою команди:

    java serveurEcho port

Сервер працює на порту, переданому як параметр. Він просто повертає клієнту запит, який той йому надіслав, разом із його ідентифікатором (IP+ім'я). Він приймає 2 з'єднання у свій список очікування. Тут є всі складові TCP-сервера. Програма виглядає так:

// виклик: serveurEcho порт
// сервер відлуння
// повертає клієнту рядок, який той йому надіслав


import java.net.*;
import java.io.*;

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // головна програма
    public static void main (String arg[]){

     // чи є аргумент
     if(arg.length != 1)
        erreur(syntaxe,1);

     // цей аргумент має бути цілим числом >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // створюється сокет для прийому
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // продовження
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // цикл обслуговування
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // очікування клієнта
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // ідентифікація з'єднання
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // обслуговування здійснюється іншим завданням
        new traiteClientEcho(service).start();
     }// кінець циклу while
    }// кінець main

// виведення помилок
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // ідентифікує
    private static String identifie(InetAddress Host){
        // ідентифікація хосту
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// кінець класу


// забезпечує обслуговування клієнта сервером відлуння

class traiteClientEcho extends Thread{

    private Socket service;            // сервісний сокет
    private BufferedReader in;        // вхідний потік
    private PrintWriter out;            // вихідний потік

     // конструктор
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // метод run
    public void run(){

         // створення вхідного та вихідного потоків
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// кінець блоку try
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// кінець блоку try

         // ідентифікатор з'єднання надсилається клієнту
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // цикл читання запиту/запису відповіді
        String demande,reponse;
        try{
             // служба зупиняється, коли клієнт надсилає маркер кінця файлу
            while ((demande=in.readLine())!=null){
                // відлуння запиту
                reponse="["+demande+"]";
                out.println(reponse);
                 // сервіс зупиняється, коли клієнт надсилає «кінець»
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// кінець циклу while
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// кінець циклу try

         // закриваємо сокет
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// кінець try
    }// кінець виконання

     // виведення помилок
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// кінець помилки

     // ідентифікація
    private String identifie(InetAddress Host){
         // ідентифікація хосту
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// кінець класу

Два класи, необхідні для роботи сервісу, були об'єднані в один вихідний файл. Лише один із них, той, що має функцію main, має атрибут public. Структура сервера відповідає загальній архітектурі TCP-серверів. До неї додано метод (identifie), що дозволяє ідентифікувати з’єднання між сервером і клієнтом. Ось деякі результати:

Сервер запускається командою

    java serveurEcho 187

Після цього у вікні контролю з’являється таке повідомлення:

Serveur d'écho lancé sur le port 187

Для тестування цього сервера використовується програма telnet, яка доступна як для Unix, так і для Windows. Telnet — це універсальний TCP-клієнт, сумісний з усіма серверами, які в процесі взаємодії приймають текстові рядки, що закінчуються символом кінця рядка. Саме так працює наш сервер-ехо. Запускаємо перший клієнт telnet у Windows (у цьому прикладі — Windows 2000), ввівши telnet у вікні DOS:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1

Le caractère d'échappement est 'CTRL+$'

Microsoft Telnet> help

Les commandes peuvent être abrégées. Les commandes prises en charge sont :

close           ferme la connexion en cours
display         affiche les paramètres d'opération
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' pour afficher la liste)
status          affiche les informations d'état
unset           annule les options (entrez 'unset ?' pour afficher la liste)
? ou help       affiche des informations d'aide

Microsoft Telnet> set ?
NTLM            Active l'authentification NTLM.
LOCAL_ECHO      Active l'écho local.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF

Microsoft Telnet> set local_echo

Microsoft Telnet> open localhost 187

За замовчуванням програма telnet не відображає команди, що вводяться з клавіатури. Щоб увімкнути це відображення, потрібно виконати команду:

Microsoft Telnet> set local_echo

Щоб встановити з’єднання з сервером, вказавши порт служби відбиття (187) та адресу комп’ютера, на якому він розташований (localhost), потрібно виконати таку команду:

Microsoft Telnet> open localhost 187

У вікні DOS клієнта з’являється таке повідомлення:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

У вікні сервера з’являється повідомлення:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Тут tahe та localhost позначають один і той самий комп’ютер. У вікні клієнта telnet можна вводити рядки тексту. Сервер відбиває їх:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

Слід зауважити, що порт клієнта (1059) виявлено правильно, але порт служби (187) збігається з портом прослуховування (187), що є несподіваним. Адже можна було очікувати, що буде отримано порт сокета служби, а не порт прослуховування. Слід перевірити, чи отримаємо ми такі самі результати в Unix. Тепер запустимо другого клієнта telnet. Вікно сервера виглядатиме так:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

У вікні другого клієнта також можна вводити рядки тексту:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

Отже, бачимо, що сервер-ехо може обслуговувати декількох клієнтів одночасно. Клієнти telnet можна завершити, закривши вікно DOS, у якому вони виконуються.

8.4.2. Java-клієнт для сервера ехо

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

// виклик: clientEcho порт машини
// клієнт сервера відлуння
// надсилає рядки на сервер, який відбиває їх

import java.net.*;
import java.io.*;

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // головна програма    
    public static void main (String arg[]){

     // чи є два аргументи
     if(arg.length != 2)
        erreur(syntaxe,1);

     // перший аргумент має бути ім'ям існуючої машини
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // порт має бути цілим числом >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // здійснюється підключення до сервера
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // ідентифікується з'єднання
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // створення потоку для зчитування рядків, введених з клавіатури
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // створення потоку введення, пов'язаного з сокетом клієнта
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // створення потоку виводу, пов'язаного з сокетом клієнта
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // цикл запитів-відповідей
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // читання повідомлення, надісланого сервером одразу після встановлення з'єднання 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // виведення відповіді
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // читання рядка, введеного з клавіатури
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // відправлення запиту в мережу
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // очікування/читання відповіді
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // відображення відповіді
        System.out.println("Serveur : " +reponse);
         // чи все закінчилося?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // все закінчилося
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// рука

// відображення помилок
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // ідентифікація
    private static String identifie(InetAddress Host){
        // ідентифікатор хосту
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// кінець класу

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

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

Рядки, що починаються з Client, — це рядки, надіслані клієнтом, а ті, що починаються з Serveur, — це рядки, які сервер відправив у відповідь.

8.4.3. Типовий клієнт TCP

Багато служб, створених на початку розвитку Інтернету, працюють за моделлю сервера-відлуння, розглянутою раніше: обмін даними між клієнтом і сервером відбувається шляхом обміну рядками тексту. Ми напишемо загальний TCP-клієнт, який запускатиметься таким чином: java cltTCPgenerique сервер порт

Цей клієнт TCP підключиться до порту port сервера serveur. Після цього він створить два потоки:

  1. потік, відповідальний за зчитування команд, введених з клавіатури, та їх надсилання на сервер
  2. потік, відповідальний за зчитування відповідей сервера та їх відображення на екрані

Чому два потоки, якщо в попередньому додатку така потреба не виникала? У тому додатку протокол діалогу був відомий: клієнт надсилав один рядок, а сервер відповідав одним рядком. Кожна служба має свій особливий протокол, і також трапляються такі ситуації:

  • клієнт повинен надіслати кілька рядків тексту, перш ніж отримати відповідь
  • відповідь сервера може містити кілька рядків тексту

Тому цикл «відправлення одного рядка на сервер — отримання одного рядка від сервера» не завжди підходить. Тому ми створимо два окремі цикли:

  • цикл зчитування команд, введених з клавіатури для відправки на сервер. Користувач позначатиме кінець команд ключовим словом fin.
  • цикл прийому та відображення відповідей від сервера. Це буде нескінченний цикл, який перериватиметься лише у разі закриття мережевого потоку сервером або користувачем за допомогою клавіатури, який введе команду fin.

Щоб ці два цикли працювали незалежно один від одного, нам потрібні два незалежні потоки. Розглянемо приклад виконання, у якому наш загальний TCP-клієнт підключається до сервісу SMTP (SendMail Transfer Protocol). Цей сервіс відповідає за доставку електронної пошти адресатам. Він працює на порту 25 і використовує протокол взаємодії типу обміну текстовими рядками.


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Проаналізуємо ці обміни даними між клієнтом і сервером:

  • сервіс SMTP надсилає привітальне повідомлення, коли клієнт підключається до нього:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • деякі сервіси мають команду help, яка надає інформацію про команди, що можуть використовуватися з сервісом. У цьому випадку це не так. Команди SMTP, що використовуються в прикладі, такі:
    • mail from: expéditeur — для вказівки електронної адреси відправника повідомлення
    • rcpt to: destinataire — для вказівки електронної адреси одержувача повідомлення. Якщо одержувачів декілька, команду rcpt to: повторюють стільки разів, скільки потрібно, для кожного з одержувачів.
    • data, що повідомляє серверу SMTP про те, що ми збираємося надіслати повідомлення. Як зазначено у відповіді сервера, це послідовність рядків, що закінчується рядком, який містить лише один символ — крапку. Повідомлення може мати заголовки, відокремлені від тіла повідомлення порожнім рядком. У нашому прикладі ми вказали тему за допомогою ключового слова Subject:
  • після відправлення повідомлення можна повідомити серверу про завершення роботи за допомогою команди quit. Після цього сервер закриває мережеве з’єднання. Потік читання може виявити цю подію та зупинитися.
  • Потім користувач вводить «fin» на клавіатурі, щоб також зупинити потік читання команд, введених з клавіатури.

Якщо перевірити отриману пошту, ми побачимо таке (Outlook):

Image

Слід зауважити, що служба SMTP не може визначити, чи є відправник дійсним. Тому ніколи не можна довіряти полю from у повідомленні. У цьому випадку відправник machin@univ-angers.fr не існував.

Цей загальний TCP-клієнт дозволяє нам виявити протокол взаємодії інтернет-сервісів і на цій основі створити спеціалізовані класи для клієнтів цих сервісів. Давайте виявимо протокол взаємодії сервісу POP (Post Office Protocol), який дозволяє отримувати електронні листи, що зберігаються на сервері. Він працює на порту 110.


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Основні команди такі:

  • user login, де вказується логін на сервері, на якому зберігаються наші електронні листи
  • pass password, де вказується пароль, пов’язаний із попереднім логіном
  • list — щоб отримати список повідомлень у форматі «номер, розмір у байтах»
  • retr i — для читання повідомлення № i
  • quit — для завершення діалогу.

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


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Веб-клієнт надсилає свої команди на сервер за такою схемою:

commande1
commande2
...
commanden
[ligne vide]

Лише після отримання порожнього рядка веб-сервер надає відповідь. У цьому прикладі ми використали лише одну команду:

GET /index.html HTTP/1.0

яка запитує у сервера URL /index.html та вказує, що працює з протоколом HTTP версії 1.0. Найновіша версія цього протоколу — 1.1. Приклад показує, що сервер відповів, надіславши вміст файлу index.html, а потім закрив з’єднання, оскільки видно, що потік читання відповідей завершився. Перед надсиланням вмісту файлу index.html веб-сервер надіслав серію заголовків, що закінчувалася порожнім рядком:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

Рядок <html> є першим рядком файлу /index.html. Вищезазначене називається заголовками HTTP (HyperText Transfer Protocol). Ми не будемо тут детально розглядати ці заголовки, але нагадаємо, що наш універсальний клієнт надає до них доступ, що може бути корисним для їх розуміння. Наприклад, перший рядок:

<-- HTTP/1.1 200 OK

вказує, що веб-сервер, до якого звернулися, підтримує протокол HTTP/1.1 і що він успішно знайшов запитуваний файл (200 OK), де 200 — це код відповіді HTTP. Рядки

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

повідомляють клієнту, що він отримає 11251 байт, які представляють текст HTML (HyperText Markup Language), і що після завершення передачі з’єднання буде закрито.

Отже, ми маємо дуже зручний TCP-клієнт. Він, безперечно, робить менше, ніж програма telnet, яку ми використовували раніше, але було цікаво написати його самостійно. Програма загального TCP-клієнта виглядає так:

// імпортовані пакети
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // отримує як параметр характеристики служби у вигляді
     // порт сервера
     // підключається до служби
     // створює потік для зчитування команд, введених з клавіатури
     // ці команди будуть надіслані на сервер
     // створює потік для зчитування відповідей від сервера
     // вони будуть відображатися на екрані
     // усе завершується командою «fin», введеною з клавіатури

   // змінна екземпляру
  private static Socket client;

    public static void main(String[] args){

         // синтаксис
        final String syntaxe="pg serveur port";

         // кількість аргументів
        if(args.length != 2)
            erreur(syntaxe,1);

         // записуємо ім’я сервера
        String serveur=args[0];

         // порт повинен бути цілим числом >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // можуть виникнути проблеми
        try{
             // здійснюється підключення до служби
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // помилка
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // кінець
            return;
        }//catch

         // створюються потоки читання/запису
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // завершення головного потоку
        return;
    }// main

     // виведення повідомлень про помилки
    public static void erreur(String msg, int exitCode){
         // виведення помилки
        System.err.println(msg);
         // зупинка з помилкою
        System.exit(exitCode);
    }//помилка
}//клас  

class ClientSend extends Thread {
    // клас, відповідальний за зчитування команд, введених з клавіатури
     // та надсилати їх на сервер через TCP-клієнт, переданий як параметр

    private Socket client;    // TCP-клієнт

     // конструктор
    public ClientSend(Socket client){
         // зазначаємо TCP-клієнт
        this.client=client;
    }//конструктор

     // метод Run потоку
    public void run(){

        // локальні дані
        PrintWriter OUT=null;            // потік запису в мережу
    BufferedReader IN=null;        // потік клавіатури
        String commande=null;            // команда, зчитана з клавіатури

         // обробка помилок
        try{
             // створення потоку запису в мережу
            OUT=new PrintWriter(client.getOutputStream(),true);
      // створення потоку введення з клавіатури
      IN=new BufferedReader(new InputStreamReader(System.in));
            // цикл введення-відправлення команд
            System.out.println("Commandes : ");
            while(true){
                 // зчитування команди, введеної з клавіатури
                commande=IN.readLine().trim();
                // завершено?
                if (commande.toLowerCase().equals("fin")) break;
                // відправлення команди на сервер
                OUT.println(commande);
                 // наступна команда
            }//while
        }catch(Exception ex){
             // помилка
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // кінець — закриваємо потоки
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // повідомляємо про завершення потоку
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//run
}//клас

class ClientReceive extends Thread{
    // клас, відповідальний за зчитування рядків тексту, призначених для 
     // TCP-клієнта, переданого як параметр

    private Socket client;    // TCP-клієнт

     // конструктор
    public ClientReceive(Socket client){
         // записуємо TCP-клієнта
        this.client=client;
    }//конструктор

     // метод Run потоку
    public void run(){

        // локальні дані
        BufferedReader IN=null;        // потік читання мережі
        String réponse=null;        // відповідь сервера

         // обробка помилок
        try{
             // створення потоку читання з мережі
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // цикл читання рядків тексту з потоку IN
            while(true){
                 // читання мережевого потоку
                réponse=IN.readLine();
                 // потік закритий?
                if(réponse==null) break;
                // виведення на екран
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // помилка
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // кінець — закриваємо потоки
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // повідомляємо про завершення потоку
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//run
}//клас

8.4.4. Загальний TCP-сервер

Тепер розглянемо сервер,

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

Програму запускають за допомогою команди: java serveurTCPgenerique portEcoute, де portEcoute — це порт, до якого повинні підключатися клієнти. Обслуговування клієнтів забезпечуватимуть два потоки:

  • потік, який присвячений виключно зчитуванню рядків тексту, надісланих клієнтом
  • потік, який присвячений виключно зчитуванню відповідей, введених користувачем з клавіатури. Цей потік за допомогою команди fin повідомить про завершення з’єднання з клієнтом.

Сервер створює два потоки на кожного клієнта. Якщо є n клієнтів, одночасно буде активних 2n потоків. Сам сервер ніколи не зупиняється, за винятком випадків, коли користувач натискає комбінацію клавіш Ctrl-C. Розглянемо кілька прикладів.

Сервер запущено на порту 100, і для зв’язку з ним використовується загальний клієнт. Вікно клієнта виглядає так:

E:\data\serge\MSNET\c#\мережа\клієнт tcp загальний> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

Рядки, що починаються з <--, — це дані, надіслані сервером клієнту, інші — дані, надіслані клієнтом серверу. Вікно сервера виглядає так:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

Рядки, що починаються з <--, — це ті, що надсилаються клієнтом на сервер. Рядки N: — це рядки, що надсилаються сервером клієнту № N. Вищезазначений сервер все ще активний, тоді як клієнт 1 завершив роботу. Запускаємо другого клієнта для того самого сервера:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

Вікно сервера виглядає так:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

Тепер змоделюємо веб-сервер, запустивши наш універсальний сервер на порту 88:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

Тепер відкриємо браузер і введемо адресу http://localhost:88/exemple.html. Браузер підключиться до порту 88 машини localhost, а потім завантажить сторінку /exemple.html:

Image

Тепер поглянемо на вікно нашого сервера:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Таким чином ми виявляємо заголовки HTTP, надіслані браузером. Це дозволяє нам поступово розібратися з протоколом HTTP. У попередньому прикладі ми створили веб-клієнт, який надсилав лише одну команду GET. Цього було достатньо. Тут ми бачимо, що браузер надсилає серверу й іншу інформацію. Її мета — вказати серверу, з яким типом клієнта він має справу. Ми також бачимо, що заголовки HTTP закінчуються порожнім рядком.

Складімо відповідь для нашого клієнта. Користувач за клавіатурою тут є справжнім сервером і може скласти відповідь вручну. Згадаймо відповідь, яку надав веб-сервер у попередньому прикладі:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

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

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

Рядки, що починаються з 2: надсилаються сервером клієнту № 2. Команда fin завершує з’єднання сервера з клієнтом. У нашій відповіді ми обмежилися такими заголовками HTTP:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

Ми не вказуємо розмір файлу, який збираємося надіслати (Content-Length), а лише повідомляємо, що після його надсилання ми закриємо з’єднання (Connection: close). Цього достатньо для браузера. Побачивши, що з’єднання закрито, браузер зрозуміє, що відповідь сервера завершена, і відобразить сторінку HTML, яку йому надіслали. Остання має такий вигляд:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

Потім користувач закриває з'єднання з клієнтом, ввівши команду fin. Після цього браузер розуміє, що відповідь сервера завершена, і може її відобразити:

Image

Якщо після цього виконати команду View/Source, щоб переглянути, що отримав браузер, отримаємо:

Image

тобто саме те, що було надіслано з загального сервера.

Код загального TCP-сервера такий:

// пакети
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // головна програма
    public static void main (String[] args){

    // приймає запити клієнтів на порту прослуховування
     // створює потік для зчитування запитів клієнта
     // вони будуть відображатися на екрані
     // створює потік для зчитування команд, введених з клавіатури
     // ці дані будуть надіслані клієнту у відповіді
     // усе завершується командою «fin», введеною з клавіатури

    final String syntaxe="Syntaxe : pg port";
   // змінна екземпляру
         // чи є аргумент
     if(args.length != 1)
        erreur(syntaxe,1);

         // порт повинен бути цілим числом >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // створюємо службу прослуховування
    ServerSocket ecoute=null;
    int nbClients=0;    // кількість оброблених клієнтів
        try{
             // створюється служба
            ecoute=new ServerSocket(port);
             // моніторинг
            System.out.println("Serveur générique lancé sur le port " + port);

             // цикл обслуговування клієнтів
            Socket client=null;
            while (true){ // нескінченний цикл  зупиняється за допомогою Ctrl-C
                 // очікування клієнта
                client=ecoute.accept();

                 // сервіс виконується окремими потоками
                nbClients++;

                 // створюються потоки читання/запису
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // повернення до очікування запитів
            }// кінець циклу while
        }catch(Exception ex){
             // повідомляється про помилку
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// кінець main

     // виведення помилок
    public static void erreur(String msg, int exitCode){
         // виведення помилки
        System.err.println(msg);
         // зупинка з помилкою
        System.exit(exitCode);
    }//помилка
}//клас

class ServeurSend extends Thread{
    // клас, відповідальний за зчитування відповідей, введених з клавіатури
     // та надсилання їх клієнту через TCP-клієнт, переданий у конструктор

    Socket client;    // TCP-клієнт
    int numClient;        // номер клієнта

     // конструктор
    public ServeurSend(Socket client, int numClient){
         // зазначаємо клієнт TCP
        this.client=client;
         // та його номер
        this.numClient=numClient;
    }//виробник

     // метод Run потоку
    public void run(){

        // локальні дані
        PrintWriter OUT=null;        // потік запису в мережу
        String réponse=null;        // відповідь, зчитана з клавіатури
    BufferedReader IN=null;    // потік клавіатури

        // відстеження
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // обробка помилок
        try{
             // створення потоку запису в мережі
            OUT=new PrintWriter(client.getOutputStream(),true);
      // створення потоку клавіатури
      IN=new BufferedReader(new InputStreamReader(System.in));
            // цикл введення-відправлення команд
            while(true){
                 // ідентифікація клієнта
                System.out.print("--> " + numClient + " : ");
                 // читання відповіді, введеної з клавіатури
                réponse=IN.readLine().trim();
                // закінчено?
                if (réponse.toLowerCase().equals("fin")) break;
                // відправлення відповіді на сервер
                OUT.println(réponse);
                 // наступна відповідь
            }//while
        }catch(Exception ex){
             // помилка
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // кінець  закриваємо потоки
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // повідомляємо про завершення потоку
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//run
}//клас

class ServeurReceive extends Thread{
    // клас, відповідальний за зчитування рядків тексту, надісланих на сервер 
     // через TCP-клієнт, переданий у конструктор

    Socket client;    // TCP-клієнт
    int numClient;        // номер клієнта

     // конструктор
    public ServeurReceive(Socket client, int numClient){
         // відзначаємо TCP-клієнт
        this.client=client;
         // та його номер
        this.numClient=numClient;
    }//виробник

     // метод Run потоку
    public void run(){

        // локальні дані
        BufferedReader IN=null;        // потік читання мережі
        String réponse=null;        // відповідь сервера

         // моніторинг
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // обробка помилок
        try{
             // створення потоку читання мережі
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // цикл читання рядків тексту з потоку IN
            while(true){
                 // читання мережевого потоку
                réponse=IN.readLine();
                 // потік закритий?
                if(réponse==null) break;
                // виведення на екран
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // помилка
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // кінець  закриваємо потоки
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // повідомляємо про завершення потоку
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//run
}//клас

8.4.5. Веб-клієнт

У попередньому прикладі ми бачили деякі заголовки HTTP, які надсилав браузер:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Ми напишемо веб-клієнт, якому передаватиметься як параметр URL і який відображатиме на екрані вміст цього URL. Припустимо, що веб-сервер, до якого звертаються для отримання URL, підтримує протокол HTTP 1.1. З наведених заголовків ми будемо використовувати лише такі:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • перший заголовок вказує, яку сторінку ми хочемо
  • другий — до якого сервера ми звертаємося
  • третій — що ми хочемо, щоб сервер закрив з’єднання після надання відповіді.

Якщо у наведеному вище прикладі замінити GET на HEAD, сервер надішле нам лише заголовки HTTP, а не сторінку HTML.

Наш веб-клієнт буде викликатися наступним чином: java clientweb URL cmd, де URL — цебажаний URL, а cmd — одне з двох ключових слів GET або HEAD, що вказує, чи потрібні лише заголовки (HEAD), чи також вміст сторінки (GET). Розглянемо перший приклад. Запускаємо сервер IIS, а потім веб-клієнт на тій самій машині:

dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

Відповідь

HTTP/1.1 302 Object moved

означає, що запитувана сторінка змінила місце розташування (тобто з URL). Нова адреса URL вказана у заголовку Location:

Location: /IISSamples/Default/welcome.htm

Якщо ми використаємо GET замість HEAD у виклику веб-клієнта:

dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private

<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

Ми отримуємо той самий результат, що й із HEAD, а також основний текст сторінки HTML. Програма виглядає так:

// імпортовані пакети
import java.io.*;
import java.net.*;

public class clientweb{

    // запитує URL
     // відображає її вміст на екрані

    public static void main(String[] args){
        // синтаксис
        final String syntaxe="pg URI GET/HEAD";

        // кількість аргументів
        if(args.length != 2)
            erreur(syntaxe,1);

         // зазначено запитуваний URI
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // перевірка дійсності URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI неправильний
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//виняток
         // перевірка замовлення
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // неправильне замовлення
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // витягуємо корисну інформацію з URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // можна працювати
        Socket  client=null;                        // клієнт
        BufferedReader IN=null;                    // потік читання клієнта
        PrintWriter OUT=null;                        // потік запису клієнта
        String réponse=null;                        // відповідь сервера
        try{
             // встановлюється з'єднання з сервером
            client=new Socket(host,port);

            // створення потоків вводу-виводу клієнта TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // запит на URL — надсилання заголовків HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // зчитується відповідь
            while((réponse=IN.readLine())!=null){
                 // обробляємо відповідь
                System.out.println(réponse);
            }//while
             // процес завершено
            client.close();
        } catch(Exception e){
            // обробка винятку
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // виведення помилок
    public static void erreur(String msg, int exitCode){
         // виведення помилки
        System.err.println(msg);
         // зупинка з помилкою
        System.exit(exitCode);
    }//помилка
}//клас

Єдиною новинкою в цій програмі є використання класу URL. Програма отримує URL (Uniform Resource Locator) або URI (Uniform Resource Identifier) у вигляді http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Клас URL дозволяє розкласти рядок URL на окремі елементи. Об’єкт URL створюється на основі рядка URLstring, отриманого як параметр:

         // перевірка дійсності URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI неправильний
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//виняток

Якщо рядок URL, отриманий як параметр, не є дійсним URL (відсутність протоколу, сервера тощо), генерується виняток. Це дозволяє перевірити дійсність отриманого параметра. Після створення об’єкта URL ми отримуємо доступ до його різних елементів. Отже, якщо об’єкт url з попереднього коду було створено на основі рядка

http://сервер:порт/cheminPageHTML?param1=val1;param2=val2;... 

, ми отримаємо:

url.getHost()=serveur

url.getPort()=port або -1, якщо порт не вказано

url.getPath()=cheminPageHTML або порожній рядок, якщо шлях відсутній

url.getQuery() = param1=val1;param2=val2;... або null, якщо запит відсутній

uri.getProtocol()=http

8.4.6. Веб-клієнт, що підтримує перенаправлення

Попередній веб-клієнт не обробляє можливе перенаправлення URL, яке він сам ініціював. Наступний клієнт обробляє його.

  1. він зчитує перший рядок заголовків HTTP, надісланих сервером, щоб перевірити, чи міститься там рядок 302 Object moved, який вказує на перенаправлення
  2. він зчитує наступні заголовки. Якщо перенаправлення є, він шукає рядок «Location: url», який вказує нову адресу URL запитуваної сторінки, і записує цю адресу URL.
  3. він відображає решту відповіді сервера. Якщо є перенаправлення, кроки 1–3 повторюються з новим URL. Програма не допускає більше одного перенаправлення. Це обмеження визначається константою, яку можна змінити.

Ось приклад:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

Програма така:

// імпортовані пакети
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // запитує URL
     // відображає її вміст на екрані

    public static void main(String[] args){
        // синтаксис
        final String syntaxe="pg URL GET/HEAD";

        // кількість аргументів
        if(args.length != 2)
            erreur(syntaxe,1);

         // фіксується запитуваний URI
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // перевірка дійсності URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI неправильний
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//виняток
         // перевірка замовлення
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // неправильне замовлення
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // можна працювати
        Socket  client=null;                        // клієнт
        BufferedReader IN=null;                    // потік читання клієнта
        PrintWriter OUT=null;                        // потік запису клієнта
        String réponse=null;                        // відповідь сервера
        final int nbRedirsMax=1;                // допускається не більше одного перенаправлення
        int nbRedirs=0;                                    // кількість поточних перенаправлень
        String premièreLigne;                        // перший рядок відповіді
        boolean redir=false;                        // вказує, чи є перенаправлення
        String locationString="";                // рядок URL можливого перенаправлення

         // регулярний вираз для пошуку URL перенаправлення
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // обробка помилок
        try{
             // може бути кілька URL, які потрібно перевірити на наявність перенаправлень
            while(nbRedirs<=nbRedirsMax){

                // витягуємо корисну інформацію з URL
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // здійснюється підключення до сервера
                client=new Socket(host,port);

                // створюються потоки вхідних та вихідних даних клієнта TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // запитується URL — надсилання заголовків HTTP
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // зчитується перший рядок відповіді
                premièreLigne=IN.readLine();
                 // відображення на екрані
                System.out.println(premièreLigne);

                // перенаправлення?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // є перенаправлення
                    redir=true;
                    nbRedirs++;
                }//if

                // наступні заголовки HTTP, доки не буде знайдено порожній рядок, що сигналізує про кінець заголовків
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // виводиться відповідь
                    System.out.println(réponse);
                     // якщо є перенаправлення, шукаємо заголовок Location
                    if(redir && ! locationFound){
                        // порівняти рядок із реляційним виразом location
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // якщо знайдено, записується URL перенаправлення
                            locationString=résultat.group(1);             
                             // фіксується, що знайдено
                            locationFound=true;
                        }//if
                    }//якщо
                    // наступний заголовок
                }//, поки

                 // наступні рядки відповіді
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // відображається відповідь
                    System.out.println(réponse);
                }//while
                 // закривається з'єднання
                client.close();
                 // чи завершено?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // необхідно виконати перенаправлення — створюється новий URL
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // відстеження
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//while
        } catch(Exception e){
            // обробляємо виняток
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // виведення помилок
    public static void erreur(String msg, int exitCode){
         // виведення помилки
        System.err.println(msg);
         // зупинка з помилкою
        System.exit(exitCode);
    }//помилка
}//клас

8.4.7. Сервер розрахунку податків

Ми повертаємося до вправи IMPOTS, яку вже розглядали в різних варіантах. Нагадаємо останню версію:

Було створено базовий клас «податки». Його атрибутами є три масиви чисел:

public class impots{

  // дані, необхідні для розрахунку податку
   // походять із зовнішнього джерела

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // поле «виробник» порожнє
  protected impots(){}

   // конструктор
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

Клас impots має два конструктори:

  • конструктор, якому передаються три масиви даних, необхідні для розрахунку податку
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • конструктор без параметрів, який можуть використовувати лише дочірні класи
  protected impots(){}

Від цього класу було похідним клас impotsJDBC, який дозволяє заповнювати три масиви limites, coeffR, coeffN на основі вмісту бази даних:

public class impotsJDBC extends impots{
  // додавання конструктора, що дозволяє побудувати
   // таблиці limit, coeffr, coeffn на основі таблиці
   // податків з бази даних
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: назва DSN бази даних
     // userIMPOTS, mdpIMPOTS: логін/пароль для доступу до бази даних

Було написано графічний додаток. Додаток використовував об’єкт класу impotsJDBC. Додаток і цей об’єкт знаходилися на одній машині. Ми пропонуємо розмістити тестову програму та об’єкт impotsJDBC на різних машинах. Ми отримаємо клієнт-серверну програму, в якій віддалений об’єкт impotsJDBC буде виконувати роль сервера. Новий клас називається ServeurImpots і є похідним від класу impotsJDBC:

// імпортовані пакети
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // атрибути
    int portEcoute;                // порт, на якому очікуються запити клієнтів
    boolean actif;                // стан сервера

     // конструктор
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // батьківська конструкція
        super(DSNimpots, USERimpots, MDPimpots);
         // відзначається порт прийому
        this.portEcoute=portEcoute;
         // наразі неактивний
        actif=false;
         // створює та запускає потік для зчитування команд, введених з клавіатури
         // сервер керуватиметься за допомогою цих команд
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

Єдиним новим параметром у конструкторі є порт, на якому сервер очікує запитів від клієнтів. Інші параметри передаються безпосередньо до базового класу impotsJDBC. Сервер податків керується командами, що вводяться з клавіатури. Тому створюється потік для зчитування цих команд. Можливі два варіанти: start для запуску служби, stop для її остаточного зупинення. Метод admin, який обробляє ці команди, виглядає наступним чином:

    public void admin() throws IOException{
        // зчитує команди адміністрування сервера, введені з клавіатури
         // у нескінченному циклі
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // запрошує
            System.out.print("Serveur d'impôts>");
             // читання команди
            commande=IN.readLine().trim().toLowerCase();
             // виконання команди
            if(commande.equals("start")){
                // активний?
                if(actif){
                     //помилка
                    System.out.println("Le serveur est déjà actif");
                     // продовжуємо
                    continue;
                }//if
                 // створюємо та запускаємо службу прослуховування
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // завершення всіх потоків виконання
                System.exit(0);
            }//якщо
            else {
                // помилка
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//адміністратор

Якщо команда, введена з клавіатури, має вигляд start, запускається потік, що очікує на запити клієнтів. Якщо введена команда має вигляд stop, усі потоки зупиняються. Потік, що очікує на запити, виконує метод ecoute:

    public void ecoute(){
         // потік, що обробляє запити клієнтів
         // створюється служба прийому
        ServerSocket ecoute=null;
        try{
            // створюється служба
            ecoute=new ServerSocket(portEcoute);
             // моніторинг
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // цикл обслуговування
            Socket liaisonClient=null;
            while (true){ // нескінченний цикл
                 // очікування клієнта
                liaisonClient=ecoute.accept();

                 // сервіс обслуговується іншим завданням
                new traiteClientImpots(liaisonClient,this).start();

                 // повернення до очікування запитів
            }// кінець циклу while
        }catch(Exception ex){
             // повідомляється про помилку
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//потік очікування

Ми бачимо класичний TCP-сервер, що слухає на порту portEcoute. Запити клієнтів обробляються методом run потоку traiteCientImpots, конструктору якого передаються два параметри:

  1. об’єкт Socket liaisonClient, який дозволить встановити з’єднання з клієнтом
  2. об'єкт impotsJDBC this, який надасть доступ до методу this.calculer для розрахунку податку.
// -------------------------------------------------------
// забезпечує обслуговування клієнта податкового сервера

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // зв'язок із клієнтом
    private BufferedReader IN;                // вхідний потік
    private PrintWriter OUT;                    // вихідний потік
    private impotsJDBC objImpots;            // об’єкт «Податок»

         // конструктор
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//конструктор

Метод run обробляє запити клієнтів. Це рядки тексту, які можуть мати дві форми:

  1. розрахунок для одружених (так/ні) nbEnfants salaireAnnuel
  2. fincalculs

Форма 1 дозволяє розрахувати податок, форма 2 завершує з’єднання «клієнт-сервер».

     // метод run
    public void run(){
         // надає послугу клієнту
        try{
             // вхідний потік
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // вихідний потік
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // надсилання привітального повідомлення клієнту
            OUT.println("Bienvenue sur le serveur d'impôts");

             // цикл читання запиту/запису відповіді
            String demande=null;
            String[] champs=null;    // елементи запиту
            String commande=null;    // команда клієнта: розрахунок або завершення розрахунків
            while ((demande=IN.readLine())!=null){
                 // розбиття запиту на поля
                champs=demande.trim().toLowerCase().split("\\s+");
                 // два прийняті запити: «calcul» та «fincalculs»
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // помилка клієнта
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // наступне замовлення
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // повідомлення про завершення роботи для клієнта
                    OUT.println("Au revoir...");
                     // звільнення ресурсів
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // кінець
                    return;
                }//if
                 //наступний запит
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// кінець try
    }// кінець Run

Розрахунок податку здійснюється за допомогою методу calculerImpôt, який отримує як параметр масив полів запиту, зробленого клієнтом. Перевіряється правильність запиту, після чого, за необхідності, розраховується податок і повертається клієнту.

     // розрахунок податків
    public void calculerImpôt(String[] champs){
         // обробляє запит: розрахунок для одружених nbEnfants salaireAnnuel
         // розбито на поля в таблиці полів

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // правильність аргументів
        try{
             // потрібно щонайменше 4 поля
            if(champs.length!=4) throw new Exception();
            // одружений
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // діти
            nbEnfants=Integer.parseInt(champs[2]);
             // зарплата
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // помилка формату
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // завершено
            return;
        }//якщо
         // можна розрахувати податок
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // відправляємо відповідь клієнту
        OUT.println(""+impot);
    }//обчислити

Програма для тестування може виглядати так:

// виклик: serveurImpots порт dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // головна програма
    public static void main (String[] args){

        // потрібно 4 аргументи
        if(args.length != 4)
            erreur(syntaxe,1);

         // порт має бути цілим числом >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // створюється сервер податків
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //помилка
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // виведення помилок
    public static void erreur(String msg, int exitCode){
         // виведення помилки
        System.err.println(msg);
         // зупинка з помилкою
        System.exit(exitCode);
    }//помилка
}// завершення класу

Ми передаємо в тестову програму дані, необхідні для створення об'єкта ServeurImpots, і на їх основі програма створює цей об'єкт.

Спробуємо виконати програму вперше:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

Команда

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

створює об’єкт ServeurImpots, який ще не приймає запити від клієнтів. Саме команда start, введена з клавіатури, запускає цей процес прийому запитів. Команда stop зупиняє сервер. Тепер скористаємося клієнтом. Ми будемо використовувати раніше створений універсальний клієнт. Сервер запущено:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

Універсальний клієнт запускається в іншому вікні DOS:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

Бачимо, що клієнт успішно отримав привітальне повідомлення від сервера. Надсилаємо інші команди:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Повертаємося до вікна сервера, щоб його зупинити:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. Вправи

8.5.1. Вправа 1 — Загальний графічний клієнт TCP

8.5.1.1. Опис програми

Ми пропонуємо створити програму, здатну взаємодіяти через Інтернет з основними сервісами TCP. Ми назвемо її універсальним TCP-клієнтом. Коли ми розберемося з цією програмою, то побачимо, що всі TCP-клієнти схожі між собою. Вікно програми виглядає наступним чином:

Image

Значення різних елементів управління такі:

назва
тип
роль
1
TxtRemoteHost
JTextField
назва машини, що надає бажану послугу
2
TxtPort
JTextField
порт запитуваної послуги
3
TxtSend
JTextField
текст повідомлення, яке клієнт надішле на сервер
4
OptRCLF
OptLF
JCheckBox
кнопки, що дозволяють вказати, як закінчуються рядки у діалозі клієнт/сервер
RCLF: повернення каретки (#13) + перехід на новий рядок (#10)
LF: перехід на новий рядок (#10)
5
LstSuivi
JList
відображає повідомлення про стан зв’язку між клієнтом і сервером
6
LstDialogue
JList
відображає повідомлення, якими обмінюються клієнт (->) та сервер (<-)
7
CmdAnnuler
JButton
приховане — розташоване під списком діалогу — з’являється під час встановлення з’єднання та дозволяє його перервати, якщо сервер не відповідає

Доступні такі опції меню:

опція
підпункти
роль
Підключення
Підключити
підключає клієнта до сервера
 
Відключити
закриває з'єднання
 
Вийти
Завершує роботу програми
Повідомлення
Надіслати
Надіслати повідомлення з елемента управління TxtSend на сервер
 
RazSuivi
Очистити список LstSuivi
 
RazDialogue
Очистити список LstDialogue
Автор
 
відображає вікно з інформацією про авторські права

8.5.1.2. FONCTIONNEMENT DE APPLICATION

Ініціалізація програми

Коли завантажується головний аркуш додатка, відбуваються такі дії:

  • сторінка центрується на екрані
  • активними є лише пункти меню Connexion/Quitter та Auteur
  • кнопка Annuler прихована
  • списки LstSuivi та LstDialogue порожні
Меню «Вхід»/«Підключитися»

Ця опція доступна лише тоді, коли поля «Віддалений хост» та «Номер порту» заповнені, а активне з’єднання наразі відсутнє. Натискання на цю опцію призводить до таких дій:

  • перевіряється правильність порту: це має бути ціле число >0
  • запускається потік для встановлення з’єднання з сервером
  • з’являється кнопка Annuler, що дозволяє користувачеві перервати поточне з’єднання
  • усі опції меню вимкнені, крім Quitter та Auteur

З'єднання може завершитися кількома способами:

  1. Користувач натиснув кнопку Annuler: потік з’єднання зупиняється, а меню повертається до початкового стану. У журналі подій зазначається, що з’єднання було закрито користувачем.
  2. З'єднання завершується з помилкою: виконується те саме, що й раніше, а також у журналі подій вказується причина помилки.
  3. З’єднання завершується успішно: видаляємо кнопку Annuler, у журналі фіксуємо, що з’єднання встановлено, дозволяємо меню RazSuivi, блокуємо меню Connecter, дозволяємо меню Déconnecter
Меню «Підключення/Відключення»

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

Меню «Підключення/Вихід»

Ця опція закриває можливе активне з’єднання з сервером і завершує роботу програми.

Меню «Повідомлення»/«Надіслати»

Ця опція доступна лише за умови дотримання таких умов:

  • встановлено з’єднання з сервером

  • є повідомлення, яке потрібно надіслати

Якщо ці умови виконані, на сервер надсилається текст, що міститься в полі TxtSend (3), який закінчується послідовністю RCLF, якщо встановлено прапорець опції RCLF, або послідовністю LF в іншому випадку. Про можливі помилки під час відправлення повідомляється у списку відстеження.

Меню RazSuivi та RazDialogue

Очищають відповідно списки LstSuivi та LstDialogue. Ці опції вимкнені, коли відповідні списки порожні.

Кнопка «Скасувати»

Ця кнопка, розташована внизу форми, з’являється лише тоді, коли клієнт намагається встановити з’єднання з сервером. Це з’єднання може не відбутися через те, що сервер не відповідає або відповідає некоректно. У такому разі кнопка Annuler дає користувачеві можливість перервати запит на з’єднання.

Списки відстеження

Список LstSuivi (5) відстежує процес підключення. Він вказує ключові моменти підключення:

  • його встановлення клієнтом

  • його закриття сервером або клієнтом

  • усі помилки, що можуть виникнути, поки з’єднання активне

Список LstDialogue (6) відстежує діалог, що встановлюється між клієнтом і сервером. Потоком у фоновому режимі відстежується, що відбувається на комунікаційному сокеті клієнта, і відображається у списку 6.

Пункт меню «Автор»

Це меню відкриває вікно, яке називається «Авторські права»:

Image

Управління помилками

Помилки підключення відображаються у списку моніторингу 6, а помилки, пов’язані з діалогом «клієнт-сервер», — у списку діалогу 7. У разі помилки підключення діалог «клієнт-сервер» закривається, а форма повертається до початкового стану, готового до нового підключення.

8.5.1.3. TRAVAIL до FAIRE

Реалізувати описану вище роботу у двох формах:

  • автономний додаток
  • аплет

8.5.2. Вправа 2 — Сервер ресурсів

8.5.2.1. INTRODUCTION

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

Переваги такої організації є численними:

  • Цю послугу може використовувати будь-який тип клієнта (PC, Mac, Unix тощо)
  • клієнт може перебувати в будь-якій точці Інтернету
  • обчислювальні ресурси оптимізовано: потрібні лише кілька потужних машин. Таким чином, невелика організація, що не має власних обчислювальних ресурсів, може користуватися цією послугою, сплачуючи фінансовий внесок, розмір якого розраховується відповідно до витраченого часу на обчислення.

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

Ми пропонуємо написати сервер GRC.

8.5.2.2. INTERFACE VISUELLE

Візуальний інтерфейс буде таким:

Image

Інтерфейс містить два списки серверів:

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

Структура меню така:

Головне меню
Додаткове меню
Роль
Послуга
Запустити
Запустити службу TCP на порту 864
 
Зупинити
Зупиняє службу
 
Вийти
Закриває програму
Автор
 
Інформація про авторські права

Структура елементів управління у формі така:

Ім'я
Тип
Роль
listLibres
JList
Список вільних серверів
listOccupés
JList
Список зайнятих серверів

8.5.2.3. FONCTIONNEMENT DE APPLICATION

Завантаження програми

Під час завантаження програми список listLibres заповнюється списком імен обчислювальних серверів, що керуються GRC. Вони визначені у файлі «Сервери», який передається як параметр. Цей файл містить список імен серверів, по одному на рядок, і тому використовується для заповнення списку listLibres. Меню «Запустити» доступне, меню «Зупинити» заблоковано.

Параметр «Сервіс/Запустити»

Ця опція

  • запускає службу, що прослуховує порт 864 на комп’ютері
  • блокує меню «Запустити»
  • увімкнути меню «Зупинити»
Опція «Служба/Зупинити»

Ця опція зупиняє службу:

  • список зайнятих серверів очищується
  • список вільних серверів заповнюється вмістом файлу «Сервери»
  • меню «Запустити» стає доступним
  • меню «Зупинити» блокується
Опція «Сервіс/Вийти»

Програма завершується.

Діалог «клієнт/сервер»

Діалог «клієнт/сервер» здійснюється шляхом обміну текстовими рядками, що закінчуються послідовністю RCLF. Сервер GRC розпізнає дві команди: getserveur та finservice. Детально розглянемо роль цих двох команд:

  • 1-getserveur

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

Сервер GRC бере перший сервер, знайдений у своєму списку вільних серверів, і повертає його ім’я клієнту у такому форматі:

        100-nom du serveur

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

        serveur (IP du client)

як показано в наступному прикладі, де сервер calcul1.istia.univ-angers.fr обслуговує клієнта з адресою IP 193.52.43.5:

Image

Клієнт не може надіслати команду getserveur, якщо йому вже призначено обчислювальний сервер. Тому перед тим, як відповісти клієнту, сервер GRC перевіряє, чи адреса клієнта IP ще не присутня серед тих, що зареєстровані у списку зайнятих серверів. Якщо це так, сервер GRC відповідає:

        501-Vous avez actuellement une demande en cours

Нарешті, існує випадок, коли жоден обчислювальний сервер не доступний: список вільних серверів порожній. У цьому випадку сервер GRC відповідає:

        502- Il n’y a aucun serveur de calcul disponible

У будь-якому випадку, після надання відповіді клієнту сервер GRC закриває з’єднання з ним, щоб мати змогу обслуговувати інших клієнтів.

  • 2-finservice

Клієнт повідомляє, що йому більше не потрібен обчислювальний сервер, яким він користувався.

Сервер GRC спочатку перевіряє, чи дійсно цей клієнт є тим, якого він обслуговував. Для цього він перевіряє, чи адреса клієнта IP є серед тих, що записані у списку зайнятих серверів. Якщо це не так, сервер GRC відповідає:

        503-Aucun serveur ne vous a été attribué

Якщо клієнт розпізнається, сервер GRC відповідає йому:

        101-Fin de service acceptée

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

Image

Після надсилання відповіді, незалежно від її змісту, сервер GRC закриває з’єднання.

8.5.2.4. TRAVAIL до FAIRE

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

8.5.3. Вправа 3 — SMTP-клієнт

8.5.3.1. INTRODUCTION

Тут ми хочемо створити клієнт для служби SMTP (SendMail Transfer Protocol), яка дозволяє надсилати електронну пошту. У системах Unix або Windows програма telnet є клієнтом, що працює з протоколом tcp. Вона може «спілкуватися» з будь-яким сервісом tcp, що приймає команди у текстовому форматі, які закінчуються послідовністю RCLF, тобто символами з кодами ASCII 13 та 10. Ось приклад «спілкування» зі службою smtp для відправлення електронного листа:

$ telnet istia.univ-angers.fr 25        // виклик служби SMTP

// відповідь сервера SMTP


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// коментарі --------------

Програма telnet може викликати будь-яку службу за допомогою синтаксису

telnet machine_service port_service

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

Відповіді служби smtp мають такий вигляд:

номер-повідомлення або

номер повідомлення

SMTP-сервер може надсилати кілька рядків відповіді. Останній рядок відповіді позначається номером, за яким іде пробіл, тоді як у попередніх рядках відповіді за номером іде тире -.

Номер, більший або рівний 500, вказує на повідомлення про помилку.

// кінець коментарів

help                        // команда, введена з клавіатури

// відповідь сервера SMTP

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
mail from: serge.tahe@istia.univ-angers.fr    // нова команда, введена з клавіатури

// коментарі ---------

Команда mail має такий синтаксис:

mail from: електронна адреса відправника повідомлення

// кінець коментарів

// відповідь сервера SMTP

250 serge.tahe@istia.univ-angers.fr... Sender ok

// коментарі

Сервер smtp не перевіряє правильність адреси відправника: він приймає її такою, якою її йому надали

// кінець коментарів

rcpt to: user1@istia.univ-angers.fr        // нова команда, введена з клавіатури

// коментарі ---------

Команда rcpt має такий синтаксис:

rcpt to: електронна адреса одержувача повідомлення

Якщо електронна адреса належить комп’ютеру, на якому працює сервер smtp, він перевіряє її існування; в іншому випадку перевірка не проводиться. Якщо перевірка відбулася і було виявлено помилку, про це повідомляється кодом >= 500.

Можна відправляти скільки завгодно команд rcpt to: це дозволяє надіслати повідомлення кільком особам.

// кінець коментарів

// відповідь сервера SMTP

250 user1@istia.univ-angers.fr... Recipient ok
data                        // нова команда, введена з клавіатури

// коментарі ---------

Команда data має такий синтаксис:

data

рядок1

рядок2

...

.

За нею йдуть рядки тексту, що складають повідомлення, яке має закінчуватися рядком, що містить лише один символ «крапка».

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

// кінець коментарів

// відповідь сервера SMTP

354 Enter mail, end with "." on a line by itself

// текст повідомлення, введений з клавіатури


subject: essai smtp

essai smtp a partir de telnet
.

// коментарі

У текстових рядках команди data можна вказати рядок subject: для зазначення теми листа. За цим рядком має йти порожній рядок.

// відповідь сервера SMTP

250 HAA11627 Message accepted for delivery
quit                            // нова команда, введена з клавіатури

// коментарі

Команда quit завершує з'єднання зі службою smtp

// кінець коментарів

// відповідь сервера SMTP

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. INTERFACE VISUELLE

Пропонується створити програму з таким візуальним інтерфейсом:

Image

Елементи управління виконують такі функції:

Номер
Тип
Функція
1
JTextField
Ряд електронних адрес, розділених комою
2
JTextField
Текст теми повідомлення
3
JTextField
Ряд електронних адрес, розділених комою
4
JTextField
Послідовність електронних адрес, розділених комою
5
JTextArea
Текст повідомлення
6
JList
список для відстеження
7
JList
список діалогів
8
JButton
Кнопка «Скасувати», яка не відображається, з’являється, коли клієнт надсилає запит на з’єднання з сервером SMTP. Дозволяє користувачеві перервати цей запит, якщо сервер не відповідає.

8.5.3.3. LES MENUS

Структура меню додатка така:

Головне меню
Додаткове меню
Роль
Пошта
  
 
Надіслати
Надіслати повідомлення з контролю 5
 
Вийти
Вийти з програми
Параметри
  
 
Приховати відстеження
Зробити елемент управління 6 невидимим
 
Очистити список відстеження
Очистити список спостереження 6
 
Приховати діалог
Приховати список діалогів 7
 
Очистити діалог
Очистити список діалогів 7
 
Налаштувати
Дозволяє користувачеві вказати
- адресу SMTP-сервера, який використовує програма
- свою електронну адресу
 
Зберегти...
Зберегти попередні налаштування у файлі .ini
Автор
 
Інформація про авторські права

8.5.3.4. FONCTIONNEMENT DE APPLICATION

Меню «Параметри/Налаштування»

Це меню викликає появу наступного вікна:

Image

Обидва поля потрібно заповнити, щоб кнопка OK стала активною. Обидва значення потрібно зберегти у глобальних змінних, щоб вони були доступні для інших модулів.

Меню «Пошта»/«Надіслати»

Ця опція доступна лише за умови дотримання таких вимог:

  • конфігурація виконана
  • є повідомлення, яке потрібно надіслати
  • є тема
  • у полях 1, 3 та 4 вказано принаймні одного одержувача

Якщо ці умови виконані, послідовність подій така:

  • форма переводиться у стан, у якому всі дії, що можуть перешкоджати діалогу «клієнт-сервер», блокуються
  • встановлюється з’єднання на порту 25 сервера, вказаного в налаштуваннях
  • потім клієнт взаємодіє з SMTP-сервером згідно з протоколом, описаним вище
  • у полі «mail from:» використовується електронна адреса відправника, вказана в налаштуваннях
  • поле «rcpt to:» використовується для кожної з електронних адрес, знайдених у полях 1, 3 та 4
  • у рядках, надісланих після команди data, містяться такі тексти:
    • рядок Subject: — текст теми перевірки 2
    • рядок Cc: — адреси перевірки 3
    • рядок Bcc: — адреси перевірки 4
    • текст повідомлення перевірки 5
    • кінцева точка
Кнопка «Скасувати»

Ця кнопка, розташована внизу форми, з’являється лише тоді, коли клієнт намагається встановити з’єднання з сервером smtp. Це з’єднання може не відбутися через те, що сервер smtp не відповідає або відповідає неправильно. У такому разі кнопка Annuler дає користувачеві можливість перервати запит на підключення.

Списки відстеження

Список (6) відстежує з’єднання. Він вказує ключові моменти з’єднання:

  • його встановлення клієнтом
  • його закриття сервером або клієнтом
  • усі помилки підключення

Список (7) відстежує діалог smtp, що встановлюється між клієнтом і сервером.

Ці два списки пов’язані з пунктами меню:

Приховати список для відстеження
Приховує список спостереження 6, а також напис, що знаходиться над ним. Якщо висота, яку займають ці два елементи управління, становить H, усі елементи управління, розташовані нижче, зміщуються вгору на висоту H, а загальний розмір форми зменшується на H. Крім того, «Приховати відстеження» робить невидимою опцію RazSuivi, розташовану нижче.
Очистити список відстеження
Очищає список відстеження 6
«Приховати діалогове вікно»
Приховує список діалогових вікон 7, напис над ним, а також пункт меню RazDialogue, розташований нижче. Як і у випадку з «Приховати відстеження», положення елементів керування, розташованих нижче (можливо, кнопка Annuler), перераховується, а розмір вікна зменшується.
Очистити діалогове вікно
Очищає список діалогів 7
Пункт меню «Автор»

Це меню відкриває вікно, яке називається «Авторські права»:

Image

Управління помилками

Помилки підключення відображаються у списку моніторингу 6, а помилки, пов’язані з взаємодією клієнт/сервер, — у списку діалогових вікон 7. У разі помилки користувач отримує відповідне повідомлення у вікні помилки, а список із причиною помилки відображається, якщо раніше він був прихований. Крім того, діалог «клієнт/сервер» закривається, а форма повертається до початкового стану.

8.5.3.5. GESTION, UN, FICHIER, DE, CONFIGURATION

Бажано, щоб користувачеві не доводилося перенастроювати програмне забезпечення кожного разу, коли він ним користується. Для цього, якщо встановлено прапорець «Параметри/Зберегти конфігурацію при виході», під час закриття програми зберігаються обидва параметри, задані опцією Options/Configurer, а також стан обох списків відстеження у файлі sendmail.ini, розташованому в тому ж каталозі, що й файл .exe програми. Цей файл має такий вигляд:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

Рядки SmtpServer та ReplyAddress містять дві інформації, отримані за допомогою опції «Параметри/Налаштування». Рядки Suivi та Dialogue вказують стан списків «Відстеження» та «Діалог»: 1 (присутній), 0 (відсутній).

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

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

Якщо файл sendmail.ini існує, але є неповним (відсутні рядки), відсутній рядок замінюється відповідним рядком із наведеного вище списку. Отже, оскільки рядок Suivi=... відсутній, ми діємо так, ніби маємо Suivi=1.

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

    mot clé= valeur

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

В опції Options/Configurer відображаються значення SmtpServer та ReplyAddress, які наразі використовуються. Користувач може змінити їх, якщо бажає.

8.5.3.6. TRAVAIL → FAIRE

Виконайте описану вище роботу. Рекомендується обробити файл конфігурації в останню чергу.

8.5.4. Вправа 4 — клієнт POPPASS

8.5.4.1. Introduction

Пропонується створити клієнт TCP, здатний взаємодіяти з сервером POPPASSD, який працює на порту 106. Ця служба дозволяє змінити пароль на машині UNIX. Протокол взаємодії «клієнт-сервер» є таким:

1 - Взаємодія здійснюється шляхом обміну повідомленнями, що закінчуються послідовністю RCLF

2 - Клієнт надсилає команди на сервер

  • Сервер відповідає повідомленнями, що починаються з трицифрових чисел: XXX. Якщо XXX=200, команда була виконана правильно, інакше сталася помилка.

3 - Хронологія обміну даними така:

A    - le client se connecte
  • сервер відповідає привітальним повідомленням
B    - le client envoie USER login
  • сервер відповідає запитом пароля, якщо логін прийнято, або повідомленням про помилку, якщо ні
C    - le client envoie PASS mot_de_passe
  • сервер відповідає запитом на введення нового пароля, якщо пароль прийнято; в іншому разі — повідомленням про помилку
D    - le client envoie NEWPASS nouveau_mot_de_passe
  • сервер відповідає підтвердженням, що новий пароль прийнято; у разі відмови — видає помилку
E    - le client envoie la commande QUIT
  • сервер надсилає повідомлення про завершення та закриває з'єднання

8.5.4.2. Форма клієнта

Image

Значення різних елементів форми такі:

назва
тип
роль
1
txtRemoteHost
JTextField
назва сервера
2
txtLogin
JTextField
логін користувача
3
txtMdp
JTextField
Пароль користувача
4
txtNewMdp
JTextField
Новий пароль користувача
5
txtConfirmation
JTextField
Підтвердження нового пароля
6
lstSuivi
JList
Повідомлення про стан з'єднання
7
lstDialogue
JList
Повідомлення діалогу «Клієнт/Сервер»
10
cmdAnnuler
JButton
не зображено — Кнопка, що з’являється під час встановлення з’єднання з сервером. Дозволяє припинити з’єднання.

8.5.4.3. Меню

Назва
Назва елемента управління
Функція
Підключення
mnuconnexion
 
Підключити
mnuconnecter
запускає підключення до сервера
Вийти
mnuQuitter
закриває програму
Повідомлення
mnuMessages
 
RazSuivi
mnuRazSuivi
очистити список lstSuivi
RazDialogue
mnuRazDialogue
видалити список lstDialogue
Автор
mnuAuteur
відображає вікно з інформацією про авторські права

8.5.4.4. Робота програми

Ініціалізація програми

Після завантаження головного вікна програми виконуються такі дії:

  • сторінка центрується на екрані
  • активними є лише пункти меню Connexion/Quitter та Auteur
  • кнопка Annuler прихована
  • списки LstSuivi та LstDialogue порожні
Меню «Вхід/Підключитися»

Ця опція доступна лише після заповнення полів 1–5. Натискання на цю опцію призводить до таких дій:

  • запускається потік для забезпечення з’єднання з сервером
  • з’являється кнопка Annuler, що дозволяє користувачеві перервати поточне підключення
  • усі опції меню вимкнені, крім Quitter та Auteur

Подальша послідовність подій така:

  1. Користувач натиснув кнопку Annuler: потік з’єднання зупиняється, а меню повертається до початкового стану. У журналі подій зазначається, що користувач закрив з’єднання.
  2. Запит на підключення приймається сервером. Далі розпочинається діалог із сервером для зміни пароля. Обмін даними в цьому діалозі записується у список LstDialogue. Після завершення діалогу з’єднання з сервером закривається, а меню форми повертається до початкового стану.
  3. Поки діалог активний, кнопка «Скасувати» залишається видимою, щоб користувач міг закрити з’єднання, якщо захоче.
  4. Якщо під час зв’язку трапляється будь-яка помилка, з’єднання закривається, а причина помилки відображається у списку відстеження LstSuivi.
Меню «Підключення/Вихід»

Ця опція закриває можливе активне з’єднання з сервером і завершує роботу програми.

Меню RazSuivi та RazDialogue

Очищають відповідно списки LstSuivi та LstDialogue. Ці опції недоступні, коли відповідні списки порожні.

Кнопка «Скасувати»

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

Списки відстеження

Список LstSuivi (5) відстежує з’єднання. Він вказує ключові моменти з’єднання:

  • його встановлення клієнтом

  • його закриття сервером або клієнтом

  • усі помилки, що можуть виникнути, поки з’єднання активне

Список LstDialogue (6) відстежує діалог, що встановлюється між клієнтом і сервером.

Пункт меню «Автор»

Це меню відкриває вікно, яке називається «Авторські права»:

Image

Управління помилками

Помилки зв’язку відображаються у списку відстеження 6, а помилки, пов’язані з діалогом «клієнт-сервер», — у списку діалогів 7. У разі помилки з’єднання діалог «клієнт-сервер» закривається, а форма повертається до початкового стану, готового до нового з’єднання.

8.5.4.5. TRAVAIL до FAIRE

Виконати описану вище роботу у вигляді автономного додатка, а потім — аплету.