Обмен данными по сети на основе сокетов в Java

Передача данных — одна из краеугольных задач информационных технологий. От неё зависит как работа компаний, так и повседневная жизнь обычных людей.

Для решения этой задачи Java предоставляет различные механизмы, среди которых особое место занимают сокеты.

Сокет (англ. socket — разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Процессы при таком обмене могут исполняться как на одной ЭВМ, так и на различных ЭВМ, связанных между собой сетью. Сокет — абстрактный объект, представляющий конечную точку соединения (Википедия).

Ключевое отличие сокетов от других сетевых инструментов Java (таких как HttpRequest, SMTPTransport и др.) состоит в том, что:

  • Сокеты представляют собой достаточно низкоуровневый интерфейс.
    Это позволяет работать напрямую через протокол TCP/IP и тем самым обеспечивает универсальность.
  • Сокеты позволяют обеспечить обмен данными в режиме достаточно приближенном к реальному времени.
    При отсутствии задержек при обработке и передачи данных обмен происходит с очень высокой скоростью.

Недостаток сокетов, по сути является продолжением их достоинств. Универсальность и работа на низком уровне неизбежно порождает неудобство при работе с распространёнными протоколами (того же HTTP). Поэтому для них лучше использовать высокоуровневые средства. Но, подобные протоколы, к сожалению, не покрывают весь спектр задач сетевого программирования. Поэтому программирование на сокетах по-прежнему остаётся актуальным.

Ниже мы рассмотрим примеры создания и работы серверных и клиентских сокетов на примере несложного клиент-серверного приложения.

Серверная часть

Существует два вида сокетов. Серверные и клиентские. В этой части мы рассмотрим серверные сокеты.

Серверные сокеты реализуются на основе класса ServerSocket. Они прослушивают определённый порт и по получении данных от клиента могут сформировать и передать ответ.

Ниже приведён пример создания серверного сокета для 5000 порта.

Прослушивание порта производится в бесконечном цикле while.

В начале цикла при помощи метода accept объект ServerSocket подключает к порту. Затем при помощи объекта BufferedReader считываются данные поступившие от клиента. Далее при помощи объекта PrintWriter клиенту направляется ответ сервера. Объекты BufferedReader и PrintWriter работают с потоками входных и выходных данных сокета соответственно.

Работа указанного цикла приведена в примере ниже:

Так как этот цикл бесконечный, то во избежание блокировки основного потока программы следует либо создавать отдельный поток для работы сокета.

При необходимости перед запуском цикла можно установить таймаут.

Выше приведён пример установки таймаута продолжительностью 20 секунд. Если по истечении этого времени с момента последнего обмена данными с клиентом новых запросов не поступает, сокет завершит свою работу.

При завершении работы сокета по таймауту возбуждается исключение SocketTimeoutException (класс наследник от IOException), которое необходимо соответствующим образом обработать.

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

Клиентская часть

Клиентские сокеты реализуются на основе класса Socket и предназначены для отправки данных на определённый сервер и получения от него ответа.

Но, если серверные сокеты просто слушают порт в ожидании запроса от клиента, то клиентским необходимо знать о том, по какому адресу расположен сервер.

Также клиентскому сокету не всегда требуется постоянное прослушивание порта. Часто достаточно просто подключиться к серверу и получить от него ответ на единичный запрос. Такой режим обмена данными не требует бесконечного цикла и сокет может включаться в работу только в случае необходимости. Однако работу с сокетами в любом случае лучше выполнять в отдельном потоке. Потому, что даже если вам требуется получить или отправить данные единовременно, при их большом объёме или низкой скорости сети, это может привести к «зависанию» программы.

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

Ниже представлен пример реализации клиентского сокета, который производит обмен с сервером по запросу.

В этом примере использован так называемый «try-with-resourse». Особый синтаксис оператора try, который стал доступен в Java 7 и позволяет безопасно работать с различного рода не управляемыми JVM ресурсами.

В частности сокет после работы необходимо закрыть методом close. Если бы мы создавали сокет через конструктор, то освобождение задача правильного освобождения ресурсов как в штатной ситуации, так и при выбросе исключения легла на плечи программиста. В случае «try-with-resourse» она благополучно решается Java.

Кто знаком с C#, «try-with-resourse» это своего рода аналог using в Java.

Также в этом примере использован один из двух доступных для клиентских сокетов видов таймаутов.

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

Больший интерес представляет второй вид таймаутов, который подразумевает, что сокет завершит работу, если в течение определённого времени не удаётся подключиться к серверу. Или, по другому, таймаута для подключения.

Такой таймаут можно задействовать только с помощью перегрузки метода connect, которая принимает два параметра. Первый — объект InetAddress, представляющий собой адрес сервера. Второй — длительность таймаута в миллисекундах.

Так как конструктор класса Socket, который мы использовали в приведённом ранее примере, открывает соединение сразу же после создания объекта, использование совместно с ним таймаута для подключения невозможно. Попытка вызвать метод connect просто завершится исключением.

Поэтому, если подобный таймаут необходим, следует использовать конструктор класса Socket по умолчанию, как это показано ниже:

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

Что касается совместимости между собой таймаутов клиентских сокетов, то таймаут для подключения можно использовать совместно с таймаутом для ожидания.

Сетевое взаимодействие и работа программы

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

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

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

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