«1. Обзор

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

В этом уроке мы покажем, как мы можем читать данные, отправленные нам через сокет.

2. Чтение данных из сокета

Предположим, у нас есть базовое понимание программирования сокетов.

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

Во-первых, нам нужно объявить и инициализировать переменные ServerSocket, Socket и DataInputStream:

ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();
DataInputStream in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));

Обратите внимание, что мы решили обернуть InputStream сокета в DataInputStream. Это позволяет нам читать строки текстовых и примитивных типов данных Java переносимым способом.

Это хорошо, поскольку теперь, если мы знаем тип данных, которые мы получим, мы можем использовать специализированные методы, такие как readChar(), readInt(), readDouble() и readLine().

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

В этом случае мы получим поток байтов из сокета, используя низкоуровневую функцию read(). Но в этом подходе есть небольшая проблема: как узнать длину и тип данных, которые мы получим?

Давайте рассмотрим этот сценарий в следующем разделе.

3. Чтение двоичных данных из сокета

При чтении данных в байтах нам необходимо определить собственный протокол для связи между сервером и клиентом. Самый простой протокол, который мы можем определить, называется TLV (Type Length Value). Это означает, что каждое сообщение, записываемое в сокет, имеет форму значения длины типа.

Итак, мы определяем каждое отправленное сообщение как:

    1-байтовый символ, представляющий тип данных, например s для строки 4-байтовое целое число, указывающее длину данных И затем фактические данные, длина которых была только что указана

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

Давайте посмотрим, как это можно реализовать на простом примере со строковым сообщением.

Во-первых, нам нужно вызвать функцию readChar(), чтобы прочитать тип данных, а затем вызвать функцию readInt(), чтобы прочитать их длину:

char dataType = in.readChar();
int length = in.readInt();

После этого нам нужно прочитать данные, которые мы получаем. Здесь важно отметить, что функция read() может не иметь возможности прочитать все данные за один вызов. Итак, нам нужно вызвать read() в цикле while:

if(dataType == 's') {
    byte[] messageByte = new byte[length];
    boolean end = false;
    StringBuilder dataString = new StringBuilder(length);
    int totalBytesRead = 0;
    while(!end) {
        int currentBytesRead = in.read(messageByte);
        totalBytesRead = currentBytesRead + totalBytesRead;
        if(totalBytesRead <= length) {
            dataString
              .append(new String(messageByte, 0, currentBytesRead, StandardCharsets.UTF_8));
        } else {
            dataString
              .append(new String(messageByte, 0, length - totalBytesRead + currentBytesRead, 
              StandardCharsets.UTF_8));
        }
        if(dataString.length()>=length) {
            end = true;
        }
    }
}

4. Код клиента для отправки данных

А как насчет кода на стороне клиента? На самом деле все очень просто:

char type = 's'; // s for string
String data = "This is a string of length 29";
byte[] dataInBytes = data.getBytes(StandardCharsets.UTF_8);

out.writeChar(type);
out.writeInt(dataInBytes.length);
out.write(dataInBytes);

Вот и все, что делает наш клиент!

5. Заключение

В этой статье мы обсудили, как читать данные из сокета. Мы рассмотрели различные функции, которые помогают нам читать данные определенного типа. Кроме того, мы увидели, как читать двоичные данные.

Полную реализацию этого руководства можно найти на GitHub.