Send and Receive Serialized Objects via Java SocketChannel
Networking is crucial in modern applications, and Java offers powerful tools like SocketChannel for efficient networked systems. Using Java NIO’s non-blocking I/O, SocketChannel
enables fast TCP/IP communication. This article covers how to send and receive serialized objects in a SocketChannel using Java’s serialization features.
1. What is a Socket?
A socket represents one endpoint in a two-way communication link between two programs running on a network. Java offers both traditional (Socket
, ServerSocket
) and non-blocking (SocketChannel
, ServerSocketChannel
) I/O APIs. The SocketChannel
class is part of Java NIO and supports selectable, asynchronous socket communication.
2. The Client and Server Architecture
A typical client-server architecture using sockets involves two components:
- Server: Listens for incoming connections, accepts them, and processes incoming data.
- Client: Initiates the connection to the server and sends data.
When using SocketChannel
(client) and ServerSocketChannel
(server), the data is exchanged in the form of ByteBuffers. To send Java objects, we must serialize them into byte arrays first.
3. Why Use Serialization?
Serialization is the process of converting an object into a byte stream so it can be sent over the network or saved to a file. Deserialization reconstructs the object on the receiving end. This mechanism is crucial when transferring complex data structures between systems.
Java provides native support for serialization through the Serializable
interface.
4. Define the Serializable Class
Let’s define a simple Java class Person
that we will serialize and send over the network.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import java.io.Serializable; public class Person implements Serializable{ private String name; private int age; public Person(String name, int age) { this .name = name; this .age = age; } public String getName() { return name; } public void setName(String name) { this .name = name; } public int getAge() { return age; } public void setAge( int age) { this .age = age; } @Override public String toString() { return "Person{" + "name=" + name + ", age=" + age + '}' ; } } |
The Person
class implements Serializable
, allowing it to be converted to and from a byte stream. The toString()
method is overridden to give a readable format of the object when logged or printed.
5. Create a Utility Class for Serialization
Next, utility methods for converting objects to byte arrays and vice versa are created.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | public class SerializationUtils { public static byte [] serialize(Object obj) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(obj); return bos.toByteArray(); } } public static Object deserialize( byte [] data) throws IOException, ClassNotFoundException { try (ByteArrayInputStream bis = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bis)) { return ois.readObject(); } } } |
The serialize()
method converts any object that implements Serializable
into a byte array, making it suitable for transmission. Conversely, the deserialize()
method takes this byte array and reconstructs the original object from it, effectively reversing the serialization process.
6. Implement the Server
The server will wait for a client connection, read the byte stream and deserialize it into a Person
object
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class Server { private static final Logger logger = Logger.getLogger(Server. class .getName()); public static void main(String[] args) { logger.info( "Server started..." ); try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { serverSocketChannel.bind( new InetSocketAddress( "localhost" , 5000 )); logger.info( "Waiting for client connection..." ); try (SocketChannel socketChannel = serverSocketChannel.accept()) { logger.info( "Client connected." ); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); socketChannel.read(buffer); buffer.flip(); byte [] data = new byte [buffer.remaining()]; buffer.get(data); // Deserialize using utility class Person person = (Person) SerializationUtils.deserialize(data); logger.info( "Received object from client:" ); logger.info(person.toString()); } catch (ClassNotFoundException e) { logger.log(Level.SEVERE, "Class not found while deserializing" , e); } } catch (IOException e) { logger.log(Level.SEVERE, "IOException in server" , e); } } } |
The Server
class listens for incoming client connections on port 5000 using a ServerSocketChannel
. Once a connection is established, it reads the incoming byte stream into a ByteBuffer
, extracts the data as a byte array, and reconstructs the original Person
object using SerializationUtils.deserialize()
. The use of try-with-resources guarantees that all channels are closed correctly to prevent resource leaks.
7. Implement the Client
The client will create a Person
object, serialize it, and send it to the server.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class Client { private static final Logger logger = Logger.getLogger(Client. class .getName()); public static void main(String[] args) { logger.info( "Client started..." ); try (SocketChannel socketChannel = SocketChannel.open()) { socketChannel.connect( new InetSocketAddress( "localhost" , 5000 )); logger.info( "Connected to server." ); Person person = new Person( "John" , 30 ); // Serialize the object using utility class byte [] data = SerializationUtils.serialize(person); ByteBuffer buffer = ByteBuffer.wrap(data); socketChannel.write(buffer); logger.info( "Serialized object sent to server." ); } catch (IOException e) { logger.log(Level.SEVERE, "IOException in client" , e); } } } |
The Client
class connects to the server via a non-blocking SocketChannel
on port 5000, constructs a Person
object, and sends it to the server in serialized form. The object is first converted into a byte array using the SerializationUtils.serialize()
method. The resulting byte array is wrapped in a ByteBuffer
and sent through the socket.
8. Compile and Run the Program
Running the Server
First, compile the project using mvn clean install
from the root directory and then run the Server
class to start listening for incoming connections. The server will wait for a client to connect and send a serialized object. Once received, the server will deserialize the object.
Run the Server (in Terminal 1)
1 | mvn exec :java -Dexec.mainClass=com.jcg.examples.Server |
You should see:
1 2 3 4 | Apr. 10, 2025 8:44:01 A.M. com.jcg.examples.Server main INFO: Server started... Apr. 10, 2025 8:44:01 A.M. com.jcg.examples.Server main INFO: Waiting for client connection... |
Run the Client (in Terminal 2)
After the server is up and listening, execute the Client
class to initiate the connection and send the serialized Person
object.
1 | mvn exec :java -Dexec.mainClass=com.jcg.examples.Client |
The client will log successful connection establishment and indicate when the serialized data is sent. Ensure the client and server are running in the same network so that the connection can be successfully established and data transmitted.
Output:
1 2 3 4 5 6 | Apr. 10, 2025 8:48:07 A.M. com.jcg.examples.Client main INFO: Client started... Apr. 10, 2025 8:48:07 A.M. com.jcg.examples.Client main INFO: Connected to server. Apr. 10, 2025 8:48:07 A.M. com.jcg.examples.Client main INFO: Serialized object sent to server. |
Back in the server terminal, you will see:

9. Managing Multiple Clients Simultaneously
In real-world applications, servers rarely deal with just one client at a time. To support scalability, the server must handle multiple client connections concurrently. This is often achieved by spawning a new thread for each incoming client or by using a thread pool to manage connections efficiently. In our example, we use a FixedThreadPool
from the ExecutorService
to handle each client in a separate thread. This ensures that the server remains responsive and can continue accepting new connections without blocking.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | public class Server2 { private static final Logger logger = Logger.getLogger(Server2. class .getName()); private static final int PORT = 5000 ; private static final ExecutorService executor = Executors.newFixedThreadPool( 10 ); // support up to 10 clients public static void main(String[] args) { logger.info( "Server started..." ); try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { serverSocketChannel.bind( new InetSocketAddress( "localhost" , PORT)); logger.info( "Listening for clients on port " + PORT); while ( true ) { SocketChannel clientChannel = serverSocketChannel.accept(); logger.info( "Client connected from: " + clientChannel.getRemoteAddress()); // Handle each client in a separate thread executor.submit(() -> handleClient(clientChannel)); } } catch (IOException e) { logger.log(Level.SEVERE, "IOException in server main loop" , e); } finally { executor.shutdown(); logger.info( "Server shutdown." ); } } private static void handleClient(SocketChannel clientChannel) { try { ByteBuffer buffer = ByteBuffer.allocate( 1024 ); clientChannel.read(buffer); buffer.flip(); byte [] data = new byte [buffer.remaining()]; buffer.get(data); Person person = (Person) SerializationUtils.deserialize(data); logger.info( "Received object from client:" ); logger.info(person.toString()); } catch (IOException | ClassNotFoundException e) { logger.log(Level.SEVERE, "Error handling client" , e); } finally { try { if (clientChannel != null && clientChannel.isOpen()) { clientChannel.close(); logger.info( "Client connection closed." ); } } catch (IOException e) { logger.log(Level.WARNING, "Failed to close client channel" , e); } } } } |
The Server2
class begins by declaring an ExecutorService
with a fixed thread pool size of ten. This thread pool enables the server to handle up to ten client connections concurrently without creating an excessive number of threads.
In the main()
method, the server is initialized and begins listening for connections on port 5000. The ServerSocketChannel
is opened inside a try-with-resources block, which ensures it is properly closed when the server shuts down. The server enters an infinite loop, continuously accepting incoming client connections. When a client connects, a log message is generated, and the connection is passed to a worker thread using the thread pool.
Each client connection is then processed by the handleClient()
method, which reads the client’s data, deserializes the byte array into a Person
object, and logs the received object for verification.
Output from Running the Handle Multiple Clients Example
After running the server and multiple clients, the server will log messages indicating the sequence of events as each client connects and their data is processed. The output will include log entries for each client connection, the data received, and the deserialized object.
Here’s an example of the output that would be generated by running the Server2
:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | Apr. 10, 2025 9:23:16 A.M. com.jcg.examples.Server2 main INFO: Server started... Apr. 10, 2025 9:23:17 A.M. com.jcg.examples.Server2 main INFO: Listening for clients on port 5000 Apr. 10, 2025 9:24:43 A.M. com.jcg.examples.Server2 main INFO: Client connected from: /127.0.0.1:59279 Apr. 10, 2025 9:24:43 A.M. com.jcg.examples.Server2 handleClient INFO: Received object from client: Apr. 10, 2025 9:24:43 A.M. com.jcg.examples.Server2 handleClient INFO: Person{name=John, age=30} Apr. 10, 2025 9:24:44 A.M. com.jcg.examples.Server2 handleClient INFO: Client connection closed. Apr. 10, 2025 9:25:23 A.M. com.jcg.examples.Server2 main INFO: Client connected from: /127.0.0.1:59283 Apr. 10, 2025 9:25:23 A.M. com.jcg.examples.Server2 handleClient INFO: Received object from client: Apr. 10, 2025 9:25:23 A.M. com.jcg.examples.Server2 handleClient INFO: Person{name=Jane, age=35} Apr. 10, 2025 9:25:23 A.M. com.jcg.examples.Server2 handleClient INFO: Client connection closed. |
The log output shows how the server handles multiple client connections sequentially.
- Client connected from: /127.0.0.1:59279 – The server accepts a connection from the client at port 59279.
- Received object from client: – The server successfully receives data from the client.
- Person{name=John, age=30} – The server deserializes the received data into a
Person
object with name “John” and age 30. - Client connection closed. – The server closes the connection with the client.
Next, a second client connects from port 59283, and similar steps are followed:
- Client connected from: /127.0.0.1:59283 – The server accepts the second client’s connection.
- Received object from client: – The server receives the second client’s data.
- Person{name=Jane, age=35} – The second client’s data is deserialized into a
Person
object with name “Jane” and age 35.
This sequence demonstrates the server’s ability to handle multiple client connections, process their requests, and manage the connections efficiently.
10. Conclusion
In this article, we explored how to send and receive a serialized object in a SocketChannel in Java. By establishing a server-client architecture, we demonstrated the efficient use of serialization and deserialization to exchange objects over a network. The implementation of a fixed thread pool helps manage multiple client connections concurrently, ensuring that the server remains performant while handling several requests.
11. Download the Source Code
This article explored how to send and receive a serialized object in a SocketChannel using Java.
You can download the full source code of this example here: java send receive serialized object in socketchannel