SocketChannel.write: примеры (JAVA)

Пособие по использованию SocketChannel.write
Раздел: Ввод-вывод (I/O) сетевой (NIO/Сокеты), NIO
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

Пример java
// 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 и обработка частичных записей

Пример java
// 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 (пользовательские кодировки и потоковые сценарии)

Пример java
// 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. Протокол с фреймингом: заголовок с длиной и частичная запись

Пример java
// 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

Комментарий: комбинирование заголовка и тела через массив буферов упрощает реализацию фреймированных протоколов.

джава SocketChannel.write function comments

En
SocketChannel.write Writes a sequence of bytes to this channel from a buffer