1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
15.4 数据报.md 25 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

15.4 Датаграммы

Примеры, которые вы до сих пор видели, используют "транспортный контрольный протокол" (TCP), также известный как "поточные сокеты". По дизайну этого протокола он обеспечивает высокую надёжность и гарантирует доставку данных в пункт назначения. В других словах, это позволяет повторной отправке тех данных, которые были потеряны по различным причинам во время передачи. При этом порядок получаемых байтов совпадает с их отправленным порядком. Конечно, такая надёжность требует некоторой платы: TCP имеет очень высокие затраты.Ещё один протокол, известный как "протокол пользовательских датаграмм" (UDP), не стремится гарантировать полную доставку пакетов данных и не гарантирует, что они будут доставлены в том же порядке, в котором были отправлены. Мы можем назвать его "ненадёжным протоколом" (в то время как TCP является "надёжным протоколом"). Это может звучать плохо, но благодаря своей скорости этот протокол часто оказывается полезным. Для некоторых приложений, таких как передача аудио, потеря нескольких пакетов данных во время передачи не является критичной, поскольку скорость передачи важнее всего. Большинство сетевых игр, таких как Diablo, используют UDP для связи, потому что скорость сети является ключевым фактором для плавности игры. Также можно представить сервер времени, где потеря одного сообщения не вызывает особого беспокойства. Кроме того, некоторые приложения могут отправлять UDP-сообщение обратно на сервер для восстановления. Если нет ответа в течение определённого периода времени, сообщение будет считаться потерянным.Поддержка Java для датаграмм аналогична её поддержке TCP-сокетов, однако существует заметная разница. Для датаграмм мы можем создать DatagramSocket (сокет для датаграмм) как клиентскую, так и серверную программу, но в отличие от ServerSocket, первый не просто ждёт запроса на соединение. Это связано с тем, что "соединения" больше не существуют, вместо них используется набор датаграмм. Основное отличие заключается в том, что для TCP-сокетов после установления соединения нам не важно, кто говорит с кем — достаточно просто передавать данные через поток сессии. Однако для датаграмм каждый пакет должен знать своё место происхождения и направление. Это значит, что нам необходимо знать эту информацию для каждого пакета датаграмм, чтобы информация могла корректно передаваться. DatagramSocket используется для отправки и получения datagram-пакетов, а DatagramPacket содержит конкретные данные. При подготовке к принятию одного datagram-пакета достаточно предоставить буфер для хранения полученных данных. Когда пакет приходит, адрес источника в интернете и номер порта автоматически инициализируются через DatagramSocket. Поэтому конструктор DatagramPacket, используемый для принятия datagram-пакетов, выглядит следующим образом:

new DatagramPacket(buf, buf.length);
```где `buf`  это массив байтов. Поскольку `buf` является массивом, может возникнуть вопрос, почему конструктор сам не может вычислить его длину? На самом деле, мне также так кажется; единственной причиной, которую можно предположить, связана с программированием в стиле C, где массивы сами по себе не могут сообщить свою длину.Код приема datagram-пакетов можно использовать повторно, создавая новый объект не каждый раз. Каждый раз при использовании пакета (повторное использование) данные в буфере будут перезаписываться.

Максимальный размер буфера ограничен максимальным размером datagram-пакета, который немного меньше 64 КБ. Однако во многих приложениях мы предпочитаем, чтобы этот размер был ещё меньше, особенно при отправке данных. Конкретный размер пакета зависит от специальных требований приложения.

При отправке datagram-пакета, `DatagramPacket` должен содержать как само сообщение, так и интернет-адрес и номер порта для определения места назначения. Поэтому конструктор для вывода `DatagramPacket` имеет следующий вид:

DatagramPacket(buf, length, inetAddress, port)


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

②: Мы считаем, что TCP и UDP порты являются независимыми. То есть, можно одновременно запустить TCP и UDP службы на одном и том же порту, например, 8080, и они не будут конфликтовать друг с другом.Вы можете подумать, что два конструктора создают два разных объекта: один для получения datagram-пакетов, другой для отправки их. В хорошей модели объектно-ориентированного проектирования было бы рекомендовано создать эти два объекта в виде двух различных классов вместо одного класса с различным поведением (в зависимости от того, как мы создаем объект). Это могло бы стать серьезной проблемой, но к счастью, использование `DatagramPacket` достаточно просто, и нам не нужно заморачиваться над этим вопросом. Это будет продемонстрировано более подробно в следующем примере. Этот пример аналогичен ранее рассмотренным примерам `MultiJabberServer` и `MultiJabberClient` для TCP-сокетов. Несколько клиентов будут отправлять datagram-пакеты серверу, а последний вернет их обратно тому же клиенту, который первым послал сообщение.Для упрощения создания `DatagramPacket` из строки (или создания строки из `DatagramPacket`) этот пример использует утилитный класс, названный `Dgram`:

```java
//: Dgram.java
// Утилитный класс для преобразования в обратную сторону
// между строками и DatagramPackets.
import java.net.*;

public class Dgram {
  public static DatagramPacket toDatagram(String s, InetAddress destIA, int destPort) {
    // Устарел в Java  Yöntem 1.1, но работает:
    byte[] buf = new byte[s.length() + 1];
    s.getBytes(0, s.length(), buf, 0);
    // Верный подход Java 1.1, но он
    // поврежден (обрезает строку):
    // byte[] buf = s.getBytes();
    return new DatagramPacket(buf, buf.length, destIA, destPort);
  }

  public static String toString(DatagramPacket p) {
    // Подход Java 1.0:
    // return new String(p.getData(), 0, 0, p.getLength());
    // Подход Java 1.1:
    return new String(p.getData(), 0, p.getLength());
  }
} ///:~
```Первый метод `Dgram` принимает один `String`, один `InetAddress` и номер порта как свои аргументы, копирует содержимое строки в буфер байтов и передаёт этот буфер в конструктор `DatagramPacket`, чтобы создать объект `DatagramPacket`. Обратите внимание на `" +1"` при распределении буфера — это важно для предотвращения обрезки. Метод `getBytes()` строки представляет собой специальное действие, которое копирует `char` из строки в буфер байтов. Этот метод теперь считается "устаревшим"; Java 1.1 предлагает "лучший" способ выполнения этой задачи, но здесь он закомментирован, так как обрезает часть строки. Поэтому, хотя мы получаем сообщение "устаревшего" при компиляции этого кода с использованием Java 1.1, его поведение остаётся правильным (этот недостаток должен был быть исправлен к тому времени, когда вы прочли это).Метод `Dgram.toString()` демонстрирует подходы Java 1.0 и Java 1.1 (они различаются, поскольку существует новый тип конструктора `String`).

Ниже приведён код сервера для демонстрации Datagram:

```java
//: ChatterServer.java
// Сервер, который эхо-отправляет Datagram
import java.net.*;
import java.io.*;
import java.util.*;
``````java
public class ChatterServer {
    static final int INPORT = 1711;
    private byte[] buf = new byte[1000];
    private DatagramPacket dp =
        new DatagramPacket(buf, buf.length);
    
    // Можно прослушивать и отправлять соединения на одном сокете:
    private DatagramSocket socket;

    public ChatterServer() {
        try {
            socket = new DatagramSocket(INPORT);
            System.out.println("Сервер запущен");
            while (true) {
                // Ожидание появления datagram:
                socket.receive(dp);
                String received = Dgram.toString(dp)
                    + ", от адреса: " + dp.getAddress()
                    + ", порт: " + dp.getPort();
                System.out.println(received);
                String echoString = 
                    "Эхо: " + received;
                // Извлечение адреса и порта из полученного datagram для определения места отправки обратно:
                DatagramPacket echo = 
                    Dgram.toDatagram(echoString,
                        dp.getAddress(), dp.getPort());
                socket.send(echo);
            }
        } catch (SocketException e) {
            System.err.println("Невозможно открыть сокет");
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Ошибка связи");
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new ChatterServer();
    }
}
///:~

ChatterServer создает DatagramSocket (сокет данных) для приема сообщений, а не создает новый каждый раз при подготовке к получению нового сообщения. Этот единственный DatagramSocket может повторно использоваться. У него есть номер порта, так как это сервер, клиент должен точно знать, куда отправляет данные (адрес).Несмотря на наличие номера порта, ему не требуется назначение интернет-адреса, поскольку он находится на этой машине, поэтому известен его интернет-адрес (в настоящее время это значение по умолчанию localhost). В бесконечном цикле while, сокету говорят принимать данные (receive()). Затем временно приостанавливается до появления пакета данных, после чего он возвращается обратно к нам — DatagramPacket dp. Пакет данных (Packet) преобразуется в строку, вместе с ней также передаются адрес источника и сокет. Эта информация отображается, затем добавляется дополнительная строка, указывающая, что сообщение было возвращено сервером. Пользователи могут почувствовать некоторое замешательство. Как вы увидите, множество различных интернет-адресов и портов может быть источником сообщений — другими словами, клиентское приложение может находиться на любой машине (в данном демонстрационном примере они расположены на localhost, но каждый клиент использует разные номера портов). Чтобы отправить ответное сообщение истинному источнику клиента, требуется знать его интернет-адрес и номер порта. К счастью, все эти данные уже аккуратно упакованы внутрь объекта DatagramPacket, поэтому нам остаётся только использовать методы getAddress() и getPort(). На основе этих данных можно создать DatagramPacket echo — он будет возвращаться через тот же самый сокет, который использовался для приема.Кроме того, как только сокет отправляет datagram, адрес и порт этой машины автоматически добавляются к нему, так что когда клиент получает сообщение, он может использовать методы getAddress() и getPort() для получения информации о том, откуда было отправлено сообщение. Единственная ситуация, когда методы getAddress() и getPort() не могут указать источник сообщения, — это когда мы создаем datagram для отправки и вызываем методы getAddress() и getPort() до фактической отправки datagram. Когда datagram действительно отправляется, адрес и порт текущей машины записываются в него. Таким образом, мы получаем важное правило работы с datagram: нет необходимости отслеживать источник сообщения! Этот источник гарантированно хранится внутри самого datagram. В реальности, наиболее надежным подходом является то, чтобы не пытаться отслеживать источник самостоятельно, а всегда извлекать информацию об адресе и порте из целевого datagram (как здесь). Для тестирования работы сервера, ниже приведённая программа создаёт множество клиентов (потоков), которые отправляют данные пакеты на сервер и ожидают получения этих данных обратно в том же виде.```java //: ChatterServer.java // Сервер, который эхо-отправляет полученные datagram'ы import java.net.; import java.io.; import java.util.*;

public class ChatterServer { static final int INPORT = 1711; private byte[] buf = new byte[1000]; private DatagramPacket dp = new DatagramPacket(buf, buf.length); // Можно прослушивать и отправлять сообщения с одного сокета: private DatagramSocket socket;

public ChatterServer() { try { socket = new DatagramSocket(INPORT); System.out.println("Сервер запущен"); while(true) { // Ожидание получения datagram'а: socket.receive(dp); String received = Dgram.toString(dp) + ", от адреса: " + dp.getAddress() + ", порт: " + dp.getPort(); System.out.println(received); String echoString = "Эхо: " + received; // Получение адреса и номера порта из полученного datagram'а для отправки ответа: DatagramPacket echo = Dgram.toDatagram(echoString, dp.getAddress(), dp.getPort()); socket.send(echo); } } catch(SocketException e) { System.err.println("Не удалось открыть сокет"); System.exit(1); } catch(IOException e) { System.err.println("Ошибка связи"); e.printStackTrace(); } }

public static void main(String[] args) { new ChatterServer(); } } ///:~ ```Класс ChatterClient создан как поток (`Thread`), поэтому можно использовать несколько клиентов для «атаки» на сервер. Из этого видно, что используемый для приема `DatagramPacket` и тот, который используется в `ChatterServer`, очень похожи. В конструкторе при создании `DatagramPacket` не передаются никакие аргументы, так как нет необходимости явно указывать конкретный номер порта. Интернет-адрес, используемый этим сокетом, будет представлять собой «этот компьютер» (например, `localhost`), и ему автоматически будет назначен номер порта, что видно из вывода программы. Как и в случае с сервером, этот `DatagramPacket` также используется для отправки и приема сообщений одновременно. `hostAddress` — это интернет-адрес машины, с которой мы хотим установить связь. В программе, если требуется создать `DatagramPacket`, предназначенный для отправки, то необходим точный интернет-адрес и номер порта. Безусловно, хост должен находиться по известному адресу и номеру порта, чтобы клиент мог начать «сеанс» связи с хостом. Каждый поток имеет уникальный идентификатор (хотя автоматически присвоенный порт также обеспечивает уникальность). В методе `run()`, мы создаем сообщение типа `String`, которое включает идентификационный номер потока и номер сообщения, которое он намерен отправить.Мы используем этот строковый объект для создания datagramma, который направляем на указанный адрес хоста; номер порта берётся непосредственно из константы внутри `ChatterServer`. После отправки сообщения вызов `receive()` временно "блокируется", пока сервер не ответит на это сообщение. Все данные, сопровождающие сообщение, позволяют нам точно определить, что полученный ответ предназначен именно для данного потока. В этом примере, хотя протокол является "нестабильным" (unreliable), всё же можно проверить, были ли datagramma доставлены в нужное место (что верно для локальных сетей и `localhost`, но может вызывать ошибки при удалённой связи).При запуске программы вы заметите, что каждый поток завершается. Это означает, что каждое отправленное datagram было успешно возвращено обратно к правильному получателю. Если бы это не было так, один или несколько потоков могли бы зависнуть и войти в состояние "блокировки", до тех пор, пока их входные данные не будут явно указаны.

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

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master