Какой класс необходимо использовать для открытия серверного сокета
Перейти к содержимому

Какой класс необходимо использовать для открытия серверного сокета

  • автор:

Использование сокетов в Android

Создано большое количество приложений как для Android, так и для других ОС, которые взаимодействуют друг с другом с помощью установления соединенией по сети. К таким приложениям относятся, например, мессенджеры социальных сетей WhatsApp, Viber. Как правило, для установления соединения между такими приложениями используются сокеты.

Сокет (socket) — это интерфейс, позволяющий связывать между собой программы различных устройств, находящихся в одной сети. Сокеты бывают двух типов: клиентский (Socket) и серверный (ServerSocket). Главное различие между ними связано с тем, что сервер «открывает» определенный порт на устройстве, «слушает» его и обрабатывает поступающие запросы, а клиент должен подключиться к этому серверу, зная его IP-адрес и порт. В Android сокеты для передачи данных используют по умолчанию протокол TCP/IP, важной особенностью которого является гарантированная доставка пакетов с данными от одного устройства до другого.

Особенности использования сокетов

Что важно знать при использовании сокетов в Android ?

  • соединения сокетов отключаются при переходе устройства в спящий режим;
  • чтобы не «рвать» соединение при наступлении спящего режима в устройстве можно использовать сервис;
  • для использования интернет-сети необходимо Android-приложению предоставить нужные права в манифесте.

Для определения прав в манифесте необходимо в файл AndroidManifest.xml добавить следующую строку :

Теперь android-приложения будет иметь доступ к сети.

Далее в статье рассмотрим пример клиент-серверного сокетного соединения с передачей сообщения. Функции клиента будет выполнять android-приложение. Серверное java-приложение выполним в IDE Eclipse с использованием пакета concurrent. В конце страницы можно скачать оба приложения.

Клиентский android-сокет

Интерфейс andriod-приложения представлен на следующем скриншоте. Форма приложения включает поле ввода текстового сообщения и кнопки установления соединения сервером, передачи сообщения и закрытия соединения.

Клиентское приложение создадим из двух классов : класс взаимодействия с серверным сокетом Connection и класс стандартной активности MainActivity.

Класс Connection

Класс взаимодействия с сервером Connection получает при создании (через конструктор) параметры подключения : host и port. Методы Connection вызываются из активности и выполняют следующие функции :

Метод Описание
openConnection Метод открытия сокета/соединения. Если сокет открыт, то он сначала закрывается.
closeConnection Метод закрытия сокета
sendData Метод отправки сообщения из активности.
finalize Метод освобождения ресурсов
Листинг Connection
import android.util.Log; import java.io.IOException; import java.net.Socket; public class Connection < private Socket mSocket = null; private String mHost = null; private int mPort = 0; public static final String LOG_TAG = "SOCKET"; public Connection() <>public Connection (final String host, final int port) < this.mHost = host; this.mPort = port; >// Метод открытия сокета public void openConnection() throws Exception < // Если сокет уже открыт, то он закрывается closeConnection(); try < // Создание сокета mSocket = new Socket(mHost, mPort); >catch (IOException e) < throw new Exception("Невозможно создать сокет: " + e.getMessage()); >> /** * Метод закрытия сокета */ public void closeConnection() < if (mSocket != null && !mSocket.isClosed()) < try < mSocket.close(); >catch (IOException e) < Log.e(LOG_TAG, "Ошибка при закрытии сокета :" + e.getMessage()); >finally < mSocket = null; >> mSocket = null; > /** * Метод отправки данных */ public void sendData(byte[] data) throws Exception < // Проверка открытия сокета if (mSocket == null || mSocket.isClosed()) < throw new Exception("Ошибка отправки данных. " + "Сокет не создан или закрыт"); >// Отправка данных try < mSocket.getOutputStream().write(data); mSocket.getOutputStream().flush(); >catch (IOException e) < throw new Exception("Ошибка отправки данных : " + e.getMessage()); >> @Override protected void finalize() throws Throwable < super.finalize(); closeConnection(); >>

Класс активности MainActivity

В активности MainActivity определены параметры сервера : host, port. Помните, что IP-адрес сервера для Вашего android-примера не может быть localhost (127.0.0.1), иначе Вы будете пытаться связаться с сервером внутри Andriod-системы. Кнопки интерфейса связаны с методами обращения к классу Connection. Кнопки отправки сообщения mBtnSend и закрытия соединения mBtnClose с сервером блокируются при старте приложения. После установления соединения с сервером доступ к кнопкам открывается.

Листинг активности
public class MainActivity extends AppCompatActivity < private Button mBtnOpen = null; private Button mBtnSend = null; private Button mBtnClose = null; private EditText mEdit = null; private Connection mConnect = null; private String HOST = "10.120.51.22"; private int PORT = 9876; private String LOG_TAG = "SOCKET"; @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtnOpen = (Button) findViewById(R.id.btn_open ); mBtnSend = (Button) findViewById(R.id.btn_send ); mBtnClose = (Button) findViewById(R.id.btn_close); mEdit = (EditText) findViewById(R.id.edText ); mBtnSend .setEnabled(false); mBtnClose.setEnabled(false); mBtnOpen.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onOpenClick(); >>); mBtnSend.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onSendClick(); >>); mBtnClose.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onCloseClick(); >>); > >
Методы управления сокетным соединением

Ниже представлены методы обработки событий, связанных с нажатием кнопок интерфейса. Обратите внимание, что подключение к серверу выполняется в отдельном потоке, а открытие доступа к кнопкам в основном потоке, для чего вызывается метод runOnUiThread. Для отправки сообщения серверу также создается отдельный поток.

private void onOpenClick() < // Создание подключения mConnect = new Connection(HOST, PORT); // Открытие сокета в отдельном потоке new Thread(new Runnable() < @Override public void run() < try < mConnect.openConnection(); // Разблокирование кнопок в UI потоке runOnUiThread(new Runnable() < @Override public void run() < mBtnSend.setEnabled(true); mBtnClose.setEnabled(true); >>); Log.d(LOG_TAG, "Соединение установлено"); Log.d(LOG_TAG, "(mConnect != null) = " + (mConnect != null)); > catch (Exception e) < Log.e(LOG_TAG, e.getMessage()); mConnect = null; >> >).start(); > private void onSendClick() < if (mConnect == null) < Log.d(LOG_TAG, "Соединение не установлено"); >else < Log.d(LOG_TAG, "Отправка сообщения"); new Thread(new Runnable() < @Override public void run() < try < String text; text = mEdit.getText().toString(); if (text.trim().length() == 0) text = "Test message"; // отправляем сообщение mConnect.sendData(text.getBytes()); >catch (Exception e) < Log.e(LOG_TAG, e.getMessage()); >> >).start(); > > private void onCloseClick() < // Закрытие соединения mConnect.closeConnection(); // Блокирование кнопок mBtnSend .setEnabled(false); mBtnClose.setEnabled(false); Log.d(LOG_TAG, "Соединение закрыто"); >

Серверное приложение

Серверное приложение включает 2 класса : Server и ConnectionWorker. Серверный класс Server будет выполнять обработку взаимодействия с клиентом с использованием ConnectionWorker в отдельном потоке. Конструктор ConnectionWorker в качестве параметра получает объект типа Socket для чтения сообщений клиента из потока сокета.

Листинг ConnectionWorker

ConnectionWorker получает входной поток inputStream из клиентского сокета и читает сообщение. Если сообщение отсутствует, т.е. количество прочитанных байт равно -1, то это значит, что соединение разорвано, то клиентский сокет закрывается. При закрытии клиентского соединения входной поток сокета также закрывается.

public class ConnectionWorker implements Runnable < // Сокет для взаимодействия с клиентом private Socket clientSocket = null; // Входной поток получения данных из сокета private InputStream inputStream = null; public ConnectionWorker(Socket socket) < clientSocket = socket; >@Override public void run() < try < // Определение входного потока inputStream = clientSocket.getInputStream(); >catch (IOException e) < System.err.println("Can't get input stream"); >// Буфер для чтения информации byte[] data = new byte[1024*4]; while(true) < try < /* * Получение информации : * count - количество полученных байт */ int count; count=inputStream.read(data,0,data.length); if (count >0) < String msg=new String(data, 0, count); // Вывод в консоль сообщения System.out.println(msg); >else if (count == -1 ) < // Если count=-1, то поток прерван System.out.println("socket is closed"); clientSocket.close(); break; >> catch (IOException e) < System.err.println(e.getMessage()); >> System.out.println("ConnectionWorker stoped"); > >

Серверный класс

Серверный класс Server создадим с использованием многопоточного пакета util.concurrent. На странице описания сетевого пакета java.net и серверного ServerSocket был приведен пример серверного модуля с использованием обычного потока Thread, при работе с которым необходимо решать задачу его остановки : cтарый метод Thread.stop объявлен Deprecated и предан строжайшей анафеме, а безопасная инструкция Thread.interrupt безопасна, к сожалению, потому, что ровным счетом ничего не делает (отправляет сообщение потоку : «Пожалуйста, остановись»). Услышит ли данный призыв поток остается под вопросом – все зависит от разаработчика.

Чтобы иметь возможность остановить сервер «снаружи» в серверный класс Server включим 2 внутренних реализующих интерфейс Callable класса : CallableDelay и CallableServer. Класс CallableDelay будет функционировать определенное время, по истечении которого завершит свою работу и остановит 2-ой серверный поток взаимодействия с клиентами. В данном примере CallableDelay используется только для демонстрации остановки потока, организуемого пакетом util.concurrent.

Листинг CallableDelay

CallableDelay организует цикл с задержками. После завершения последнего цикла cycle поток завершает цикл, останавливает вторую задачу futureTask[1] и закрывает сокет. В консоль выводится соответствующее сообщение.

class CallableDelay implements Callable  < private int cycle; public CallableDelay(int cycle) < this.cycle = cycle; >@Override public String call() throws Exception < while (cycle >0) < System.out.println("" + cycle); Thread.sleep(1000); cycle--; >// Останов 2-ой задачи futureTask[1].cancel(true); // Закрытие серверного сокета serverSoket.close(); System.out.println("Thread '" + Thread.currentThread().getName() + "' stoped" ); // Наименование потока, выполняющего задачу return "" + Thread.currentThread().getName(); > >
Листинг CallableServer

Конструктор CallableServer в качестве параметров получает значение открываемого порта для подключения клиентов. При старте (метод call) создается серверный сокет ServerSocket и поток переходит в режим ожидания соединения с клиентом. Остановить поток можно вызовом метода stopTask, либо завершением «задачи» типа FutureTask с данным потоком.

При подключении клиента метод serverSoket.accept возвращает сокет, который используется для создания объекта ConnectionWorker и его запуска в отдельном потоке. А сервер (поток) переходит к ожиданию следующего подключения.

В случае закрытия сокета (завершение внешней задачи FutureTask с данным потоком) будет вызвано исключение Exception, где выполняется проверка закрытия сокета; при положительном ответе основной цикл прерывается и поток завершает свою работу.

class CallableServer implements Callable  < private int port; private boolean started; public CallableServer(int port) < this.port = port; this.started = true; >public void stopTask() < started = false; >@Override public String call() throws Exception < // Создание серверного сокета serverSoket = new ServerSocket(port); System.out.println("Server start on port : " + port); // Цикл ожидания соединений клиентов с сервером while(started) < ConnectionWorker worker = null; try < // Ожидание соединения с клиентом worker = new ConnectionWorker( serverSoket.accept()); /* * Обработка соединения выполняется * в отдельном потоке */ Thread t = new Thread(worker); t.start(); >catch (Exception e) < System.err.println("Connection error : " + e.getMessage()); // Завершение цикла. if (serverSoket.isClosed()) break; >> System.out.println("Thread '" + Thread.currentThread().getName() + "' stoped" ); futureTask[1].cancel(true); // Наименование потока, выполняющего задачу return "" + Thread.currentThread().getName(); > >
Листинг серверного класса Server

Cерверный класс Server создает два потоковых объекта (callable1, callable2), формирует из них две задачи futureTask и запускает задачи на выполнение методом execute исполнителя executor. После этого контролируется завершение выполнение обоих задач методом isTasksDone. При завершении выполнения обеих задач завершается также и цикл работы executor’а.

Два внутренних описанных выше класса (CallableDelay, CallableServer) не включены в листинг.

public class Server < // Открываемый сервером порт для клиентов private final int SERVER_PORT = 9876; // Сокет соединения с клиентами private ServerSocket serverSoket = null; // Поток контроля времени работы сервера private CallableDelay callable1 = null; // Поток соединения с клиентами private CallableServer callable2 = null; // Список задач private FutureTask[] futureTask = null; // Исполнитель задач private ExecutorService executor = null; private Server() < // 1-ый поток контролирует задержку работы сервера callable1 = new CallableDelay (50); // 2-йй поток открывает соединение callable2 = new CallableServer(SERVER_PORT); // Создание задач futureTask = new FutureTask[2]; futureTask[0] = new FutureTask(callable1); futureTask[1] = new FutureTask(callable2); // Выполнение задач executor = Executors.newFixedThreadPool(2); executor.execute(futureTask[0]); executor.execute(futureTask[1]); // Цикл работы executor'а while (true) < if (isTasksDone()) < // Завершение работы executor'а executor.shutdown(); System.out.println("\nexecutor shutdown"); break; >> > //----------------------------------------------------- private boolean isTasksDone() < return futureTask[0].isDone() && futureTask[1].isDone(); >//----------------------------------------------------- public static void main(String[] args) < new Server(); >>

Скачать пример

Архив примера android-socket.zip (98 Кб) включает два проекта : клиентский android (client-android/p12socket) и серверный server-eclipse.

19 способов сделать сокет-сервер на Python. Эволюционный подход. Часть 3. Первый подход к асинхронности

В прошлый раз мы рассмотрели, что такое синхронное программирование, и с какими проблемами с ним сталкивается разработчик. На примере простого сервера с блокирующими сокетами мы увидели, что в синхронно выполняющейся программе все инструкции выполняются строго по очереди, и если встречается системный вызов ввода-вывода, то он может полностью остановить выполнение программы на довольно продолжительное время — пока не завершится. Весь этот период процессор простаивает в ожидании, хотя мог бы выполнять другие задачи, которых накапливается немало. В результате сервер одновременно может обрабатывать только одно подключение. Чтобы перейти ко второму, предыдущее должно быть закрыто.

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

Но, к счастью, существует и третий вариант — кооперативная многозадачность с помощью системного вызова select и его аналогов (poll, epoll и других). Он позволяет мультеплексировать несколько задач в одном потоке выполнения и в сущности является обычной синхронной программой. А потому никаких дополнительных трат процессорного времени и времени разработчиков не требуется.

I. Вытесняющая многозадачность:

1. Процессы (выполняются в одной ОС).

2. Потоки (выполняются в одном процессе).

II. Кооперативная многозадачность:

3. Сопрограммы (выполняются в одном потоке).

Кооперативная многозадачность для сокетов

Суть данного способа в том, что когда синхронная программа достигает запроса системы ввода-вывода, мы не ждем, когда данный запрос завершится, а спрашиваем у системы: а какие есть другие уже завершившиеся запросы? Системный вызов select как раз и возвращает список готовых к обработке дескрипторов ввода-вывода (файлов, сокетов и других). Поэтому вместо пустого ожидания мы просто переключаемся на выполнение другой подпрограммы.

Со временем поверх вызова select программисты для собственного удобства сооружали все более сложные системы. Так постепенно они дошли до колбек-функций (callback), а потом и до сопрограмм (coroutine) и асинхронного (asynchronous) программирования. Всю эту эволюцию нам и предстоит проследить в данном материале. Ведь чтобы писать грамотные программы, нужно хорошо себе представлять, как все там внутри устроено, и как получается вся эта магия.

Но перед тем рассмотрим еще более простой способ, который позволяет не применять select. Начнем разбор с использования неблокирующих сокетов.

Неблокирующие сокеты

Как известно, когда у сокетов вызывается метод accept() или recv() , выполнение программы останавливается до тех пор, пока в систему ввода-вывода не придут необходимые данные из сети (пока не подсоединится новый клиент — в случае accept() , или не придут новые пакеты с данными для recv() ). А все потому, что вызовы этих методов блокирующие.

Но, к счастью, сокеты допускают переключение в неблокирующий режим работы. Для этого нужно перед первым использованием сокета вызвать у него метод setblocking(False) . Тогда ранее блокирующие методы будут сразу или возвращать данные, если они есть в наличии, или генерировать исключение BlockingIOError , если их нет. Поэтому мы в бесконечном цикле можем опрашивать все соединения по очереди на предмет готовности данных. Если возникает исключение BlockingIOError , мы просто переходим к следующему сокету:

import socket HOST, PORT = "", 50007 connections = [] if __name__ == "__main__": with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serv_sock: serv_sock.bind((HOST, PORT)) serv_sock.listen(1) serv_sock.setblocking(False) # Important! while True: try: # print("Try to accept a new connection. ") sock, addr = serv_sock.accept() sock.setblocking(False) print("Connected by", addr) connections.append((sock, addr)) except BlockingIOError: # print("No connections are waiting to be accepted") pass for sock, addr in connections.copy(): print("Try to receive data from:", sock, addr) try: data = sock.recv(1024) except ConnectionError: print(f"Client suddenly closed while receiving from ") connections.remove((sock, addr)) sock.close() continue except BlockingIOError: # No data received continue print(f"Received: from: ") if not data: connections.remove((sock, addr)) sock.close() print("Disconnected by", addr) continue data = data.upper() print(f"Send: to: ") try: sock.sendall(data) except ConnectionError: print(f"Client suddenly closed, cannot send to ") connections.remove((sock, addr)) sock.close() continue

Не трудно догадаться, что такое решение будет полностью загружать процессор, как и всякий другой бесконечный цикл, вне зависимости от того, есть там что обрабатывать или нет. При этом чаще всего данные будут поступать медленнее, чем будет проходить полный цикл опроса всех клиентских сокетов. А значит, большая часть кода будет исполняться впустую — sock.recv() → BlockingIOError → sock.recv() → BlockingIOError и т.д.

Также, если первый сокет в списке готов, а мы сейчас опрашиваем только второй, то придется пройтись по всему списку неготовых сокетов, пока мы дойдем до первого — реально готового соединения.

Я уже не говорю про затраты времени на обработку исключений, которые являются неотъемлемой составляющей данного способа. Даже если один из сокетов был бы всегда готов, когда бы его не спросили, то прежде, чем к нему попасть, пришлось бы поймать и обработать множество исключений BlockingIOError от других сокетов.

Вот бы каким-нибудь образом опрашивать только те сокеты, про которых достоверно известно, что данные для них уже пришли и готовы для обработки, чтобы не перебирать все сокеты подряд. Это сэкономило бы нам немало процессорного времени. Специально для таких случаев в ОС и создали системный вызов select.

Системный вызов select

Чтобы не вызывать sock.accept() и sock.recv() наудачу в надежде, что они вернут данные, был придуман системный вызов select. Он всегда возвращает список только готовых сокетов, а потому, когда мы вызываем sock.accept() или sock.recv() мы точно знаем, что выполнение программы не остановится, и результат будет возвращен сразу.

Метод не заблокирует выполнение, даже если setblocking(True) , и не возбудит исключение BlockingIOError , если setblocking(False) . Поэтому нам вообще не важно, блокирующие или неблокирующие сокеты мы используем.

О системных вызовах

Что такое select? Select, сокеты, процессы, потоки, получение текущего времени, вывод на экран и многое другое — все это части операционной системы (ОС). ОС предоставляет своим прикладным программам API (программный интерфейс) для вызова этих функций. Он представляет собой совокупность системных вызовов (system calls, или syscalls). Если программе нужно что-то помимо процессора или оперативной памяти (послать данные по сети или отобразить что-то на экране), то она всегда за этим должна обращаться к ОС. Да и многие сложные операции по управлению памятью и процессором также требуют обращения к системным вызовам.

Чтобы лучше себе представлять как они работают, приведем один пример на языке ассемблера и проследим, как он эволюционирует до вызова функции в Python.

Каждому системному вызову присвоен уникальный код, по которому их можно отличать друг от друга. Также у них может быть несколько параметров. Код обычно помещается в аккумулятор процессора, а для параметров отводятся другие заранее условленные регистры. После того, как все регистры установлены осуществляется вызов прерывания 80h ( int 0x80 ), инструкции sysenter (для архитектуры i386) или инструкции syscall (архитектура x86-64). Ассемблерный код для x86-64 выглядит примерно так (подробнее):

 mov rax,function_number mov rdi,param_1 ; если есть mov rsi,param_2 ; если есть mov rdx,param_3 ; если есть mov r10,param_4 ; если есть mov r8,param_5 ; если есть mov r9,param_6 ; если есть syscall ; вызов! (регистры RCX и R11 будут уничтожены)

Набор кодов системных вызовов и их параметров (каким регистрам процессора они соответствуют и какие значения могут принимать) вместе образуют программный интерфейс (API) операционной системы. Чтобы не запоминать всех этих деталей, которые к тому же могут отличаться для разных архитектур и систем, для каждого системного вызова пишется отдельная функция на C. C является кроссплатформенным языком. А это значит, что на нем можно написать программу один раз, а потом компилировать ее под разные платформы. Для каждой платформы функция для системного вызова может иметь разную внутреннюю реализацию, но сигнатура функции, ее внешний интерфейс будет для всех система одинакова.

Набор таких функций объединяются в библиотеку, которая становится частью ОС. Так появляется стандартная библиотека C, служащая программным интерфейсом к ОС. А так как Python и большинство его модулей написаны на C, то весь питоновский код в конце концов заканчивается вызовом си-шных функций. В том числе и функций стандартной библиотеки. Поэтому, когда мы в Python вызываем sock.recv() или select() , то это значит, что будет вызвана соответствующая функция стандартной библиотеки C. Она, в свою очередь, заполнит нужные регистры подходящими значениями и вызовет соответствующую инструкцию процессора для данной архитектуры ( int 0x80 , sysenter , syscall ). Результаты вызова будут помещены операционной системой также в регистры.

Вот как работает механизм системных вызовов. Теперь вы имеете общее представление о том, что происходит при вызове из Python функции select() . Теперь рассмотрим подробнее саму эту функцию.

Системный вызов select — это такой вызов, которые принимает в качестве параметров список дескрипторов потоков ввода-вывода, которые нужно проанализировать, а на выходе возвращает список тех из них, которые готовы к работе.

Каждый дескриптор — это уникальное целое число, которое возвращает система при открытии файла, сокета или другого потока ввода-вывода. С помощью этого числа потоки можно отличать друг от друга. Класс socket в Python — это просто удобная обертка вокруг такого числа-дескриптора.

Если выразиться проще, то передавая в select() список всех сокетов, мы на выходе получим список тех из них, которые готовы к обработке. Вот и все. Вот как изменится наша программа, если мы добавим в нее select() :

import select import socket def handle(sock, addr): try: data = sock.recv(1024) # Should be ready except ConnectionError: print(f"Client suddenly closed while receiving") return False print(f"Received from: ") if not data: print("Disconnected by", addr) return False data = data.upper() print(f"Send: to: ") try: sock.send(data) # Hope it won't block except ConnectionError: print(f"Client suddenly closed, cannot send") return False return True HOST, PORT = "", 50007 if __name__ == "__main__": with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serv_sock: serv_sock.bind((HOST, PORT)) serv_sock.listen(1) # serv_sock.setblocking(False) inputs = [serv_sock] outputs = [] while True: print("Waiting for connections or data. ") readable, writeable, exceptional = select.select(inputs, outputs, inputs) for sock in readable: if sock == serv_sock: sock, addr = serv_sock.accept() # Should be ready print("Connected by", addr) # sock.setblocking(False) inputs.append(sock) else: addr = sock.getpeername() if not handle(sock, addr): # Disconnected inputs.remove(sock) if sock in outputs: outputs.remove(sock) sock.close()

Когда выполнение программы достигается вызова select, программа останавливается до тех пор, пока один из сокетов в списке не получит данные и не перейдет в состояние готовности. Тогда select() возвратит этот сокет и он поступит на обработку. Если этот сокет серверный, то вызывается метод accept() , если клиентский — то recv() . Обработка клиентского сокета была вынесена для большей ясности и чистоты кода в функцию handle() . Данный способ еще допускает написание всего сервера без единой функции, но позже без функций будет уже не обойтись.

Для простоты и понятности примеров мы не будем проверять сокеты на готовность к записи (writeable). Выходной сетевой буфер обычно всегда свободен, и любой здоровый сокет вернется как готовый к записи. Хотя в реальном production-коде, конечно же, необходимо предусмотреть и такой случай.

Таким образом, множество блокирующих вызовов сокетов мы заменили лишь одним блокирующим вызовом — select. В результате этого процессорное время равномерно распределяется между всеми соединениями. Это и называется многозадачность — многозадачность через мультиплексирование. При этом все выполняется в одном потоке и абсолютно синхронным образом. Поэтому никакой дополнительной синхронизации, как в случае разных потоков, не требуется.

Использование колбек-функций

На разных системах существуют и другие альтернативы select — poll, epoll, devpoll, kqueue — использование которых для тех или иных случаев может быть выгоднее, чем select. Но API и способ вызова у них у всех может отличаться. Поэтому, чтобы унифицировать процесс получения готовых к работе дескрипторов ввода-вывода в Python был создан модуль selectors, в котором содержатся разные имплементации единого интерфейса селекторов.

Каждый класс селектора имеет три основных метода. С помощью методов register() и unregister() можно добавлять или удалять дескриптор (в том числе и сокет) из списка активных, а метод select() выбирает из этого списка массив готовых к обработке:

if __name__ == "__main__": with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serv_sock: serv_sock.bind((HOST, PORT)) serv_sock.listen(1) # serv_sock.setblocking(False) sel = selectors.DefaultSelector() sel.register(serv_sock, selectors.EVENT_READ) while True: print("Waiting for connections or data. ") events = sel.select() for key, mask in events: sock = key.fileobj if sock == serv_sock: sock, addr = serv_sock.accept() # Should be ready print("Connected by", addr) # sock.setblocking(False) sel.register(sock, selectors.EVENT_READ) else: addr = sock.getpeername() if not handle(sock, addr): # Disconnected sel.unregister(sock) sock.close() continue

На первый взгляд все то же самое, и мы не получили ни одного преимущества от рефакторинга кроме того, что можно перейти к другому методу выбора (poll, epoll) заменой всего одной строчки: sel = selectors.DefaultSelector() .

Но это не совсем так. Дело в том, что у метода register() есть еще третий параметр — data — который передается вместе с сокетом в объекте key. И сюда можно поместить любое значение, в том числе и функцию. Так мы, регистрируя сокет, можем тут же задать и колбек, который его будет обрабатывать:

def on_accept_ready(sel, serv_sock, mask): sock, addr = serv_sock.accept() # Should be ready print("Connected by", addr) # sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, on_read_ready) def on_read_ready(sel, sock, mask): addr = sock.getpeername() if not handle(sock, addr): sel.unregister(sock) sock.close() if __name__ == "__main__": with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serv_sock: serv_sock.bind((HOST, PORT)) serv_sock.listen(1) # sock.setblocking(False) sel = selectors.DefaultSelector() sel.register(serv_sock, selectors.EVENT_READ, on_accept_ready) while True: print("Waiting for connections or data. ") events = sel.select() for key, mask in events: callback = key.data callback(sel, key.fileobj, mask)

Тут мы уже вынуждены вынести код обработки нового соединения из общего цикла в отдельную функцию: on_read_ready() . В результате код основного цикла работы приложения становится полностью стандартным. Как и код создания серверного сокета. Их можно объединить в функцию вроде run_server(host, port, on_connect, on_read) , вынести ее в библиотеку и использовать в других проектах. Весь кастомный код приложения выносится в колбеки, а в run_server() остается только стандартная функциональность, которая всегда одинакова:

def on_connect(sock, addr): print("Connected by", addr) def on_disconnect(sock, addr): print("Disconnected by", addr) def run_server(host, port, on_connect, on_read, on_disconnect): def on_accept_ready(sel, serv_sock, mask): sock, addr = serv_sock.accept() # Should be ready # sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, on_read_ready) if on_connect: on_connect(sock, addr) def on_read_ready(sel, sock, mask): addr = sock.getpeername() if not on_read or not on_read(sock, addr): if on_disconnect: on_disconnect(sock, addr) sel.unregister(sock) sock.close() with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as serv_sock: serv_sock.bind((host, port)) serv_sock.listen(1) # sock.setblocking(False) sel = selectors.DefaultSelector() sel.register(serv_sock, selectors.EVENT_READ, on_accept_ready) while True: print("Waiting for connections or data. ") events = sel.select() for key, mask in events: callback = key.data callback(sel, key.fileobj, mask) HOST, PORT = "", 50007 if __name__ == "__main__": run_server(HOST, PORT, on_connect, handle, on_disconnect) 

Вот так нами был создан простой асинхронный серверный движок на колбеках. Данный движок позволяет нам вообще абстрагироваться от того, как реализована асинхронность. Мы просто пишем функции-обработчики для событий: connect, read, disconnect.

Хотя данная система — уже большой прогресс, по сравнению с предыдущими версиями, но она все же не лишена серьезных недостатков. Во-первых, из колбеков нельзя вызывать блокирующие функции, кроме единственного вызова recv() в on_read, иначе все выполнение программы остановится.

Во-вторых, если мы начинаем использовать колбеки, то рано или поздно мы начинаем использовать вложенные колбеки, а для них еще вложенные и так далее. Рано или поздно это приводит к такому явлению, как callback hell. Это когда код становится таким запутанным из-за многочисленных вложений, что проще застрелится, чем его поддерживать.

Обе проблемы решаются переходом к сопрограммам (coroutines). Именно сопрограммы позволяют писать асинхронный код так, как будто он синхронный. То есть без колбеков и вложенных функций. Но прежде, чем начать, нелишним будет узнать, как в Python появились сопрограммы, и из чего они эволюционировали. Чем мы и займемся в следующий раз.

Сетевое программирование с Сокетами и Каналами

Одна из сильных сторон Java заключается в безпроблеммной сетевой работе. Дизайнеры сетевой библиотеки Java сделали ее достаточно простой для чтения и записи файлов, за исключением случая, когда «файл» существует на удаленной машине и удаленная машина может решать что ей делать с информацией, которую вы запрашиваете или посылаете. Насколько это возможно, низлежащие детали сетевого взаимодействия были абстрагированы и о них заботится ядро JVM и локальный пакет установки Java. Программная модель, которую вы используете для такого файла, фактически, это обертка сетевого соединения («сокет») с объектом потока, так что в конечном счете вы используете те же вызовы методов, которые вы используете для других потоков. Кроме того, встроенная многопоточность Java исключительно удобна, когда вы имеете дело с такой сетевой возможностью, как обработка множества соединений одновременно.

Этот раздел является вводным в сетевое взаимедействие Java с использованием легких в понимании примеров.

Идентификация машины

Конечно, для того, чтобы передать данные с одной машины на другую необходимо убедиться, что вы подсоединились к определенной машине в сети. Ранние варианты сетей были удовлетворены предоставлением уникальных имен машинам внутри локальной сети. Однако, Java работает в пределах Internet, что требует способа для уникальной идентификации машины из любой точки всего мира. Это выполняется с помощью IP (Internet Protocol) адреса, который может существовать в двух формах:

  1. Привычная форма DNS (Domain Name System). Мое доменное имя — bruceeckel.com, и если у меня есть компьютер, называемый Opus в моем домене, его доменное имя должно быть Opus.bruceeckel.com. Это в точности имя такого рода, которое вы используете при отсылке электронной почты людям, и часто он встроен в адрес World Wide Web.
  2. Альтернативный вариант: вы можете использовать форму из четырех чисел, разделенных точками, например 123.255.28.120.

В обоих случаях IP адрес представляется как 32-х битное число [1] (так как каждое из четырех чисел не может превышать 255), и вы можете получить специальный Java объект для представления этого числа из любой из перечисленных выше форм, используя статический метод InetAddress.getByName( ), который определен в java.net. Результатом будет объект типа InetAddress, который вы можете использовать для создания «сокета», как вы это увидите далее.

В качестве простейшего примера использования InetAddress.getByName() рассмотрим, что произойдет при использовании коммутируемого доступа (dial-up Internet service provider (ISP)). При каждом дозвоне вам назначается временный IP адрес. Но пока вы соединены, ваш IP адрес имеет такую же силу, как и другие IP адреса в Internet. Если кто-либо соединится с вашей машиной использую ваш IP адрес, то он может соединится с Web сервером или FTP сервером, который запущен на вашей машине. Конечно, ему необходимо знать ваш IP адрес, а так как при каждом дозвоне вам назначается новый адрес, то как вы можете определеть какой у вас адрес?

Приведенная ниже программа использует InetAddress.getByName( ) для воспроизведения вашего IP адреса. Для ее использования вы должны знать имя вашего компьютера. Под управлением Windows 95/98 перейдите в «Settings», «Control Panel», «Network» и выберите закладку «Identification». Содержимое в поле «Computer name» является той строкой, которую необходимо поместить в командную строку.

//: c15:WhoAmI.java
// Нахождение вашего сетевого адреса, когда
// вы соединены с Internet’ом.
// Должно быть установлено соединение с Internet
//
import java.net.*;

public class WhoAmI public static void main ( String [] args ) throws Exception if ( args.length != 1 ) System.err.println ( «Usage: WhoAmI MachineName» ) ;
System.exit ( 1 ) ;
>
InetAddress a = InetAddress.getByName ( args [ 0 ]) ;
System.out.println ( a ) ;
>
> // /:~

В моем случае, машина называется «peppy». Так что, когда я соединюсь с моим провайдером и запущу программу:

java WhoAmI peppy

Я получу назад сообщение такого типа (конечно же, адрес отличается при каждом новом соединении):

peppy/ 199.190.87.75

Если я скажу этот адрес моему другу и у меня будет запущен Web Сервер на моем компьютере, он сможет соединится с сервером, перейдя по ссылке http://199.190.87.75 (только до тех пор, пока я остаюсь соединенным во время одной сессии). Иногда это может быть ручным способом распределения информации кому-то еще или использоваться для тестирования конфигурации Web сайта перед размещением его на «реальном» сервере.

Серверы и клиенты

Основное назначение сети состоит в том, чтобы позволить двум машинам соединиться и пообщаться друг с другом. Так как две машины могут найти друг друга, они могут провести милую, двусторонню беседу. Но как они могут найти друг друга? Это похоже на поиск потерянных в парке аттракционов: одна машина стоит в одном месте и слушает, пока другая машина скажет: «Эй, ты где?».

Машина, которая стоит в одном месте, называется сервером, а машина, которая ищет, называется клиентом. Это различие важно лишь до тех пор, пока клиент пробует соединится с сервером. Как только они соединятся, они становятся двумя сторонами коммуникационного процесса и более не имеет значения, какая машина принимала роль сервера, а какая принимала роль клиента.

Таким образом, работа сервера состоит в прослушивании соединения, она выполняется с помощью специального объекта, который вы создаете. Работа клиента состоит в попытке создать соединение с сервером, и это выполняется с помощью специального клиентского объекта, который вы создаете. Как только соединение установлено, вы увидите, что и клиентская, и серверная сторона соединения магическим образом превращается потоковый объект ввода/вывода, таким образом вы можете трактовать соединение, как будто вы читаете и пишете файл. Таким образом, после установки соединения, вы просто используете хорошо знакомые команды ввода/вывода из главы 11. Это одна из прекраснейших особенностей работы по сети в Java.

Тестирование программ без сети

По многим причинам, вы можете не иметь клиентской машины, серверной машины и сети, доступных для тестирования ваших программ. Вы можете выполнять упражнения в обстановке классной комнаты, или, возможно, вы пишите программы, которые еще не достаточно стабильны и не могут быть выложены в сеть. Создатели Internet Protocol учли эту возможность и создали специальный адрес, называемый localhost, IP адрес «локальной заглушки (local loopback)» для тестирования без использования сети. Общий способ для получения такого адреса в Java такой:

InetAddress addr = InetAddress.getByName ( null ) ;

Если вы передадите в getByName( ) значение null, метод по умолчанию будет использовать localhost. InetAddress является тем, что вы используете для указания определенной машины, и вы должны произвести его прежде, чем вы можете двинуться далее. Вы не можете манипулировать содержимым InetAddress (но вы можете напечатать его, как это будет показано в следующем примере). Единственный способ, которым вы можете создать InetArddress, это через один из перегруженных статических методов класса getByName( ) (который является тем, что вы уже использовали), getAllByName(), или getLocalHost( ).

Вы также можете получить адрес локальной заглушки, передав строку localhost:

InetAddress.getByName ( «localhost» ) ;

(предполагается, что «localhost» сконфигурирован в таблице «hosts» на вашей машине), или используя цифровую четырехзначную форму для имени, представляющем заглушку:

InetAddress.getByName ( «127.0.0.1» ) ;

Все три формы произовдят одинаковый результат.

Порт: уникальное место внутри машины

IP адреса не достаточно для уникальной идентификации сервера, так как многие сервера могут существовать на одной машине. Каждая IP машина также содержит порты, и когда вы устанавливаете клиента или сервер, вы должны выбрать порт, через который и клиент, и сервер согласны соединиться.

Порт — это не физическое расположение в машине, а программная абстракция (в основном для целей учета). Клиентская программа знает, как соединится к машине через ее IP адрес, но как она может присоединится к определенной службе (потенциально, к одной из многих на этой машине)? Таким образом номер порта стал вторым уровнем адресации. Идея состоит в том, что при запросе определенного порта вы запрашиваете службу, ассоциированную с этим номером порта. Служба времени — простейший пример службы. Обычно каждая служба ассоциируется с уникальным номером порта на определенной серверной машине. Клиент должен предварительно знать, на каком порту запущена нужная ему служба.

Системные службы зарезервировали использование портов с номерам от 1 до 1024, так что вы не можете использовать этот или любой другой порт, про который вы знаете, что он задействован. Первым выбором, например, в этой книге будет порт 8080 (в память многоуважаемого 8-битного процессора 8080 от Intel в моем первом компьютере, CP/M машине).

Сокеты

Сокет — это программная абстракция, используемая для представления «терминалов» соединения между двумя машинами. Для данного соединения есть сокет на каждой машине, и вы можете представить гипотетический «кабель», включенный в сокет. Конечно, физическое оборудование и каблирование между машинами полностью неизвестно. Главное назначение абстракции состоит в том, что мы не должны знать более того, что нам необходимо.

В Java вы создаете сокет, чтобы создать соединение с другой машиной, затем вы получаете InputStream и OutputStream (или, с соответствующими конверторами, Reader и Writer) из сокета, чтобы получить возможность трактовать соединение, как объект потока ввода/вывода. Существует два класса сокетов, основанных на потоках: ServerSocket, который использует сервер для «прослушивания» входящих соединения, и Socket, который использует клиент для инициализации соединения. Как только клиент создаст сокетное соединение, ServerSocket возвратит (посредством метода accept( )) соответствующий Socket, через который может происходить коммуникация на стороне сервера. После этого вы общаетесь в соединении через Socket с Socket’ом и вы трактуете оба конца одинаково, посколько они и являются одним и тем же. На этой стадии вы используете методы getInputStream( ) и getOutputStream( ) для получения соответствующих объектов InputStream’а и outputStream’а для каждого сокета. Они должны быть обернуты внутрь буферных и форматирующих классов точно так же, как и другие объекты потоков, описанные в Главе 11.

Использование термина ServerSocket может показаться другим примером сбивающей с толку схемы именования в библиотеках Java. Вы можете подумать, что ServerSocket лучше было бы назвать «ServerConnector» или как-то подругому, без слова «Socket» внутри. Вы также можете подумать, что ServerSocket и Socket должны оба наследоваться от какого-то общего базового класса. На самом деле, два калсса имеют некоторые общие методы, но не настолько, чтобы дать им общий базовый класс. Вместо этого, работа ServerSocket’а состоит в том, чтобы ждать, пока некоторая машина не присоединится к нему, а затем он возвращает реальный Socket. Вот почему кажется, что ServerSocket назван немножко неправильно, так как его работа состоит не в том, чтобы быть реальным сокетом, а в том, чтобы создавать объект Socket’а, когда кто-то присоединяется к нему.

Однако, ServerSocket создает физический «сервер» или слушающий сокет на хост-машине. Этот сокет слушает входящие соединения, а затем возвращает «связанный» сокет (с определенными локальной и удаленной конечными точками) посредством метода accept( ). Сбивающая часть состоит в том, что оба эти сокета (слушающий и связанный) ассоциированы с одним и тем же серверным сокетом. Слушающий сокет может принять только новый запрос на соединение, а не пакет данных. Так что, не смотря на то, что ServerSocket имеет мало смыла с точки зрения программирования, в нем много смысла «физически».

Когда вы создаете ServerSocket, вы даете ему только номер порта. Вы не даете ему IP адрес, поскольку он уже есть на той машине, на которой он представлен. Однако когда вы создаете Socket, вы должны передать ему и IP адрес, и номер порта, к которому вы хотите присоединиться. (Однако Socket, который возвращается из метода ServerSocket.accept( ) уже содержит всю эту информацию.)

Простейший сервер и клиент

Этот пример покажет простейшее использование серверного и клиентского сокета. Все, что делает сервер, это ожидает соединения, затем использует сокет, полученный при соединении, для создания InputStream’а и OutputStream’а. Они конвертируются в Reader и Writer, которые оборачиваются в BufferedReader и PrintWriter. После этого все, что будет прочитано из BufferedReader’а будет переправлено в PrintWriter, пока не будет получена строка «END», означающая, что пришло время закрыть соединение.

Клиент создает соединение с сервером, затем создает OutputStream и создает некоторую обертку, как и в сервере. Строки текста посылаются через полученный PrintWriter. Клиент также создает InputStream (опять таки, с соответствующей конвертацией и оберткой), чтобы слушать, что говорит сервер (который, в данном случае, просто отсылает слова назад).

И сервер, и клиент используют одинаковый номер порта, а клиент использует адрес локальной заглушки для соединения с сервером на этой же самой машине, так что вы не можете провести тест по сети. (Для некоторых конфигураций вам может понадобиться сетевое соединения для работы программы даже, если вы не используете сетевую коммуникацию.)

//: c15:JabberServer.java
// Очень простой сервер, который просто отсылает
// назад все, что посылает клиент.
//
import java.io.*;

public class JabberServer // Выбираем порт вне пределов 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try // Блокирует до тех пор, пока не возникнет соединение:
Socket socket = s.accept () ;
try System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается из буфера PrintWriter’ом
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
while ( true ) String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
// Всегда закрываем два сокета.
>
finally System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally s.close () ;
>
>
> // /:~

Вы можете видеть, что для ServerSocket’а необходим только номер порта, а не IP адрес (так как он запускается на локальной машине!). Когда вы вызываете accept( ), метод блокирует выполнение до тех пор, пока клиент не попробует подсоединится к серверу. То есть, сервер ожидает соединения, но другой процесс может выполнятся (смотрите Главу 14). Когда соединение установлено, метод accept( ) возвращает объект Socket, представляющий это соединение.

Здесь тщательно обработана отвественность за очистку сокета. Если конструктор ServerSocket завершится неудачей, программа просто звершится (обратите внимание, что мы должны предположить, что конструктор ServerSocket не оставляет никаких открытых сокетов, если он зваершается неудачей). По этой причине main( ) выбрасывает IOException, так что в блоке try нет необходимости. Если конструктор ServerSocket завершится успешно, то все вызовы методов должны быть помещены в блок try-finally, чтобы убедиться, что блок не будет покинут ни при каких условиях и ServerSocket будет правильно закрыт.

Аналогичная логика используется для сокета, возвращаемого из метода accept( ). Если метод accept( ) завершится неудачей, то мы должны предположить, что сокет не существует и не удерживает никаких ресурсов, так что он не нуждается в очистке. Однако если он закончится успешно, то следующие выражения должны быть помещены в блок try-finally, чтобы при каких-либо ошибках все равно произошла очистка. Позаботится об этом необходимо, потому что сокеты используют важные ресурсы, не относящиеся к памяти, так что вы должны быть прилежны и очищать их (так как в Java нет деструкторов, чтобы сделать это за вас).

И ServerSocket и Socket, производимый методом accept( ), печатаются в System.out. Это означает, что автоматически вызывается их метод toString( ). Вот что он выдаст:

ServerSocket [ addr= 0.0.0.0 ,PORT= 0 ,localport= 8080 ]
Socket [ addr= 127.0.0.1 ,PORT= 1077 ,localport= 8080 ]

Короче говоря, вы увидите как это соответствует тому, что делает клиент.

Следующая часть программы выглядит, как открытие файла для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream’а и OutputStream’а конвертируются в объекты Reader’а и Writer’а с помощью «классов-конвертеров» InputStreamReader и OutputStreamreader, соответственно. Вы можете также использовать классы из Java 1.0 InputStream и OutoutStream напрямую, но, с точки зрения вывода, есть явное преимущество в использовании этого подхода. Оно проявляется в PrintWriter’е, который имеет перегруженный конструктор, принимающий в качестве второго аргумента флаг типа boolean, указывающий, нужно ли автоматическое выталкивание буфера вывода в конце каждого выражения println( ) (но не print( )). Каждый раз, когда вы записываете в вывод, буфер вывода должен выталкиваться, чтобы информация проходила по сети. Выталкивание важно для этого конкретного примера, поскольку клиент и сервер ожидают строку от другой стороны, прежде, чем приступят к ее обработке. Если выталкивание буфера не произойдет, информация не будет помещена в сеть до тех пор, пока буфер не заполнится, что может привести к многочисленным проблемам в этом примере.

Когда пишите сетевую программу, вам необходимо быть осторожным при использовании автоматического выталкивания буфера. При каждом выталкивании буфера пакеты должны создаваться и отправляться. В данном случае это именно то, что нам надо, так как если пакет, содержащий строку, не будет отослан, то общение между сервером и клиентом остановится. Другими словами, конец строки является концом сообщения. Но во многих случаях, сообщения не ограничиваются строками, так что будет более эффективным использовать автоматическое выталкивание буфера, поэтому позвольте встроенному механизму буфферизации построить и отослать пакет. В таком случае могут быть посланы пакеты большего размера и процесс обработки пойдет быстрее.

Обратите внимание, что фактически все открытые вами потоки, буфферезированы. В конце этой главы есть упражнение, которое покажет вам, что происходит, если вы не буфферезируете потоки (вещи становятся медленнее).

В бесконечном цикле while происходит чтение строк из входного BufferedReader’а и запись информации в System.out и в выходной PrintWriter. Обратите внимание, что вход и выход могут быть любыми потоками, так случилось, что они связаны с сетью.

Когда клиент посылает строку, содержащую «END», программа прекращает цикл и закрывает сокет.

//: c15:JabberClient.java
// Очень простой клиент, который просто посылает
// строки на сервер и читает строки,
// посылаемые сервером.
//
import java.net.*;

public class JabberClient public static void main ( String [] args ) throws IOException // Передаем null в getByName(), получая
// специальный IP адрес «локальной заглушки»
// для тестирования на машине без сети:
InetAddress addr = InetAddress.getByName ( null ) ;
// Альтернативно, вы можете использовать
// адрес или имя:
// InetAddress addr =
// InetAddress.getByName(«127.0.0.1»);
// InetAddress addr =
// InetAddress.getByName(«localhost»);
System.out.println ( «addr = » + addr ) ;
Socket socket = new Socket ( addr, JabberServer.PORT ) ;
// Помещаем все в блок try-finally, чтобы
// быть уверенным, что сокет закроется:
try System.out.println ( «socket = » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Вывод автоматически Output быталкивается PrintWriter’ом.
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
for ( int i = 0 ; i < 10 ; i++ ) out.println ( "howdy " + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
finally System.out.println ( «closing. » ) ;
socket.close () ;
>
>
> // /:~

В main( ) вы можете видеть все три способа получение InetAddress IP адреса локальной заглушки: с помощью null, localhost или путем явного указания зарезервированного адреса 127.0.0.1, если вы хотите соединится с машиной по сети, вы замените это IP адресом машины. Когда печатается InetAddress (с помощью автоматического вызова метода toString( )), то получается результат:

При передачи в getByName( ) значения null, он по умолчанию ищет localhos и затем производит специальныйы адрес 127.0.0.1.

Обратите внимание, что Socket создается при указании и InetAddress’а, и номера порта. Чтобы понять, что это значит, когда будете печатать один из объектов Socket помните, что Интернет соединение уникально определяется четырьмя параметрами: клиентским хостом, клиентским номером порта, серверным хостом и серверным номером порта. Когда запускается сервер, он получает назначаемый порт (8080) на localhost (127.0.0.1). Когда запускается клиент, он располагается на следующем доступном порту на своей машине, 1077 — в данном случае, который так же оказался на той же самой машине (127.0.0.1), что и сервер. Теперь, чтобы передать данные между клиентом и сервером, каждая сторона знает, куда посылать их. Поэтому, в процессе соединения с «известным» сервером клиент посылает «обратный адрес», чтобы сервер знал, куда посылать данные. Вот что вы видите среди выводимого стороной сервера:

Socket [ addr= 127.0.0.1 ,port= 1077 ,localport= 8080 ]

Это означает, что сервер просто принимает соединение с адреса 127.0.0.1 и порта 1077 во время прослушивания локального порта (8080). На клиентской стороне:

Socket [ addr=localhost/ 127.0.0.1 ,PORT= 8080 ,localport= 1077 ]

Это значит, что клиент установил соединение с адресом 127.0.0.1 по порту 8080, используя локальный порт 1077.

Вы заметите, что при каждом повторном запуске клиента номер локального порта увеличивается. Он начинается с 1025 (первый после зарезервированного блока портов) и будет увеличиваться до тех пор, пока вы не перезапустите машину, в таком случае он снова начнется с 1025. (На машинах под управлением UNIX, как только будет достигнут верхний предел диапазона сокетов, номер будет возвращен снова к наименьшему доступному номеру.)

Как только объект Socket будет создан, процесс перейдет к BufferedReader и PrintWriter, как мы это уже видели в сервере (опять таки, в обоих случаях вы начинаете с Socket’а). В данном случае, клиент инициирует обмен путем посылки строки «howdy», за которой следует число. Обратите внимание, что буфер должен опять выталкиваться (что происходит автоматически из-за второго аргумента в конструкторе PrintWriter’а). Если буфер не будет выталкиваться, процесс обмена повиснет, поскольку начальное «howdy» никогда не будет послана (буфер недостаточно заполнен, чтобы отсылка произошла автоматически). Каждая строка, посылаемая назад сервером, записывается в System.out, чтобы проверить, что все работает корректно. Для завершения обмена посылается ранее оговоренный «END». Если клиент просто разорвет соединение, то сервер выбросит исключение.

Вы можете видеть, что аналогичные меры приняты, чтобы быть уверенным в том, что сетевые ресурсы, представляемые сокетом, будут правильно очищены. Для этого используется блок try-finally.

Сокет производит «посвященную» связь, которая остается постоянной до тех пор, пока не будет явного рассоединения. (Посвященная связь может быть рассоединена неявно, если одна из сторон, или посредническая связь соединения рушатся.) Это означает, что два партнера замкнуты в коммуникации и соединение постоянно открыто. Это выглядит, как логический подход к сети, но это вносит дополнительную нагрузку на сеть. Позднее в этой главе вы увидите отличный подход к сетевому взаимодействию, при котором соединение является временным.

Обслуживание множества клиентов

JabberServer работает, но он может обработать только одного клиента одновременно. В обычных серверах вы захотите, чтобы была возможность иметь дело со многими клиентами одновременно. Ответом является многопоточность, и в языках, которые не поддерживают многопоточность напрямую, это означает что вы встретите все возможные трудности. В Главе 14 вы видели, что многопоточность в Java проста насколько это возможно, учитывая это, можно сказать, что многопоточность весьма сложная тема. Поскольку нити (потоки) в Java достаточно прамолинейны, то создание сервера, который обрабатывает несколько клиентов, относительно простое заняте.

Основная схема состоит в создании единственного ServerSocket’а на сервере и вызове метода accept( ) для ожидания новых соединений. Когда accept( ) возвращается, вы получаете результирующий сокет и используете его для создания новой нити (потока), работа которой будет состоять в ослуживании определенного клиента. Затем вы вызовите метод accept( ) снова, чтобы подождать нового клиента.

В следующем коде сервера вы можете видеть, что он очень похож на пример JabberServer.java, за исключением того, что все операции по обслуживанию определенного клиента былы помещены внутрь отдельного thread-класса:

//: c15:MultiJabberServer.java
// Сервер, который использует многопоточность
// для обработки любого числа клиентов.
//
import java.io.*;

class ServeOneJabber extends Thread private Socket socket;
private BufferedReader in;
private PrintWriter out;

public ServeOneJabber ( Socket s ) throws IOException socket = s;
in = new BufferedReader ( new InputStreamReader ( socket.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter ( socket
.getOutputStream ())) , true ) ;
// Если любой из вышеприведенных вызовов приведет к
// возникновению исключения, то вызывающий отвечает за
// закрытие сокета. В противном случае, нить
// закроет его.
start () ; // вызываем run()
>

public void run () try while ( true ) String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
System.out.println ( «closing. » ) ;
>
catch ( IOException e ) System.err.println ( «IO Exception» ) ;
>
finally try socket.close () ;
>
catch ( IOException e ) System.err.println ( «Socket not closed» ) ;
>
>
>
>

public class MultiJabberServer static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Server Started» ) ;
try while ( true ) // Блокируется до возникновения нового соединения:
Socket socket = s.accept () ;
try new ServeOneJabber ( socket ) ;
>
catch ( IOException e ) // Если завершится неудачей, закрывается сокет,
// в противном случае, нить закроет его:
socket.close () ;
>
>
>
finally s.close () ;
>
>
> // /:~

Нить ServeOneJabber принимает объект Socket’а, который производится методом accept( ) в main( ) при каждом новом соединении с клиентом. Затем, как и прежде, с помощью Socket, создается BufferedReader и PrintWriter с возможностью автоматического выталкивания буфера. И наконец, вызывается специальный метод нити start( ). Здесь выполняются те же действия, что и в предыдущем примере: читается что-то из сокета и затем отсылается обратно до тех пор, пока не будет прочитан специальный сигнал «END».

Ответственность за очистку сокета должна быть, опять таки, внимательно спланирована. В этом случае, сокет создается вне ServeOneJabber, так что ответственность может быть совместная. Если конструктор ServeOneJabber завершится неудачей, он просто выбросит исключение тому, кто его вызвал, и кто должен очистить нить. Но если конструктор завершился успешно, то объект ServeOneJabber принимает ответственность за очистку нити на себя, в своем методе run( ).

Обратите внимание на упрощенность MultiJabberServer. Как и прежде создается ServerSocket и вызывается метод accept( ), чтобы позволить новое соединение. Но в это время возвращаемое значение метода accept( ) (сокет) передается в конструктор для ServeOneJabber, который создает новую нить для обработки этого соединения. Когда соединение завершиется, нить просто умирает.

Если создание ServerSocket’а проваливается, то из метода main( ), как и прежде, выбрасывается исключение. Но если создание завершается успешно, внешний блок try-finally гарантирует очистку. Внутренний try-catch гарантирует только от сбоев в конструкторе ServeOneJabber. Если конструктор завершится успешно, то нить ServeOneJabber закроет соответствующий сокет.

Для проверки этого сервера, который реально обрабатывает несколько клиентов, приведенная ниже программа создает несколько клиентов (используя нити), которые соединяются с одним и тем же сервером. Максимальное допустимое число нитей определяется переменной final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Клиент, который проверяет MultiJabberServer,
// запуская несколько клиентов.
//
import java.net.*;

class JabberClientThread extends Thread private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0 ;
private int id = counter++;
private static int threadcount = 0 ;

public static int threadCount () return threadcount;
>

public JabberClientThread ( InetAddress addr ) System.out.println ( «Making client » + id ) ;
threadcount++;
try socket = new Socket ( addr, MultiJabberServer.PORT ) ;
>
catch ( IOException e ) System.err.println ( «Socket failed» ) ;
// Если создание сокета провалилось,
// ничего ненужно чистить.
>
try in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) , true ) ;
start () ;
>
catch ( IOException e ) // Сокет должен быть закрыт при любой
// ошибке, кроме ошибки конструктора сокета:
try socket.close () ;
>
catch ( IOException e2 ) System.err.println ( «Socket not closed» ) ;
>
>
// В противном случае сокет будет закрыт
// в методе run() нити.
>

public void run () try for ( int i = 0 ; i < 25 ; i++ ) out.println ( "Client " + id + ": " + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
catch ( IOException e ) System.err.println ( «IO Exception» ) ;
>
finally // Всегда закрывает:
try socket.close () ;
>
catch ( IOException e ) System.err.println ( «Socket not closed» ) ;
>
threadcount—; // Завершаем эту нить
>
>
>

public class MultiJabberClient static final int MAX_THREADS = 40 ;

public static void main ( String [] args ) throws IOException,
InterruptedException InetAddress addr = InetAddress.getByName ( null ) ;
while ( true ) if ( JabberClientThread.threadCount () < MAX_THREADS )
new JabberClientThread ( addr ) ;
Thread.currentThread () .sleep ( 100 ) ;
>
>
> // /:~

Конструктор JabberClientThread принимает InetAddress и использует его для открытия сокета. Вероятно, вы заметили шаблон: сокет всегда используется для создания определенного рода объектов Reader’а и Writer’а (или InputStream и/или OutputStream), которые являются тем единственным путем, которым может быть использован сокет. (Вы можете, конечно, написать класс или два для автоматизации этого процесса вместо набора этого текста, если вас это беспокоит.) Далее, start( ) выполняет инициализацию нити и запуск run( ). Здесь сообщение посылается на сервер, а информация с сервера отображается на экране. Однако, нить имеет ограниченноен время жизни и, в конечном счете, завершается. Обратите внимание, что сокет очищается, если конструктор завершился неудачей после создания сокета, но перед тем, как конструктор завершится. В противном случае, ответственность за вызов close( ) для сокета ложиться на метод run( ).

Threadcount хранит информацию о том, сколько в настоящее время существует объектов JabberClientThread. Эта переменная инкрементируется, как часть конструктора и декрементируется при выходе из метода run( ) (что означает, что нить умерла). В методе MultiJabberClient.main( ) вы можете видеть, что количество нитей проверяется, и если их много, то нить более не создается. Затем метод засыпает. Таким образом, некоторые нити, в конечном счете, умрут, и другие будут созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы увидеть, когда ваша конкретная система почувствует затруднения со множеством соединений.

Дейтаграммы

Пример, который вы недавно видели, использует Transmission Control Protocol (TCP, также известный, как сокет, основанный на потоках), который предназначен для наибольшей надежности и гарантии, что данные будут доставлены. Он позволяет передавать повторно потерянные данные, он обеспечивает множественные пути через различные маршрутизаторы в случае, если один из них отвалится, а байты будут доставлены в том порядке, в котором они посланы. Весь этот контроль и надежность добавляют накладные расходы: TCP сильно перегружен.

Существует второй потокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакет будет доставлен и не гарантирует, что пакеты достигнут точки назначения в том же порядке, в котором они были отправлены. Он называется «ненадежным протоколом» (TCP является «надежным протоколом»), что звучит плохо, но так как он намного быстрее, он может быть полезнее. Существуют приложения, такие как аудио сигнал, в которых не критично, если несколько пакетов потеряются здесь или там, а скорость жизненно необходима. Или например сервер времени, для которого реально не имеет значения, если одно из сообщений будет потеряно. Также, некоторые приложения могут быть способны отправлять UDP сообщения к серверу и затем считать, если нет ответа в разумный период времени, что сообщения были потеряны.

Обычно вы будете выполнять ваше прямое сетевое программирование с помощью TCP, и только иногда вы будете использовать UDP. Есть более общее толкование UDP, включая пример, в первой редакции этой книги (доступра на CR-ROM’е, сопровождающем это книгу или может быть свободно загружено с www.BruceEckel.com).

Использование URL’ов из апплета

Для апплета есть возможность стать причиной отображения любого URL с помощью Web броузера, в котором запущен апплет. Вы можете сделать это с помощью следующей строки:

getAppletContext () .showDocument ( u ) ;

в которой u является объектом типа URL. Вот простой пример, который перенаправляет вас на другую страницу. Хотя вы просто перенаправляете на HTML страницу, вы можете также перенаправить на вывод, который дает CGI программа.

public class ShowHTML extends JApplet JButton send = new JButton ( «Go» ) ;
JLabel l = new JLabel () ;

public void init () Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
send.addActionListener ( new Al ()) ;
cp.add ( send ) ;
cp.add ( l ) ;
>

class Al implements ActionListener public void actionPerformed ( ActionEvent ae ) try // Это может быть CGI программа вместо
// HTML страницы.
URL u = new URL ( getDocumentBase () , «FetcherFrame.html» ) ;
// Отображается вывод URL с помощью
// Web броузера, как обычная страница:
getAppletContext () .showDocument ( u ) ;
>
catch ( Exception e ) l.setText ( e.toString ()) ;
>
>
>

public static void main ( String [] args ) Console.run ( new ShowHTML () , 100 , 50 ) ;
>
> // /:~

Красота класса URL состоит в том, что он отлично защищает вас. Вы можете соединится с Web серверами без знания многого из того, что происходит за занавесом.

Чтение файла с сервера

Вариация приведенной выше программы, читающей файл, расположенный на сервере. В этом случае файл указывается клиентом:

public class Fetcher extends JApplet JButton fetchIt = new JButton ( «Fetch the Data» ) ;
JTextField f = new JTextField ( «Fetcher.java» , 20 ) ;
JTextArea t = new JTextArea ( 10 , 40 ) ;

public void init () Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
fetchIt.addActionListener ( new FetchL ()) ;
cp.add ( new JScrollPane ( t )) ;
cp.add ( f ) ;
cp.add ( fetchIt ) ;
>

public class FetchL implements ActionListener public void actionPerformed ( ActionEvent e ) try URL url = new URL ( getDocumentBase () , f.getText ()) ;
t.setText ( url + «n» ) ;
InputStream is = url.openStream () ;
BufferedReader in = new BufferedReader (
new InputStreamReader ( is )) ;
String line;
while (( line = in.readLine ()) != null )
t.append ( line + «n» ) ;
>
catch ( Exception ex ) t.append ( ex.toString ()) ;
>
>
>

public static void main ( String [] args ) Console.run ( new Fetcher () , 500 , 300 ) ;
>
> // /:~

Создание объекта URL похоже на предыдущий пример — getDocumentBase( ) является начальной точкой, как и прежде, но в то же время, имя файла читается из JTextField. Как только объект URL создан, его строковая версия помещается в JTextArea, так что вы можем видеть, как он выглядит. Затем из URL’а получается InputStream, который в данном случае может просто производить поток символов из файла. После конвертации в Reader и буферизации, каждая строка читается и добавляется в JTextArea. Обратите внимание, что JTextArea помещается внутрь JScrollPane, так что скроллирование обрабатывается автоматически.

Мультиплексирование, Основанное на Переключении в JDK 1.4

Когда вы читаете из сокета или пишете в него, вам нужно сделать передачу данных рациональной. Давайте рассмотрим сначала операцию записи. Когда вы пишите данные на уровне приложения (TCP или UDP сокет), вы пишите данные в рабочий буфер системы. Эти данные, в конечном счете, формируют (TCP или UDP) пакеты, которые необходимо передать на машину назначения по сети. Когда вы пишите в сокет и, если в буфере нет достаточно доступного места, запись может блокироваться. Если вы читаете из сокета и нет достаточного количества информации для чтения из буфера операционной системы, куда попадают данные после получения из сети, чтение будет блокировано. Если есть нить (поток) для операции чтения или записи, эта нить не может делать ничего и может стать причиной снижения произовдительности вашей программы. До появления JDK 1.4 не было способа вывести такую нить из заблокированного состояния. С помощью каналов вы можете выполнить асинхронную операцию закрытия на канале и нить, блокированная на этом канале примет AsynchronousCloseException.

Асинхронный ввод-вывод в Java достигается тем же способом, который дает вызов метода select( ) в UNIX подобных системах. Вы можете дать список дескрипторов (чтения или записи) в функцию select( ) и она отследит этот дескриптор на возникновение некоторых событий. Для дескриптора, представляющего сокет, из которого вы читаете, данные в буфере операционной системы для этого буфера представляются событием. Для дескрипторов, представляющих сокеты, в которые вы пишите, наличие места для записи во внутреннем буфере операционной системы для этого сокета представляется событием. Поэтому вызов метода select( ) исследует различные дескрипторы для проверки событий.

Что, если вы просто читаете и пишите в дескриптор когда бы вы не захотели? Select может обрабатывать множество дескрипторов, что позволит вам мониторить множество сокетов. Рассмотрим пример чат-сервера, когда сервер имеет соединения с различными клиентами. Тип данных, достигающих сервера, перемежается. Сервер предназначен для чтения данных из сокета и отображения их в GUI, то есть для показа каждому клиенту — чтобы достич этого, вы читаете данные от каджого клиента и пишите эти данные всем остальным клиентам. Например 5 клиентов: 1, 2, 3, 4 и 5. Если сервер запрограммирован на выполнение чтения от 1 и записи в 2, 3, 4 и 5, затем происходит чтения от 2 и запись в 1, 3, 4, 5 и так далее, то может так случиться, что пока нить сервера заблокирована на чтении одного из клиентских сокетов, могут появиться данные на других сокетах. Одно из решений состоит в том, чтобы создавать различные нити для кадого клиента (до JDK1.4). Но это не масштабируемое решение. Вместо этого вы можете иметь селектор, основанный на механизме, следящем за всеми клиентскими сокетами. Он знает какой сокет имеет данные для чтения без блокирования. Но если единственная нить выполняет эту работу (выбор и запись каждому клиенту) он не будет хорошо откликаться. Таким образом в таких ситуациях одна нить мониторит сокеты на чтение, выбирает сокет, из которого можно осуществить чтение, и делегирует остальную ответственность (запись другим клиентам) другой нити (нитям) или пулу нитей.

Этот шаблон называется шаблоном реактора, когда события отсоединяются от действия, ассоциированного с событиями (Pattern Oriented Software Architecture — Doug Schmidt).

В JDK 1.4 вы создаете канал, регестрируете объект Селектора в канале, который (объект) будет следить за событиями в канале. Многие каналы регестрируют один и тот же объект Селектора. Единственная нить, которая вызывает Selector.select(), наблюдает множество каналов. Каждый из классов ServerSocket, Socket и DatagramSocket имеют метод getChannel( ), но он возвращает null за исключением того случая, когда канал создается с помощью вызова метода open( ) (DatagramChannel.open( ), SocketChannel.open( ), ServerSocketChannel.open( )). Вам необходимо ассоциировать сокет с этим каналом.

Вы мультиплексируете несколько каналов (то есть сокеты), используя Селектор. Статический вызов Selector.select( ) блокирует выполнение до возникновения события в одном из каналов. Существует так же и не блокирующая версия этого метода, которая принимает количество милисекунд для засыпания или блокирования до того момента, когда вызов метода завершится.

ByteBuffer используется для копирования данных из канала и в канал. ByteBuffer является потоком октетов и вы декодируете этот поток, как символы. Со стороны клиента в MultiJabberClient.java это выполняется путем использования классов Writer’а и OutputStreamWriter’а. Эти классы конвертируют символы в поток байтов.

Приведенная ниже программа NonBlockingIO.java объясняет, как вы можете использовать Селектор и Канал для выполнения мультиплексирования. Эта программа требует запущенного Сервера. Она может стать причиной исключения на сервере, но ее назначение не в коммуникации с сервером, а в том, чтобы показать, как работает select( ).

//: TIEJ:X1:NonBlockingIO.java
// Сокет и Селектор сконфигурированы для не блокированного
// Соединения с JabberServer.java
//
import java.net.*;
import java.nio.channels.*;
import java.util.*;
import java.io.*;

/**
* Цель: Показать как использовать селектор. Нет чтения/записи, просто
* показывается готовность к совершению операции.
*
* Алгоритм: -> Создаем селектор. -> Создаем канал -> Связываем сокет,
* ассоциированный с каналом, с -> Конфигурируем канал, как
* не блокирующий -> Регестрируем канал в селекторе. -> Вызываем метод select( ),
* чтобы он блокировал выполнение до тех пор, пока канал не будет готов. (как
* это предполагается методом select(long timeout) -> Получаем множество ключей,
* относящихся к готовому каналу для работы, основной интерес состоит в том,
* когда они зарегестрированя с помощью селектора. -> Перебираем ключи. -> Для
* каждого ключа проверяем, что соответствующий канал готов к работе, в которой
* он заинтересован. -> Если он готов, печатаем сообщение о готовности.
*
* Примечание: -> Необходим запущенный MultiJabberServer на локальной машине. Вы
* запускаете его и соединяетесь с локальным MultiJabberServer -> Он может стать
* причиной исключения в MultiJabberServer, но это исключение ожидаемо.
*/
public class NonBlockingIO public static void main ( String [] args ) throws IOException if ( args.length < 2 ) System.out.println ( "Usage: java " ) ;
System.exit ( 1 ) ;
>
int cPort = Integer.parseInt ( args [ 0 ]) ;
int sPort = Integer.parseInt ( args [ 1 ]) ;
SocketChannel ch = SocketChannel.open () ;
Selector sel = Selector.open () ;
try ch.socket () .bind ( new InetSocketAddress ( cPort )) ;
ch.configureBlocking ( false ) ;
// Канал заинтересован в выполнении чтения/записи/соединении
ch.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
// Разблокируем, когда готовы к чтению/записи/соединению
sel.select () ;
// Ключи, относящиеся к готовому каналу, канал заинтересован
// в работе, которая может быть выполненаin can be
// без блокирования.
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
// Если связанный с ключом канал готов к соединению?
// if((key.readyOps() & SelectionKey.OP_CONNECT) != 0) if ( key.isConnectable ()) InetAddress ad = InetAddress.getLocalHost () ;
System.out.println ( «Connect will not block» ) ;
// Вы должны проверить возвращаемое значение,
// чтобы убедиться, что он соединен. Этот не блокированный
// вызов может вернуться без соединения, когда
// нет сервера, к которому вы пробуете подключиться
// Поэтому вы вызываете finishConnect(), который завершает
// операцию соединения.
if ( !ch.connect ( new InetSocketAddress ( ad, sPort )))
ch.finishConnect () ;
>
// Если канал, связанный с ключом, готов к чтению?
// if((key.readyOps() & SelectionKey.OP_READ) != 0)
if ( key.isReadable ())
System.out.println ( «Read will not block» ) ;
// Готов ли канал, связанный с ключом, к записи?
// if((key.readyOps() & SelectionKey.OP_WRITE) != 0)
if ( key.isWritable ())
System.out.println ( «Write will not block» ) ;
>
>
finally ch.close () ;
sel.close () ;
>
>
> // /:~

Как указано выше, вам необходимо создать канал, используя вызов метода open( ). SocketChannel.open( ) создает канал. Так как он наследован от AbstractSelectableChannel (DatagramChannel и SocketChannel), он имеет функциональность для регистрации себя в селекторе. Вызов метода регистрации совершает это. В качестве аргумента он принимает Селектор для регистрации канала, и события, которые интересны для этого канала. Здесь показано, что SocketChannel заинтересован в соединении, чтении и записи — поэтому в вызове метода регистрации указано SelectionKey.OP_CONNECT, SelectionKey.OP_READ и SelectionKey.OP_WRITE наряду с Селектором.

Статический вызов метода Selector.select( ) наблюдает все каналы, зарегистрированные в нем, относительно тех событий, которые указаны (второй аргумент при регистрации). Вы можете иметь каналы, заинтересованные в нескольких событиях.

Следующий пример работает так же, как и JabberClient1.java, но использует Селектор.

//: TIEJ:X1:JabberClient1.java
// Очень простой клиент, которй просто посылает строки на сервер
// и читает строки, посылаемые сервером.
//
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class JabberClient1 public static void main ( String [] args ) throws IOException if ( args.length < 1 ) System.out.println ( "Usage: java JabberClient1 " ) ;
System.exit ( 1 ) ;
>
int clPrt = Integer.parseInt ( args [ 0 ]) ;
SocketChannel sc = SocketChannel.open () ;
Selector sel = Selector.open () ;
try sc.configureBlocking ( false ) ;
sc.socket () .bind ( new InetSocketAddress ( clPrt )) ;
sc.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
int i = 0 ;
// По причине ассинхронной природы, вы не знаете
// когда чтение и запись закончены, поэтому вам необходимо
// следить за этим, переменная boolean written используется для
// переключения между чтением и записью. Во время записи
// отосланные назад символы должны быть прочитаны.
// Переменная boolean done используется для проверки, когда нужно
// прервать цикл.
boolean written = false, done = false ;
// JabberServer.java, которому этот клиент подсоединяется, пишет с
// помощью
// BufferedWriter.println(). Этот метод выполняет
// перекодировку в соответствии с кодовой страницей по умолчанию
String encoding = System.getProperty ( «file.encoding» ) ;
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buf = ByteBuffer.allocate ( 16 ) ;
while ( !done ) sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
sc = ( SocketChannel ) key.channel () ;
if ( key.isConnectable () && !sc.isConnected ()) InetAddress addr = InetAddress.getByName ( null ) ;
boolean success = sc.connect ( new InetSocketAddress (
addr, JabberServer.PORT )) ;
if ( !success )
sc.finishConnect () ;
>
if ( key.isReadable () && written ) if ( sc.read (( ByteBuffer ) buf.clear ()) > 0 ) written = false ;
String response = cs
.decode (( ByteBuffer ) buf.flip ()) .toString () ;
System.out.print ( response ) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
>
>
if ( key.isWritable () && !written ) if ( i < 10 )
sc.write ( ByteBuffer.wrap ( new String ( «howdy » + i
+ ‘n’ ) .getBytes ())) ;
else if ( i == 10 )
sc.write ( ByteBuffer.wrap ( new String ( «ENDn» )
.getBytes ())) ;
written = true ;
i++;
>
>
>
>
finally sc.close () ;
sel.close () ;
>
>
> // /:~

Следующий пример показывает простой механизм, основанный на селекторе, для MultiJabberServer, обсуждаемый ранее. Этот сервер работает таким же образом, как и старый сервер, но он более эффективен в том, что он не требует отдельной нити для обработки кадого клиента.

//: TIEJ:X1:MultiJabberServer1.java
// Имеет туж е семантику, что и многопоточный
// MultiJabberServer
//
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

/**
* Сервер принимает соединения не блокирующим способом. Когда соединение
* установлено, создается сокет, который регистрируется с селектором для
* чтения/записи. Чтение/запись выполняется над этим сокетом, когда селектор
* разблокируется. Эта программа работает точно так же, как и MultiJabberServer.
*/
public class MultiJabberServer1 public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException // Канал будет читать данные в ByteBuffer, посылаемые
// методом PrintWriter.println(). Декодирование этого потока
// байт требует кодовой страницы для кодировки по умолчанию.
String encoding = System.getProperty ( «file.encoding» ) ;
// Инициализируем здесь, так как мы не хотим создавать новый
// экземпляр кодировки каждый раз, когда это необходимо
// Charset cs = Charset.forName(
// System.getProperty(«file.encoding»));
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
SocketChannel ch = null ;
ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try ssc.configureBlocking ( false ) ;
// Локальныйы адрес, на котором он будет слушать соединения
// Примечание: Socket.getChannel() возвращает null, если с ним не
// ассоциирован канал, как показано ниже.
// т.е выражение (ssc.socket().getChannel() != null) справедливо
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
// Канал заинтересован в событиях OP_ACCEPT
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) ch = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ ch.socket ()) ;
ch.configureBlocking ( false ) ;
ch.register ( sel, SelectionKey.OP_READ ) ;
>
else // Обратите внимание, что не выполняется проверка, если
// в канал
// можно писать или читать — для упрощения.
ch = ( SocketChannel ) skey.channel () ;
ch.read ( buffer ) ;
CharBuffer cb = cs.decode (( ByteBuffer ) buffer.flip ()) ;
String response = cb.toString () ;
System.out.print ( «Echoing : » + response ) ;
ch.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
ch.close () ;
buffer.clear () ;
>
>
>
>
finally if ( ch != null )
ch.close () ;
ssc.close () ;
sel.close () ;
>
>
> // /:~

Здесь приведена простейшая реализация Пула Нитей. В этой реализации нет полинга (занят-ожидает) нитей. Она полностью основана на методах wait( ) и notify( ).

//: TIEJ:X1:Worker.java
// Instances of Worker are pooled in threadpool
//
//
import java.io.*;
import java.util.logging.*;

public class Worker extends Thread public static final Logger logger = Logger.getLogger ( «Worker» ) ;
private String workerId;
private Runnable task;
// Необходима ссылка на пул нитей в котором существует нить, чтобы
// нить могла добавить себя в пул нитей по завершению работы.
private ThreadPool threadpool;
static try logger.setUseParentHandlers ( false ) ;
FileHandler ferr = new FileHandler ( «WorkerErr.log» ) ;
ferr.setFormatter ( new SimpleFormatter ()) ;
logger.addHandler ( ferr ) ;
>
catch ( IOException e ) System.out.println ( «Logger not initialized..» ) ;
>
>

public Worker ( String id, ThreadPool pool ) workerId = id;
threadpool = pool;
start () ;
>

// ThreadPool, когда ставит в расписание задачу, использует этот метод
// для делегирования задачи Worker-нити. Кроме того для установки
// задачи (типа Runnable) он также переключает ожидающий метод
// run() на начало выполнения задачи.
public void setTask ( Runnable t ) task = t;
synchronized ( this ) notify () ;
>
>

public void run () try while ( !threadpool.isStopped ()) synchronized ( this ) if ( task != null ) try task.run () ; // Запускаем задачу
>
catch ( Exception e ) logger.log ( Level.SEVERE,
«Exception in source Runnable task» , e ) ;
>
// Возвращает себя в пул нитей
threadpool.putWorker ( this ) ;
>
wait () ;
>
>
System.out.println ( this + » Stopped» ) ;
>
catch ( InterruptedException e ) throw new RuntimeException ( e ) ;
>
>

public String toString () return «Worker : » + workerId;
>
> // /:~

Основной алгоритм:
while true:

  1. Проверить очередь задач.
  2. Если она пуста, подождать, пока в очередь будет добавлена задача.
    (вызов метода addTask( ) добавляет задачу и уведомляет очередь для разблокирования)
  3. Пробуем получить рабочую (Worker) нить из пула нитей.
  4. Если нет ни одной доступной нити, ожидаем в пуле нитей.
    (Когда нить освободится, она уведомит пул нитей для разблокировки)
  5. На этой стадии есть задачи в очереди и есть свободная рабочая нить.
  6. Делегируем задачу из очереди рабочей нити.

//: TIEJ:X1:ThreadPool.java
// Пул нитей, которые выполняют задачи.
//
import java.util.*;

public class ThreadPool extends Thread private static final int DEFAULT_NUM_WORKERS = 5 ;
private LinkedList workerPool = new LinkedList () ,
taskList = new LinkedList () ;
private boolean stopped = false ;

public ThreadPool () this ( DEFAULT_NUM_WORKERS ) ;
>

public ThreadPool ( int numOfWorkers ) for ( int i = 0 ; i < numOfWorkers; i++ )
workerPool.add ( new Worker ( «» + i, this )) ;
start () ;
>

public void run () try while ( !stopped ) if ( taskList.isEmpty ()) synchronized ( taskQueue ) // Если очередь пустая, подождать, пока будет добавлена
// задача
taskList.wait () ;
>
>
else if ( workerPool.isEmpty ()) synchronized ( workerPool ) // Если нет рабочих нитей, подождать, пока
// пока не появится
workerPool.wait () ;
>
>
// Запускаем следующую задачу из расписания задач
getWorker () .setTask (( Runnable ) taskList.removeLast ()) ;
>
>
catch ( InterruptedException e ) throw new RuntimeException ( e ) ;
>
>

public void addTask ( Runnable task ) taskList.addFirst ( task ) ;
synchronized ( taskList ) taskList.notify () ; // Если добавлена новая задача, уведомляем
>
>

public void putWorker ( Worker worker ) workerPool.addFirst ( worker ) ;
// Здесь может быть случай, когда вы будете иметь пул из 5 нитей,
// а будет требоваться больше. Это происходит тогда, когда требуется
// рабочая нить,
// но ее нет (свободной), тогда просто блокируем пул нитей.
// Это событие, при котором появляется свободная рабочая нить в пуле
// нитей
// Поэтому эта нить посылает уведомление и разблокирует
// нить ThreadPool, ожидающую пул нитей
synchronized ( workerPool ) workerPool.notify () ;
>
>

private Worker getWorker () return ( Worer ) workerPool.removeLast () ;
>

public boolean isStopped () return stopped;
>

public void stopThreads () stopped = true ;
Iterator it = workerPool.iterator () ;
while ( it.hasNext ()) Worker w = ( Worker ) it.next () ;
synchronized ( w ) w.notify () ;
>
>
> // Junit test

public void testThreadPool () ThreadPool tp = new ThreadPool () ;
for ( int i = 0 ; i < 10 ; i++ ) tp.addTask ( new Runnable () public void run () System.out.println ( "A" ) ;
>
>) ;
>
tp.stopThreads () ;
>
> // /:~

Следующий пример MultiJabberServer2.java использует пул нитей. Это шаблон Реактора. Как установлено выше, события отделяются от ассоциированных с ними действий. Пул нитей ассинхронно разделяет действия, ассоциированные с событиями. В системах масштаба предприятия такое разделение обычно достигается путем использования Системы Cообщений Java — Java Messaging System (JMS).

//: TIEJ:X1:MultiJabberServer2.java
// Семантика аналогична MultiJabberServer1, с использованием пула нитей.
//
import java.io.*;

class ServeOneJabber implements Runnable private SocketChannel channel;
private Selector sel;

public ServeOneJabber ( SocketChannel ch ) throws IOException channel = ch;
sel = Selector.open () ;
>

public void run () ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
boolean read = false, done = false ;
String response = null ;
try channel.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE ) ;
while ( !done ) sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
if ( key.isReadable () && !read ) if ( channel.read ( buffer ) > 0 )
read = true ;
CharBuffer cb = MultiJabberServer2.CS
.decode (( ByteBuffer ) buffer.flip ()) ;
response = cb.toString () ;
>
if ( key.isWritable () && read ) System.out.print ( «Echoing : » + response ) ;
channel.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
buffer.clear () ;
read = false ;
>
>
>
>
catch ( IOException e ) // будет поймано Worker.java и залогировано.
// Необходимо выбросить исключение времени выполнения, так как мы не
// можем
// оставить IOException
throw new RuntimeException ( e ) ;
>
finally try channel.close () ;
>
catch ( IOException e ) System.out.println ( «Channel not closed.» ) ;
// Выбрасываем это, чтобы рабочая нить могла залогировать.
throw new RuntimeException ( e ) ;
>
>
>
>

public class MultiJabberServer2 public static final int PORT = 8080 ;
private static String encoding = System.getProperty ( «file.encoding» ) ;
public static final Charset CS = Charset.forName ( encoding ) ;
// Создаем пул нитей с 20 рабочими нитями.
private static ThreadPool pool = new ThreadPool ( 20 ) ;

public static void main ( String [] args ) throws IOException ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try ssc.configureBlocking ( false ) ;
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) SocketChannel channel = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ channel.socket ()) ;
channel.configureBlocking ( false ) ;
// Отделяем события и ассоциированное действие
pool.addTask ( new ServeOneJabber ( channel )) ;
>
>
>
>
finally ssc.close () ;
sel.close () ;
>
>
> // /:~

Это минимальное обновления для JabberServer.java. Изначально, когда клиент посылает ‘END’, JabberServer не отправляет его назад. Эта версия JabberServer отсылает строку ‘END’ назад. Эти изменения были сделаны, чтобы упростить JabberClient1.java.

//: TIEJ:X1:JabberServer.java
// Очень простой сервер, который просто
// отсылает назад то, что получил от клиента.
//
import java.io.*;

public class JabberServer // Выбираем порт за пределами диапазона 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try // Блокируем до возникновения соединения:
Socket socket = s.accept () ;
try System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается PrintWriter’ом:
BufferedWriter out = new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) ;
while ( true ) String str = in.readLine () ;
System.out.println ( «Echoing: » + str ) ;
out.write ( str, 0 , str.length ()) ;
out.newLine () ;
out.flush () ;
if ( str.equals ( «END» ))
break ;
>
// Всегда закрываем два сокета.
>
finally System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally s.close () ;
>
>
> // /:~

Еще о работе с сетью

На самом деле есть очень много тем, касающихся сетевой работы, которые могут быть освещены в этой вводной статье. Сетевая работа Java также предоставляет четкую и всестороннюю поддержку для URL, включая обработчики протоколов для различных типов содержимого, которое может быть доступно на Интернет сайте. Вы можете найти полное и подробное описание других особенностей сетевого взаимодействия Java в книге Elliotte Rusty Harold «Java Network Programming» (O’Reilly, 1997).

Упражнения

  1. Скомпилируйте и запустите программы JabberServer и JabberClient из этой главы. Теперь отредактируйте файл и удалите всю буфферизацию для ввода и вывода, затем скомпилируйте и запустите программу опять, чтобы посмотреть результат.
  2. Создайте сервер, который спрашивает пароль, затем открывает файл и посылает файл по сетевому соединению. Создайте клиента, который соединяется с этим сервером, передает пароль, затем получает и сохраняет файл. Проверьте эту пару программ на вашей машине, использую localhost (IP адрес локальной заглушки 127.0.0.1, производимый при вызове InetAddress.getByName(null)).
  3. Измените сервер в урпажнении 2 так, чтобы он использовал множественные нити для обработки множества клиентов.
  4. Измените JabberClient.java таким образом, чтобы для вывода не происходило выталкивания буфера и пронаблюдайте эффект.
  5. Измените MultiJabberServer таким образом, чтобы он использовал пул нитей. Вместо выбрасывания нити при каждом отсоединении клиента, нить должна помещать себя в «пул доступных» нитей. Когда новый клиет хочет соединиться, сервер будет искать нить в пуле доступных нитей, чтобы обработать запрос, а если нет доступной нити, создается новая. Таким образом, количество необходимых нитей будет расти до требуемого количества. Назначение пулинга нитей в том, что нет затрат на накладные расходы при создании и разрушении новой нити для каждого нового клиента.
  6. Начиная с ShowHTML.java, создайте апплет, который будет защищенным паролем шлюзом к определенной части вашего web сайта.

34. Java – Сеть

Термин «сетевое программирование» относится к написанию программ, которые выполняются на нескольких устройствах (компьютерах), в которых все устройства подключены друг к другу с помощью сети.

Пакет java.net набора интерфейсов прикладного программирования J2SE содержит набор классов и интерфейсов, которые предоставляют подробные сведения о низкоуровневом взаимодействии, что позволяет писать программы, ориентированные на решение существующей проблемы.

Протоколы TCP и UDP

Пакет java.net обеспечивает поддержку двух общих сетевых протоколов:

  • TCP — TCP — это протокол управления передачей, который обеспечивает надежную связь между двумя приложениями. В Java TCP обычно используется через Интернет-протокол, который называется TCP/IP.
  • UDP — UDP — это протокол пользовательских дейтаграмм, протокол без установления соединения, который позволяет передавать пакеты данных между приложениями.

Данная глава надлежащим образом раскрывает следующие две темы:

  • Программирование сокетов — это наиболее широко используемая концепция в сетевой конфигурации, о которой было подробно изложено.
  • Обработка URL (унифицированный указатель ресурсов) — этот процесс будет рассмотрен отдельно. Перейдите по ссылке, чтобы узнать об обработке URL на языке Java.

Программирование сокетов

В Java сокеты обеспечивают механизм связи между двумя компьютерами, использующими TCP. Клиентская программа создает сокет на своем конце связи и пытается подключить этот сокет к серверу.

Когда соединение установлено, сервер создает объект сокета на своем конце связи. Клиент и сервер теперь могут общаться, записывая и считывая данные с сокета.

Класс java.net.Socket представляет собой сокет, а класс java.net.ServerSocket предоставляет механизм серверной программы для прослушивания клиентов и установления соединений с ними.

При установлении соединения TCP между двумя компьютерами с использованием сокетов, выполняются следующие этапы:

  • Сервер создает экземпляр объекта ServerSocket, определяющий, по какому номеру порта должна происходить связь.
  • Сервер вызывает метод accept() класса ServerSocket. Этот метод ожидает, пока клиент не подключится к серверу по указанному порту.
  • По завершению ожидания сервера клиент создает экземпляр объекта сокета, указывая имя сервера и номер порта подключения.
  • Конструктор класса Socket осуществляет попытку подключить клиента к указанному серверу и номеру порта. Если связь установлена, у клиента теперь есть объект Socket, способный связываться с сервером.
  • На стороне сервера метод accept() возвращает ссылку к новому сокету на сервере, который подключен к клиентскому сокету.

После того, как соединения установлены, связь может происходить с использованием потоков входных/выходных данных. Каждый сокет имеет и OutputStream (поток выходных данных), и InputStream (поток входных данных). OutputStream клиента подключен к InputStream сервера, а InputStream клиента подключен к OutputStream сервера.

TCP является двусторонним протоколом связи, поэтому данные могут передаваться по обоим потокам одновременно. Ниже приведены полезные классы, предоставляющие полный набор методов внедрения сокетов.

Конструкторы класса ServerSocket

Класс java.net.ServerSocket используется серверными приложениями для получения порта и прослушивания клиентских запросов.

Класс ServerSocket имеет четыре конструктора:

Конструктор и описание
1 public ServerSocket(int port) throws IOException
Попытки создания серверного сокета, связанного с указанным портом. Исключение происходит, если порт уже связан другим приложением.
2 public ServerSocket(int port, int backlog) throws IOException
Как и в предыдущем конструкторе, параметр backlog указывает, сколько входящих клиентов нужно сохранить в очереди ожидания.
3 public ServerSocket(int port, int backlog, InetAddress address) throws IOException
Как и в предыдущем конструкторе, параметр InetAddress указывает локальный IP-адрес для осуществления привязки. InetAddress используется в серверах, которые могут иметь несколько IP-адресов, что позволяет серверу указывать IP-адрес приема запросов клиентов.
4 public ServerSocket() throws IOException
Создает непривязанный сокет сервера. При использовании этого конструктора используйте метод привязки (), когда будете готовы привязать сокет сервера.

Если конструктор ServerSocket не выдает исключение, это означает, что ваше приложение успешно связано с указанным портом и готово к клиентским запросам.

Методы класса ServerSocket

Ниже приведены некоторые из распространенных методов в Java класса ServerSocket.

Методы и описание
1 public int getLocalPort()
Возвращает порт, который прослушивает сокет сервера. Этот метод полезен, если вы передали 0 в качестве номера порта в конструкторе и позволили серверу найти порт.
2 public Socket accept() throws IOException
Ожидает входящего клиента. Этот метод блокируется до тех пор, пока клиент не подключится к серверу на указанном порту или не истечет время ожидания сокета, при условии, что значение времени ожидания было установлено с помощью метода setSoTimeout(). В противном случае этот метод блокируется на неопределенный срок.
3 public void setSoTimeout(int timeout)
Устанавливает значение времени ожидания клиента сокетом сервера во время accept().
4 public void bind (хост SocketAddress, int backlog)
Привязывает сокет к указанному серверу и порту в объекте SocketAddress. Используйте этот метод, если вы создали ServerSocket с помощью конструктора без аргументов.

Когда ServerSocket вызывает accept(), метод не возвращается, пока клиент не подключится. После того, как клиент все-таки подключится, ServerSocket создает новый сокет для неуказанного порта и возвращает ссылку на этот новый сокет. Теперь между клиентом и сервером существует TCP-соединение, и связь может установиться.

Конструкторы класса Socket

Класс java.net.Socket представляет собой сокет, который клиент и сервер используют для связи друг с другом. Клиент получает объект сокета, создав его, тогда как сервер получает объект сокета из возвращаемого значения метода accept().

Класс Socket имеет пять конструкторов, которые клиент использует для подключения к серверу.

Конструктор и описание
1 public Socket(String host, int port) throws UnknownHostException, IOException.
Этот метод предпринимает попытку подключения к указанному серверу через указанный порт. Если этот конструктор не выдает исключение, то соединение установлено успешно, и клиент подключен к серверу.
2 public Socket(InetAddress host, int port) throws IOException
Этот метод идентичен предыдущему конструктору, за исключением того, что хост обозначается объектом InetAddress.
3 public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException.
Подключается к указанному хосту и порту, создавая сокет на локальном хосте по указанному адресу и порту.
4 public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException.
Этот метод идентичен предыдущему конструктору, за исключением того, что хост обозначается объектом InetAddress вместо строки адреса.
5 public Socket()
Создает неподключенный сокет. Используйте метод connect() для подключения такого сокета к серверу.

При возврате конструктора Socket, он не просто создает экземпляр объекта сокета, но фактически пытается подключиться к указанному серверу и порту.

Некоторые методы, изучающие класс сокета, перечислены здесь. Обратите внимание, что и клиент, и сервер имеют объект сокета, поэтому эти методы могут вызываться как клиентом, так и сервером.

Методы класса Socket

Методы класса InetAddress

Этот класс представляет адрес Интернет-протокола (IP). Следующие полезные методы, которые понадобятся при программировании сокетов, представлены ниже:

и описание
1 static InetAddress getByAddress(byte[] addr)
Возвращает объект InetAddress с учетом необработанного IP-адреса.
2 static InetAddress getByAddress(String host, byte[] addr)
Создает InetAddress на основе предоставленного имени хоста и IP-адреса.
3 static InetAddress getByName(String host)
Определяет IP-адрес хоста, учитывая имя хоста.
4 String getHostAddress()
Возвращает строку IP-адреса в текстовой форме.
5 String getHostName()
Получает имя хоста для данного IP-адреса.
6 static InetAddress InetAddress getLocalHost()
Возвращает локальный хост.
7 String toString()
Конвертирует этот IP-адрес в адресную строку.

Пример Socket Client

Следующая GreetingClient – это клиентская программа, которая подключается к серверу с помощью сокета, отправляет приветствие, а затем ожидает ответа.

// Название файла GreetingClient.java import java.net.*; import java.io.*; public class GreetingClient < public static void main(String [] args) < String serverName = args[0]; int port = Integer.parseInt(args[1]); try < System.out.println("Подключение к " + serverName + " на порт " + port); Socket client = new Socket(serverName, port); System.out.println("Просто подключается к " + client.getRemoteSocketAddress()); OutputStream outToServer = client.getOutputStream(); DataOutputStream out = new DataOutputStream(outToServer); out.writeUTF("Привет из " + client.getLocalSocketAddress()); InputStream inFromServer = client.getInputStream(); DataInputStream in = new DataInputStream(inFromServer); System.out.println("Сервер ответил " + in.readUTF()); client.close(); >catch (IOException e) < e.printStackTrace(); >> > 

Пример Socket Server

Следующая программа GreetingServer является примером серверного приложения, которое использует класс сокета для прослушивания клиентов по номеру порта, указанному в аргументе командной строки.

// Название файла GreetingServer.java import java.net.*; import java.io.*; public class GreetingServer extends Thread < private ServerSocket serverSocket; public GreetingServer(int port) throws IOException < serverSocket = new ServerSocket(port); serverSocket.setSoTimeout(10000); >public void run() < while(true) < try < System.out.println("Ожидание клиента на порт " + serverSocket.getLocalPort() + ". "); Socket server = serverSocket.accept(); System.out.println("Просто подключается к " + server.getRemoteSocketAddress()); DataInputStream in = new DataInputStream(server.getInputStream()); System.out.println(in.readUTF()); DataOutputStream out = new DataOutputStream(server.getOutputStream()); out.writeUTF("Спасибо за подключение к " + server.getLocalSocketAddress() + "\nПока!"); server.close(); >catch (SocketTimeoutException s) < System.out.println("Время сокета истекло!"); break; >catch (IOException e) < e.printStackTrace(); break; >> > public static void main(String [] args) < int port = Integer.parseInt(args[0]); try < Thread t = new GreetingServer(port); t.start(); >catch (IOException e) < e.printStackTrace(); >> > 

Скомпилируйте клиент и сервер, а затем запустите сервер следующим образом:

$ java GreetingServer 6066 Ожидание клиента на порт 6066. 

Проверьте клиентскую программу следующим образом:

$ java GreetingClient localhost 6066 Подключение к localhost на порт 6066 Просто подключается к localhost/127.0.0.1:6066 Сервер ответил Спасибо за подключение к /127.0.0.1:6066 Пока! 

Оглавление

  • 1. Java – Самоучитель для начинающих
  • 2. Java – Обзор языка
  • 3. Java – Установка и настройка
  • 4. Java – Синтаксис
  • 5. Java – Классы и объекты
  • 6. Java – Конструкторы
  • 7. Java – Типы данных и литералы
  • 8. Java – Типы переменных
  • 9. Java – Модификаторы
  • 10. Java – Операторы
  • 11. Java – Циклы и операторы цикла
  • 11.1. Java – Цикл while
  • 11.2. Java – Цикл for
  • 11.3. Java – Улучшенный цикл for
  • 11.4. Java – Цикл do..while
  • 11.5. Java – Оператор break
  • 11.6. Java – Оператор continue
  • 12. Java – Операторы принятия решений
  • 12.1. Java – Оператор if
  • 12.2. Java – Оператор if..else
  • 12.3. Java – Вложенный оператор if
  • 12.4. Java – Оператор switch..case
  • 12.5. Java – Условный оператор (? 🙂
  • 13. Java – Числа
  • 13.1. Java – Методы byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()
  • 13.2. Java – Метод compareTo()
  • 13.3. Java – Метод equals()
  • 13.4. Java – Метод valueOf()
  • 13.5. Java – Метод toString()
  • 13.6. Java – Метод parseInt()
  • 13.7. Java – Метод Math.abs()
  • 13.8. Java – Метод Math.ceil()
  • 13.9. Java – Метод Math.floor()
  • 13.10. Java – Метод Math.rint()
  • 13.11. Java – Метод Math.round()
  • 13.12. Java – Метод Math.min()
  • 13.13. Java – Метод Math.max()
  • 13.14. Java – Метод Math.exp()
  • 13.15. Java – Метод Math.log()
  • 13.16. Java – Метод Math.pow()
  • 13.17. Java – Метод Math.sqrt()
  • 13.18. Java – Метод Math.sin()
  • 13.19. Java – Метод Math.cos()
  • 13.20. Java – Метод Math.tan()
  • 13.21. Java – Метод Math.asin()
  • 13.22. Java – Метод Math.acos()
  • 13.23. Java – Метод Math.atan()
  • 13.24. Java – Метод Math.atan2()
  • 13.25. Java – Метод Math.toDegrees()
  • 13.26. Java – Метод Math.toRadians()
  • 13.27. Java – Метод Math.random()
  • 14. Java – Символы
  • 14.1. Java – Метод Character.isLetter()
  • 14.2. Java – Метод Character.isDigit()
  • 14.3. Java – Метод Character.isWhitespace()
  • 14.4. Java – Метод Character.isUpperCase()
  • 14.5. Java – Метод Character.isLowerCase()
  • 14.6. Java – Метод Character.toUpperCase()
  • 14.7. Java – Метод Character.toLowerCase()
  • 14.8. Java – Метод Character.toString()
  • 15. Java – Строки
  • 15.1. Java – Метод charAt()
  • 15.2. Java – Метод compareTo()
  • 15.3. Java – Метод compareToIgnoreCase()
  • 15.4. Java – Метод concat()
  • 15.5. Java – Метод contentEquals()
  • 15.6. Java – Метод copyValueOf()
  • 15.7. Java – Метод endsWith()
  • 15.8. Java – Метод equals()
  • 15.9. Java – Метод equalsIgnoreCase()
  • 15.10. Java – Метод getBytes()
  • 15.11. Java – Метод getChars()
  • 15.12. Java – Метод hashCode()
  • 15.13. Java – Метод indexOf()
  • 15.14. Java – Метод intern()
  • 15.15. Java – Метод lastIndexOf()
  • 15.16. Java – Метод length()
  • 15.17. Java – Метод matches()
  • 15.18. Java – Метод regionMatches()
  • 15.19. Java – Метод replace()
  • 15.20. Java – Метод replaceAll()
  • 15.21. Java – Метод replaceFirst()
  • 15.22. Java – Метод split()
  • 15.23. Java – Метод startsWith()
  • 15.24. Java – Метод subSequence()
  • 15.25. Java – Метод substring()
  • 15.26. Java – Метод toCharArray()
  • 15.27. Java – Метод toLowerCase()
  • 15.28. Java – Метод toString()
  • 15.29. Java – Метод toUpperCase()
  • 15.30. Java – Метод trim()
  • 15.31. Java – Метод valueOf()
  • 15.32. Java – Классы StringBuilder и StringBuffer
  • 15.32.1. Java – Метод append()
  • 15.32.2. Java – Метод reverse()
  • 15.32.3. Java – Метод delete()
  • 15.32.4. Java – Метод insert()
  • 15.32.5. Java – Метод replace()
  • 16. Java – Массивы
  • 17. Java – Дата и время
  • 18. Java – Регулярные выражения
  • 19. Java – Методы
  • 20. Java – Потоки ввода/вывода, файлы и каталоги
  • 20.1. Java – Класс ByteArrayInputStream
  • 20.2. Java – Класс DataInputStream
  • 20.3. Java – Класс ByteArrayOutputStream
  • 20.4. Java – Класс DataOutputStream
  • 20.5. Java – Класс File
  • 20.6. Java – Класс FileReader
  • 20.7. Java – Класс FileWriter
  • 21. Java – Исключения
  • 21.1. Java – Встроенные исключения
  • 22. Java – Вложенные и внутренние классы
  • 23. Java – Наследование
  • 24. Java – Переопределение
  • 25. Java – Полиморфизм
  • 26. Java – Абстракция
  • 27. Java – Инкапсуляция
  • 28. Java – Интерфейсы
  • 29. Java – Пакеты
  • 30. Java – Структуры данных
  • 30.1. Java – Интерфейс Enumeration
  • 30.2. Java – Класс BitSet
  • 30.3. Java – Класс Vector
  • 30.4. Java – Класс Stack
  • 30.5. Java – Класс Dictionary
  • 30.6. Java – Класс Hashtable
  • 30.7. Java – Класс Properties
  • 31. Java – Коллекции
  • 31.1. Java – Интерфейс Collection
  • 31.2. Java – Интерфейс List
  • 31.3. Java – Интерфейс Set
  • 31.4. Java – Интерфейс SortedSet
  • 31.5. Java – Интерфейс Map
  • 31.6. Java – Интерфейс Map.Entry
  • 31.7. Java – Интерфейс SortedMap
  • 31.8. Java – Класс LinkedList
  • 31.9. Java – Класс ArrayList
  • 31.10. Java – Класс HashSet
  • 31.11. Java – Класс LinkedHashSet
  • 31.12. Java – Класс TreeSet
  • 31.13. Java – Класс HashMap
  • 31.14. Java – Класс TreeMap
  • 31.15. Java – Класс WeakHashMap
  • 31.16. Java – Класс LinkedHashMap
  • 31.17. Java – Класс IdentityHashMap
  • 31.18. Java – Алгоритмы Collection
  • 31.19. Java – Iterator и ListIterator
  • 31.20. Java – Comparator
  • 32. Java – Дженерики
  • 33. Java – Сериализация
  • 34. Java – Сеть
  • 34.1. Java – Обработка URL
  • 35. Java – Отправка Email
  • 36. Java – Многопоточность
  • 36.1. Java – Синхронизация потоков
  • 36.2. Java – Межпоточная связь
  • 36.3. Java – Взаимная блокировка потоков
  • 36.4. Java – Управление потоками
  • 37. Java – Основы работы с апплетами
  • 38. Java – Javadoc

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *