
1. Introduction
In this tutorial, we’ll explore how to send and receive serialized objects using Java’s SocketChannel from the java.nio package. This approach enables efficient, non-blocking network communication between a client and a server.
2. Understanding Serialization
Serialization is the process of converting an object into a byte stream, allowing it to be transmitted over a network or stored in a file. When combined with socket channels, serialization enables the seamless transfer of complex data structures between applications. This technique is essential for distributed systems where objects must be exchanged over a network.
2.1. Key Classes in Java Serialization
The ObjectOutputStream and ObjectInputStream classes are essential in Java serialization. They handle the conversion between objects and byte streams:
- ObjectOutputStream is used to serialize an object into a sequence of bytes. For example, when sending a Message object over a network, ObjectOutputStream writes the object’s fields and metadata into an output stream.
- ObjectInputStream reconstructs the object from the byte stream on the receiving side.
3. Understanding Socket Channels
Socket channels are part of Java’s NIO package, which offers a flexible, scalable alternative to traditional socket-based communication. They support both blocking and non-blocking modes, making them suitable for high-performance network applications where handling multiple connections efficiently is crucial.
A socket channel is essential for creating a client-server communication system, where the client can connect to a server over TCP/IP. By using SocketChannel, we can implement asynchronous communication that allows for better performance and lower latency.
3.1. Key Components of Socket Channels
There are three key components of socket channels:
- ServerSocketChannel: Listens for incoming TCP connections. It binds to a specific port and waits for clients to connect
- SocketChannel: Represents a connection between a client and server. It supports both blocking and non-blocking modes
- Selector: Used to monitor multiple socket channels with a single thread. It helps handle events like incoming connections or data being readable, reducing the overhead of having a dedicated thread for each connection.
4. Setting up the Server and Client
Before implementing the server and client, let’s first define a sample object that we want to send over the socket. In Java, an object must implement the Serializable interface to be converted into a byte stream, which is necessary for transmitting it over a network connection.
4.1. Creating a Serializable Object
Let’s write the MyObject class, which serves as an example of a serializable object that we’ll be sending and receiving through a SocketChannel:
class MyObject implements Serializable {
private String name;
private int age;
public MyObject(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
The MyObject class implements the Serializable interface, which is required for the object to be converted into a byte stream and transmitted over a socket connection.
4.2. Implementing the Server
On the server side, we’ll use ServerSocketChannel to listen for incoming client connections and handle the received serialized objects:
private static final int PORT = 6000;
try (ServerSocketChannel serverSocket = ServerSocketChannel.open()) {
serverSocket.bind(new InetSocketAddress(PORT));
logger.info("Server is listening on port " + PORT);
while (true) {
try (SocketChannel clientSocket = serverSocket.accept()) {
System.out.println("Client connected...");
// To receive object here
}
}
} catch (IOException e) {
// handle exception
}
The server listens for incoming client connections on port 6000. Upon accepting a client, it will wait for an object to be received.
4.3. Implementing the Client
The client will create an instance of MyObject, serialize it, and send it to the server. We use SocketChannel to connect to the server and transmit the object:
private static final String SERVER_ADDRESS = "localhost";
private static final int SERVER_PORT = 6000;
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.connect(new InetSocketAddress(SERVER_ADDRESS, SERVER_PORT));
logger.info("Connected to the server...");
// To send object here
} catch (IOException e) {
// handle exception
}
This code connects to the server running on the localhost at port 6000, where it will send the serialized object to the server.
5. Serializing and Sending the Object
To send an object over a SocketChannel, we first need to serialize it into a byte array. Since SocketChannel only works with ByteBuffer, we’ll convert the object into a byte array and wrap it in a ByteBuffer before sending it over the network:
void sendObject(SocketChannel channel, MyObject obj) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
try (ObjectOutputStream objOut = new ObjectOutputStream(byteStream)) {
objOut.writeObject(obj);
}
byte[] bytes = byteStream.toByteArray();
ByteBuffer buffer = ByteBuffer.wrap(bytes);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
Here, we first serialize the MyObject into a byte array, then wrap it into a ByteBuffer, and write it to the socket channel. Then, we send the object from the client:
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.connect(new InetSocketAddress(SERVER_ADDRESS, SERVER_PORT));
MyObject objectToSend = new MyObject("Alice", 25);
sendObject(socketChannel, objectToSend); // Serialize and send
}
In this example, the client connects to the server and sends a serialized MyObject containing the name “Alice” and age 25.
6. Receiving and Deserializing the Object
On the server side, we read the bytes from the SocketChannel and deserialize it into a MyObject instance:
MyObject receiveObject(SocketChannel channel) throws IOException, ClassNotFoundException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
while (channel.read(buffer) > 0) {
buffer.flip();
byteStream.write(buffer.array(), 0, buffer.limit());
buffer.clear();
}
byte[] bytes = byteStream.toByteArray();
try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
return (MyObject) objIn.readObject();
}
}
We read the bytes from the SocketChannel into a ByteBuffer, store them in a ByteArrayOutputStream, and then deserialize the byte array into the original object. Then, we can receive the object on the server:
try (SocketChannel clientSocket = serverSocket.accept()) {
MyObject receivedObject = receiveObject(clientSocket);
logger.info("Received Object - Name: " + receivedObject.getName());
}
7. Handling Multiple Clients
To handle multiple clients concurrently, we can use a Selector to manage multiple socket channels in non-blocking mode. This ensures that the server can handle many connections simultaneously without blocking any single connection:
class NonBlockingServer {
private static final int PORT = 6000;
public static void main(String[] args) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
MyObject obj = receiveObject(client);
System.out.println("Received from client: " + obj.getName());
}
}
}
}
}
In this example, configureBlocking(false) sets the server in non-blocking mode, meaning that operations like accept() and read() won’t block the execution while waiting for events. This allows the server to continue processing other tasks instead of getting stuck waiting for a client to connect.
Next, we use a Selector to listen for events on multiple channels. It detects when a new connection (OP_ACCEPT) or incoming data (OP_READ) is available and processes them accordingly, ensuring smooth and scalable communication.
8. Test Cases
Let’s validate the serialization and deserialization of objects over SocketChannel:
@Test
void givenClientSendsObject_whenServerReceives_thenDataMatches() throws Exception {
try (ServerSocketChannel server = ServerSocketChannel.open().bind(new InetSocketAddress(6000))) {
int port = ((InetSocketAddress) server.getLocalAddress()).getPort();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<MyObject> future = executor.submit(() -> {
try (SocketChannel client = server.accept();
ObjectInputStream objIn = new ObjectInputStream(Channels.newInputStream(client))) {
return (MyObject) objIn.readObject();
}
});
try (SocketChannel client = SocketChannel.open()) {
client.configureBlocking(true);
client.connect(new InetSocketAddress("localhost", 6000));
while (!client.finishConnect()) {
Thread.sleep(10);
}
try (ObjectOutputStream objOut = new ObjectOutputStream(Channels.newOutputStream(client))) {
objOut.writeObject(new MyObject("Test User", 25));
}
}
MyObject received = future.get(2, TimeUnit.SECONDS);
assertEquals("Test User", received.getName());
assertEquals(25, received.getAge());
executor.shutdown();
}
}
This test validates that the serialization and deserialization process works correctly over a SocketChannel.
9. Conclusion
In this article, we demonstrated how to set up a client-server system using Java NIO’s SocketChannel to send and receive serialized objects. By using serialization and non-blocking I/O, we can efficiently transmit complex data structures between systems over a network.
As always, the source code is available over on GitHub.
The post Send and Receive Serialized Object in Socket Channel first appeared on Baeldung.