Core Java

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:

Screenshot showing the Server output for Java sending and receiving a serialized object via SocketChannel.
Server Output: Confirmation that the client connected and the object was successfully received and deserialized.

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.

Download
You can download the full source code of this example here: java send receive serialized object in socketchannel

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button