На языке Java традиционное программирование сокетов разделено на два метода реализации, которые также соответствуют двум различным протоколам транспортного уровня: протоколу TCP и протоколу UDP, но, как наиболее часто используемый протокол транспортного уровня в Интернете, TCP. приведет к проблемам с залипанием и половинным пакетом, поэтому для полного решения этой проблемы и была создана эта статья.
Что такое TCP-протокол?
Полное название TCP — протокол управления передачей, который определен в документе IETF RFC 793 и представляет собой протокол связи двухточечного транспортного уровня, ориентированный на соединение.
TCP предоставляет информацию от узла-отправителя о доставке пакетов узлу-получателю, используя порядковые номера и сообщения подтверждения. TCP обеспечивает надежность данных, сквозную доставку, изменение порядка и повторную передачу до тех пор, пока не будет достигнуто условие тайм-аута или не будет получено подтверждение приема пакета.
TCP является наиболее часто используемым протоколом в Интернете, а также основой для реализации связи HTTP (HTTP 1.0/HTTP 2.0).Когда мы запрашиваем веб-страницу в браузере, компьютер отправляет TCP-пакет на адрес веб-сервер запрашивает его у Веб-страница возвращается к нам, веб-сервер отвечает, отправляя поток TCP-пакетов, а браузер сшивает эти пакеты вместе, чтобы сформировать веб-страницу.
Весь смысл TCP заключается в его надежности, он упорядочивает пакеты, нумеруя их, и выполняет проверку ошибок, когда сервер отправляет ответ обратно в браузер, говоря «получено», поэтому при передаче никакие данные не будут потеряны или повреждены.
В настоящее время основной протокол HTTP на рынке использует HTTP/1.1, как показано на следующем рисунке:
В чем проблема с липким мешком и половинным мешком?
Проблема слипшихся пакетов означает, что когда отправляются два сообщения, например, ABC и DEF, но другой конец получает ABCD. читается по одному).
Проблема полупакета означает, что когда отправляется сообщение ABC, другой конец получает две части информации, AB и C. Такая ситуация называется полупакетом.
Почему возникает проблема с липким мешком и половинным мешком?
ЭтоПоскольку TCP является протоколом передачи, ориентированным на установление соединения, данные, передаваемые TCP, представляют собой потоки, а потоковые данные не имеют четких начальных и конечных границ, поэтому TCP не может определить, какой поток принадлежит сообщению..
Основные причины липких пакетов:
- Отправитель записывает данные каждый раз, когда
- Получатель не считывает данные буфера сокета своевременно.
Основные причины полупакета:
- Отправитель записывает данные каждый раз, когда > размер буфера сокета;
- Отправляемые данные больше, чем MTU (максимальная единица передачи) протокола, поэтому их необходимо распаковать.
Быстрый совет: что такое буфер?
Буфер, также известный как кеш, является частью пространства памяти. То есть в области памяти зарезервировано определенное количество места для хранения, и эти места для хранения используются для буферизации входных или выходных данных, и эта часть зарезервированного пространства называется буфером.
Преимущество буфера заключается в том, чтобы взять в качестве примера запись файлового потока.Если мы не используем буфер, ЦП будет взаимодействовать с низкоскоростным запоминающим устройством, то есть диском для каждой операции записи, и скорость записи всего файла будет ограничена низкоскоростным запоминающим устройством (диском). Однако, если используется буфер, каждая операция записи будет сначала сохранять данные в высокоскоростной буферной памяти, и когда данные в буфере достигнут определенного порога, файл будет записан на диск за один раз. Поскольку скорость записи в память намного выше, чем скорость записи на диск, при наличии буфера скорость записи файла значительно повышается.
Демонстрация проблемы с липким мешком и половиной мешка
Далее мы будем использовать код для демонстрации проблемы с липким пакетом и половинным пакетом.Для интуитивности демонстрации я задаю две роли:
- Сервер используется для приема сообщений;
- Клиент используется для отправки фиксированного сообщения.
Затем обратите внимание на проблемы с липким пакетом и половинным пакетом, распечатав информацию, полученную на стороне сервера.
Код на стороне сервера выглядит следующим образом:
/**
* 服务器端(只负责接收消息)
*/
class ServSocket {
// 字节数组的长度
private static final int BYTE_LENGTH = 20;
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(9999);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 得到客户端发送的流对象
try (InputStream inputStream = clientSocket.getInputStream()) {
while (true) {
// 循环获取客户端发送的信息
byte[] bytes = new byte[BYTE_LENGTH];
// 读取客户端发送的信息
int count = inputStream.read(bytes, 0, BYTE_LENGTH);
if (count > 0) {
// 成功接收到有效消息并打印
System.out.println("接收到客户端的信息是:" + new String(bytes));
}
count = 0;
}
}
}
}
Код клиента выглядит следующим образом:
/**
* 客户端(只负责发送消息)
*/
static class ClientSocket {
public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 9999);
// 发送的消息内容
final String message = "Hi,Java.";
// 使用输出流发送消息
try (OutputStream outputStream = socket.getOutputStream()) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 发送消息
outputStream.write(message.getBytes());
}
}
}
}
Результат связи вышеуказанной программы показан на следующем рисунке:
Из приведенных выше результатов видно, что проблема с прилипшими пакетами и половинными пакетами возникла на стороне сервера, потому что клиентская сторона отправила 10 фиксированных сообщений «Привет, Java.», и нормальным результатом должно быть то, что серверная сторона также получила 10 раз Фиксированное сообщение верно, а реальный результат нет.
Решения для липких и полуупаковок
Есть 3 решения для липких упаковок и половинных упаковок:
- Отправитель и получатель задают буфер фиксированного размера, то есть и отправка, и получение используют фиксированную длину массива byte[], а когда длины символа недостаточно, используйте нулевой символ, чтобы компенсировать это;
- Инкапсулируйте уровень протокола запроса данных на основе протокола TCP, который инкапсулирует пакет данных в форму заголовка данных (размер тела данных хранилища) + тело данных, чтобы сервер мог знать конкретную длину каждого пакета данных. После определения конкретной границы отправки данных можно решить проблему половинчатых пакетов и липких пакетов;
- Заканчивайте специальным символом, например "\n", чтобы мы знали конечный символ, избегая, таким образом, проблемы полуупаковки и липкой упаковки (Рекомендуемое решение).
Затем давайте продемонстрируем конкретную реализацию приведенного выше решения в коде.
Решение 1. Фиксированный размер буфера
Реализации фиксированного размера буфера нужно только контролировать длину (массива) байтов, отправленных и полученных сервером и клиентом, чтобы они были одинаковыми.
Код реализации на стороне сервера выглядит следующим образом:
/**
* 服务器端,改进版本一(只负责接收消息)
*/
static class ServSocketV1 {
private static final int BYTE_LENGTH = 1024; // 字节数组长度(收消息用)
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9091);
// 获取到连接
Socket clientSocket = serverSocket.accept();
try (InputStream inputStream = clientSocket.getInputStream()) {
while (true) {
byte[] bytes = new byte[BYTE_LENGTH];
// 读取客户端发送的信息
int count = inputStream.read(bytes, 0, BYTE_LENGTH);
if (count > 0) {
// 接收到消息打印
System.out.println("接收到客户端的信息是:" + new String(bytes).trim());
}
count = 0;
}
}
}
}
Код реализации клиента выглядит следующим образом:
/**
* 客户端,改进版一(只负责接收消息)
*/
static class ClientSocketV1 {
private static final int BYTE_LENGTH = 1024; // 字节长度
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9091);
final String message = "Hi,Java."; // 发送消息
try (OutputStream outputStream = socket.getOutputStream()) {
// 将数据组装成定长字节数组
byte[] bytes = new byte[BYTE_LENGTH];
int idx = 0;
for (byte b : message.getBytes()) {
bytes[idx] = b;
idx++;
}
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
outputStream.write(bytes, 0, BYTE_LENGTH);
}
}
}
}
Результат выполнения приведенного выше кода показан на следующем рисунке:
Анализ преимуществ и недостатков
Как видно из приведенного выше кода, хотя этот метод может решить проблему залипания пакетов и половинных пакетов, этот метод с фиксированным размером буфера увеличивает ненужную передачу данных, поскольку этот метод используется, когда отправляемые данные относительно малы. Нулевые символы используются для компенсировать, поэтому этот метод сильно увеличивает нагрузку на передачу по сети, поэтому это не лучшее решение.
Решение 2. Инкапсулируйте протокол запроса
Идея реализации этого решения заключается в том, чтобы инкапсулировать запрошенные данные в две части: заголовок данных + тело данных, сохранить размер тела данных в заголовке данных и продолжить чтение данных, когда прочитанные данные меньше размера в заголовке данных, пока длина считываемых данных не сравняется с длиной в заголовке.
Поскольку этот метод может получить границу данных, он не вызовет проблемы с липкими пакетами и полупакетами, но стоимость кодирования этого метода реализации высока и не элегантна, поэтому это не лучшее решение для реализации, поэтому мы Пропустите это и сразу переходите к окончательному решению.
Решение 3: закончить специальными символами, читать построчно
Границу потока можно узнать, закончив его специальным символом, поэтому его также можно использовать для решения проблемы липких пакетов и половинных пакетов.Эта реализация является нашим рекомендуемым окончательным решением..
Суть этого решения заключается в использованииBufferedReader
иBufferedWriter
, то есть поток входных символов и поток выходных символов с буфером, добавить при записи\n
Чтобы закончить, используйте при чтенииreadLine
Читать данные построчно, чтобы была известна граница потока, таким образом решая проблему липких пакетов и половинчатых пакетов.
Код реализации на стороне сервера выглядит следующим образом:
/**
* 服务器端,改进版三(只负责收消息)
*/
static class ServSocketV3 {
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器端
ServerSocket serverSocket = new ServerSocket(9092);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 使用线程池处理更多的客户端
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
threadPool.submit(() -> {
// 消息处理
processMessage(clientSocket);
});
}
/**
* 消息处理
* @param clientSocket
*/
private static void processMessage(Socket clientSocket) {
// 获取客户端发送的消息流对象
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()))) {
while (true) {
// 按行读取客户端发送的消息
String msg = bufferedReader.readLine();
if (msg != null) {
// 成功接收到客户端的消息并打印
System.out.println("接收到客户端的信息:" + msg);
}
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
PS: В приведенном выше коде используется пул потоков для решения проблемы одновременного доступа нескольких клиентов к стороне сервера, что реализует ответ сервера «один ко многим».
Код реализации клиента выглядит следующим образом:
/**
* 客户端,改进版三(只负责发送消息)
*/
static class ClientSocketV3 {
public static void main(String[] args) throws IOException {
// 启动 Socket 并尝试连接服务器
Socket socket = new Socket("127.0.0.1", 9092);
final String message = "Hi,Java."; // 发送消息
try (BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 注意:结尾的 \n 不能省略,它表示按行写入
bufferedWriter.write(message + "\n");
// 刷新缓冲区(此步骤不能省略)
bufferedWriter.flush();
}
}
}
}
Результат выполнения приведенного выше кода показан на следующем рисунке:
Суммировать
В этой статье мы говорили о проблеме прилипших пакетов TCP и полупакетов. Прилипшие пакеты означают, что читаются две части информации. В нормальных условиях сообщения должны считываться одно за другим, а проблема полупакета означает, что половина информации считывается. Причина липких пакетов и полупакетов в том, что передача TCP осуществляется в виде потоков, а потоковые данные не имеют четкой идентификации начала и конца, что и приводит к этой проблеме.
В этой статье мы предлагаем 3 решения для липких пакетов и половинных пакетов, наиболее рекомендуемым из которых является использованиеBufferedReader
иBufferedWriter
Читайте, пишите и различайте сообщения по строкам, что является третьим решением в этой статье.
Ссылки и благодарности
zhuanlan.zhihu.com/p/126279630
у-у-у. Краткое описание.com/afraid/6ah 4EC6095…
Подпишитесь на официальный аккаунт «Java Chinese Community», чтобы узнать больше о галантерейных товарах.
Посетите Github, чтобы узнать больше интересных вещей:GitHub.com/VIP камень/Али…