書接上文
上一篇博客說了一下SelectionKey這個對象的一些常用屬性,爲這一篇文章的NIO服務器實例算是做了一些鋪墊。不過在展示服務端實例代碼之前,還要說幾點細節上的東西。
Selector如何選擇就緒的通道
//這個方法可能會阻塞,直到至少有一個已註冊的事件發生,或者當一個或者更多的事件發生時
selector.select();
NIO明明是非阻塞的IO,爲何會阻塞
這是第一章Java NIO學習(一)NIO相關概念中說的內容,作爲非阻塞的IO操作,NIO爲何會有這麼一個可能會造成阻塞的方法呢,別急,selector對象有很多方法可以解決阻塞的問題!
//阻塞在select()方法上的線程也可以立刻返回,不阻塞
selector.selectNow();
//可以設置超時時間,防止進程阻塞
selector.select(long timeout);
//可以喚醒阻塞狀態下的selector
selector.wakeup();
NIO服務端實例
本代碼實例模擬一個遊戲服務器,監聽玩家上線的事件。
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
private int port;
//聲明通道
private ServerSocketChannel server;
//Selector
private Selector selector;
//緩衝區
private ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
public NIOServer(int port){
this.port = port;
try{
server = ServerSocketChannel.open();
//非阻塞
server.configureBlocking(false);
//服務端通道綁定地址
server.bind(new InetSocketAddress("127.0.0.1", this.port));
selector = Selector.open();
//給服務器通道註冊接收操作,可以在這個點接收客戶端連接
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("[系統消息提示]NIO服務器初始化完畢,監聽端口" + this.port);
} catch (IOException e) {
e.printStackTrace();
}
}
//監聽客戶端連接
public void Listener(){
while(true){
try {
//這個方法可能會阻塞,直到至少有一個已註冊的事件發生,或者當一個或者更多的事件發生時
//selector.select(long timeout);可以設置超時時間,防止進程阻塞
//selector.wakeup();可以喚醒阻塞狀態下的selector
//selector.selectNow();也可以立刻返回,不阻塞
selector.select();
//輪詢所有選擇器接收到的操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIte = selectionKeys.iterator();
while(selectionKeyIte.hasNext()){
SelectionKey selectionKey = selectionKeyIte.next();
selectionKeyIte.remove();
//業務處理
handleKey(selectionKey);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//業務處理
public void handleKey(SelectionKey selectionKey) throws IOException{
//服務端的channel
ServerSocketChannel server = null;
//獲得和客戶端連接的通道
SocketChannel channel = null;
//接收到的信息
String receiveText;
//數據的標記位,判斷客戶端是否關閉
int count;
//客戶端請求連接事件
if(selectionKey.isAcceptable()){
try {
server = (ServerSocketChannel) selectionKey.channel();
//獲得服務端和客戶端連接的通道
channel = server.accept();
//將服務端和客戶端連接的設置非阻塞
channel.configureBlocking(false);
//註冊服務端和客戶端通道的讀事件
channel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
} else if(selectionKey.isReadable()){
//獲取通道對象,方便後面將通道內的數據讀入緩衝區
channel = (SocketChannel) selectionKey.channel();
receiveBuffer.clear();
count = channel.read(receiveBuffer);
//如果讀出來的客戶端數據不爲空
if(count>0){
receiveText = new String(receiveBuffer.array(),0,count);
System.out.println("[系統消息提示]服務器發現["+receiveText+"]玩家上線");
}else{
System.out.println("[系統消息提示]玩家下線");
//檢測到客戶端關閉(玩家下線),刪除該selectionKey監聽事件,否則會一直收到這個selectionKey的動作
selectionKey.cancel();
}
}
}
public static void main(String[] args) {
int port = 7080;
NIOServer server = new NIOServer(port);
server.Listener();
}
}
運行效果
啓動服務器
玩家上線
打開windows的cmd,用telnet命令模擬客戶端玩家上線訪問服務器。
telnet 127.0.0.1 7080
這裏稍微提一句,博主用的是win7系統,win7的telnet命令有點問題,輸入完telnet 127.0.0.1 7080命令後,正常輸入的話,服務端只能接受輸入的第一個字符,所以要通過“Ctrl+}”(回車鍵左邊第一個按鍵)來進入telnet的命令行界面,通過“send”命令去輸入玩家姓名傳給服務器。
玩家下線
關閉一個telnet窗口