Sockets use TCP/IP transport protocol and they are the last piece of a network communication between two hosts. You do not usually have to deal with them, since there are protocols built on top of sockets like HTTP or FTP, however it is important to know how they work.
TCP: It is a reliable data transfer protocol that ensures that the data sent is complete and correct and requires to establish a connection.
Java offers a blocking and non blocking alternative to create sockets, and depending on your requirements you might consider the one or the other.
Java Blocking IO
The Java blocking IO API is included in JDK under the package java.net
and is generally the simplest to use.
This API is based on flows of byte streams and character streams that can be read or written. There is not an index that you can use to move back and forth, like in an array, it is simply a continuous flow of data.
Every time a client requests a connection to the server, it will block a thread. Therefore, you have to create a pool of threads large enough if you expect to have many simultaneous connections.
ServerSocket serverSocket = new ServerSocket(PORT_NUMBER);
while (true) {
Socket client = serverSocket.accept();
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream())
);
OutputStream out = client.getOutputStream();
in.lines().forEach(line -> {
try {
out.write(line.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
});
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
- A
ServerSocket
is created with a given port to listen on. - The server will block when
accept()
is invoked and starts listening for clients connections. - If a client requests a connection a
Socket
is returned byaccept()
. - Now you can read from the client (
InputStream
) and send data back to the client (OutputStream
).
If you want to allow multiple connections, you have to create a Thread Pool:
ExecutorService threadPool = Executors.newFixedThreadPool(100);
threadPool.execute(() -> {
// SOCKET CREATION
});
As you can see, this API has some limitations. You will not be able to accept more connections than threads available in you machine. Therefore, if you are expecting to have many connections, you need an alternative.
Java NIO
java.nio is a non blocking API for socket connections which means you are not tight to the number of threads available. With this library one thread can handle multiple connections at once.
Elements:
- Channel: channels are a combination of input and output streams, so they allow you to read and write, and they use buffers to do this operations.
-
Buffer: it is a block of memory used to read from a
Channel
and write into it. When you want to read data from aBuffer
you need to invokeflip()
, so that it will setpos
to 0.int read = socketChannel.read(buffer); // pos = n & lim = 1024 while (read != -1) { buffer.flip(); // set buffer in read mode - pos = 0 & lim = n while(buffer.hasRemaining()){ System.out.print((char) buffer.get()); // read 1 byte at a time } buffer.clear(); // make buffer ready for writing - pos = 0 & lim = 1024 read = socketChannel.read(buffer); // set to -1 }
- On line 1, pos will be equals to the number of bytes written into the
Buffer
. - On line 3,
flip()
is called to set position to 0 and limit to the number of bytes previously written. - On line 5, it reads from
Buffer
one byte at a time up to the limit. - On line 7, finally it clears the
Buffer
.
- On line 1, pos will be equals to the number of bytes written into the
- Selector: A
Selector
can register multiple Channels and will check which ones are ready for accepting new connections. Similar toaccept()
method of blocking IO, whenselect()
is invoked it will block the application until aChannel
is ready to do an operation. Because aSelector
can register many channels, only one thread is required to handler multiple connections. - Selection Key: It contains properties for a particular Channel (interest set, ready set, selector/channel and an optional attached object). Selection keys are mainly use to know the current interest of the channel (
isAcceptable()
,isReadable()
,isWritable()
), get the channel and do operations with that channel.
Example
You will use an Echo Socket Channel server to show how NIO works.
var serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
var selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
var keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
var selectionKey = (SelectionKey) keys.next();
if (selectionKey.isAcceptable()) {
createChannel(serverSocketChannel, selectionKey);
} else if (selectionKey.isReadable()) {
doRead(selectionKey);
} else if (selectionKey.isWritable()) {
doWrite(selectionKey);
}
keys.remove();
}
}
- From lines 1 to 3 a
ServerSocketChannel
is created, and you have to set it to non-blocking mode explicitly. The socket is also configure to listen on port 8080. - On line 5 and 6, a
Selector
is created andServerSocketChannel
is registered on theSelector
with aSelectionKey
pointing to ACCEPT operations. - To keep the application listening all the time the blocking method
select()
is inside an infinite while loop, andselect()
will return when at least one channel is selectedwakeup()
is invoked or the thread is interrupted. - Then on line 10 a set of keys are returned from the
Selector
and it will iterate through them in order to execute the ready channels.
private static void createChannel(ServerSocketChannel serverSocketChannel, SelectionKey selectionKey) throws IOException {
var socketChannel = serverSocketChannel.accept();
LOGGER.info("Accepted connection from " + socketChannel);
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap(("Welcome: " + socketChannel.getRemoteAddress() +
"\nThe thread assigned to you is: " + Thread.currentThread().getId() + "\n").getBytes()));
dataMap.put(socketChannel, new LinkedList<>()); // store socket connection
LOGGER.info("Total clients connected: " + dataMap.size());
socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); // selector pointing to READ operation
}
- Every time a new connection is created
isAcceptable()
will be true and a newChannel
will be registered into theSelector
. - To keep track of the data of each channel, it is put in a
Map
with the socket channel as the key and a list ofByteBuffers
. - Then the selector will point to READ operation.
private static void doRead(SelectionKey selectionKey) throws IOException {
LOGGER.info("Reading...");
var socketChannel = (SocketChannel) selectionKey.channel();
var byteBuffer = ByteBuffer.allocate(1024); // pos=0 & lim=1024
int read = socketChannel.read(byteBuffer); // pos=numberOfBytes & lim=1024
if (read == -1) { // if connection is closed by the client
doClose(socketChannel);
} else {
byteBuffer.flip(); // put buffer in read mode by setting pos=0 and lim=numberOfBytes
dataMap.get(socketChannel).add(byteBuffer); // find socket channel and add new byteBuffer queue
selectionKey.interestOps(SelectionKey.OP_WRITE); // set mode to WRITE to send data
}
}
- In the read block the channel will be retrieved and the incoming data will be written into a
ByteBuffer
. - On line 6 it checks if the connection has been closed.
- On line 9 and 10, the buffer is set to read mode with
flip()
and added to theMap
. - Then,
interestOps()
is invoked to point to WRITE operation.
private static void doWrite(SelectionKey selectionKey) throws IOException {
LOGGER.info("Writing...");
var socketChannel = (SocketChannel) selectionKey.channel();
var pendingData = dataMap.get(socketChannel); // find channel
while (!pendingData.isEmpty()) { // start sending to client from queue
var buf = pendingData.poll();
socketChannel.write(buf);
}
selectionKey.interestOps(SelectionKey.OP_READ); // change the key to READ
}
- Once again, the channel is retrieved in order to write the data saved in the
Map
into it. - Then, it sets the
Selector
to READ operations.
private static void doClose(SocketChannel socketChannel) throws IOException {
dataMap.remove(socketChannel);
var socket = socketChannel.socket();
var remoteSocketAddress = socket.getRemoteSocketAddress();
LOGGER.info("Connection closed by client: " + remoteSocketAddress);
socketChannel.close(); // closes channel and cancels selection key
}
- In case the connection is closed, the channel is removed from the
Map
and it closes the channel.
Java IO vs NIO
Choosing between IO and NIO will depend on the use case. For fewer connections and a simple solution, IO might be better fit for you. On the other hand, if you want something more efficient which can handle thousands of connections simultaneously NIO is probably a better choice, but bear in mind that it will add much code complexity, however, there are frameworks like Netty or Apache MINA that are built on top of NIO and hide the programming complexity.