1 簡介
NIO(Non-Blocking I/O或叫New I/O)是一種同步非阻塞的I/O模型,主要用於服務端解決高併發或者大量連接的情況的IO處理。它是JDK1.4中引入的,位於java.nio包中,主要用於彌補原來同步阻塞I/O(Blocking I/O或叫BIO)的不足。在NIO出現之前大多服務端主要使用BIO通過新建線程的方式來解決併發請求,如上一篇博文《Android網絡編程(十三) 之 Socket和長連接》中的長連接Demo,在每個客戶端請求連接後都會創建一個新的Socket對象並內部創建線程來處理相關連接,這樣就很容易因線程瓶頸而造成很多限制。
NIO在處理讀寫是採用了內存映射文件的方式,它基於通道(Channel)和緩衝區(Buffer)進行操作,數據從通道讀取到緩衝區或者從緩衝區寫入到通道,再通過選擇器(Selector)進行監聽多個通道的事件,所以區別於BIO的面向流方式,NIO可更加高效地進行文件的讀寫操作。
2 NIO的組件
2.1 Buffer(緩衝區)
BIO的操作是面向數據流的讀寫,而NIO所有的數據都是用Buffer緩衝區處理的,緩衝區其實就是一塊連續的內存空間,這塊內存空間就像一個數據容器般,可以重複的讀取數據和寫入數據。
2.2 Channel(通道)
Channel通道跟BIO中的Stream類似,都是用於跟連接的對象進行IO操作。它們區別於,Stream是阻塞的單向操作的,即要麼讀要麼寫,比如InputStream和OutputStream;而Channel是非阻塞且是線程安全的雙向操作的,通過一個Channel既可以進行讀也可進行寫操作,其所有數據都是映射到內存中通過Buffer來處理
2.3 Selector(選擇器)
在BIO中當一個Server端連接着多個Client端時,Server端會爲其創建一個線程來提升併發吞吐量,但是一旦併發量上升就會出現明顯的弊端。在這情況Selector的優勢就出現了。Selector叫做選擇器,或者叫做多路複用器,Selector運行在單個線程中但可同時管理一個或多個Channel。它通過不斷地輪詢進行Channel的狀態的檢查處理其連接、讀、寫等操作。意味着可以使用更少的線程來處理多個Client端的請求,避免了使用線程的開銷。
3 Socket與NIO
我們還是用一個簡單的Demo來實現一個Socket,不過這次使用了NIO的方式。Demo中服務端在App的Service中進行,而客戶端在App的Activity中進行,爲了展示出服務端可以同時接收多個客戶端,Activity的界面特意做了兩套客戶端,如下圖所示。
3.1 服務端代碼
TCPServerService.java
public class TCPServerService extends Service {
public final static int SERVER_PORT = 9527; // 跟客戶端絕定的端口
private TCPServer mTCPServer;
@Override
public void onCreate() {
super.onCreate();
initTcpServer();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
unInitTcpServer();
}
/**
* 初始化TCP服務
*/
private void initTcpServer() {
new Thread(new Runnable() {
@Override
public void run() {
mTCPServer = new TCPServer();
mTCPServer.init();
}
}).start();
}
/**
* 反初始化TCP服務
*/
private void unInitTcpServer() {
mTCPServer.close();
}
}
服務端的實現在TCPServerService中,TCPServerService服務啓動後,便創建一個線程來創建一個TCPServer對象並執行初始化。
TCPServer.java
public class TCPServer {
private final static String TAG = "TCPServer----------";
private String mSendMsg;
public final static int SERVER_PORT = 9527; // 跟客戶端約定的端口
private Selector mSelector;
public void init() {
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = ServerSocketChannel.open();
// 設置非阻塞
serverSocketChannel.configureBlocking(false);
// 獲取與此Channel關聯的ServerSocket並綁定端口
serverSocketChannel.socket().bind(new InetSocketAddress(SERVER_PORT));
// 註冊到Selector,等待連接
mSelector = Selector.open();
serverSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT);
while (mSelector != null && mSelector.isOpen()) {
// 選擇一組對應Channel已準備好進行I/O的Key
int select = mSelector.select();
if (select <=0) {
continue;
}
// 獲得Selector已選擇的Keys
Set<SelectionKey> selectionKeys = mSelector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 移除當前的key
iterator.remove();
if (selectionKey.isValid() && selectionKey.isAcceptable()) {
handleAccept(selectionKey);
}
if (selectionKey.isValid() && selectionKey.isReadable()) {
handleRead(selectionKey);
}
if (selectionKey.isValid() && selectionKey.isWritable()) {
handleWrite(selectionKey);
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (mSelector != null) {
mSelector.close();
mSelector = null;
}
if (serverSocketChannel != null) {
serverSocketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleAccept(SelectionKey selectionKey) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// 註冊讀就緒事件
client.register(mSelector, SelectionKey.OP_READ);
Log.d(TAG, "服務端 已經跟 客戶端(" + client.getRemoteAddress() + ") 連接上");
}
private void handleRead(SelectionKey selectionKey) throws IOException {
SocketChannel client = (SocketChannel) selectionKey.channel();
//讀取服務器發送來的數據到緩衝區中
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(byteBuffer);
if (bytesRead > 0) {
String inMsg = new String(byteBuffer.array(), 0, bytesRead);
// 處理數據
processMsg(selectionKey, inMsg);
}
else {
Log.d(TAG, "服務端 收到 客戶端(" + client.getRemoteAddress() + ") 斷開請求");
client.close();
}
}
private void handleWrite(SelectionKey selectionKey) throws IOException {
if (TextUtils.isEmpty(mSendMsg)) {
return;
}
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
sendBuffer.put(mSendMsg.getBytes());
sendBuffer.flip();
client.write(sendBuffer);
mSendMsg = null;
client.register(mSelector, SelectionKey.OP_READ);
}
/**
* 處理數據
*
* @param selectionKey
* @param inMsg
* @throws IOException
*/
private void processMsg(SelectionKey selectionKey, String inMsg) throws IOException {
SocketChannel client = (SocketChannel) selectionKey.channel();
Log.d(TAG, "服務端 收到 客戶端(" + client.getRemoteAddress() + ") 數據:" + inMsg);
// 估計1億的AI代碼
String outMsg = inMsg;
outMsg = outMsg.replace("嗎", "");
outMsg = outMsg.replace("?", "!");
outMsg = outMsg.replace("?", "!");
sendMsg(selectionKey, outMsg);
}
/**
* 發送數據
*
* @param selectionKey
* @param msg
* @throws IOException
*/
public void sendMsg(SelectionKey selectionKey, String msg) throws IOException {
mSendMsg = msg;
SocketChannel client = (SocketChannel) selectionKey.channel();
client.register(mSelector, SelectionKey.OP_WRITE);
Log.d(TAG, "服務端 回覆 客戶端(" + client.getRemoteAddress() + ") 發送數據:" + msg);
}
/**
* 斷開連接
*/
public void close() {
try {
Log.d(TAG, "服務端中斷所有連接");
mSelector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCPServer類核心代碼就是init方法,可見方法內存在ServerSocketChannel和Selector,它們便是我們上面介紹的通道和選擇器。除此外還有一個SelectionKey,它是用於維護Channel和Selector的對應關係。
SelectionKey裏頭有四個常量:SelectionKey.OP_CONNECT、SelectionKey.OP_ACCEPT、SelectionKey.OP_READ、SelectionKey.OP_WRITE,它們表示Channel註冊到Selectort感興趣的事件。對應selectionKey.isConnectable()、selectionKey.isAcceptable()、selectionKey.isReadable()、selectionKey.isWritable()方法會返回true,所以可以理解成,主要註冊了相應的事件,上述循環中便會執行相應返回true的動作。
3.2 客戶端代碼
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TCPClient mTcpClient1;
private TCPClient mTcpClient2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent service = new Intent(this, TCPServerService.class);
startService(service);
mTcpClient1 = new TCPClient("客戶端A");
mTcpClient2 = new TCPClient("客戶端B");
Button btnConnection1 = findViewById(R.id.btn_connection1);
btnConnection1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient1.connectServer();
}
});
Button btnSend1 = findViewById(R.id.btn_send1);
btnSend1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient1.sendMsg("你好嗎?");
}
});
Button btnDisconnect1 = findViewById(R.id.btn_disconnect1);
btnDisconnect1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient1.disconnectService();
}
});
Button btnConnection2 = findViewById(R.id.btn_connection2);
btnConnection2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient2.connectServer();
}
});
Button btnSend2 = findViewById(R.id.btn_send2);
btnSend2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient2.sendMsg("喫飯了嗎?");
}
});
Button btnDisconnect2 = findViewById(R.id.btn_disconnect2);
btnDisconnect2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTcpClient2.disconnectService();
}
});
}
}
客戶端的實現在MainActivity中,MainActivity主要是創建了兩個TCPClient對象,然後對應界面中的按鈕作相應的邏輯。
TCPClient.java
public class TCPClient {
private static final String TAG = "TCPClient**********";
private String mSendMsg;
private Selector mSelector;
private SocketChannel mSocketChannel;
private String mClientName; // 客戶端命名
public TCPClient(String clientName) {
mClientName = clientName;
}
/**
* 連接服務端
*/
public void connectServer() {
new Thread(new Runnable() {
@Override
public void run() {
init();
}
}).start();
}
public void init() {
try {
mSocketChannel = SocketChannel.open();
// 設置爲非阻塞方式
mSocketChannel.configureBlocking(false);
// 連接服務端地址和端口
mSocketChannel.connect(new InetSocketAddress("127.0.0.1", TCPServerService.SERVER_PORT));
// 註冊到Selector,請求連接
mSelector = Selector.open();
mSocketChannel.register(mSelector, SelectionKey.OP_CONNECT);
while (mSelector != null && mSelector.isOpen()) {
// 選擇一組對應Channel已準備好進行I/O的Key
int select = mSelector.select();
if (select <=0) {
continue;
}
Set<SelectionKey> selectionKeys = mSelector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 移除當前的key
iterator.remove();
if (selectionKey.isValid() && selectionKey.isConnectable()) {
handleConnect();
}
if (selectionKey.isValid() && selectionKey.isReadable()) {
handleRead();
}
if (selectionKey.isValid() && selectionKey.isWritable()) {
handleWrite();
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (mSelector != null) {
mSelector.close();
mSelector = null;
}
if (mSocketChannel != null) {
mSocketChannel.close();
mSocketChannel = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleConnect() throws IOException {
// 判斷此通道上是否正在進行連接操作。
if (mSocketChannel.isConnectionPending()) {
mSocketChannel.finishConnect();
mSocketChannel.register(mSelector, SelectionKey.OP_READ);
Log.d(TAG, mClientName + " 已經跟服務端連接上");
}
}
private void handleRead() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int bytesRead = mSocketChannel.read(byteBuffer);
if (bytesRead > 0) {
String inMsg = new String(byteBuffer.array(), 0, bytesRead);
Log.d(TAG, mClientName + " 收到 服務端 數據: " + inMsg);
} else {
mSocketChannel.close();
}
}
private void handleWrite() throws IOException {
if (TextUtils.isEmpty(mSendMsg)) {
return;
}
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
sendBuffer.put(mSendMsg.getBytes());
sendBuffer.flip();
mSocketChannel.write(sendBuffer);
Log.d(TAG, "--------------------------------------");
Log.d(TAG, mClientName + " 發送數據: " + mSendMsg);
mSendMsg = null;
mSocketChannel.register(mSelector, SelectionKey.OP_READ);
}
/**
* 發送數據
*
* @param msg
* @throws IOException
*/
public void sendMsg(String msg) {
if (mSelector == null || !mSelector.isOpen() || mSocketChannel == null || !mSocketChannel.isOpen()) {
return;
}
try {
mSendMsg = msg;
mSocketChannel.register(mSelector, SelectionKey.OP_WRITE);
// 進行呼醒,因爲在int select = mSelector.select();中阻塞住了
mSelector.wakeup();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 斷開連接
*/
public void disconnectService() {
if (mSelector == null || !mSelector.isOpen() || mSocketChannel == null || !mSocketChannel.isOpen()) {
return;
}
try {
Log.d(TAG, "--------------------------------------");
Log.d(TAG, mClientName + " 主動斷開跟 服務端 連接");
if (mSelector != null) {
mSelector.close();
mSelector = null;
}
if (mSocketChannel != null) {
mSocketChannel.close();
mSocketChannel = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCPClient類對外就是對應兩種按鈕事件:連接服務端、斷開連接,基本上跟服務端TCPServer類的邏輯很像。
3.3 輸出日誌
運行程序後,相應執行連接和斷開按鈕會能輸出以下日誌:
2020-03-03 17:45:15.967 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:15.968 30533-30533/com.zyx.myapplication I/Choreographer: Skipped 4 frames! The application may be doing too much work on its main thread.
2020-03-03 17:45:15.974 30533-30601/com.zyx.myapplication D/FlymeTrafficTracking: tag (69) com.zyx.myapplication Thread-4 uid 10238
2020-03-03 17:45:15.976 30533-30601/com.zyx.myapplication D/TCPClient**********: 客戶端A 已經跟服務端連接上
2020-03-03 17:45:15.976 30533-30580/com.zyx.myapplication D/TCPServer----------: 服務端 已經跟 客戶端(/127.0.0.1:41996) 連接上
2020-03-03 17:45:17.816 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:17.819 30533-30601/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-03-03 17:45:17.819 30533-30601/com.zyx.myapplication D/TCPClient**********: 客戶端A 發送數據: 你好嗎?
2020-03-03 17:45:17.820 30533-30580/com.zyx.myapplication D/TCPServer----------: 服務端 收到 客戶端(/127.0.0.1:41996) 數據:你好嗎?
2020-03-03 17:45:17.820 30533-30580/com.zyx.myapplication D/TCPServer----------: 服務端 回覆 客戶端(/127.0.0.1:41996) 發送數據:你好!
2020-03-03 17:45:17.822 30533-30601/com.zyx.myapplication D/TCPClient**********: 客戶端A 收到 服務端 數據: 你好!
2020-03-03 17:45:20.020 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:20.020 30533-30533/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-03-03 17:45:20.021 30533-30533/com.zyx.myapplication D/TCPClient**********: 客戶端A 主動斷開跟 服務端 連接
2020-03-03 17:45:20.021 30533-30601/com.zyx.myapplication D/FlymeTrafficTracking: untag(69) com.zyx.myapplication Thread-4 uid 10238 4047ms
2020-03-03 17:45:20.023 30533-30580/com.zyx.myapplication D/TCPServer----------: 服務端 收到 客戶端(/127.0.0.1:41996) 斷開請求
4 總結
好了,到此Socket的使用包括長連接、NIO都已通過上篇和本篇博文介紹完畢,有興趣的朋友可以將兩篇文章中的兩個Demo結合來搭建一個屬於自己長連接框架。點擊下載Demo