1. 程式人生 > >Java NIO之套接字通道

Java NIO之套接字通道

1.簡介

前面一篇文章講了檔案通道,本文繼續來說說另一種型別的通道 – 套接字通道。在展開說明之前,咱們先來聊聊套接字的由來。套接字即 socket,最早由伯克利大學的研究人員開發,所以經常被稱為Berkeley sockets。UNIX 4.2BSD 核心版本中加入了 socket 的實現,此後,很多作業系統都提供了自己的 socket 介面實現。通過 socket 介面,我們就可以與不同地址的計算機實現通訊。

如果大家使用過 Unix/Linux 系統下的 socket 介面,那麼對 socket 程式設計的過程應該有一些瞭解。對於 TCP 服務端,介面呼叫的順序為socket() -> bind() -> listen() -> accept() -> 其他操作 -> close()

,客戶端的順序為socket() -> connect() -> 其他操作 -> close()。如下圖所示:


* 圖片來源於《深入理解計算機系統》

如上所示,直接呼叫作業系統 socket 相關介面還是比較麻煩的。所以我們的 Java 語言對上面的步驟進行了封裝,方便使用。比如我們今天要講的套接字通道就比原生的介面好用的多。好了,關於 socket 的簡介先說到這,接下進入正題吧。

 2 通道型別

Java 套接字通道包含三種類型,分別是

型別 說明
DatagramChannel UDP 網路套接字通道
SocketChannel TCP 網路套接字通道
ServerSocketChannel TCP 服務端套接字通道

Java 套接字通道型別對應於兩種通訊協議 TCP 和 UDP,這個大家應該都知道。本文將介紹 TCP 網路套接字通道的使用,並在最後實現一個簡單的聊天功能。至於 UDP 型別的通道,大家可以自己看看。

 3.基本操作

 3.1 開啟通道

SocketChannel 和 ServerSocketChannel 都是抽象類,所以不能直接通過構造方法建立通道。這兩個類均是使用 open 方法建立通道,如下:

1
2
SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

 3.2 關閉通道

SocketChannel 和 ServerSocketChannel 均提供了 close 方法,用於關閉通道。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80));
// do something...
socketChannel.close();

/*******************************************************************/

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
SocketChannel socketChannel = serverSocketChannel.accept();
// do something...
socketChannel.close();
serverSocketChannel.close();

 3.3 讀寫操作

 讀操作

通過使用 SocketChannel 的 read 方法,並配合 ByteBuffer 位元組緩衝區,即可以從 SocketChannel 中讀取資料。示例如下:

1
2
ByteBuffer buffer = ByteBuffer.allocate(32);
int num = socketChannel.read(buffer);

 寫操作

讀取資料使用的是 read 方法,那麼寫入自然也就是 write 方法了。NIO 通道是面向緩衝的,所以向管道中寫入資料也需要和緩衝區配合才行。示例如下

1
2
3
4
5
6
7
8
String data = "Test data..."

ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.clear();
buffer.put(data.getBytes());

bbuffer.flip();
channel.write(buffer);

 3.4 非阻塞模式

與檔案通道不同,套接字通道可以執行在非阻塞模式下。在此模式下,呼叫 connect(),read() 和 write() 等方法時,程序/執行緒會立即返回。設定非阻塞模式的方法為configureBlocking,我們來看一下該方法的使用示例:

1
2
3
4
5
6
7
8
9
10
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("www.coolblog.xyz", 80));

// 這裡要迴圈檢測是否已經連線上
while(!socketChannel.finishConnect()){
    // do something
}

// 連線建立起來後,才能進行讀取或寫入操作

由於在非阻塞模式下,呼叫 connect 方法會立即返回。如果在連線未建立起來的情況下,從管道中讀取,或向管道寫入資料,會觸發 NotYetConnectedException 異常。所以要進行迴圈檢測,以保證連線完成建立。如果程式碼按照上面那樣去寫,會引發另外一個問題。非阻塞模式雖然不會阻塞執行緒,但是在方法返回後,還要進行迴圈檢測,執行緒實際上還是被阻塞。出現這個問題的原因是和 Java NIO 套接字通道的 IO 模型有關,套接字通道採用的是“同步非阻塞”式 IO 模型,使用者發起一個 IO 操作後,即可去做其他事情,不用等待 IO 完成。但是 IO 是否已完成,則需要使用者自己時不時的去檢測,這樣實際上還是會浪費 CPU 資源。

關於 IO 模型相關的知識,大家可以參考我之前的一篇文章I/O模型簡述 ,這裡不再贅述。另外,大家還需要去參考一下權威資料《UNIX網路程式設計卷 第1卷:套介面API》第6章關於 IO 模型的介紹,那一章除了對5種 IO 模型進行了介紹,還介紹了同步與非同步的概念,值得一讀。好了,本節就先說到這裡。

 3.5 例項演示

本節用一個簡單的例子來演示套接字通道的使用,這個例子演示了一個客戶端與服務端互相聊天的場景。首先服務端會監聽某個埠,等待客戶端來連線。客戶端連線後,由客戶端先向服務端傳送訊息,然後服務端再回復一條訊息。這樣,客戶端和服務端就能你一句我一句的聊起來了。背景先介紹到這,我們來看看程式碼實現吧,首先看看服務端的程式碼:

1
2
3
4
5
6
7
8
9
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
57
58
59
60
61
62
package wetalk;

import static wetalk.WeTalkUtils.recvMsg;
import static wetalk.WeTalkUtils.sendMsg;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * WeTalk 服務端
 * @author coolblog.xyz
 * @date 2018-03-22 12:43:26
 */
public class WeTalkServer {

    private static final String EXIT_MARK = "exit";

    private int port;

    WeTalkServer(int port) {
        this.port = port;
    }

    public void start() throws IOException {
        // 建立服務端套接字通道,監聽埠,並等待客戶端連線
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(port));
        System.out.println("服務端已啟動,正在監聽 " + port + " 埠......");
        SocketChannel channel = ssc.accept();
        System.out.println("接受來自" + channel.getRemoteAddress().toString().replace("/", "") + " 請求");

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 等待並接收客戶端傳送的訊息
            String msg = recvMsg(channel);
            System.out.println("\n客戶端:");
            System.out.println(msg + "\n");

            // 輸入資訊
            System.out.println("請輸入:");
            msg = sc.nextLine();
            if (EXIT_MARK.equals(msg)) {
                sendMsg(channel, "bye~");
                break;
            }

            // 回覆客戶端訊息
            sendMsg(channel, msg);
        }
        
        // 關閉通道
        channel.close();
        ssc.close();
    }

    public static void main(String[] args) throws IOException {
        new WeTalkServer(8080).start();
    }
}

上面的程式碼基本上進行了逐步註釋,應該不難理解,這裡就不囉嗦了。上面有兩個方法沒有貼程式碼,就是sendMsgrecvMsg,由於通用操作,在下面的客戶端程式碼裡也可以使用,所以這裡做了封裝。封裝程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package wetalk;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * 工具類
 *
 * @author coolblog.xyz
 * @date 2018-03-22 13:13:41
 */
public class WeTalkUtils {

    private static final int BUFFER_SIZE = 128;

    public static void sendMsg(SocketChannel channel, String msg) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        buffer.put(msg.getBytes());
        buffer.flip();
        channel.write(buffer);
    }

    public static String recvMsg(SocketChannel channel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        channel.read(buffer);

        buffer.flip();
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        return new String(bytes);
    }
}

工具類的程式碼比較簡單,沒什麼好說的。接下來再來看看客戶端的程式碼。

1
2
3
4
5
6
7
8
9
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
57
58
59
60
package wetalk;

import static wetalk.WeTalkUtils.recvMsg;
import static wetalk.WeTalkUtils.sendMsg;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * WeTalk 客戶端
 * @author coolblog.xyz
 * @date 2018-03-22 12:38:21
 */
public class WeTalkClient {

    private static final String EXIT_MARK = "exit";

    private String hostname;

    private int port;

    WeTalkClient(String hostname, int port) {
        this.hostname = hostname;
        this.port = port;
    }

    public void start() throws IOException {
        // 開啟一個套接字通道,並向服務端發起連線
        SocketChannel channel = SocketChannel.open();
        channel.connect(new InetSocketAddress(hostname, port));

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 輸入資訊
            System.out.println("請輸入:");
            String msg = sc.nextLine();
            if (EXIT_MARK.equals(msg)) {
                sendMsg(channel, "bye~");
                break;
            }
            
            // 向服務端傳送訊息
            sendMsg(channel, msg);
            
            // 接受服務端返回的訊息
            msg = recvMsg(channel);
            System.out.println("\n服務端:");
            System.out.println(msg + "\n");
        }
        
        // 關閉通道
        channel.close();
    }

    public static void main(String[] args) throws IOException {
        new WeTalkClient("localhost", 8080).start();
    }
}

客戶端做的事情也比較簡單,首先是開啟通道,然後連線服務單。緊接著進入 while 迴圈,然後就可以和服務端愉快的聊天了。

上面的程式碼和敘述都沒啥意思,最後我們還是來看看上面程式碼的執行效果,一圖勝前言。

 4.總結

到這裡,關於套接字通道的相關內容就講完了,不知道大家有沒有看懂。本文僅從使用的角度分析了套接字通道的用法,至於套接字通道的實現,這並不是本文關注的重點。實際上,我在上一篇文章中就說過,Java 所提供的很多類實際上是對作業系統層面上一些系統呼叫做了一層包裝。所以大家在學習 Java 的同時,還應該去了解底層的一些東西,這樣才算是知其然,又知其所以然。

好了,本文到這裡就結束了,有錯誤的地方歡迎大家指出來。最後謝謝大家的閱讀,祝週末愉快。

 參考

from: http://www.tianxiaobo.com/2018/03/25/Java-NIO%E4%B9%8B%E5%A5%97%E6%8E%A5%E5%AD%97%E9%80%9A%E9%81%93/