1. 程式人生 > >網路程式設計Socket的阻塞和非阻塞IO

網路程式設計Socket的阻塞和非阻塞IO

網路應用程式一個很重要的工作是傳輸資料。傳輸資料的過程不一樣取決於使用哪種“交通工具“,但是傳輸的方式都是一樣的:都是以位元組碼傳輸。JAVA開發網路程式傳輸資料的過程和方式是被抽象了的,我們不需要關注底層介面,只需要使用Java API 或其他網路框架就能達到資料傳輸的目的。傳送資料和接收資料都是位元組碼。

Socket網路程式設計我就不多囉嗦了,這裡我通過兩個簡單的示例比較下阻塞式IO(OIO)和非阻塞式IO(NIO)。

OIO中,每個執行緒只能處理一個channel,該執行緒和該channel繫結。也就是同步的,客戶端在傳送請求後,必須得在服務端有迴應後才傳送下一個請求。所以這個時候的所有請求將會在服務端得到同步。

NIO中,每個執行緒可以處理多個channel。也就是非同步的,客戶端在傳送請求後,不必等待服務端的迴應就可以傳送下一個請求,這樣對於所有的請求動作來說將會在服務端得到非同步,這條請求的鏈路就象是一個請求佇列,所有的動作在這裡不會得到同步的。

你可能使用過Java提供的網路介面工作過,遇到過想從阻塞傳輸切換到非阻塞傳輸的情況,這種情況是比較困難的,因為阻塞IO和非阻塞IO使用的API有很大的差異。當我們想切換傳輸方式時要花很大的精力和時間來重構程式碼。

先看一個傳統的阻塞IO傳輸實現的Socket服務端:

/**
 *    傳統阻塞IO(OIO),原始socket
 *
 *   <p>Title: PlainOioServer</p>
 *    @author wyx
 *    @date 2016-6-15 下午1:36:04
 */
public class PlainOioServer {
    public void server(int port) throws Exception{
        // bind server to port
        final ServerSocket socket = new ServerSocket(port);
        while(true){
            // accept connection
            final Socket clientSocket = socket.accept();
            System.out.println("Accepted connection form " + clientSocket);
            // create new thread to handle connection
            new Thread(new Runnable() {
               
                @Override
                public void run() {
                    OutputStream out;
                    try {
                        out = clientSocket.getOutputStream();
                        // write  message to connected client
                        out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));
                        out.flush();
                        // close connection once message written and flushed
                        clientSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start(); // start thread to begin handling
        }
    }
}

上面的方式很簡潔,但是這種阻塞模式在大連線的情況就會有嚴重的問題,如:客戶端連線超時,伺服器響應嚴重延遲等。為了解決這一問題,我們可以使用非同步網路處理所有的併發連線,但問題在於NIO和OIO的API是完全不同的,所以一個用OIO開發的網路應用程式想要使用NIO重構程式碼幾乎是重新開發。

下面程式碼是使用Java NIO實現的例子:

/**
 *    傳統非阻塞式IO(NIO),原始socket
 *
 *    <p>Title: PlainNioServer</p>
 *    @author wyx
 *    @date 2016-6-15 下午1:46:09
 */
public class PlainNioServer {
    public void server(int port) throws Exception{
        System.out.println("Listening for connections on port " + port);
        // open selector that handles channels
        Selector selector = Selector.open();
        // open ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // get ServerSocket
        ServerSocket serverSocket = serverChannel.socket();
        // bind server to port   
        serverSocket.bind(new InetSocketAddress(port));
        // set to non-blocking
        serverChannel.configureBlocking(false);
        // register ServerSocket to selector and specify than it is interested in new accepted clients
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
        while(true){
            // Wait for new events that are ready for process. this will block until something happens
            int n = selector.select();
            if(n > 0){
                // Obtain all SelectionKey instances that received enents
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    iter.remove();
                    //Check if event was because new client ready to get accepted
                    if(key.isAcceptable()){
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                        System.out.println("Accepted connection from " + client);
                        client.configureBlocking(false);
                        // Accept client and register it to seletor
                        client.register(selector, SelectionKey.OP_WRITE, msg.duplicate());
                    }
                   
                    // Check if event was because socket is ready to write data
                    if(key.isWritable()){
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buff = (ByteBuffer) key.attachment();
                        // Write date to connected client
                        while(buff.hasRemaining()){
                            if(client.write(buff) == 0){
                                break;
                            }
                        }
                        client.close();
                    }
                }
            }
        }
    }
}

如你所見,即使它們實現的功能時候一樣的,但是程式碼完全不同。根據不同需求選用不同的實現方式,當然,也可以直接選擇流行的網路傳輸框架實現,如:Netty。以便於後期維護。

本文永久更新連結地址:http://www.linuxidc.com/Linux/2016-07/133496.htm