Java NIO Socket Programming
Server:
public class NIOServer {
private static final String HOST = "localhost";
private static final int PORT = 10086;
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
Selector selector = null;
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(HOST, PORT));
serverSocketChannel.configureBlocking(false);
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select() == 0) {
continue;
}
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
if (socketChannel == null) {
continue;
}
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(4 * 1024);
int bytesRead = sc.read(buffer);
if (bytesRead == -1) {
selectionKey.cancel();
sc.close();
} else {
StringBuilder data = new StringBuilder();
while (bytesRead > 0) {
buffer.flip();
data.append(new String(buffer.array()));
buffer.clear();
bytesRead = sc.read(buffer);
}
System.out.println("Server received: " + data.toString());
sc.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
SocketChannel sc = (SocketChannel) selectionKey.channel();
String response = "Server response: " + Math.random();
ByteBuffer sendBuffer = ByteBuffer.wrap(response.getBytes());
while (sendBuffer.hasRemaining()) {
sc.write(sendBuffer);
}
sc.register(selector, SelectionKey.OP_READ);
System.out.println(response);
}
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client:
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
if (!clientChannel.connect(new InetSocketAddress("localhost", 10086))) {
while (!clientChannel.finishConnect()) {
System.out.print(".");
}
}
System.out.print("\n");
ByteBuffer writeBuffer = ByteBuffer.wrap("Hello, Server!".getBytes());
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while (writeBuffer.hasRemaining()) {
clientChannel.write(writeBuffer);
}
Thread.sleep(10000);
StringBuilder receivedData = new StringBuilder();
while ((clientChannel.read(readBuffer)) > 0) {
readBuffer.flip();
receivedData.append(new String(readBuffer.array(), 0, readBuffer.limit()));
readBuffer.clear();
}
System.out.println("Client Received: " + receivedData.toString());
clientChannel.close();
}
}
Note that when the client disconnects, the serve'rs corresponding channel should be handled properly to avoid a continuous readable state, which can cause an infinite loop.
When the client calls clientChannel.close(), it sends a close signal to the server. The server's socketChannel.read(byteBuffer) will return -1, indicating the close signal. However, if the byte buffer is full, it may return 0 instead of -1. You should handle this based on your specific use case.
In non-blocking mode, read can return -1 or 0 under different conditions. Here are some key points:
- When
readreturns -1: This indicates that the client has finished sending data and closed the connection. The server should close thesocketChanneland cancel theSelectionKey. - When
readreturns 0: This can happen in three scenarios:
- There is no data to read at the moment.
- The
ByteBuffer's position equals its limit. - The cliant has finished sending data and is waiting for a response.
If the client does not close the channel after sending data, and the server sends a responce, the client's read method may continuously return 0.
Handling OP_WRITE Events:
OP_WRITEevents are ready when there is space in the underlying buffer. RegisteringOP_WRITEcan cause the event to be continuously ready, consuming CPU resources. Only registerOP_WRITEwhen you have data to write, and deregister it after writing.- Example of handling
OP_WRITE:
while (buffer.hasRemaining()) {
int len = socketChannel.write(buffer);
if (len == 0) {
selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
selector.wakeup();
break;
}
}
- After writing, deregister
OP_WRITE:
selectionKey.interestOps(selectionKey.interestOps() & ~SelectionKey.OP_WRITE);
Each SocketChannel corresponds to one SelectionKey, so use key.interestOps() to manage event registration and deregistration.
Example of writing data using a buffer and interestOps:
public void write(MessageSession session, ByteBuffer buffer) throws ClosedChannelException {
SelectionKey key = session.key();
if ((key.interestOps() & SelectionKey.OP_WRITE) == 0) {
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}
writebuf.put(buffer);
selector.wakeup();
}
while (true) {
selector.select();
// ...
if (key.isWritable()) {
MessageSession session = (MessageSession) key.attachment();
synchronized (session) {
writebuf.flip();
SocketChannel channel = (SocketChannel) key.channel();
int count = channel.write(writebuf);
writebuf.clear();
key.interestOps(SelectionKey.OP_READ);
}
}
// ...
}
Key Points:
- Use buffers and
interestOpsto manage data writing. - Each
SocketChannelhas oneSelectionKey, so useinterestOpsto change event registrations.