Java NIO之套接字通道
1.簡(jiǎn)介
前面一篇文章講了文件通道,本文繼續(xù)來(lái)說(shuō)說(shuō)另一種類型的通道 – 套接字通道。在展開(kāi)說(shuō)明之前,咱們先來(lái)聊聊套接字的由來(lái)。套接字即 socket,最早由伯克利大學(xué)的研究人員開(kāi)發(fā),所以經(jīng)常被稱為Berkeley sockets。UNIX 4.2BSD 內(nèi)核版本中加入了 socket 的實(shí)現(xiàn),此后,很多操作系統(tǒng)都提供了自己的 socket 接口實(shí)現(xiàn)。通過(guò) socket 接口,我們就可以與不同地址的計(jì)算機(jī)實(shí)現(xiàn)通信。
如果大家使用過(guò) Unix/Linux 系統(tǒng)下的 socket 接口,那么對(duì) socket 編程的過(guò)程應(yīng)該有一些了解。對(duì)于 TCP 服務(wù)端,接口調(diào)用的順序?yàn)閟ocket() -> bind() -> listen() -> accept() -> 其他操作 -> close(),客戶端的順序?yàn)閟ocket() -> connect() -> 其他操作 -> close()。如下圖所示:
* 圖片來(lái)源于《深入理解計(jì)算機(jī)系統(tǒng)》
如上所示,直接調(diào)用操作系統(tǒng) socket 相關(guān)接口還是比較麻煩的。所以我們的 Java 語(yǔ)言對(duì)上面的步驟進(jìn)行了封裝,方便使用。比如我們今天要講的套接字通道就比原生的接口好用的多。好了,關(guān)于 socket 的簡(jiǎn)介先說(shuō)到這,接下進(jìn)入正題吧。
?2 通道類型
Java 套接字通道包含三種類型,分別是
| DatagramChannel | UDP 網(wǎng)絡(luò)套接字通道 |
| SocketChannel | TCP 網(wǎng)絡(luò)套接字通道 |
| ServerSocketChannel | TCP 服務(wù)端套接字通道 |
Java 套接字通道類型對(duì)應(yīng)于兩種通信協(xié)議 TCP 和 UDP,這個(gè)大家應(yīng)該都知道。本文將介紹 TCP 網(wǎng)絡(luò)套接字通道的使用,并在最后實(shí)現(xiàn)一個(gè)簡(jiǎn)單的聊天功能。至于 UDP 類型的通道,大家可以自己看看。
?3.基本操作
?3.1 打開(kāi)通道
SocketChannel 和 ServerSocketChannel 都是抽象類,所以不能直接通過(guò)構(gòu)造方法創(chuàng)建通道。這兩個(gè)類均是使用 open 方法創(chuàng)建通道,如下:
| 1 2 | SocketChannel socketChannel = SocketChannel.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); |
?3.2 關(guān)閉通道
SocketChannel 和 ServerSocketChannel 均提供了 close 方法,用于關(guān)閉通道。示例如下:
| 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 讀寫(xiě)操作
?讀操作
通過(guò)使用 SocketChannel 的 read 方法,并配合 ByteBuffer 字節(jié)緩沖區(qū),即可以從 SocketChannel 中讀取數(shù)據(jù)。示例如下:
| 1 2 | ByteBuffer buffer = ByteBuffer.allocate(32); int num = socketChannel.read(buffer); |
?寫(xiě)操作
讀取數(shù)據(jù)使用的是 read 方法,那么寫(xiě)入自然也就是 write 方法了。NIO 通道是面向緩沖的,所以向管道中寫(xiě)入數(shù)據(jù)也需要和緩沖區(qū)配合才行。示例如下
| 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 非阻塞模式
與文件通道不同,套接字通道可以運(yùn)行在非阻塞模式下。在此模式下,調(diào)用 connect(),read() 和 write() 等方法時(shí),進(jìn)程/線程會(huì)立即返回。設(shè)置非阻塞模式的方法為configureBlocking,我們來(lái)看一下該方法的使用示例:
| 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));// 這里要循環(huán)檢測(cè)是否已經(jīng)連接上 while(!socketChannel.finishConnect()){// do something }// 連接建立起來(lái)后,才能進(jìn)行讀取或?qū)懭氩僮? |
由于在非阻塞模式下,調(diào)用 connect 方法會(huì)立即返回。如果在連接未建立起來(lái)的情況下,從管道中讀取,或向管道寫(xiě)入數(shù)據(jù),會(huì)觸發(fā) NotYetConnectedException 異常。所以要進(jìn)行循環(huán)檢測(cè),以保證連接完成建立。如果代碼按照上面那樣去寫(xiě),會(huì)引發(fā)另外一個(gè)問(wèn)題。非阻塞模式雖然不會(huì)阻塞線程,但是在方法返回后,還要進(jìn)行循環(huán)檢測(cè),線程實(shí)際上還是被阻塞。出現(xiàn)這個(gè)問(wèn)題的原因是和 Java NIO 套接字通道的 IO 模型有關(guān),套接字通道采用的是“同步非阻塞”式 IO 模型,用戶發(fā)起一個(gè) IO 操作后,即可去做其他事情,不用等待 IO 完成。但是 IO 是否已完成,則需要用戶自己時(shí)不時(shí)的去檢測(cè),這樣實(shí)際上還是會(huì)浪費(fèi) CPU 資源。
關(guān)于 IO 模型相關(guān)的知識(shí),大家可以參考我之前的一篇文章I/O模型簡(jiǎn)述?,這里不再贅述。另外,大家還需要去參考一下權(quán)威資料《UNIX網(wǎng)絡(luò)編程卷 第1卷:套接口API》第6章關(guān)于 IO 模型的介紹,那一章除了對(duì)5種 IO 模型進(jìn)行了介紹,還介紹了同步與異步的概念,值得一讀。好了,本節(jié)就先說(shuō)到這里。
?3.5 實(shí)例演示
本節(jié)用一個(gè)簡(jiǎn)單的例子來(lái)演示套接字通道的使用,這個(gè)例子演示了一個(gè)客戶端與服務(wù)端互相聊天的場(chǎng)景。首先服務(wù)端會(huì)監(jiān)聽(tīng)某個(gè)端口,等待客戶端來(lái)連接。客戶端連接后,由客戶端先向服務(wù)端發(fā)送消息,然后服務(wù)端再回復(fù)一條消息。這樣,客戶端和服務(wù)端就能你一句我一句的聊起來(lái)了。背景先介紹到這,我們來(lái)看看代碼實(shí)現(xiàn)吧,首先看看服務(wù)端的代碼:
| 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 服務(wù)端* @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 {// 創(chuàng)建服務(wù)端套接字通道,監(jiān)聽(tīng)端口,并等待客戶端連接ServerSocketChannel ssc = ServerSocketChannel.open();ssc.socket().bind(new InetSocketAddress(port));System.out.println("服務(wù)端已啟動(dòng),正在監(jiān)聽(tīng) " + port + " 端口......");SocketChannel channel = ssc.accept();System.out.println("接受來(lái)自" + channel.getRemoteAddress().toString().replace("/", "") + " 請(qǐng)求");Scanner sc = new Scanner(System.in);while (true) {// 等待并接收客戶端發(fā)送的消息String msg = recvMsg(channel);System.out.println("\n客戶端:");System.out.println(msg + "\n");// 輸入信息System.out.println("請(qǐng)輸入:");msg = sc.nextLine();if (EXIT_MARK.equals(msg)) {sendMsg(channel, "bye~");break;}// 回復(fù)客戶端消息sendMsg(channel, msg);}// 關(guān)閉通道channel.close();ssc.close();}public static void main(String[] args) throws IOException {new WeTalkServer(8080).start();} } |
上面的代碼基本上進(jìn)行了逐步注釋,應(yīng)該不難理解,這里就不啰嗦了。上面有兩個(gè)方法沒(méi)有貼代碼,就是sendMsg和recvMsg,由于通用操作,在下面的客戶端代碼里也可以使用,所以這里做了封裝。封裝代碼如下:
| 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);} } |
工具類的代碼比較簡(jiǎn)單,沒(méi)什么好說(shuō)的。接下來(lái)再來(lái)看看客戶端的代碼。
| 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 {// 打開(kāi)一個(gè)套接字通道,并向服務(wù)端發(fā)起連接SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress(hostname, port));Scanner sc = new Scanner(System.in);while (true) {// 輸入信息System.out.println("請(qǐng)輸入:");String msg = sc.nextLine();if (EXIT_MARK.equals(msg)) {sendMsg(channel, "bye~");break;}// 向服務(wù)端發(fā)送消息sendMsg(channel, msg);// 接受服務(wù)端返回的消息msg = recvMsg(channel);System.out.println("\n服務(wù)端:");System.out.println(msg + "\n");}// 關(guān)閉通道channel.close();}public static void main(String[] args) throws IOException {new WeTalkClient("localhost", 8080).start();} } |
客戶端做的事情也比較簡(jiǎn)單,首先是打開(kāi)通道,然后連接服務(wù)單。緊接著進(jìn)入 while 循環(huán),然后就可以和服務(wù)端愉快的聊天了。
上面的代碼和敘述都沒(méi)啥意思,最后我們還是來(lái)看看上面代碼的運(yùn)行效果,一圖勝前言。
?4.總結(jié)
到這里,關(guān)于套接字通道的相關(guān)內(nèi)容就講完了,不知道大家有沒(méi)有看懂。本文僅從使用的角度分析了套接字通道的用法,至于套接字通道的實(shí)現(xiàn),這并不是本文關(guān)注的重點(diǎn)。實(shí)際上,我在上一篇文章中就說(shuō)過(guò),Java 所提供的很多類實(shí)際上是對(duì)操作系統(tǒng)層面上一些系統(tǒng)調(diào)用做了一層包裝。所以大家在學(xué)習(xí) Java 的同時(shí),還應(yīng)該去了解底層的一些東西,這樣才算是知其然,又知其所以然。
好了,本文到這里就結(jié)束了,有錯(cuò)誤的地方歡迎大家指出來(lái)。最后謝謝大家的閱讀,祝周末愉快。
?參考
- 《深入理解計(jì)算機(jī)系統(tǒng)》
- 《UNIX網(wǎng)絡(luò)編程卷 第1卷:套接口API》
- https://www.zhihu.com/question/27991975/answer/69041973
- https://zhuanlan.zhihu.com/p/27365009
- 本文鏈接:?https://www.tianxiaobo.com/2018/03/25/Java-NIO之套接字通道/
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/?
總結(jié)
以上是生活随笔為你收集整理的Java NIO之套接字通道的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JAVA NIO之文件通道
- 下一篇: Java NIO之选择器