"Сокет" или "розетка" (Socket
) также является абстракцией программного обеспечения, используемой для представления конца соединения между двумя машинами. Для каждого конкретного соединения каждая машина имеет свой "сокет", который можно представить как виртуальное "соединение". Каждый конец этого соединения вставлен в "сокет" или "розетку". Конечно, физическое оборудование и проводные соединения между машинами остаются невидимыми. Основная идея абстракции заключается в том, чтобы сделать эти детали максимально невидимыми.В Java мы создаем сокет, который используется для установления соединения с другими машинами. Результат, полученный от сокета, представляет собой InputStream
и OutputStream
(или Reader
и Writer
, если используются подходящие преобразователи), что позволяет рассматривать соединение как объект потока ввода/вывода. Существует два типа сокетов, основанных на данных: ServerSocket
, используемый сервером для "слушания" входящих соединений; и Socket
, используемый клиентом для инициализации одного соединения. Как только клиент (программа) запрашивает установление соединения через сокет, ServerSocket
возвращает (через метод accept()
) соответствующий серверный сокет для прямого взаимодействия. С этого момента мы имеем настоящее "соединение сокет-сокет", которое можно рассматривать одинаково с обоих концов, так как они являются одним и тем же! В этот момент можно использовать методы getInputStream()
и getOutputStream()
для получения соответствующих объектов InputStream
и OutputStream
. Эти данные должны быть помещены в буферы. Классы могут быть отформатированы согласно методам, представленным в главе 10, аналогично другим объектам потока.Что касается названий в библиотеке Java, использование ServerSocket
(серверного сокета) может вызвать путаницу. Возможно, было бы лучше называть его ServerConnector
(соединителем сервера) или чем-то подобным, но не включать слово Socket
. Также возможно, что ServerSocket
и Socket
следует наследовать от общего базового класса. Однако фактически эти два класса содержат несколько общих методов, но недостаточно для того, чтобы считаться наследниками общего базового класса. Основной задачей ServerSocket
является то, что он находится там, готовый слушать других машин, когда те пытаются подключиться, а затем возвращает реальный Socket
. Это именно то место, где название ServerSocket
становится некорректным, поскольку его цель не состоит в том, чтобы стать Socket
, а в том, чтобы создать объект Socket
при подключении других машин. Однако, ServerSocket
действительно создает физический "сервер" или прослушивающий сокет на хосте. Этот сокет слушает входящие соединения и использует метод accept()
, чтобы вернуть "соединенный" сокет (локальный и удаленный конечные точки уже определены). Внешне это может вызвать путаницу, так как эти два сокета (прослушивающий и соединенный) связаны с одним и тем же серверным сокетом. Прослушивающий сокет принимает только новые запросы на подключение, но не принимает фактические пакеты данных.Поэтому, хотя ServerSocket
имеет небольшое значение при программировании, он является "физическими".При создании ServerSocket
достаточно указать номер порта. Нет необходимости присваивать ему IP-адрес, поскольку он уже существует на данной машине. Однако при создании Socket
необходимо указывать как IP-адрес, так и номер порта для подключения (с другой стороны, возвращаемый Socket
от ServerSocket.accept()
уже содержит всю необходимую информацию).
Этот пример демонстрирует использование сокетов для взаимодействия между сервером и клиентом наиболее простым образом. Все, что делает сервер, — это ждет установление соединения, затем создает InputStream
и OutputStream
на основе полученного Socket
. После этого все данные, считываемые из InputStream
, отправляются обратно через OutputStream
, пока не будет получен сигнал окончания строки (END), после чего соединение закрывается.
Клиент устанавливает соединение с сервером и создает OutputStream
. Текстовая строка передается через OutputStream
. Клиент также создает InputStream
, который используется для получения информации от сервера (в данном случае просто повторяет те же слова).Сервер и клиент используют один и тот же номер порта, а клиент подключается к серверу на локальной машине, поэтому тестирование можно провести без использования физической сети (хотя в некоторых конфигурациях может потребоваться подключение к реальной сети, чтобы программа работала корректно — хотя сама связь не осуществляется через эту сеть).Ниже представлен код сервера:```java
//: JabberServer.java
// Очень простой сервер, который просто эхо-отправляет всё,
// что отправляет клиент.
import java.io.;
import java.net.;
```markdown
публичный класс JabberServer {
// Выберите порт вне диапазона 1-1024:
публичное статическое константное целое число PORT = 8080;
публичное статическое void основной(строка[] аргументы)
бросает вводо-выводное исключение {
СерверСокет s = новый СерверСокет(PORT);
Система. вывести("Запущено: " + s);
попробуй {
// Ожидание соединения:
Сокет сокет = s. принять();
попробуй {
Система. вывести(
"Подключение принято: "+ сокет);
БуферЧтение чтение =
новое БуферЧтение(
новое ЧтениеИзСтроковогоПотока(
сокет. получитьInputStream()));
// Вывод автоматически сбрасывается
// PrintWriter:
PrintWrite вывод =
новое PrintWrite(
новое БуферНаписания(
новое НаписаниеИзСтроковогоПотока(
сокет. получитьOutputStream())),
истина);
пока (истина) {
строка str = чтение. читатьСтроку();
если (str. равняется("КОНЕЦ"))
выйти;
Система. вывести("Эхо: " + str);
вывод. печатать(str);
}
// Всегда закрывайте два сокета...
} наконец {
Система. вывести("закрытие...");
сокет. закрыть();
}
} наконец {
s. закрыть();
}
}
}
///:~
Как видно, `ServerSocket` требует только номер порта, а IP-адрес не нужен (поскольку он работает на этой машине).
Публичный класс JabberServer {
// Выберите порт вне диапазона 1-1024:
публичное статическое константное целое число PORT = 8080;
публичное статическое void основной(строка[] аргументы)
бросает вводо-выводное исключение {
СерверСокет s = новый СерверСокет(PORT);
Система. вывести("Запущено: " + s);
попробуй {
// Ожидание соединения:
Сокет сокет = s. принять();
попробуй {
Система. вывести(
"Подключение принято: "+ сокет);
БуферЧтение чтение =
новое БуферЧтение(
новое ЧтениеИзСтроковогоПотока(
сокет. получитьInputStream()));
// Вывод автоматически сбрасывается
// PrintWriter:
PrintWrite вывод =
новое PrintWrite(
новое БуферНаписания(
новое НаписаниеИзСтроковогоПотока(
сокет. получитьOutputStream())),
истина);
пока (истина) {
строка str = чтение. читатьСтроку();
если (str. равняется("КОНЕЦ"))
выйти;
Система. вывести("Эхо: " + str);
вывод. печатать(str);
}
// Всегда закрывайте два сокета...
} наконец {
Система. вывести("закрытие...");
сокет. закрыть();
}
} наконец {
s. закрыть();
}
}
}
Как видно, ServerSocket
требует только номер порта, а IP-адрес не нужен (поскольку он работает на этой машине).При вызове метода accept()
, он временно блокируется до тех пор, пока какой-либо клиент не попытается установить соединение с ним. Другими словами, хотя он ждет соединения, остальные процессы продолжают работать нормально (см. главу 14). После того как соединение установлено, метод accept()
возвращает объект типа Socket
, который представляет это соединение.Ответственность за закрытие сокетов здесь реализована искусно. В случае, если конструктор ServerSocket
завершается ошибкой, программа просто завершает работу (необходимо гарантировать, что конструктор ServerSocket
не оставляет открытых сетевых сокетов после ошибки). Для этого случая метод main()
"бросает" исключение IOException
, поэтому нет необходимости использовать блок try
. Если же конструктор ServerSocket
успешно выполнится, все последующие вызовы методов должны быть защищены блоком try-finally
, чтобы обеспечить правильное закрытие ServerSocket
при любом выходе из блока.
То же самое относится и к объекту Socket
, возвращаемому методом accept()
. Если метод accept()
завершается ошибкой, мы обязаны гарантировать, что объект Socket
больше не существует или не использует какие-либо ресурсы, чтобы не было необходимости очищать их. Однако, если выполнение успешное, следующие операторы должны находиться внутри блока try-finally
, чтобы обеспечить корректное удаление Socket
при возникновении ошибок. Поскольку сокеты используют важные несвязанные с памятью ресурсы, здесь следует проявлять особую осторожность и самостоятельно выполнять их очистку (в Java нет "деструкторов", которые могли бы помочь нам сделать это).
Независимо от того, являются ли это ServerSocket
или Socket
, созданные методом accept()
, они выводятся на System.out
. Это означает, что автоматически вызывается метод toString()
. Таким образом, получаем:ServerSocket[addr=0.0.0.0,port=0,localport=8080] Socket[addr=/localhost,port=1077,localport=8080]
Вы скоро увидите, как они взаимодействуют с действиями клиента. Программный код следующей части кажется просто открытым файлом для чтения и записи, но InputStream
и OutputStream
создаются из объекта Socket
. Используя два "преобразовательных" класса InputStreamReader
и OutputStreamWriter
, объекты InputStream
и OutputStream
уже преобразованы в объекты Reader
и Writer
Java 1.1. Также можно использовать прямым образом классы InputStream
и OutputStream
Java 1.0, однако использование Writer
для вывода имеет явные преимущества. Это преимущество проявляется через класс PrintWriter
, который имеет перегруженный конструктор, принимающий второй аргумент — булевое значение, указывающее на необходимость автоматического сброса выходных данных после каждого вызова метода println()
. После каждой операции записи (в out
) его буфер должен быть очищен, чтобы информация могла быть передана через сеть. Для данного примера очистка особенно важна, так как клиент и сервер должны ждать получения одной строки текста до выполнения следующего действия. Без очистки данные не будут отправлены в сеть, пока буфер не заполнится полностью (переполнение), что может создать множество проблем для данного примера. При создании сетевых приложений следует особо обращать внимание на использование механизма автоматической синхронизации буферов. При каждом обновлении буфера необходимо создавать и отправлять пакет данных (пакет сообщения).В настоящий момент это именно то поведение, которое мы хотим видеть, так как если в пакете содержится ещё не отправленная строка текста, процесс взаимной "рукопожатия" между сервером и клиентом прекращается. Другими словами, конец строки является концом сообщения. Однако в других случаях сообщения могут не быть разделены строками, поэтому лучше отказаться от использования механизма автоматической синхронизации и использовать встроенные механизмы управления буферами для определения времени отправки пакета. Таким образом, можно отправлять большие пакеты данных, что также увеличивает скорость обработки.## Класс JabberClient
Обратите внимание, что, как и почти со всеми открытыми потоками данных, они требуют буферизации. В конце этой главы есть упражнение, демонстрирующее последствия отсутствия буферизации (замедление скорости).
Бесконечный цикл while
читает строки текста из BufferedReader in
, а затем записывает информацию в System.out
и PrintWriter out
. Обратите внимание, что это может быть любой поток данных, они просто маскируются под сетевые соединения.
После того как клиентская программа отправляет строку, содержащую "END"
, программа прерывает цикл и закрывает Socket
.
Ниже представлен исходный код клиента:
//: JabberClient.java
// Очень простой клиент, который просто отправляет
// строки на сервер и считывает строки,
// которые отправляет сервер.
import java.net.*;
import java.io.*;
``````java
public class JabberClient {
public static void main(String[] args)
throws IOException {
// Передача null в метод getByName() производит специальный IP-адрес "Local Loopback",
// который используется для тестирования на одной машине без сети:
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()));
// Выходные данные автоматически сбрасываются 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("Закрываем...");
socket.close();
}
}
}
///~
```В методе main()
, можно увидеть три способа получения IP адреса локальной машины с помощью объекта `InetAddress`: использование `null`, использования `localhost` или прямого указания зарезервированного адреса `127.0.0.1`. Конечно, если требуется подключение к удалённой машине через сеть, можно использовать IP адрес этой машины. После вывода `InetAddress addr` (через автоматическое вызов `toString()` метода) результат будет следующим:``````
localhost/127.0.0.1
Передача `null` в метод `getByName()` приводит к автоматическому поиску `localhost` и созданию специального зарезервированного адреса `127.0.0.1`. Обратите внимание, что при создании сокета с именем `socket` используются как `InetAddress`, так и номер порта. При печати объекта типа `Socket`, чтобы правильно понять его значение, запомните, что уникальное интернет соединение идентифицируется четырьмя данными: `clientHost` (клиентская машина), `clientPortNumber` (номер порта клиента), `serverHost` (серверная машина) и `serverPortNumber` (номер порта сервера). Когда сервисное приложение запущено, оно создает порт, выделенный ему (8080) на локальной машине (`127.0.0.1`). Как только клиентское приложение отправляет запрос, следующий доступный порт будет назначен этому клиенту (в данном случае 1077), это действие происходит на той же машине, где работает сервисное приложение (`127.0.0.1`). Теперь, чтобы данные могли передаваться между клиентским и сервисным приложениями, каждое из них должно знать куда отправлять эти данные. Поэтому клиент отправляет "возвратный адрес" при установлении соединения с одним и тем же "известным" сервисным приложением, позволяя сервисному приложению знать куда отправлять свои данные. Это можно заметить в демонстрационной выходной информации на стороне сервера:```
Socket[addr=127.0.0.1,port=1077,localport=8080]
Это означает, что сервер только что принял соединение от порта 1077 машины 127.0.0.1
, слушая свой локальный порт (8080). А вот на стороне клиента:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
Это означает, что клиент уже подключился к порту 8080 машины 127.0.0.1
используя свой локальный порт 1077.Обратите внимание, что каждый раз при перезапуске клиентского приложения, номер локального порта увеличивается. Этот номер начинается с 1025 (за пределами системных зарезервированных значений от 1 до 1024) и продолжает увеличиваться, пока не произойдет перезагрузка системы. При перезагрузке системы, номер порта снова начнет увеличиваться с 1025 (на Unix-машине, после достижения верхнего предела зарезервированных значений, число снова начинается с минимально доступного значения).
При создании объекта Socket
, процесс преобразования его в BufferedReader
и PrintWriter
происходит аналогично тому, как это делается на сервере (в обоих случаях начинается с одного Socket
). В данном случае клиент инициализирует коммуникацию отправкой строки "howdy"
, за которой следует число. Обратите внимание, что буфер снова должен быть очищен (что происходит автоматически благодаря второму аргументу, переданному конструктору PrintWriter
). Если буфер не будет очищен, вся сессия (коммуникация) будет приостановлена, так как строка "howdy"
никогда не будет отправлена (буфер недостаточно заполнен для автоматической отправки данных).
Каждая строка, возвращенная сервером, записывается в System.out
, чтобы проверить, что всё работает правильно. Для завершения сессии необходимо отправить строку "END"
. Если программа клиента просто зависнет, сервер выбросит исключение.Здесь можно видеть, что мы используем те же меры для обеспечения правильной очистки сетевых ресурсов, представленных Socket
, с помощью блока try-finally
.
Соединение, созданное с помощью сокета, является "специальным", то есть оно продолжает существовать до тех пор, пока явно не будет закрыто (соединение также может быть случайно прервано, если одна из сторон или промежуточное соединение выйдет из строя). Это означает, что обе стороны остаются заблокированными в состоянии коммуникации, и соединение остается открытым независимо от того, передаются ли данные или нет. На первый взгляд, это кажется разумным способом организации сети. Однако он также создает дополнительные затраты для сети. В конце этой главы будут рассмотрены альтернативные способы организации связи. При использовании такого подхода установка соединения является временной операцией.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )