建立阻塞的伺服器
當 ServerSocketChannel
與 SockelChannel
採用預設的阻塞模式時,為了同時處理多個客戶的連線,必須使用多執行緒
public class EchoServer {
private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private ExecutorService executorService; //執行緒池
private static final int POOL_MULTIPLE = 4; //執行緒池中工作執行緒的數目
public EchoServer() throws IOException {
//建立一個執行緒池
executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_MULTIPLE);
//建立一個ServerSocketChannel物件
serverSocketChannel = ServerSocketChannel.open();
//使得在同一個主機上關閉了伺服器程式,緊接著再啟動該伺服器程式時,可以順利繫結相同的埠
serverSocketChannel.socket().setReuseAddress(true);
//把伺服器程式與一個本地埠繫結
serverSocketChannel.socket().bind(new InetSocketAddress(port));
System.out.println("伺服器啟動");
}
public void service() {
while (true) {
SocketChannel socketChannel = null;
try {
socketChannel = serverSocketChannel.accept();
//處理客戶連線
executorService.execute(new Handler(socketChannel));
} catch(IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws IOException {
new EchoServer().service();
}
//處理客戶連按
class Handler implements Runnable {
private SocketChannel socketChannel;
public Handler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public void run() {
handle(socketChannel);
}
public void handle(SocketChannel socketChannel) {
try {
//獲得與socketChannel關聯的Socket物件
Socket socket = socketChannel.socket();
System.out.println("接收到客戶連線,來自:" + socket.getInetAddress() + ":" + socket.getPort());
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg);
pw.println(echo(msg));
if (msg.equals("bye")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(socketChannel != null) {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);
}
private BufferedReader getReader(Socket socket) throws IOException {
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}
public String echo(String msg) {
return "echo:" + msg;
}
}
建立非阻塞的伺服器
在非阻塞模式下,EchoServer
只需要啟動一個主執行緒,就能同時處理三件事:
- 接收客戶的連線
- 接收客戶傳送的資料
- 向客戶發回響應資料
EchoServer
委託 Selector
來負責監控接收連線就緒事件、讀就緒事件和寫就緒事件如果有特定事件發生,就處理該事件
// 建立一個Selector物件
selector = Selector.open();
//建立一個ServerSocketChannel物件
serverSocketChannel = ServerSocketChannel.open();
//使得在同一個主機上關閉了伺服器程式,緊接著再啟動該伺服器程式時
//可以順利繫結到相同的埠
serverSocketChannel.socket().setReuseAddress(true);
//使ServerSocketChannel工作於非阻塞模式
serverSocketChannel.configureBlocking(false):
//把伺服器程式與一個本地埠繫結
serverSocketChannelsocket().bind(new InetSocketAddress(port));
EchoServer
類的 service()
方法負責處理本節開頭所說的三件事,體現其主要流程的程式碼如下:
public void service() throws IOException {
serverSocketChannel.reqister(selector, SelectionKey.OP_ACCEPT);
//第1層while迴圈
while(selector.select() > 0) {
//獲得Selector的selected-keys集合
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
//第2層while迴圈
while (it.hasNext()) {
SelectionKey key = null;
//處理SelectionKey
try {
//取出一個SelectionKey
key = (SelectionKey) it.next();
//把 SelectionKey從Selector 的selected-key 集合中刪除
it.remove();
1f (key.isAcceptable()) { 處理接收連線就緒事件; }
if (key.isReadable()) { 處理讀就緒水件; }
if (key.isWritable()) { 處理寫就緒事件; }
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
//使這個SelectionKey失效
key.cancel();
//關閉與這個SelectionKey關聯的SocketChannel
key.channel().close();
}
} catch(Exception ex) {
e.printStackTrace();
}
}
}
}
}
- 首先由
ServerSocketChannel
向Selector
註冊接收連線就緒事件,如果Selector
監控到該事件發生,就會把相應的SelectionKey
物件加入selected-keys
集合 - 第一層 while 迴圈,不斷詢問
Selector
已經發生的事件,select()
方法返回當前相關事件已經發生的SelectionKey
的個數,如果當前沒有任何事件發生,該方法會阻塞下去,直到至少有一個事件發生。Selector
的selectedKeys()
方法返回selected-keys
集合,它存放了相關事件已經發生的SelectionKey
物件 - 第二層 while 迴圈,從
selected-keys
集合中依次取出每個SelectionKey
物件並從集合中刪除,,然後呼叫isAcceptable()
、isReadable()
和isWritable()
方法判斷到底是哪種事件發生了,從而做出相應的處理
1. 處理接收連線就緒事件
if (key.isAcceptable()) {
//獲得與SelectionKey關聯的ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//獲得與客戶連線的SocketChannel
SocketChannel socketChannel = (SocketChannel) ssc.accept();
//把Socketchannel設定為非阻塞模式
socketChannel.configureBlocking(false);
//建立一個用於存放使用者傳送來的資料的級衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
//Socketchannel向Selector註冊讀就緒事件和寫就緒事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
2. 處理讀就緒事件
public void receive(SelectionKey key) throws IOException {
//獲得與SelectionKey關聯的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
//獲得與SelectionKey關聯的Socketchannel
SocketChannel socketChannel = (SocketChannel)key.channel();
//建立一個ByteBuffer用於存放讀到的資料
ByteBuffer readBuff = ByteBuffer.allocate(32);
socketChannel.read(readBuff);
readBuff.flip();
//把buffer的極限設為容量
buffer.limit(buffer.capacity());
//把readBuff中的內容複製到buffer
buffer.put(readBuff);
}
3. 處理寫就緒事件
public void send(SelectionKey key) throws IOException {
//獲得與SelectionKey關聯的ByteBuffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//獲得與SelectionKey關聯的SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.flip();
//按照GBK編碼把buffer中的位元組轉換為字串
String data = decode(buffer);
//如果還沒有讀到一行資料就返回
if(data.indexOf("\r\n") == -1)
return;
//擷取一行資料
String outputData = data.substring(0, data.indexOf("\n") + 1);
//把輸出的字串按照GBK編碼轉換為位元組,把它放在outputBuffer中
ByteBuffer outputBuffer = encode("echo:" + outputData);
//輸出outputBuffer的所有位元組
while(outputBuffer,hasRemaining())
socketChannel.write(outputBuffer);
//把outputData字元審按照GBK編碼,轉換為位元組,把它放在ByteBuffer
ByteBuffer temp = encode(outputData);
//把buffer的位置設為temp的極限
buffer.position(temp.limit()):
//刪除buffer已經處理的資料
buffer.compact();
//如果已經輸出了字串“bye\r\n”,就使SelectionKey失效,並關閉SocketChannel
if(outputData.equals("bye\r\n")) {
key.cancel();
socketChannel.close();
}
}
完整程式碼如下:
public class EchoServer {
private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private Selector selector;
private Charset charset = Charset.forName("GBK");
public EchoServer() throws IOException {
// 建立一個Selector物件
selector = Selector.open();
//建立一個ServerSocketChannel物件
serverSocketChannel = ServerSocketChannel.open();
//使得在同一個主機上關閉了伺服器程式,緊接著再啟動該伺服器程式時
//可以順利繫結到相同的埠
serverSocketChannel.socket().setReuseAddress(true);
//使ServerSocketChannel工作於非阻塞模式
serverSocketChannel.configureBlocking(false):
//把伺服器程式與一個本地埠繫結
serverSocketChannelsocket().bind(new InetSocketAddress(port));
}
public void service() throws IOException {
serverSocketChannel.reqister(selector, SelectionKey.OP_ACCEPT);
//第1層while迴圈
while(selector.select() > 0) {
//獲得Selector的selected-keys集合
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
//第2層while迴圈
while (it.hasNext()) {
SelectionKey key = null;
//處理SelectionKey
try {
//取出一個SelectionKey
key = (SelectionKey) it.next();
//把 SelectionKey從Selector 的selected-key 集合中刪除
it.remove();
1f (key.isAcceptable()) {
//獲得與SelectionKey關聯的ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//獲得與客戶連線的SocketChannel
SocketChannel socketChannel = (SocketChannel) ssc.accept();
//把Socketchannel設定為非阻塞模式
socketChannel.configureBlocking(false);
//建立一個用於存放使用者傳送來的資料的級衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
//Socketchannel向Selector註冊讀就緒事件和寫就緒事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
if (key.isReadable()) { receive(key); }
if (key.isWritable()) { send(key); }
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
//使這個SelectionKey失效
key.cancel();
//關閉與這個SelectionKey關聯的SocketChannel
key.channel().close();
}
} catch(Exception ex) {
e.printStackTrace();
}
}
}
}
}
public void receive(SelectionKey key) throws IOException {
//獲得與SelectionKey關聯的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
//獲得與SelectionKey關聯的Socketchannel
SocketChannel socketChannel = (SocketChannel)key.channel();
//建立一個ByteBuffer用於存放讀到的資料
ByteBuffer readBuff = ByteBuffer.allocate(32);
socketChannel.read(readBuff);
readBuff.flip();
//把buffer的極限設為容量
buffer.limit(buffer.capacity());
//把readBuff中的內容複製到buffer
buffer.put(readBuff);
}
public void send(SelectionKey key) throws IOException {
//獲得與SelectionKey關聯的ByteBuffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
//獲得與SelectionKey關聯的SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.flip();
//按照GBK編碼把buffer中的位元組轉換為字串
String data = decode(buffer);
//如果還沒有讀到一行資料就返回
if(data.indexOf("\r\n") == -1)
return;
//擷取一行資料
String outputData = data.substring(0, data.indexOf("\n") + 1);
//把輸出的字串按照GBK編碼轉換為位元組,把它放在outputBuffer中
ByteBuffer outputBuffer = encode("echo:" + outputData);
//輸出outputBuffer的所有位元組
while(outputBuffer,hasRemaining())
socketChannel.write(outputBuffer);
//把outputData字元審按照GBK編碼,轉換為位元組,把它放在ByteBuffer
ByteBuffer temp = encode(outputData);
//把buffer的位置設為temp的極限
buffer.position(temp.limit()):
//刪除buffer已經處理的資料
buffer.compact();
//如果已經輸出了字串“bye\r\n”,就使SelectionKey失效,並關閉SocketChannel
if(outputData.equals("bye\r\n")) {
key.cancel();
socketChannel.close();
}
}
//解碼
public String decode(ByteBuffer buffer) {
CharBuffer charBuffer = charset.decode(buffer);
return charBuffer.toStrinq();
}
//編碼
public ByteBuffer encode(String str) {
return charset.encode(str);
}
public static void main(String args[])throws Exception {
EchoServer server = new EchoServer();
server.service();
}
}
阻塞模式與非阻塞模式混合使用
使用非阻塞模式時,ServerSocketChannel
以及 SocketChannel
都被設定為非阻塞模式,這使得接收連線、接收資料和傳送資料的操作都採用非阻塞模式,EchoServer
採用一個執行緒同時完成這些操作
假如有許多客戶請求連線,可以把接收客戶連線的操作單獨由一個執行緒完成,把接收資料和傳送資料的操作由另一個執行緒完成,這可以提高伺服器的併發效能
負責接收客戶連線的執行緒按照阻塞模式工作,如果收到客戶連線,就向 Selector
註冊讀就緒和寫就緒事件,否則進入阻塞狀態,直到接收到了客戶的連線。負責接收資料和傳送資料的執行緒按照非阻塞模式工作,只有在讀就緒或寫就緒事件發生時,才執行相應的接收資料和傳送資料操作
public class EchoServer {
private int port = 8000;
private ServerSocketChannel serverSocketChannel = null;
private Selector selector = null;
private Charset charset = Charset.forName("GBK");
public EchoServer() throws IOException {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().setReuseAddress(true);
serverSocketChannelsocket().bind(new InetSocketAddress(port));
}
public void accept() {
while(true) {
try {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
synchronized(gate) {
selector.wakeup();
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
private Object gate=new Object();
public void service() throws IOException {
while(true) {
synchronized(gate){}
int n = selector.select();
if(n == 0) continue;
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
while (it.hasNext()) {
SelectionKey key = null;
try {
it.remove();
if (key.isReadable()) {
receive(key);
}
if (key.isWritable()) {
send(key);
}
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
key.cancel();
key.channel().close();
}
} catch(Exception ex) { e.printStackTrace(); }
}
}
}
}
public void receive(SelectionKey key) throws IOException {
...
}
public void send(SelectionKey key) throws IOException {
...
}
public String decode(ByteBuffer buffer) {
...
}
public ByteBuffer encode(String str) {
...
}
public static void main(String args[])throws Exception {
final EchoServer server = new EchoServer();
Thread accept = new Thread() {
public void run() {
server.accept();
}
};
accept.start();
server.service();
}
}
注意一點:主執行緒的 selector select()
方法和 Accept 執行緒的 register(...)
方法都會造成阻塞,因為他們都會操作 Selector
物件的共享資源 all-keys
集合,這有可能會導致死鎖
導致死鎖的具體情形是:Selector
中尚沒有任何註冊的事件,即 all-keys
集合為空,主執行緒執行 selector.select()
方法時將進入阻塞狀態,只有當 Accept 執行緒向 Selector
註冊了事件,並且該事件發生後,主執行緒才會從 selector.select()
方法返回。然而,由於主執行緒正在 selector.select()
方法中阻塞,這使得 Acccept
執行緒也在 register()
方法中阻塞。Accept 執行緒無法向 Selector 註冊事件,而主執行緒沒有任何事件可以監控,所以這兩個執行緒將永遠阻塞下去
為了避免對共享資源的競爭,同步機制使得一個執行緒執行 register()
時,不允許另一個執行緒同時執行 select()
方法,反之亦然