Java 網路程式設計 —— 實現非阻塞式的伺服器

低吟不作語發表於2023-05-20

建立阻塞的伺服器

ServerSocketChannelSockelChannel 採用預設的阻塞模式時,為了同時處理多個客戶的連線,必須使用多執行緒

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();
                }
            }
        }
    }
}
  • 首先由 ServerSocketChannelSelector 註冊接收連線就緒事件,如果 Selector 監控到該事件發生,就會把相應的 SelectionKey 物件加入 selected-keys 集合
  • 第一層 while 迴圈,不斷詢問 Selector 已經發生的事件,select() 方法返回當前相關事件已經發生的 SelectionKey 的個數,如果當前沒有任何事件發生,該方法會阻塞下去,直到至少有一個事件發生。SelectorselectedKeys() 方法返回 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() 方法,反之亦然


相關文章