在套接字通道中傳送和接收序列化對象
1. 簡介
在本教程中,我們將探討如何使用java.nio
套件中的 Java SocketChannel
發送和接收序列化物件。這種方法可以實現客戶端和伺服器之間的高效、無阻塞的網路通訊。
2. 理解序列化
序列化是將物件轉換為位元組流的過程,以便透過網路傳輸或儲存在檔案中。當與套接字通道結合時,序列化可以實現應用程式之間複雜資料結構的無縫傳輸。對於必須透過網路交換物件的分散式系統來說,這種技術至關重要。
2.1. Java 序列化中的關鍵類
ObjectOutputStream
和ObjectInputStream
類別是 Java 序列化中不可或缺的。它們處理物件和位元組流之間的轉換:
- **
ObjectOutputStream
**用於將物件序列化為位元組序列。例如,當透過網路傳送 Message 物件時,ObjectOutputStream
會將物件的欄位和元資料寫入輸出流。 - **
ObjectInputStream
**從接收端的位元組流重建物件。
3. 理解套接字通道
套接字通道是 Java NIO
套件的一部分,它為傳統的基於套接字的通訊提供了一個靈活、可擴展的替代方案。它們支援阻塞和非阻塞模式,使其適用於高效處理多個連接至關重要的高效能網路應用程式。
套接字通道對於建立客戶端-伺服器通訊系統至關重要,其中客戶端可以透過 TCP/IP 連接到伺服器。透過使用SocketChannel
,我們可以實現非同步通信,從而獲得更好的性能和更低的延遲。
3.1.套接字通道的關鍵組件
套接字通道有三個關鍵組件:
-
**ServerSocketChannel** :
監聽傳入的 TCP 連線。它綁定到特定連接埠並等待客戶端連接 -
**SocketChannel** :
表示客戶端與伺服器之間的連線。它支援阻塞和非阻塞模式 -
**Selector** :
用於用單一執行緒監視多個套接字通道。它有助於處理諸如傳入連線或資料可讀之類的事件,從而減少每個連線都有專用執行緒的開銷。
4. 設定伺服器和客戶端
在實作伺服器和客戶端之前,讓我們先定義一個我們想要透過套接字傳送的範例物件。在 Java 中,物件必須實作Serializable
介面才能轉換為位元組流,這對於透過網路連接傳輸物件是必要的。
4.1.建立可序列化物件
讓我們編寫MyObject
類,作為我們將透過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;
}
}
MyObject
類別實作了Serializable
接口,這是將物件轉換為位元組流並透過套接字連接傳輸所必需的。
4.2.實作伺服器
在伺服器端,我們將使用ServerSocketChannel
來監聽傳入的客戶端連線並處理接收到的序列化物件:
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
}
伺服器在連接埠6000
上監聽傳入的客戶端連線。一旦接受客戶端,它將等待接收對象。
4.3.實現客戶端
客戶端將建立MyObject
的實例,將其序列化,然後將其傳送至伺服器。我們使用SocketChannel
連接伺服器並傳輸物件:
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
}
此程式碼連接到在localhost
6000
連接埠上執行的伺服器,它將序列化物件傳送到伺服器。
5. 序列化並發送對象
要透過SocketChannel
發送對象,我們首先需要將其序列化為位元組數組。因為SocketChannel
只能與ByteBuffer
搭配使用,所以我們需要將物件轉換為位元組數組,並將其包裝在ByteBuffer
中,然後再透過網路傳送:
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);
}
}
這裡我們先將MyObject
序列化為一個位元組數組,然後將其包裝成一個ByteBuffer
,並將其寫入套接字通道。然後,我們從客戶端發送物件:
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
}
在此範例中,用戶端連接到伺服器並傳送一個包含名稱「 Alice
」和年齡25
序列化MyObject
。
6. 接收和反序列化對象
在伺服器端,我們從SocketChannel
讀取位元組並將其反序列化為MyObject
實例:
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();
}
}
我們從SocketChannel
中將位元組讀入ByteBuffer
,將它們儲存在ByteArrayOutputStream
中,然後將位元組數組反序列化為原始物件。然後,我們就可以在伺服器上接收該物件了:
try (SocketChannel clientSocket = serverSocket.accept()) {
MyObject receivedObject = receiveObject(clientSocket);
logger.info("Received Object - Name: " + receivedObject.getName());
}
7. 處理多個客戶端
為了同時處理多個客戶端,我們可以使用Selector
以非阻塞模式管理多個套接字通道。這可確保伺服器可以同時處理多個連線而不會阻塞任何單一連線:
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());
}
}
}
}
}
在這個範例中, configureBlocking(false)
將伺服器設定為非阻塞模式,這表示像accept()
和read()
這樣的操作不會在等待事件時阻塞執行。這使得伺服器可以繼續處理其他任務,而不是等待客戶端連線。
接下來,我們使用Selector
來監聽多個頻道上的事件。它會偵測何時有新的連線(OP_ACCEPT)或傳入資料(OP_READ)可用並進行相應的處理,確保通訊順暢且可擴展。
8.測試用例
讓我們驗證透過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();
}
}
此測試驗證序列化和反序列化過程是否透過SocketChannel
正確運作。
9. 結論
在本文中,我們示範如何使用 Java NIO 的SocketChannel
來設定客戶端伺服器系統來傳送和接收序列化物件。透過使用序列化和非阻塞 I/O,我們可以透過網路在系統之間有效地傳輸複雜的資料結構。
與往常一樣,原始碼可在 GitHub 上取得。