SocketChannel.write: примеры (JAVA)
SocketChannel.write(ByteBuffer src): intОписание функции SocketChannel.write
Метод SocketChannel.write используется для записи данных в сетевой сокет через неблокирующий или блокирующий канал NIO. Этот метод доступен в нескольких перегрузках: запись одного буфера и запись массива буферов (gathering write). Основное назначение - передача байтов из ByteBuffer в удалённый сокет.
Сигнатуры (основные):
int write(ByteBuffer src)- записывает содержимое одного буфера, начиная с его позиции до лимита. Возвращает количество записанных байт.long write(ByteBuffer[] srcs, int offset, int length)- записывает последовательно данные из части массива буферов. Возвращает суммарное количество записанных байт.long write(ByteBuffer[] srcs)- удобная перегрузка, эквивалентнаwrite(srcs, 0, srcs.length).
Аргументы и их значение:
- src - буфер с данными; запись производится от
src.position()доsrc.limit(). После успешной записи позиция буфера сдвигается на число записанных байт. - srcs - массив буферов; запись идёт по порядку, чтение каждого буфера от его текущей позиции до лимита.
- offset и length - указывают диапазон в массиве
srcs, который будет записан.
Возвращаемые значения:
- число записанных байт (int для одной буферной версии, long для версии с массивом буферов); может быть ноль.
- в блокирующем режиме реализация обычно стремится записать хотя бы один байт, но при закрытом или разорванном соединении вернёт 0 или выбросит исключение.
- в неблокирующем режиме возможна запись 0 байт, если сокет не готов отправлять данные.
Типичные исключения:
NotYetConnectedException- если канал не подключён.ClosedChannelException- если канал закрыт.AsynchronousCloseExceptionилиClosedByInterruptException- при прерывании потока или асинхронном закрытии.IOException- общие ошибки ввода/вывода, например, сбой соединения.
Особенности поведения:
- Запись может быть частичной. Поведение требует часто выполнять цикл записи до тех пор, пока буфер не станет пустым.
- Метод поддерживает gathering - запись нескольких буферов одним вызовом, что уменьшает число системных вызовов.
- В неблокирующем режиме рекомендуется использовать
Selectorи отслеживать интерес OP_WRITE, чтобы возобновить запись при готовности сокета.
Короткие примеры использования
1. Блокирующий клиент: запись строки
// BlockingClientWrite.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class BlockingClientWrite {
public static void main(String[] args) throws Exception {
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 9000))) {
String msg = "Hello, server";
ByteBuffer buf = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
int written = sc.write(buf);
System.out.println("Written bytes: " + written);
}
}
}
Written bytes: 13
2. Неблокирующий режим: возможна запись 0 байт
// NonBlockingZero.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NonBlockingZero {
public static void main(String[] args) throws Exception {
try (SocketChannel sc = SocketChannel.open()) {
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 9000));
while (!sc.finishConnect()) {}
ByteBuffer b = ByteBuffer.wrap(new byte[1024]);
int n = sc.write(b); // может быть 0, если сокет временно не готов
System.out.println("Written: " + n);
}
}
}
Written: 0
3. Gathering write: заголовок и тело
// GatheringWriteExample.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class GatheringWriteExample {
public static void main(String[] args) throws Exception {
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 9000))) {
byte[] body = "payload".getBytes(StandardCharsets.UTF_8);
ByteBuffer header = ByteBuffer.allocate(4).putInt(body.length);
header.flip();
ByteBuffer payload = ByteBuffer.wrap(body);
long written = sc.write(new ByteBuffer[]{header, payload});
System.out.println("Total written: " + written);
}
}
}
Total written: 11
Аналоги внутри Java и их особенности
- Socket.getOutputStream().write - старый блокирующий API; удобен для простых клиентов, не требующих селекторов и неблокирующего ввода/вывода.
- AsynchronousSocketChannel.write - асинхронная альтернатива с колбэками или CompletableFuture; полезна для высокопараллельных серверов без явного управления потоками.
- DatagramChannel.send - для UDP; не сохраняет порядок и не гарантирует доставку.
- FileChannel.transferTo - может использоваться для передачи файлов на SocketChannel с минимальными копированиями; хорош для отдачи больших файлов.
Когда что применимо:
- для простых клиентских задач с блокировкой -
OutputStreamудобнее; - для масштабируемых серверов с неблокирующим вводом/выводом -
SocketChannelсSelectorпредпочтительнее; - для асинхронной модели без селекторов -
AsynchronousSocketChannelподходит лучше; - для передачи больших файлов -
FileChannel.transferToчасто быстрее из-за возможности нулевого копирования.
Аналоги в других языках и отличия
Краткие примеры и особенности в разных языках:
- Node.js (JavaScript):
socket.writeпишет асинхронно и возвращает boolean, указывая состояние буфера. Пример:
// Node.js
const net = require('net');
const s = net.createConnection(9000, 'localhost', () => {
const ok = s.write('Hello');
console.log('write returned', ok);
s.end();
});
write returned true
- Python:
socket.sendвозвращает количество отправленных байт,sendallпытается отправить всё или выбрасывает исключение.
# Python
import socket
s = socket.create_connection(('localhost', 9000))
n = s.send(b'Hello')
print('sent', n)
s.close()
sent 5
- PHP:
socket_writeили потокfwrite. Возвращает число записанных байт или false на ошибку.
// PHP
$fp = stream_socket_client("tcp://localhost:9000");
$r = fwrite($fp, "Hello");
echo $r; // 5
5
- C#:
Socket.SendиNetworkStream.Write.Socket.Sendвозвращает число отправленных байт, есть опции для поведения.
// C#
using System.Net.Sockets;
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sock.Connect("localhost", 9000);
int sent = sock.Send(System.Text.Encoding.UTF8.GetBytes("Hello"));
Console.WriteLine(sent);
5
- Go:
net.Conn.Writeвозвращает количество записанных байт и ошибку. Модель блокирующая, но горутины дешевы.
// Go
package main
import (
"fmt"
"net"
)
func main() {
c, _ := net.Dial("tcp", "localhost:9000")
n, _ := c.Write([]byte("Hello"))
fmt.Println(n)
}
5
Отличия от Java:
- многие языки предлагают блокирующую модель по умолчанию; Java NIO даёт гибкость между блокирующим и неблокирующим режимом;
- у Node.js и Python есть высокоуровневые абстракции буферизации и событий, у Java - явные селекторы и буферы;
- в C# и Go сетевые операции также возвращают число байт, но моделирование масштабируемости делается другими средствами (потоки, горутины, async/await).
Типичные ошибки и примеры
1. Попытка записи в неподключённый канал - NotYetConnectedException.
// NotConnectedExample.java
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NotConnectedExample {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open(); // не подключён
ByteBuffer b = ByteBuffer.wrap(new byte[]{1,2,3});
sc.write(b); // NotYetConnectedException
}
}
Exception in thread "main" java.nio.channels.NotYetConnectedException at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:...) ...
2. Закрытие канала из другого потока - AsynchronousCloseException или ClosedByInterruptException.
// CloseFromOtherThread.java
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class CloseFromOtherThread {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.connect(new java.net.InetSocketAddress("localhost",9000));
Thread t = new Thread(() -> {
try { Thread.sleep(10); sc.close(); } catch (Exception ignored) {}
});
t.start();
ByteBuffer b = ByteBuffer.wrap(new byte[1000000]);
sc.write(b); // может выбросить AsynchronousCloseException
}
}
Exception in thread "main" java.nio.channels.AsynchronousCloseException at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:...) ...
3. Частичные записи: неверное ожидание, что один вызов запишет всё. Пример неверного кода:
// WrongAssumption.java
ByteBuffer buf = ByteBuffer.wrap(largeArray);
socketChannel.write(buf);
// предполагается, что buf полностью записан, но в неблокирующем режиме это не гарантировано
В результате буфер может иметь remaining() > 0 и данные будут потеряны, если не выполнять повторные записи.
4. Ошибки кодирования: использование ByteBuffer без flip или с неправильной позицией приведёт к нулевой записи.
ByteBuffer b = ByteBuffer.allocate(10);
b.put((byte)1);
// забыт flip
socketChannel.write(b); // ничего не отправится
Written: 0 (позиция не подготовлена для чтения)
Изменения и история API
Метод SocketChannel.write является частью NIO с Java 1.4 и с тех пор API перегрузок и возвращаемых типов оставался стабильным. Основные изменения, влияющие на поведение записи, относятся не к сигнатурам, а к улучшениям производительности и платформенным оптимизациям реализации JVM.
- С появлением
GatheringByteChannelбыло добавлено групповое чтение/запись массивов буферов. - В более поздних версиях JDK происходили оптимизации сетевого стека, улучшения zero-copy при использовании
FileChannel.transferToи настройка сетевых опций черезSocketChannel.socket(). - Появление
AsynchronousSocketChannelдало альтернативную модель асинхронного ввода/вывода без селекторов.
Прямых изменений в семантике write не отмечено в последних релизах; однако рекомендуется следить за JEP и релиз-нотами JVM ради платформенных оптимизаций и новых сетевых опций.
Расширенные и редкие сценарии использования
1. Отправка файла с минимальным копированием через FileChannel.transferTo
// FileTransferZeroCopy.java
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class FileTransferZeroCopy {
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("large.bin", "r");
FileChannel fc = raf.getChannel();
SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 9000))) {
long pos = 0;
long size = fc.size();
while (pos < size) {
long transferred = fc.transferTo(pos, size - pos, sc);
if (transferred <= 0) break;
pos += transferred;
System.out.println("transferred chunk: " + transferred);
}
}
}
}
transferred chunk: 1048576 transferred chunk: 1048576 ... (до конца файла)
Комментарий: на некоторых системах transferTo использует нулевое копирование, что снижает нагрузку на CPU.
2. Неблокирующая запись с Selector и обработка частичных записей
// NonBlockingWithSelector.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NonBlockingWithSelector {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 9000));
Selector sel = Selector.open();
sc.register(sel, SelectionKey.OP_CONNECT);
ByteBuffer buf = ByteBuffer.wrap("Large message...".getBytes());
while (true) {
sel.select();
Iterator it = sel.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
SocketChannel ch = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (ch.finishConnect()) {
key.interestOps(SelectionKey.OP_WRITE);
}
} else if (key.isWritable()) {
ch.write(buf);
if (!buf.hasRemaining()) {
System.out.println("All data written");
key.interestOps(0); // запись завершена
ch.close();
return;
}
}
}
}
}
}
All data written
Комментарий: цикл с Selector позволяет реагировать на готовность сокета и корректно завершать запись при частичных отправках.
3. Инкрементальное кодирование текста через CharsetEncoder (пользовательские кодировки и потоковые сценарии)
// CharsetEncoderExample.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.channels.SocketChannel;
public class CharsetEncoderExample {
public static void main(String[] args) throws Exception {
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 9000))) {
CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
CharBuffer in = CharBuffer.wrap("Привет, сервер!");
ByteBuffer out = ByteBuffer.allocate(32);
encoder.encode(in, out, true);
out.flip();
while (out.hasRemaining()) {
sc.write(out);
}
System.out.println("Encoded bytes sent");
}
}
}
Encoded bytes sent
4. Протокол с фреймингом: заголовок с длиной и частичная запись
// FramedProtocolWriter.java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class FramedProtocolWriter {
public static void main(String[] args) throws Exception {
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 9000))) {
String payload = "Some payload data";
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
ByteBuffer header = ByteBuffer.allocate(4).putInt(data.length);
header.flip();
ByteBuffer body = ByteBuffer.wrap(data);
ByteBuffer[] arr = new ByteBuffer[]{header, body};
while (header.hasRemaining() || body.hasRemaining()) {
long w = sc.write(arr);
if (w == 0) {
// в реальном приложении следует ждать OP_WRITE
Thread.sleep(1);
}
}
System.out.println("Frame sent");
}
}
}
Frame sent
Комментарий: комбинирование заголовка и тела через массив буферов упрощает реализацию фреймированных протоколов.