Национална академия по разработка на софтуер



страница6/14
Дата25.07.2016
Размер2.68 Mb.
#6706
1   2   3   4   5   6   7   8   9   ...   14

1.4.TCP сокети


Както вече знаем от краткия преглед на Интернет протоколите, който направихме в началото, TCP сокетите представляват надежден двупосочен транспортен канал за данни между две приложения. Приложенията, които си комуникират през сокет, могат да се изпълняват на един и същ компютър или на различни компютри, свързани по между си чрез Интернет или друга TCP/IP мрежа. Тези приложения биват два вида – сървъри и клиенти. Клиентите се свързват към сървърите по IP адрес и номер на порт чрез класа java.net.Socket. Сървърите приемат клиенти чрез класа java.net.ServerSocket. При разработка на сървъри обикновено трябва да се съобразяваме с необходимостта от обслужване на много потребители едновременно и независимо един от друг. Най-често този проблем се решава с използване на нишки за всеки потребител. Нека първо разгледаме по-простия вариант – обслужване само на един клиент в даден момент.

Прост TCP сървър


Да разгледаме сорс-кода на едно просто сървърско приложение – DateServer:

DateServer.java

import java.util.Date;

import java.io.OutputStreamWriter;

import java.io.IOException;

import java.net.Socket;

import java.net.ServerSocket;
public class DateServer {

public static void main(String[] args) throws IOException {

ServerSocket serverSocket = new ServerSocket(2002);



while (true) {

Socket socket = serverSocket.accept();

OutputStreamWriter out =

new OutputStreamWriter(

socket.getOutputStream());

out.write(new Date()+ "\n");

out.close();

socket.close();

}

}



}

Този сървър отваря за слушане TCP порт 2002, след което в безкраен цикъл приема клиенти, изпраща им текущата дата и час и веднага след това затваря сокета с тях. Отварянето на сокет за слушане става като се създава обект от класа ServerSocket, в конструктора на който се задава номера на порта. Приемането на клиент се извършва от метода accept() на класа ServerSocket, при извикването на който текущата нишка блокира до пристигането на клиентска заявка, след което създава сокет връзка между сървъра и пристигналия клиент. От създадената сокет връзка сървърът взема изходния поток за изпращане на данни към клиента (чрез метода getOutputStream()) и изпраща в него текущата дата и час, записани на една текстова линия. Затварянето на изходния поток е важно. То предизвиква действителното изпращане на данните към клиента, понеже извиква метода flush() на изходния поток. Ако нито един от методите close() или flush() не бъде извикан, клиентът няма да получи нищо, защото изпратените данни ще останат в буфера на сокета и няма да отпътуват по него. Накрая, затварянето на сокета предизвиква прекъсване на комуникацията с клиента. Сървърът можем да изтестваме със стандартната програмка telnet, която е включена в повечето версии на Windows, Linux и Unix като напишем на конзолата следната команда:

telnet localhost 2002

Резултатът е получената от сървъра дата:

Wed Mar 03 20:31:05 EET 2004

Прост TCP клиент


Нека сега напишем клиент за нашия сървър – програма, която се свързва към него, взема датата и часа, които той връща и ги отпечатва на конзолата. Ето как изглежда една примерна такава програмка:

DateServerClient.java

import java.io.*;

import java.net.Socket;

public class DateServerClient {

public static void main(String[] args) throws IOException {

Socket socket = new Socket("localhost", 2002);

BufferedReader in = new BufferedReader(

new InputStreamReader(

socket.getInputStream() ) );

System.out.println("The date on the server is: " +

in.readLine());

socket.close();

}

}



Свързването към TCP сървър става чрез създаването на обект от класа java.net.Socket, като в конструктора му се задават IP адреса или името на сървъра и номера на порта. От свързания успешно сокет се взема входния поток и се прочита това, което сървърът изпраща. След приключване на работа сокетът се затваря. Ето какъв би могъл да е изхода от изпълнението на горната програмка, ако сървърът е стартиран на локалната машина и работи нормално:

The date on the server is: Wed Mar 03 20:34:12 EET 2004

Обработка на изключения


Ако стартираме клиентската програмка, но преди това спрем сървъра, ще получим малко по-различен резултат:

java.net.ConnectException: Connection refused: connect

at java.net.PlainSocketImpl.socketConnect(Native Method)

at java.net.PlainSocketImpl.doConnect(

PlainSocketImpl.java:305)

at java.net.PlainSocketImpl.connectToAddress(

PlainSocketImpl.java:171)

at java.net.PlainSocketImpl.connect(

PlainSocketImpl.java:158)

at java.net.Socket.connect(Socket.java:426)

at java.net.Socket.connect(Socket.java:376)

at java.net.Socket.(Socket.java:291)

at java.net.Socket.(Socket.java:119)

at DateServerClient.main(DateServerClient.java:6)

Exception in thread "main" Process terminated with exit code 1



Полученото изключение обяснява, че при опит за свързване към сървъра в конструктора на класа java.net.Socket се е получил проблем, защото съответният порт е бил затворен.

При работа със сокети и входно-изходни потоци понякога възникват грешки, в резултат на което се хвърлят изключения (exceptions). Затова е задължително и в двете програми, които дадохме за пример, кодът, който комуникира по сокет да бъде поставен или в try ... catch блок или методът, в който се използва входно-изходна комуникация, да бъде обявен като метод, който може да породи изключението java.io.IOException. Изключения възникват в най-разнообразни ситуации. Например ако сървърът не е пуснат и клиентът се опита да се свърже с него, ако връзката между клиента и сървъра се прекъсне при опит за писане в нея, ако сървърът се опита да слуша на зает вече порт, ако сървърът няма право да слуша на поискания порт, ако е изтекъл лимита от време за дадена блокираща операция и в много други случаи.


Четенето от сокет е блокираща операция


Една важна особеност при четенето от сокет е, че ако клиентът се опита да прочете данни от сървъра, а той не му изпрати нищо, клиентът ще блокира до затваряне на сокета, а при някои условия може да блокира дори за вечни времена (до спирането му). Затова сървърът и клиентът трябва да комуникират по предварително известен и за двамата протокол и да го спазват стриктно. Протоколът трябва да индикира по някакъв начин на клиента и на сървъра дали и кога да очакват получаването на още данни, както и колко данни да очакват.

Има няколко начина в един протокол да се укаже колко данни да очаква другата страна.

Единият начин е да се възприеме някой символ за край на изпращаните данни и отсрещната страна да чете от сокета докато не получи този символ. Най-често за такъв символ се възприема символът за край на ред. Много протоколи работят на принципа един текстов ред заявка, следван от един текстов ред отговор.

Другият начин е при изпращането на данни да се укаже първо дължината им в байтове, а след това да се изпратят толкова байта, колкото е указано. Така във всеки един момент отсрещната страна ще знае колко байта още й остават да прочете.


Писането във сокет също е блокираща операция


Трябва да се съобразяваме, че писането във сокет също е блокираща операция. Това означава, че ако се опитаме да изпратим някакви данни, не е гарантирано, че това няма да причини временно блокиране на текущата нишка. Обикновено при изпращането на някакво малко количество данни по сокет операцията не блокира, защото тези данни просто се прехвърлят в буфера за изпращане на изходния поток или в буфера за изпращане на сокета и реално не се изпращат докато не се извика flush() метода. При извикване на flush() метода също е възможно да не се получи блокиране и операцията да се изпълни без забавяне, но не винаги е така. Напълно е възможно при писане във сокет или при извикване на flush() да имаме забавяне от няколко секунди, дори и повече.

Какво става при прекъсване на връзката


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

Има два вида прекъсване на връзката:



  • нормален начин – чрез Socket.close() или чрез спиране на приложението – при него другата страна получава известяване, че сокетът е бил затворен;

  • внезапен начин – при прекъсване на физическата свързаност между клиента и сървъра – при него никоя от страните не получава известяване и сокетът може да си остане отворен за вечни времена (ако не се вземат мерки).

Нормално прекъсване на TCP връзка


Да разгледаме какво се случва при нормално прекъсване на отворена TCP връзка съответно когато правим опит за четене или писане в нея.

Да разгледаме първо какво става при затваряне на сокет по време на четене от него. Нека имаме сървър, който очаква данни от клиента и е блокирал по операцията четене от сокет. Ако клиентът в даден момент прекрати връзката, например чрез метода close() на класа Socket, сървърът ще получи от клиента специален пакет (с вдигнат флаг FIN), по който ще разбере, че връзката се прекратява. Същото ще се получи и ако клиентското приложение внезапно бъде спряно. В този случай операционната система ще затвори всички сокети, свързани със завършилия процес като изпрати пакети до отсрещната страна за известяване на затварянето им.

За сървъра затварянето на сокета, от който той чете, означава край на входния поток, свързан с този сокет. В резултат на това сървърът ще излезе от блокираното си състояние и ще получи индикация за достигнат край на поток от прекъснатата операция за четене.

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



java.net.SocketException: Connection reset

Нека сега разгледаме какво се случва при затваряне на сокет по време на писане в него. Нека имаме сървър, който от време на време изпраща към клиента някакви данни по отворен TCP сокет. Ако в даден момент клиентът затвори сокета, сървърът ще получи специален FIN пакет, който известява затварянето и ще си отбележи в обекта, свързан със съответния сокет, че той вече е затворен. При последващ опит за писане в изходния потока, свързан със затворения сокет, ще се получи изключението:

java.net.SocketException: Connection reset by peer: socket write error

Внезапно прекъсване на TCP връзка


При внезапното (аварийно) прекъсване на дадена TCP връзка нещата стоят по-различно. Да предположим, че клиент и сървър си говорят по отворен TCP сокет. В даден момент връзката между тях се разпада, например заради физическо прекъсване на кабела, който ги свързва. От този момент нататък по отворения сокет нито клиентът нито сървърът ще получи някакви данни, но сокетът няма да се затвори.

Ако някоя от страните се опита да изпрати нещо по този сокет, след известно време (някакъв timeout) ще получи изключението:



java.net.SocketException: Connection reset by peer: socket write error

Ако някоя от страните е блокирала по четене от този сокет, има проблем. Тя никога няма да разбере, че връзката се е разпаднала, защото няма да получи пакет, който да съобщава това (понеже линията е разрушена). Дори операционната система няма да разбере, че сокетът е невалиден. Ако напишем командата “netstat”, можем да видим, че дори и след няколко часа сокетът ще си стои в състояние „отворен”, въпреки че връзката реално е прекратена.

Описаният сценарий може да породи много сериозни проблеми за един TCP сървър. Ако от време на време клиенти се свързват към сървъра и връзката им се разпада, а сървърът никога не разбира за това, след няколко дни или седмици ресурсите на сървъра ще свършат и той ще спре да работи или ще започне да се държи неадекватно.


Как да установяваме прекъсната TCP връзка


Има няколко препоръки, които трябва да спазваме, ако не искаме да попаднем в ситуация, в която безкрайно дълго се опитваме да четем от невалиден (разрушен) TCP сокет, както в описания сценарий.

Едната препоръка е никога да не четем от сокет без ограничение откъм време. В класа java.net.Socket има метод setSoTimeout(int), с който може да се задава максималното време в милисекунди, за което една операция read() от InputStream-а, свързан с даден сокет трябва да приключи. Ако за зададеното време по сокета не пристигне нищо, се поражда изключението java.net.SocketTimeoutException и операцията четене се прекратява. По подразбиране стойността, зададена в setSoTimeout(int) е 0, което означава, че ограничение във времето няма.

Подходът със задаване на timeout при четене от сокет решава проблема с безкрайното чакане на данни от невалиден сокет, но не винаги е подходящ.

Понякога е възможно един сокет да е валиден, но по него да не преминават никакви данни в продължение на часове. Например, ако имаме сървър, който приема някаква информация от клиентите си от време на време, когато някой клиент реши да му изпрати нещо, е възможно с часове нищо да не бъде изпратено нито от клиента към сървъра, нито в обратната посока. Въпреки продължителната липса на активност, връзката не трябва да се прекъсва след изминаване на някакъв timeout (примерно 1, 5 или 10 минути). Сървърът, обаче иска ако се случи нещо с клиента и връзката с него се разпадне внезапно, да разбере за това и да освободи ресурсите, отделени за обслужването на този клиент.

Основният проблем е, че при липса на трафик по даден сокет няма начин да се провери дали връзката е активна или е разрушена.

Най-добрият начин да се справим с този проблем е да реализираме разширение на протокола, което осигурява възможност за изпращане на проверяващи пакети от сървъра към клиента от време на време, примерно на 2-3 минути. Така по сокета през определено време ще преминават някакви данни и ако връзката е прекъсната, ще настъпва изключение и сървърът ще разбира, че клиентът е недостъпен.

Този подход е най-надеждният и най-сигурният, но може да причини значителни усложнения при разработката на клиента и сървъра заради синхронизационни проблеми, защото проверяващите пакети трябва да се изпращат и обработват отделно и независимо от другите пакети.

Има и друг вариант да се справим с проблема. Той не изисква промяна на протокола, но и не е толкова надежден. TCP сокетите имат стандартна възможност да бъдат автоматично проверявани през определено време дали са свързани чрез специални keep-alive пакети, на които отсрещната страна е длъжна да отговаря. Тази възможност се поддържа вътрешно от TCP протокола и операционната система и ако бъде включена, при липса на отговори на тези keep-alive пакети за определено време (някакъв системен timeout, който обикновено е 2 часа), се счита, че сокетът е невалиден. Класът Socket в Java има метод setKeepAlive(boolean), с който се задава дали да бъде включена keep-alive опцията за даден сокет. По подразбиране тази опция е изключена. Проблемът на този подход е, че не знаем със сигурност колко е keep-alive timeout стойността и за различните платформи тя е различна. Обикновено стойността е няколко часа, което означава, че при сриване на връзката сървърът ще разбере за това не веднага, а едва след няколко часа.



Ако при четене от сокет, за който е зададена keep-alive опцията, се установи, че връзката се е разпаднала, в нишката, която е блокирала по операцията четене, се предизвиква изключението:

java.net.SocketException: Connection reset

Като правило, ако не искаме да попаднем в ситуация, в която безкрайно дълго се опитваме да четем от невалиден сокет, трябва или да имаме ограничение на максималното време за четене (timeout) или трябва да реализираме изпращането на проверяващи данни от време на време, или поне трябва да включваме keep-alive опцията на сокета, от който четем.

Обслужване на много потребители едновременно


Даденият по-горе пример за сървър обслужва клиентите си последователно един след друг. Ако двама клиенти едновременно дадат заявка, първият ще бъде обслужен веднага, а вторият едва след приключване на обслужването на първия. Тази стратегия работи, но само за прости сървъри, в които обслужването на клиент отнема много малко време. В повечето случаи обслужването на един клиент отнема известно време и останалите клиенти не могат да бъдат карани да го изчакват. Затова се налага сървърът да обслужва клиентите си едновременно и независимо един от друг. За реализация на такава стратегия в средата на Java най-често се използва многонишковият подход, при който за всеки клиент се създава отделна нишка. Това е препоръчвания начин за разработка на сървъри, предназначени да работят с повече от един клиент. Ако трябва да сме точни, от JDK 1.4 в Java се поддържат и асинхронни сокети, с които могат да се обработват едновременно много клиенти само с една нишка, но засега няма да разглеждаме този програмен модел.

Многопотребителски сървър-речник


Да си поставим за задача реализацията на прост сървър, който по зададена дума на английски език връща преводът й на български език, а за при непозната думи връща грешка. Сървърът трябва да може да обслужва много потребители едновременно и независимо един от друг, без да е необходимо някой от тях да чака докато сървърът обслужва останалите. За простота ще считаме, че думите и техните преводи са дадени като константа с ясната идея, че в една реална ситуация те трябва да се извличат от база данни или от някаква друга система. Ето как можем да реализираме нашият сървър-речник:

DictionaryServer.java

import java.io.*;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.Date;

public class DictionaryServer {

public static int LISTENING_PORT = 3333;

public static void main(String[] args) throws IOException {

ServerSocket serverSocket =



new ServerSocket(LISTENING_PORT);

System.out.println("Server started.");



while (true) {

Socket socket = serverSocket.accept();

DictionaryClientThread dictionaryClientThread =

new DictionaryClientThread(socket);

dictionaryClientThread.start();

}

}

}



class DictionaryClientThread extends Thread {

private int CLIENT_REQUEST_TIMEOUT = 15*60*1000; // 15 min.

private Socket mSocket;

private BufferedReader mSocketReader;

private PrintWriter mSocketWriter;

public DictionaryClientThread(Socket aSocket)

throws IOException {

mSocket = aSocket;

mSocket.setSoTimeout(CLIENT_REQUEST_TIMEOUT);

mSocketReader = new BufferedReader(



new InputStreamReader(mSocket.getInputStream()));

mSocketWriter = new PrintWriter(



new OutputStreamWriter(mSocket.getOutputStream()));

}

public void run() {

System.out.println(new Date().toString() + " : " +

"Accepted client : " + mSocket.getInetAddress() +

":" + mSocket.getPort());

try {

mSocketWriter.println("Dictionary server ready.");

mSocketWriter.flush();

while (!isInterrupted()) {

String word = mSocketReader.readLine();



if (word == null)

break; // Client closed the socket

String translation = getTranslation(word);

mSocketWriter.println(translation);

mSocketWriter.flush();

}

} catch (Exception ex) {



ex.printStackTrace();

}

System.out.println(new Date().toString() + " : " +



"Connection lost : " + mSocket.getInetAddress() +

":" + mSocket.getPort());

}

private String getTranslation(String aWord) {



if (aWord.equalsIgnoreCase("network")) {

return "мрежа";

} else if (aWord.equalsIgnoreCase("firewall")) {



return "защитна стена";

} else {



return "! непозната дума !";

}

}



}

Как работи сървърът-речник


Сървърът-речник е изключително прост. Той отваря за слушане сървърски сокет на порт 3333 и започва да слуша в цикъл за клиентски заявки идващи към този сокет. При приемане на клиент създава нишка, която да го обслужва, подава й създадения клиентски сокет и я стартира.

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

За клиентския сокет се задава timeout при четене 15 минути. Това се прави с цел да се прекъсват автоматично връзките на клиентите, които по някаква причина много дълго бездействат. Такива клиенти могат или да са изгубили по някаква причина връзката със сървъра или просто да са свързани, но да не са активни. И в двата случая е добре сървърът да ги премахва, за да не хаби излишни ресурси.

Забележете, че веднага след като изпратим нещо към сървъра извикваме flush() метода, за да осигурим реалното изпращане на данните по сокета. Ако не извикаме flush(), данните ще останат да чакат в буфера на класа PrintWriter и няма да отпътуват по сокета, все едно не са изпратени към потока. Тази особеност с буферирането е много важна при комуникация с потоци и винаги трябва да се съобразяваме с нея.


Клиент за сървъра-речник


Нека сега напише и клиент за нашия сървър речник. Всичко, което трябва да прави клиента е да се свърже със сървъра, да извлече от него поздравителното съобщение и след това постоянно да чете дума от конзолата, да я изпраща към сървъра за превод и да отпечатва превода получен от клиента. Ето една реализация:

DictionaryClient.java

import java.io.*;

import java.net.Socket;

public class DictionaryClient {

private static int SERVER_RESPONSE_TIMEOUT = 60*1000;

public static void main(String[] args) throws IOException {

Socket socket = new Socket("localhost", 3333);

socket.setSoTimeout(SERVER_RESPONSE_TIMEOUT);

BufferedReader socketReader = new BufferedReader(



new InputStreamReader(socket.getInputStream()) );

PrintWriter socketWriter =



new PrintWriter(socket.getOutputStream());

BufferedReader consoleReader = new BufferedReader(



new InputStreamReader(System.in) );

String welcomeMessage = socketReader.readLine();

System.out.println(welcomeMessage);

try {

while (true) {

String word = consoleReader.readLine();

socketWriter.println(word);

socketWriter.flush();

String translation = socketReader.readLine();

System.out.println(translation);

}

} finally {



socket.close();

}

}



}

Как работи клиентът за сървъра-речник


Всичко което прави клиентът е да отвори сокет към сървъра, да прочете от него поздравителното съобщение, след което в безкраен цикъл да чете дума от конзолата, да я изпраща към сървъра за превод, да прочита отговора на сървъра и да го отпечатва в конзолата. Забележете отново, че след изпращане на заявката към сървъра се извиква методът flush() на изходния поток. Ако този метод не се извика, програмата ще блокира, защото заявката няма да достигне сървъра. Ако ги няма ограниченията за максимално допустимо време за четене на сървъра и на клиента, програмата ще се опитва неограничено дълго време да прочете отговора на сървъра, а сървърът ще чака неограничено дълго време да получи заявка от клиента и така никой няма да дочака другия. В случая максималното допустимо време за чакане на отговор от сървъра се ограничава до 1 минута веднага след успешно свързване към сървъра.


Каталог: books -> inetjava
books -> В обятията на шамбала
books -> Книга се посвещава с благодарност на децата ми. Майка ми и жена ми ме научиха да бъда мъж
books -> Николай Слатински “Надеждата като лабиринт” София, Издателство “виденов & син”, 1993 год
books -> София, Издателство “Българска книжница”, 2004 год. Рецензенти доц д. ик н. Димитър Йончев, проф д-р Нина Дюлгерова Научен редактор проф д-р Петър Иванов
books -> Николай Слатински “Измерения на сигурността” София, Издателство “Парадигма”, 2000 год
books -> Книга 2 щастие и успех предисловие
books -> Превръщане на числа от една бройна система в друга
books -> Тантриското преобразяване


Сподели с приятели:
1   2   3   4   5   6   7   8   9   ...   14




©obuch.info 2024
отнасят до администрацията

    Начална страница