|
Интернет програмиране с Java
Светлин Наков
Софийски университет „Св. Климент Охридски” Българска асоциация на разработчиците на софтуер
София, 2004
|
|
Интернет програмиране с Java
© Светлин Наков, 2004 © Издателство „Фабер” Web-site: www.nakov.com E-mail: inetjava-book
Настоящата книга се разпространява свободно. Авторът Светлин Наков притежава правата върху текста на книгата и програмния код, публикуван в нея.
Читателите имат право да разпространяват безплатно оригинални или променени части от книгата, но само при изричното споменаване на източника и автора на съответния текст, програмен код или друг материал. Никой, освен авторът, няма право да разпространява настоящата книга или части от нея срещу заплащане.
Всички запазени марки, използвани в тази книга са собственост на техните притежатели.
Официален сайт:
ISBN 954-775-305-3
|
|
www.devbg.org
Българската асоциация на разработчиците на софтуер е нестопанска организация, която подпомага професионалното развитие на българските софтуерни разработчици чрез различни дейности, насърчаващи обмяната на опит между тях и усъвършенстването на техните знания и умения в областта на проектирането и разработването на софтуер. Асоциацията организира конференции, семинари, симпозиуми, работни срещи и курсове за обучение по разработка на софтуер и софтуерни технологии.
|
Отзив за книгата „Интернет програмиране с Java”
Светлин е опитен програмист, многократно доказан специалист по съвременни софтуерни технологии, талантлив лектор. Неговите курсове в Софийски Университет са сред най-високо ценените, най-полезните и най-посещаваните. Със своите лекции и презентации по научни и технически конференции и семинари той неведнъж е събирал аплодисментите на начинаещи и професионални софтуерни разработчици. В духа на своята творческа натура и нестихващ академичен идеализъм през 2002 Светлин, заедно със свои колеги, разработи курса „Интернет програмиране с Java”. По време на обучението на студентите от Факултета по математика и информатика на Софийски университет, които избраха да слушат този курс, Светлин и колегите му създадоха лекции по “Интернет програмиране с Java”, които по-късно станаха основа на серия от статии в списание “PC Magazine/Bulgaria”. В последствие, с много труд и усилия тези учебни материали достигнаха един завършен академичен вид и станаха основа на настоящия учебник. Книгата „Интернет програмиране с Java” е едно отлично въведение в най-важните аспекти на програмирането с Java за Интернет. В нея се обръща внимание на проблемите на многонишковото програмиране и синхронизацията, разработката на Java приложения, които комуникират по протоколите TCP/IP, създаването на Java аплети, комуникацията между аплет и сървър, разработката на Web-приложения с технологиите Java Servlets и Java Server Pages (JSP) и изпълнението им на сървъра за Web-приложения Tomcat. Настоящата книга е официален учебник за дисциплината „Интернет програмиране с Java”, преподавана от Светлин Наков и ръководения от него екип във факултетa по математика и информатика на Софийски Университет „Св. Климент Охридски” през 2004 г.
доц. д-р Магдалина Тодорова Софийски Университет „Св. Климент Охридски” |
Ако по принцип не четете предговорите на книгите, пропуснете и този. И той е така скучен, както всички други. В него ще бъдат изяснени стандартните въпроси „за кого е тази книга”, „какво включва тя”, „какво се очаква от читателите”, „какво се очаква да научим от книгата” и разни други неща все в тоя дух.
Няма да ви занимаваме с празни встъпителни приказки в стил „колко много Интернет е навлязъл в живота ни”, „кога е възникнал Интернет”, „колко много има нужда от Интернет приложения и Интернет програмисти”, „кой е създал Java”, “колко е велика Джавата”, „колко е всемогъщ Интернета”, „сами ли сме във вселената” и „какъв е смисъла на живота”. Вместо това направо ще пристъпим към същината.
Настоящият учебник по “Интернет програмиране с Java” е предназначен за всички, които се интересуват от разработка на Интернет-ориентирани приложения и програмиране на Java.
В настоящата книга се обръща внимание на най-важните технологии от областта на Интернет-ориентираното програмиране с езика и платформата Java:
- програмиране със сокети – разработка на Java приложения, които комуникират по Интернет и Интранет мрежи чрез протоколите TCP/IP;
- Java аплети – разработка на малки Java приложения с графичен потребителски интерфейс, които се вграждат във Web страници и се изпълняват от Web-браузърите на потребителите;
- Web-приложения – разработка на Web-приложения с технологиите Java Servlets и Java Server Pages (JSP), създаване и deploy-ване на Web-приложения съгласно стандартите на J2EE и работа със сървъра Tomcat.
За да бъде разбран материалът, е необходимо читателите да имат основни познания по обектно-ориентирано програмиране, да са запознати с езика Java, да имат обща представа за организацията на Интернет и начални знания по HTML. Не е необходимо добро владеене на езика Java. Тази книга учи на концепции, базови знания и технологии, а не на Java.
В никакъв случай тази книга не може да ни направи експерт по Интернет програмиране с Java. Целта на книгата е да ни даде начални, базови знания, които да ни послужат като основа в развитието ни като програмисти, софтуерни специалисти и Интернет разработчици.
Известно е, че когато един програмист има начални знания по някоя технология, той много бързо и лесно може да ги доразвие и да достигне високо професионално ниво. Много по-трудно е, обаче, ако започне от нулата. Това е и смисъла да учим много неща, които не използваме директно в работата си. Има смисъл да познаваме много технологии, за да избираме правилната, когато имаме конкретен проблем за решаване, но няма смисъл да сме експерти в нещо, което не използваме.
Като че ли най-добрата стратегия за един добър софтуерен разработчик е да разбира по-малко от много и най-разнообразни технологии, но да е добър специалист в тези от тях, които използва. В момента, в който един разработчик спре да учи нови неща и се пусне по течението със самочувствието, че вече знае прекалено много и няма нужда от повече, той вече не е добър разработчик.
Тази книга няма да ви направи специалисти нито по Интернет технологии, нито по Java, нито по Web-програмиране, но ще ви даде една безценна основа, с която ще можете да се развивате в тази посока, независимо от езиците за програмиране и платформите, с които ще работите.
Темите са разделени в три глави – сокет програмиране, Java аплети и Web-приложения. Всяка от тях започва с въведителна част, в която се изясняват основните концепции за съответната технология, а след това малко по малко се навлиза в материята, структурирано, последователно и с много примери.
Обичате примерите, нали? Какво е една книга за програмиране без примери? Въобще някой чете ли текста, когато търси нещо и това нещо го има в примерите?
Стремежът ни да разгледаме възможно повече основополагащи знания ни кара да започнем с изясняване на основните идеи и технологии, върху които е изградена световната мрежа Интернет.
Поради очакването, че не всички читатели познават добре Java, в началото обръщаме внимание и на някои базови знания от Java платформата като средства за вход/изход и средства за многонишково програмиране и синхронизация. По-нататък даваме решение на един класически синхронизационен проблем – проблемът „производител-потребител”, на който ще се натъкваме след това много пъти.
След въведителните теми пристъпваме към програмирането със сокети. Ще разгледаме клиент/сървър комуникацията по протокол TCP и ще разгледаме много примери, като малко по-малко ще увеличаваме сложността им, докато достигнем до проблема за създаване на многопотребителски чат сървър, при който нещата не са съвсем прости. По-нататък ще продължим с протокола UDP, ще се запознаем с multicast-сокетите и ще завършим главата с темата за достъп до ресурси по URL.
Във втора глава ще се занимаваме с технологията на Java аплетите. Ще навлезем в тайните на библиотеката AWT, нейният програмен модел и ще видим как да я използваме при създаването на аплети. Ще си изясним особеностите на аплетите, тяхната среда за изпълнение, жизненият им цикъл и ще завършим с проблемите на сигурността и начините за комуникация между аплет и Web-сървър.
В следващата глава ще навлезем в Web-програмирането. Първоначално ще изясним неговите основни концепции, базовите понятия, свързани с него, протоколите, програмния модел, езиците за описание на Web-съдържание и технологиите за динамично генериране на Web-съдържание. След това ще представим технологията на Java сървлетите, ще изясним как се използва сървъра за Web-приложения Tomcat и как да изпълняваме сървлети и Web-приложения с него. След това ще пристъпим към техниките за извличане на параметри, подадени от клиента и средствата за управление на потребителски сесии. Ще изясним и технологията Java Server Pages и таговете, свързани с нея. Накрая ще изясним цялостната концепция за Web-приложения на платформата J2EE и ще дадем пример за едно такова приложение.
Официалният сайт на книгата е на адрес:
http://www.nakov.com/books/inetjava/
В него можете да намерите допълнителни материали, ресурси и информация, свързана с учебния материал в книгата, сорс-кода от примерите, информация за самата книга, форма за изпращане на грешки и дискусионен форум.
Моля отправяйте всички коментари, въпроси и предложения във форума на книгата, а не ми ги изпращайте по e-mail. Всеки ден получавам стотици писма и има вероятност да не успея да ви отговоря, ако отправите технически въпрос директно по e-mail.
Настоящата книга е резултат от дългогодишната работа на автора по съставянето на лекции по „Интернет програмиране с Java” за едноименния курс, който се провежда от 2002 г. в Софийски университет „Св. Климент Охридски”. Книгата успя да събере в себе си най-важното от целия опит на преподавателския колектив в областта на Интернет програмирането и да го синтезира в една кратка и достъпна за българските студенти форма.
Авторът изказва най-сърдечните си благодарности на всички негови колеги и приятели, които го подкрепяха и му помагаха по време курсовете „Интернет програмиране с Java” във Факултета по Математика и Информатика на Софийски Университет „Св. Климент Охридски” и които го насърчаваха през цялото време на работата му върху книгата:
Борис Червенков
Николай Недялков
Красимир Семерджиев
Димитър Георгиев
Лъчезар Цеков
Райчо Минев
В настоящата глава ще разгледаме средствата на Java за разработка на Интернет приложения, които си комуникират със сокети по стандартните за Интернет протоколи TCP/IP.
В началото ще направим кратък преглед на TCP/IP мрежите с който ще въведем базови понятия като IP адрес, сокет, порт, клиент, сървър, протокол, услуга. Ще разгледаме двата основни транспортни протокола TCP и UDP. Ще разгледаме най-общо как работи световната мрежа Интернет и какви услуги предоставя тя.
За читателите, които не са добре запознати с Java платформата, ще направим кратък преглед на средствата на Java за осъществяване на входно-изходни операции, които ще ни трябват след това при разработката на клиентски и сървърски приложения, които си комуникират по сокети. Ще обърнем внимание на двата основни типа потоци в Java – текстови и бинарни.
Ще разгледаме в дълбочина средствата на Java за създаване на многонишкови (multithreading) приложения и най-вече проблемите със синхронизацията на достъпа до общи ресурси. Ще дадем решение на класическия проблем „производител – потребител” на базата на синхронизационните обекти „монитори”.
След всички въведения ще имаме вече достатъчно познания, за да продължим по-нататък с темата за изграждане на приложения, които си комуникират по надеждни двупосочни поточно-ориентирани канали изградени на базата на TCP сокети.
Ще обърнем внимание и на средствата на Java за ненадеждно изпращане на единични пакети с информация между две приложения по протокол UDP, след което ще видим как можем да използваме механизмите на multicast инфраструктурата за да реализираме комуникация базирана на абонамент и групова доставка на съобщения.
В края на главата ще се запознаем със средствата на Java за извличане на ресурси от глобалната разпределена информационната система WWW (World Wide Web). Ще обясним какво е URL и как ще демонстрираме леснотата, с която в Java можем да извличаме ресурс от WWW по неговия URL адрес.
Според световно възприетите стандарти за компютърни мрежи на организацията IEEE (Institute of Electrical and Electronics Engineers) комуникацията във всяка мрежа се осъществява на следните 7 нива:
|
# |
ниво |
описание и протоколи |
|
7 |
Application (приложно ниво) |
Осигурява на приложните програмисти интерфейс към мрежовата инфраструктура, осигурена от по-долните слоеве. Протоколите от това ниво задават форматите и правилата за обмяна на данни между комуникиращите приложения. Типични протоколи на това ниво са: HTTP, SMTP, POP3, FTP, SNMP, FTP, DNS, NFS и др. |
|
6 |
Presentation (представително ниво) |
Осигурява общ формат, унифицирано канонично представяне на пренасяните данни, което е еднакво за всички платформи и е разбираемо за по-долните слоеве. Типични протоколи или по-точно схеми за унифицирано представяне на данни от това ниво са XDR, ASN.1, SMB, AFP. |
|
5 |
Session (сесийно ниво) |
Организира и синхронизира прозрачната обмяна на информация между два процеса в операционните системи на комуникиращите машини. Типични протоколи от това ниво са: RPC, NetBIOS, CCITT X.225 и др. |
|
4 |
Transport (транспортно ниво) |
Осигурява поддръжката на комуникационни канали за данни между две машини. Позволява пренасяне не само на отделни пакети, но и на по-големи обеми данни. Осигурява прозрачност и надеждност на преноса на данни. Грижи се за започване, поддръжка и прекратяване на комуникацията между машините участнички. Типични протоколи на това ниво са: TCP, UDP, RTP, SPX, ATP. |
|
3 |
Network (мрежово ниво) |
Осигурява пренасяне на единици информация (пакети) между две машини в дадена мрежа, всяка от които има уникален мрежов адрес. Не е задължително двете машини да са пряко свързани една с друга и затова мрежовото ниво осигурява маршрутизиране на пакетите от една машина към друга с цел достигане на крайната цел. Типични протоколи на това ниво са IP, IPv6, ICMP, IGMP, X.25, IPX и др. |
|
2 |
Data Link (свързващо ниво) |
Осигурява директно пренасяне на информация между две мрежови комуникационни устройства (например две мрежови карти или два модема), управлява физическото ниво и се грижи за корекция на грешки възникнали в него. Типични протоколи са Ethernet, Token ring, PPP, Frame relay, ISDN и др. |
|
1 |
Physical (физическо ниво) |
Осигурява физическото пренасяне на информацията. Може да се реализира от радиовълни, оптични кабели, лазери и др. |
Всяко от 7-те нива използва за работата си по-долните нива и не се интересува от това как точно те работят, а само от това какво те осигуряват. Това позволява на разработчиците да се абстрахират от ненужните детайли и да се концентрират върху работата само на нивата, които ги засягат. Така програмирането се опростява, защото програмистът не трябва да се съобразява с цялата сложност на комуникацията, а може да разчита на вече изградената инфраструктура от по-долните нива.
“TCP/IP protocol suite” не е протокол. TCP/IP е наименованието на пакета от протоколи, с които работи световната мрежа Интернет. В този пакет се включват протоколите IP, TCP, UDP, ICMP и IGMP. Локалните мрежи, работещи с протоколите от пакета TCP/IP се наричат Интранет мрежи.
Класическият 7-слоен OSI модел засяга всички страни на организацията на комуникацията между две приложения, но често пъти с цел избягване на излишни детайли при Интернет и Интранет мрежи се използва опростен модел, т. нар. 4-слоен модел на TCP/IP мрежите. При него най-горните 3 слоя от OSI модела са обединени в един, защото реално се отнасят до организацията на комуникацията на ниво приложни програми. Най-долните 2 слоя също са обединени, защото те заедно изпълняват една обща задача – осигуряват пренасянето на информация между две машини, които са директно свързани с някаква комуникационна линия. На практика TCP/IP моделът е опростен частен случай на OSI модела, при който на мрежово и транспортно ниво се използват протоколите от пакета TCP/IP. Приликите с OSI модела могат да се видят в таблицата с описанията на 4-те слоя:
|
# |
ниво |
описание и протоколи |
|
4 |
Application (приложно ниво) |
Осигурява на приложните програмисти интерфейс към мрежовата инфраструктура, осигурена от транспортния и Интернет слоевете. Протоколите от това ниво задават форматите и правилата за обмяна на данни между комуникиращите си приложения. Типични протоколи на това ниво са: HTTP, SMTP, POP3, FTP, SNMP, FTP, DNS, NFS и др. |
|
3 |
Transport (транспортно ниво) |
Осигурява поддръжката на комуникационни канали за данни между две приложения (евентуално на отдалечени машини). Позволява пренасяне не само на отделни пакети, но и на по-големи обеми данни. Осигурява прозрачност и надеждност на преноса. Грижи се за започване, поддръжка и прекратяване на комуникацията между процесите участници. В това ниво се използват само два протокола: TCP и UDP. |
|
2 |
Internet (Интернет ниво) |
Осигурява пренасяне на единици информация (пакети) между две машини в дадена мрежа, всяка от които има уникален адрес (IP адрес). Не е задължително двете машини да са пряко свързани една с друга и затова Интернет нивото осигурява маршрутизиране на пакетите от една машина към друга с цел достигане на крайната цел. На това ниво работят протоколите IP, IPv6, ICMP и IGMP. |
|
1 |
Link (свързващо ниво) |
Осигурява директно пренасяне на информация между две мрежови комуникационни устройства (например две мрежови карти или два модема). Типични протоколи са Ethernet, Token ring, PPP, Frame relay, ISDN и др. |
Когато пишем Java програми, които комуникират по мрежата, ние програмираме най-горния слой от TCP/IP модела, така нареченият Application слой. Преносът на данни, предизвикан от нашите Java програми, се осъществява от транспортния слой посредством протоколите TCP или UDP. Транспортният слой използва по-долния мрежов слой за прехвърляне на малки количества информация, наречени IP пакети, от един компютър на друг, а тези пакети се прехвърлят чрез мрежови протоколи и връзки на още по-ниски нива. Като програмисти на Java, не е необходимо да знаем в детайли за всичко това, но все пак трябва да имаме представа поне от TCP и UDP протоколите дотолкова, доколкото е необходимо да преценим кога кой от тях да използваме и от IP протокола дотолкова, доколкото е необходимо да знаем, че всеки компютър в Интернет и Интранет мрежи си има уникален IP адрес, по който можем да се обръщаме към него.
Основно понятие в Интернет и всички други TCP/IP мрежи е IP адрес. IP адресите представляват уникални 32-битови номера на компютри и се записват като четири 8-битови числа (в десетична бройна система), разделени по между си с точки. Всеки компютър, работещ в Интернет или Интранет мрежа, има IP адрес. Пример за IP адрес е записът: 212.39.1.17. Машините в TCP/IP базирани мрежи, които имат IP адрес, се наричат хостове (hosts).
Чрез проста сметка може да се прецени, че адресното пространство на Интернет се състои от около 4 милиарда IP адреса, но това не е съвсем така, защото няколко големи области от това пространство са резервирани за специални цели. Разпределението на IP адресното пространство на Интернет се управлява от световната организация IANA.
За улеснение на потребителите някои машини в Интернет освен IP адрес могат да имат и имена. Съответствията между IP адресите и имената на компютрите (хостовете в Интернет) се поддържат от специални DNS сървъри. При заявка DNS сървърите могат да намират IP адрес по име на машина и обратното. На едно име на хост в Интернет могат да съответстват няколко IP адреса, а също и на един IP адрес може да съответства повече от едно име.
TCP (Transmission Control Protocol) е протокол, който осигурява надежден двупосочен комуникационен канал между две приложения. Можем да сравним този канал с канала, по който се осъществява при обикновен телефонен разговор. Например, ако искаме да се обадим на приятел, ние набираме неговия номер и когато той вдигне, се осъществява връзка между нас двамата. Използвайки тази връзка, ние можем да изпращаме и получаваме данни от нашия приятел, до момента, в който един от двамата затвори телефона и прекрати връзката. Подобно на телефонните линии, TCP протоколът гарантира, че данните, изпратени от едната страна на линията, ще се получат от другата страна на линията без изменение и то в същия ред, в който са изпратени. Ако това е невъзможно по някаква причина, ще възникне грешка (след определено време, наречено timeout) и ние ще разберем, че има някакъв проблем с комуникационния канал. Именно заради тази своя надеждност, TCP е най-често използваният протокол за трансфер на информация по Интернет. Примери за приложения, които комуникират по TCP са Web-браузърите, Web-сървърите, FTP клиентите и сървърите, Mail клиентите и сървърите – приложения, за които редът на изпращане и пристигане на данните е много важен.
UDP (User Datagram Protocol) е протокол, който позволява изпращане и получаване на малки независими един от друг пакети с данни, наречени дейтаграми, от един компютър на друг. За разлика от TCP, UDP не гарантира нито реда на пристигане на изпратените последователно дейтаграми, нито гарантира, че те ще пристигнат въобще. Изпращането на дейтаграма е като изпращане на обикновено писмо по пощата: редът на пристигане на писмата не е важен и всяко писмо е независимо от останалите. UDP се използва значително по-рядко от TCP заради това, че не осигурява комуникационен канал за данни, а позволява само изпращане на единични независими кратки съобщения (UDP пакети).
Както TCP, така и UDP протоколът позволява едновременно да се осъществяват няколко независими връзки между два компютъра. Например можем да зареждаме няколко различни Web-сайта чрез нашия Web-браузър и същевременно да теглим през FTP няколко различни файла от един и същ или няколко различни FTP сървъра. Реално погледнато едно и също приложение (например нашият Web-браузър) отваря едновременно няколко независими комуникационни канала до един или няколко различни сървъра, като по всеки от тях прехвърля някаква информация. За да е възможно няколко приложения да комуникират по мрежата едновременно, е необходимо пакетите информация, предназначени за всяко едно от тях да бъдат обработени от съответното приложение, а не от някое друго. Така всяко приложение изпраща и получава своите данни независимо от другите, така сякаш те не съществуват. Именно за решаване на този конфликт се използват портовете в протоколите TCP и UDP.
Портът е число между 0 и 65536 и задава уникален идентификатор на връзката в рамките на машината. Всеки TCP или UDP пакет, освен данните, които пренася, съдържа в себе си още 4 полета, описващи от кого до кого е изпратен пакета: source IP, source port, destination IP и destination port. По IP адресите се разпознават компютрите, отговорни за изпращане и получаване на съответните пакети, а по портовете се разпознават съответните приложения, работещи на тези компютри, които изпращат или трябва да получат информацията от тези пакети. Всяка TCP връзка в даден момент се определя еднозначно от 4 числа: IP източник, порт източник, IP получател и порт получател.
Сокет наричаме двойката (IP адрес; номер на порт). Комуникационният канал, който предоставя една TCP връзка наричаме сокет връзка (socket connection). Често пъти сокет връзките се наричат за краткост само сокети.
Например нека нашият IP адрес е 212.50.1.81 и сме стартирали Internet Explorer и Outlook Express. С Internet Explorer браузваме някакъв сайт при което той е отворил няколко сокета към IP адрес 212.50.1.1 на порт 80 и тегли през тях някакви Web-страници и картинки. В същото време с Outlook Express си теглим новопристигналата поща и за целта той е отворил сокет към 192.92.129.4 на порт 110. В този момент имаме няколко едновременно отворени TCP сокета (няколко независими една от друга комуникационни линии), чрез които нашият компютър комуникира с други два компютъра. Можем да ги представим схематично по следния начин:
Internet Explorer = 212.50.1.81:1033 « 212.50.1.1:80 = Apache Web Server
Internet Explorer = 212.50.1.81:1037 « 212.50.1.1:80 = Apache Web Server
Outlook Express = 212.50.1.81:1042 «192.92.129.4:110 = Microsoft Exchange POP3 Server
Първата връзка служи за изтегляне на някаква Web-страница. Тя има за източник приложението Internet Explorer и за нея е определен порт източник 1033 на нашия компютър (212.50.1.81). За получател е определен компютърът 212.50.1.1 и порт получател 80, който порт е свързан с приложението, което обслужва достъпа до Web-страниците на този компютър – Apache Web Server. Източник и получател не е съвсем точно казано, защото всички TCP връзки са двупосочни, т.е. предоставят два независими канала за данни за всяка от посоките, но все пак можем да приемем за източник това приложение, което е създало връзката (отворило сокета). Втората връзка служи за изтегляне на някаква картинка и прилича много на първата, но с една разлика – портът източник. Този порт източник е свързан също с приложението Internet Explorer на нашия компютър, но е друго число. Въпреки, че двете връзки са между едни и същи приложения, те са различни и независими, т.е. представляват два независими канала за данни. Единия служи за изтегляне на някакъв HTML документ, а другият за изтегляне на някаква картинка. Web-сървърът знае по кой от двата канала да изпрати HTML документа и по кой картинката. Internet Explorer също знае по кой от двата канала ще пристигне HTML документа и по кой картинката. Това се определя от порта източник, който е различен за двата канала. Портът източник се задава автоматично от операционната система при създаване на TCP сокет. Този порт е уникален в рамките на машината. При отваряне на нова сокет връзка програмистът трябва да знае предварително IP адреса и порта на приложението, с който иска да осъществи комуникация.
Съществуват два вида приложения, които комуникират по TCP протокола – клиентски и сървърски.
Клиентските приложения (наричани още клиенти) се свързват към сървърските като отварят сокет връзка към тях. За целта те предварително знаят техните IP адреси и портове.
Сървърските приложения (наричани още сървъри) “слушат на определен порт” и чакат клиентско приложение да се свърже към тях. При пристигане на заявка за връзка от някой клиент на порта, на който сървърът слуша, се създава сокет за връзка между клиента на неговия порт източник и сървъра на неговия порт получател.
Клиентите отварят сокети към сървърите, а сървърите създават сокети само по клиентска заявка, т.е. те не отварят сокети.
Можем да си представим едно клиент/сървър приложение като магазин с няколко щанда и клиенти, които пазаруват в него. Сървърът може да се сравни с магазин, а портът, на който слуша този сървър – с определен щанд вътре в магазина. Когато дойде клиентът, той се допуска, само ако иска да отиде на някой от щандовете, които работят (допуска се връзка само на отворен порт /порт на който слуша някое сървърско приложение/). Когато клиентът отиде на съответния щанд, той започва да си говори с продавача (осъществява комуникационна линия и прехвърля данни по нея в двете посоки) на определен език, който и двамата разбират (предварително известен протокол за комуникация). Както магазинът, така и щандът могат да обслужват няколко клиента едновременно, без да си пречат един на друг. След приключване на комуникацията клиентът си тръгва (и затваря сокета). Междувременно продавачът може да изгони клиента от магазина, ако той се държи невъзпитано или няма пари (сървърът може да затвори сокета по всяко време). За повечето операции със сокети имаме аналог с нашия пример с магазина и затова взаимодействието „клиент/сървър” лесно може да се интерпретира като взаимодействие от вида „потребител на услуга/извършител на услуга”.
Третата връзка от показаните по-горе свърза приложението Outlook Express, което се идентифицира с порт 1042 на нашата машина (212.50.1.81) с приложението Microsoft Exchange POP3 Server, което се идентифицира с порт 110 на машината с IP адрес 192.92.129.4. Пристигналите TCP пакети на нашата машина ще бъдат разпознати от операционната система по четирите полета, които идентифицират един сокет – source IP, source port, destination IP и destination port и ако са валидни, информацията от тях ще се предаде на съответното приложение. Понеже едно приложение, както видяхме, може да отвори повече от един сокет до някое друго приложение, най-правилно е да се каже, че портът източник и портът получател задават не само клиентското и сървърското приложение съответно, но и идентификатора на връзката в рамките на тези приложения, който е уникален за цялата машина.
При UDP комуникацията концепцията с портовете е същата, само че не се осъществява комуникационен канал между приложенията, а се изпращат и получават отделни единични пакети. Тези пакети носят в себе си същата допълнителна информация като TCP връзките – IP и порт на изпращач и IP и порт на получател. И при UDP протокола също има клиентски и сървърски приложения и по същият начин операционната система разпознава кой пакет за кое приложение е.
Комуникационните канали, наречени сокети, не са достатъчни за осъществяване на комуникация между две приложения. Ако се върнем на ситуацията в магазина, клиентът трябва да комуникира с продавачката на известен и за двамата език. По същия начин при клиент/сървър комуникация клиентът и сървърът могат да си общуват само ако знаят един и същ език. Формални езици, които се използват за комуникация в компютърни мрежи, се наричат протоколи. Протоколите представляват системи от правила, които задават по какъв начин клиентът и сървърът могат да общуват и описват кои са валидните действия, които клиентът и сървърът могат да извършат във всеки един момент от комуникацията.
В Интернет работят много стандартни протоколи за комуникация между приложения, като всеки от тях е свързан с някаква услуга. Всяка услуга работи с някакъв протокол, предварително известен на клиентските и сървърските приложения. Например услугата достъп за Web-ресурси работи по протокола HTTP, услугата за изпращане на e-mail работи по протокола SMTP, а услугата за достъп до файл от FTP сървър работи по протокола FTP. За всяка от тези стандартни Интернет услуги (well-known services) има и асоциирани стандартни номера на портове (well-known ports), на които тези услуги се предлагат. Стандартните портове са въведени за да се улесни създаването на клиентски приложения, понеже всяко клиентско приложение трябва да знае не само IP адреса или името на сървъра, на който се предлага услугата, до която то иска достъп, но също и порта, на който тази услуга е достъпна. Някои стандартни портове, протоколи и услуги са дадени в таблицата по-долу:
|
порт |
протокол |
услуга |
|
21 |
FTP |
Услуга за достъп до отдалечени файлове. Използва се от FTP клиенти (например Internet Explorer, GetRight, CuteFTP, wget) |
|
25 |
SMTP |
Услуга за изпращане на E-mail. Използва се от E-mail клиенти (например Outlook Express, Mozilla Mail, pine) |
|
80 |
HTTP |
Услуга за достъп до Web-ресурси. Използва се от Web-браузъри (например Internet Explorer, Mozilla, lynx) |
|
110 |
POP3 |
Услуга за извличане на E-mail от пощенска кутия. Използва се от E-mail клиенти (например Outlook Express, Mozilla Mail, pine) |
Java приложенията могат да използват
TCP и UDP протоколите за комуникация през Интернет чрез класовете от стандартния
пакет java.net.
Най-важните класове, който се използват при разработка на такива приложения
са InetAddress,
Socket,
ServerSocket,
DatagramSocket,
DatagramPacket
и URL.
По-нататък в тази глава ще разгледаме в детайли тези класове, но преди това ще направим кратък преглед на средствата за вход/изход и многонишково програмиране в Java, защото те са важна основа, без която не можем да създаваме мрежови приложения.
В тази тема ще направим съвсем кратък преглед на най-важните класове и методи за вход и изход в Java. Всичко останало може да се намери с документацията на JDK.
В езика Java входно-изходните операции са базирани на работа
с потоци от данни. Потоците са канали за данни, при които достъпът се
осъществява само последователно. Класовете, чрез които се осъществяват входно-изходните
операции се намират в пакета java.io. Има два основни типа потоци – текстови и бинарни.
Текстовите потоци служат за четене и писане на текстова
информация, а бинарните – за четене и писане на двоична информация. Базов за
всички входни текстови потоци е интерфейсът
java.io.Reader, а за всички изходни текстови потоци –
java.io.Writer.
Най-важният метод от интерфейса
java.io.Reader е методът
read(…),
който служи за четене от текстов поток и се предоставя в няколко варианта съответно
с различен набор от параметри:
| int read() – прочита един символ и го връща във вид на число. Връща -1 ако е достигнат края на потока. Предизвиква IOException ако възникне грешка при четенето. |
| int read(char[] cbuf) – прочита поредица от символи и ги записва в подадения масив. Прочита най-много толкова символа, колкото е големината на масива. Връща броя на прочетените символи или -1 ако е достигнат края на потока. Предизвиква IOException ако възникне грешка при четенето. |
| int read(char[] cbuf, int off, int len) – прочита поредица от символи с максимална дължина len и ги записва в подадения масив на подаденото отместване off. Връща броя на прочетените символи или -1 ако е достигнат края на потока. Предизвиква IOException ако възникне грешка при четенето. |
Всяка от изброените по-горе операции е блокираща, т.е. тя блокира при извикване и не връща управлението докато не прочете някакви данни или не възникне входно-изходна грешка. Винаги при четене от текстови потоци е възможно да бъдат прочетени по-малко на брой символи, отколкото са заявени и това е една от важните особености, с които трябва да се съобразяваме, когато четем от текстови потоци.
Много удобен за четене от текстови
потоци е класът java.io.BufferedReader,
защото предлага метод за четене на цял текстов ред
readLine(), а това често се
налага.
Най-важният метод от интерфейса
java.io.Writer е методът
write(…),
който служи за писане в текстов поток. Той има няколко варианта:
| void write(int c) – записва единичен символ в потока. Символът е представен като число. Предизвиква IOException ако възникне грешка при писането. |
| void write(char[] cbuf) – записва в потока последователността от символи, съдържаща се в подадения масив. Предизвиква IOException ако възникне грешка при писането. |
| void write(char[] cbuf, int off, int len) – записва в потока последователността от символи, съдържаща се в подадения масив, започваща от зададената позиция и имаща зададената дължина. Предизвиква IOException ако възникне грешка при писането. |
| void write(String str) – записва в потока даден символен низ. Предизвиква IOException ако възникне грешка при писането. |
Както и при четенето от текстов поток всяка от изброените по-горе операции е блокираща, т.е. тя блокира при извикване и не връща управлението докато не запише данните или не възникне входно-изходна грешка. Операциите за писане в поток са или напълно успешни, т.е. записват всичките указани символи, или не са успешни и предизвикват изключение.
Важна операция при работа с текстови потоци е операцията flush(). Тя предизвиква реално изпращане на записаните данни към мястото, за което са предназначени, като се грижи за изпразване на всички буфери, използвани за кеширане на изпратените данни. Без да сме извикали flush() метода не можем да сме сигурни, че данните, които сме записали в даден поток с write(…), наистина са отпътували към местоназначението си. Когато разработваме приложения, които си комуникират чрез потоци, трябва винаги да внимаваме за тази особеност.
За писане в текстови потоци
е удобен и класът java.io.PrintWriter,
който има метод println(…)
за печатане на цяла текстова линия.
Един прост пример за използване на текстови потоци е показан по-долу. Примерът представлява малка програмка, която номерира редовете на текстов файл:
TextFileLineNumberInserter.java
|
import java.io.*; import java.lang.*;
public class TextFileLineNumberInserter { public static void main(String[] args) throws IOException { FileReader inFile = new FileReader("input.txt"); BufferedReader in = new BufferedReader(inFile);
FileWriter outFile = new FileWriter("output.txt"); PrintWriter out = new PrintWriter(outFile);
int lineNumberCounter = 0; String line;
while ( (line=in.readLine()) != null ) { lineNumberCounter++;
out.println(lineNumberCounter + " " + line); }
in.close();
out.close();
}
} |
Въпреки че Java работи вътрешно с Unicode стрингове, текстовите потоци четат и пишат символите не в Unicode, а като използват стандартните 8-бита за символ. При писане и четене информацията се преобразува от и към Unicode по текущо-активната кодова таблица, което създава известни проблеми. Това е една от причините, заради която не можем да обработваме бинарна информация с текстови потоци.
Базов за всички входни двоични (бинарни) потоци е интерфейсът
java.io.InputStream,
а за всички изходни двоични потоци е интерфейсът
java.io.OutputStream. Ключов
метод на InputStream
е методът int read(byte[] b, int off,
int len), който чете данни от входния поток и ги записва в
масив, а ключови методи в
OutputStream
са
write(byte[]
b, int off, int len), който изпраща данни от масив към изходния
поток и
flush(),
който изпразва буферите и записва чакащата в тях информация към местоназначението
й. Методите read(…)
и write(…) при двоичните
потоци са напълно аналогични на съответните методи на текстовите потоци с разликата,
че работят с двоични данни, а не със символи. Тези методи също са блокиращи,
при четене също връщат броя прочетени байтове, който може да е по-малък от броя
заявени за прочитане байтове, а при достигане на края на потока връщат -1, също
хвърлят изключение при входно-изходна грешка и при писане също или се записва
всичко, или се получава изключение.
За демонстрация на двоичните потоци ще дадем пример с една програмка, която копира двоични файлове:
BinaryFileCopier.java
|
import java.io.*;
public class BinaryFileCopier { public static void main(String args[]) throws IOException { FileInputStream inFile =
new FileInputStream("input.bin"); FileOutputStream outFile =
new FileOutputStream("output.bin"); byte buf[] = new byte[1024]; while (true) { int bytesRead = inFile.read(buf); if (bytesRead == -1) break; outFile.write(buf, 0, bytesRead); }
outFile.flush();
outFile.close();
inFile.close();
}
}
|
В тази тема ще се запознаем с възможностите за многонишково програмиране в Java, тъй като тези знания ще са ни крайно необходими в по-нататъшната ни работа.
Многонишковите (multithreaded) програми представляват програми, които могат да изпълняват едновременно няколко редици от програмни инструкции. Всяка такава редица от програмни инструкции наричаме thread (нишка). Изпълнението на многонишкова програма много прилича на изпълнение на няколко програми едновременно. Например в Microsoft Windows е възможно едновременно да слушаме музика, да теглим файлове от Интернет и да въвеждаме текст. Тези три действия се изпълняват от три различни програми (процеси), които работят едновременно. Когато няколко процеса в една операционна система работят едновременно, това се нарича многозадачност. Когато няколко отделни нишки в рамките на една програма работят едновременно, това се нарича multithreading (многонишковост). Например ако пишем програма, която работи като Web-сървър и Mail-сървър едновременно, то тази програма трябва да може да изпълнява едновременно поне 3 независими нишки – една за обслужване на Web заявките (по протокол HTTP), друга за изпращане на поща (по протокол SMTP) и трета за теглене на поща (по протокол POP3). Много вероятно е освен това за всеки потребител на тази програма да се създава по още една нишка, за да може този потребител да се обслужва независимо от другите и да не бъде каран да чака, докато системата обслужва останалите.
С Java създаването на многонишкови
програми е изключително лесно. Достатъчно е да наследим класа
java.lang.Thread и да припокрием
метода run(), в който
да напишем програмния код на нашата нишка. След това можем да създаваме обекти
от нашия клас и с извикване на метода им
start() да започваме паралелно
изпълнение на написания в тях програмен код. Ето един пример, който илюстрира
как чрез наследяване на класа
Thread
можем да създадем няколко нишки, които работят едновременно в рамките на нашето
приложение:
ThreadTest.java
|
class MyThread extends Thread { private String mName; private long mTimeInterval;
public MyThread(String aName, long aTimeInterval) { mName = aName;
mTimeInterval = aTimeInterval;
}
public void run() { try { while (!isInterrupted()) { System.out.println(mName);
sleep(mTimeInterval);
}
} catch (InterruptedException intEx) { // Current thread interrupted by another thread }
}
}
public class ThreadTest {
public static void main(String[] args) { MyThread thread1 = new MyThread("thread 1", 1000); MyThread thread2 = new MyThread("thread 2", 2000); MyThread thread3 = new MyThread("thread 3", 1500); thread1.start();
thread2.start();
thread3.start();
}
}
|
След стартиране на тази програмка се създават и стартират 3 нишки
от класа MyThread. Всяка от
тях в безкраен цикъл печата на конзолата името си и изчаква някакво предварително
зададено време между 1 и 2 секунди. Понеже трите нишки работят паралелно, се
получава резултат подобен на следния:
|
|
Освен чрез наследяване на класа java.lang.Thread в Java можем да създаваме нишки и по друг начин – чрез имплементиране на интерфейса java.lang.Runnable. Начинът на работа е почти същия. Създаваме клас, който имплементира Runnable и пишем в метода му run() логиката на нишката. След това по този клас създаваме обект от класа Thread и му извикваме start() метода за стартиране на нишката. Класът Thread си има специален конструктор, който приема обекти имплементиращи Runnable.
Подходът с интерфейса Runnable се препоръчва да се използва тогава, когато поради някаква причина не можем да наследим класа Thread. Например ако нашият клас е вече наследник на някой друг клас, понеже в Java няма множествено наследяване, ако искаме да го изпълняваме в отделна нишка, нямаме друг избор освен да имплементираме Runnable.
Досега разгледахме как можем да стартираме нова нишка.
Често пъти освен да стартираме на нишки се налага и да спираме изпълнението
на работещи нишки. Прекратяването на нишки има някои особености. То в никакъв
случай
не трябва да става насилствено чрез метода
stop() на класа Thread. Вместо това нишката трябва учтиво да бъде помолена да прекрати работата
си чрез извикване на метода й
interrupt().
Затова по време на работата си всеки thread трябва от време на време да проверява,
извиквайки метода isInterrupted(),
дали не е помолен да прекрати работата си.
Други интересни
методи на класа Thread са
setPriority(),
sleep()
и setDaemon(),
но за тях можем да прочетем повече документацията.
В предходната тема изяснихме какво е нишка (thread) и как се разработват многонишкови приложения с Java. В тази тема ще се запознаем с възможностите за синхронизация при достъп до общи ресурси при многонишковото програмиране.
Има много ситуации, в които няколко нишки едновременно осъществяват достъп до общ ресурс. Например в една банка може едновременно двама клиенти да поискат да внесат пари по една и съща сметка. Да предположим, че сметките са обекти от класа Account, а операциите върху тях се извършват от класа Bank:
class Account { private double mAmmount = 0;
void setAmmount(double aAmmount) { mAmmount = aAmmount;
}
double getAmmount() { return mAmmount; }
}
class Bank { public static void deposit(Account aAcc, double aSum) { double oldAmmount = aAcc.getAmmount(); double newAmmount = oldAmmount + aSum; aAcc.setAmmount(newAmmount);
}
}
|
Нека двамата клиенти се опитат едновременно да внесат съответно
100 и 500 лева в сметката acc,
която е празна. Това би могло да стане по следния начин:
Клиент 1: Bank.deposit(acc, 100);
Клиент 2: Bank.deposit(acc, 500);
Както се вижда от кода, алгоритъмът за внасяне на пари към сметка работи съвсем просто на следните три стъпки:
1) Прочита сумата от сметката.
2) Добавя сумата за внасяне към нея.
3) Записва новата сума в сметката.
Ако заявките за внасяне на пари от двамата клиента се изпълняват едновременно, може да се получи следният неприятен ефект:
1) Клиент 1 прочита сумата от сметката – 0 лева.
2) Клиент 2 прочита сумата от сметката – също 0 лева.
3) Клиент 1 прибавя към прочетената в стъпка 1) сума 100 лева и записва в сметката новата сума – 100 лева.
4) Клиент 2 прибавя към прочетената в стъпка 2) сума 500 лева и записва в сметката новата сума – 500 лева.
В резултат в сметката се получават 500 вместо 600 лева, а това за една банка това е абсолютно недопустимо. Натъкнахме се на класически синхронизационен проблем.
Когато няколко конкурентни нишки или няколко отделни програми се опитват едновременно да осъществят достъп до общ ресурс, често пъти се получават неприятни ефекти подобни на този с банката. Такива ефекти наричаме синхронизационни проблеми, а техниката за решаването им наричаме синхронизация.
Синхронизацията решава проблемите с конкурентния достъп до общи ресурси, като прави достъпа до тях последователен. Тя предизвиква подреждане на заявките в последователност по такъв начин, така че когато една заявка се изпълнява, всички останали я чакат и се изпълняват едва след като тя приключи. Този процес съвсем не е автоматичен и се задава от програмиста чрез средствата за синхронизация, които ни дава операционната система или платформата, за която разработваме софтуер.
В Java средствата за синхронизация са вградени в самия
език и са част от самата платформа. Запазената дума
synchronized предизвиква синхронизирано изпълнение на програмен
блок. Това означава, че две нишки не могат едновременно да изпълняват програмен
код този блок. Ако едната е започнала изпълнение на код от блока, другата ще
я изчака да завърши. Проблемът с банката можем да решим много просто, като заменим
декларацията на метода deposit(…) от горната програма
public static void deposit( Account aAcc, double aSum) |
с декларацията
|
|