1.簡介
前面一篇文章講了檔案通道,本文繼續來說說另一種類型的通道 -- 套接字通道。在展開說明之前,咱們先來聊聊套接字的由來。套接字即 socket,最早由伯克利大學的研究人員開發,是以經常被稱為
Berkeley sockets
。UNIX 4.2BSD 核心版本中加入了 socket 的實作,此後,很多作業系統都提供了自己的 socket 接口實作。通過 socket 接口,我們就可以與不同位址的計算機實作通信。
如果大家使用過 Unix/Linux 系統下的 socket 接口,那麼對 socket 程式設計的過程應該有一些了解。對于 TCP 服務端,接口調用的順序為
socket() -> bind() -> listen() -> accept() -> 其他操作 -> close()
,用戶端的順序為
socket() -> connect() -> 其他操作 -> close()
。如下圖所示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZlBnaukjN3Y2M4kTY2QWNmNmM3gTZkRTZlJDNwMTN3QWM0gTYfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.jpeg)
* 圖檔來源于《深入了解計算機系統》
如上所示,直接調用作業系統 socket 相關接口還是比較麻煩的。是以我們的 Java 語言對上面的步驟進行了封裝,友善使用。比如我們今天要講的套接字通道就比原生的接口好用的多。好了,關于 socket 的簡介先說到這,接下進入正題吧。
2 通道類型
Java 套接字通道包含三種類型,分别是
類型 | 說明 |
---|---|
DatagramChannel | UDP 網絡套接字通道 |
SocketChannel | TCP 網絡套接字通道 |
ServerSocketChannel | TCP 服務端套接字通道 |
Java 套接字通道類型對應于兩種通信協定 TCP 和 UDP,這個大家應該都知道。本文将介紹 TCP 網絡套接字通道的使用,并在最後實作一個簡單的聊天功能。至于 UDP 類型的通道,大家可以自己看看。
3.基本操作
3.1 打開通道
SocketChannel 和 ServerSocketChannel 都是抽象類,是以不能直接通過構造方法建立通道。這兩個類均是使用 open 方法建立通道,如下:
SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
3.2 關閉通道
SocketChannel 和 ServerSocketChannel 均提供了 close 方法,用于關閉通道。示例如下:
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 中讀取資料。示例如下:
ByteBuffer buffer = ByteBuffer.allocate(32);
int num = socketChannel.read(buffer);
寫操作
讀取資料使用的是 read 方法,那麼寫入自然也就是 write 方法了。NIO 通道是面向緩沖的,是以向管道中寫入資料也需要和緩沖區配合才行。示例如下
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
,我們來看一下該方法的使用示例:
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 執行個體示範
本節用一個簡單的例子來示範套接字通道的使用,這個例子示範了一個用戶端與服務端互相聊天的場景。首先服務端會監聽某個端口,等待用戶端來連接配接。用戶端連接配接後,由用戶端先向服務端發送消息,然後服務端再回複一條消息。這樣,用戶端和服務端就能你一句我一句的聊起來了。背景先介紹到這,我們來看看代碼實作吧,首先看看服務端的代碼:
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();
}
}
上面的代碼基本上進行了逐漸注釋,應該不難了解,這裡就不啰嗦了。上面有兩個方法沒有貼代碼,就是
sendMsg
和
recvMsg
,由于通用操作,在下面的用戶端代碼裡也可以使用,是以這裡做了封裝。封裝代碼如下:
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);
}
}
工具類的代碼比較簡單,沒什麼好說的。接下來再來看看用戶端的代碼。
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 的同時,還應該去了解底層的一些東西,這樣才算是知其然,又知其是以然。
好了,本文到這裡就結束了,有錯誤的地方歡迎大家指出來。最後謝謝大家的閱讀,祝周末愉快。
參考
- 《深入了解計算機系統》
- https://www.zhihu.com/question/27991975/answer/69041973
- https://zhuanlan.zhihu.com/p/27365009
本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處
作者:coolblog
本文同步釋出在我的個人部落格:
http://www.coolblog.xyz/?r=cb
本作品采用
知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定進行許可。