簡體   English   中英

非阻塞套接字

[英]Non-blocking sockets

在Java中實現非阻塞套接字的最佳方法是什么?

還是有這樣的事情? 我有一個通過套接字與服務器通信的程序,但是如果數據/連接出現問題,我不希望套接字調用阻塞/導致延遲。

Java 2 Standard Edition 1.4中引入的Java 非阻塞套接字允許應用程序之間的網絡通信,而不會阻止使用套接字的進程。 但是什么是非阻塞套接字,它在哪些上下文中有用,以及它是如何工作的?

什么是非阻塞套接字?

非阻塞套接字允許在通道上進行I / O操作,而不會阻止使用它的進程。 這意味着,我們可以使用單個線程來處理多個並發連接並獲得“異步高性能”讀/寫操作(有些人可能不同意)

好的, 在哪些情況下它可能有用?

假設您希望實現接受不同客戶端連接的服務器。 同時假設您希望服務器能夠同時處理多個請求。 使用傳統方式,您有兩種選擇來開發這樣的服務器:

  • 實現一個多線程服務器,為每個連接手動處理一個線程。
  • 使用外部第三方模塊。

這兩種解決方案都有效,但是采用第一種解決方案來開發整個線程管理解決方案,具有相關的並發性和沖突問題。 第二種解決方案使應用程序依賴於非JDK外部模塊,可能您必須使庫適應您的需求。 通過非阻塞套接字,您可以實現非阻塞服務器,而無需直接管理線程或使用外部模塊。

這個怎么運作?

在詳細介紹之前,您需要了解的術語很少:

  • 在基於NIO的實現中,我們不是將數據寫入輸出流並從輸入流中讀取數據,而是從緩沖區讀取和寫入數據。 緩沖區可以定義為臨時存儲。
  • 通道將大量數據傳入和傳出緩沖區 此外,它可以被視為通信的端點。
  • 准備選擇是一個概念,它指的是“選擇在讀取或寫入數據時不會阻塞的套接字的能力”。

Java NIO有一個名為Selector的類,它允許單個線程檢查多個通道上的I / O事件。 這怎么可能? 好吧, selector可以檢查通道的“准備就緒” ,例如客戶端嘗試連接或讀/寫操作。 也就是說, Selector每個實例都可以監視更多的套接字通道 ,從而監視更多的連接。 現在,當通道上發生某些事件(發生事件)時, selector通知應用程序處理請求 selector通過創建事件鍵 (或選擇鍵)來完成它,它們是SelectionKey類的實例。 每個key包含有關發出請求的人以及請求的 類型的信息 ,如圖1所示。

圖1:結構圖 圖1:結構圖

一個基本的實現

服務器實現由無限循環組成,其中selector等待事件並創建事件密鑰。 密鑰有四種可能的類型:

  • 可接受:關聯的客戶端請求連接。
  • 可連接:服務器接受連接。
  • 可讀:服務器可以讀取。
  • 可寫:服務器可以寫。

通常在服務器端創建acceptable密鑰。 實際上,這種密鑰只是簡單地通知服務器客戶端需要連接,然后服務器將套接字通道個性化並將其與選擇器相關聯以進行讀/寫操作。 在此之后,當接受的客戶端讀取或寫入某些內容時,選擇器將為該客戶端創建readablewriteable密鑰。

現在,您已准備好按照提議的算法用Java編寫服務器。 可以通過以下方式創建套接字通道, selector和套接字選擇器注冊:

final String HOSTNAME = "127.0.0.1";
final int PORT = 8511;

// This is how you open a ServerSocketChannel
serverChannel = ServerSocketChannel.open();
// You MUST configure as non-blocking or else you cannot register the serverChannel to the Selector.
serverChannel.configureBlocking(false);
// bind to the address that you will use to Serve.
serverChannel.socket().bind(new InetSocketAddress(HOSTNAME, PORT));

// This is how you open a Selector
selector = Selector.open();
/*
 * Here you are registering the serverSocketChannel to accept connection, thus the OP_ACCEPT.
 * This means that you just told your selector that this channel will be used to accept connections.
 * We can change this operation later to read/write, more on this later.
 */
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

首先,我們使用ServerSocketChannel.open()方法創建一個SocketChannel實例。 接下來, configureBlocking(false)調用將此channel設置為非阻塞 與服務器的連接由serverChannel.socket().bind()方法完成。 HOSTNAME表示服務器的IP地址, PORT是通信端口。 最后,調用Selector.open()方法創建一個selector實例並將其注冊到channel和注冊類型。 在此示例中,注冊類型為OP_ACCEPT ,這意味着選擇器僅報告客戶端嘗試連接到服務器。 其他可能的選項是: OP_CONNECT ,將由客戶端使用; OP_READ ; OP_WRITE

現在我們需要使用無限循環來處理這些請求。 一個簡單的方法如下:

// Run the server as long as the thread is not interrupted.
while (!Thread.currentThread().isInterrupted()) {
    /*
     * selector.select(TIMEOUT) is waiting for an OPERATION to be ready and is a blocking call.
     * For example, if a client connects right this second, then it will break from the select()
     * call and run the code below it. The TIMEOUT is not needed, but its just so it doesn't
     * block undefinable.
     */
    selector.select(TIMEOUT);

    /*
     * If we are here, it is because an operation happened (or the TIMEOUT expired).
     * We need to get the SelectionKeys from the selector to see what operations are available.
     * We use an iterator for this.
     */
    Iterator<SelectionKey> keys = selector.selectedKeys().iterator();

    while (keys.hasNext()) {
        SelectionKey key = keys.next();
        // remove the key so that we don't process this OPERATION again.
        keys.remove();

        // key could be invalid if for example, the client closed the connection.
        if (!key.isValid()) {
            continue;
        }
        /*
         * In the server, we start by listening to the OP_ACCEPT when we register with the Selector.
         * If the key from the keyset is Acceptable, then we must get ready to accept the client
         * connection and do something with it. Go read the comments in the accept method.
         */
        if (key.isAcceptable()) {
            System.out.println("Accepting connection");
            accept(key);
        }
        /*
         * If you already read the comments in the accept() method, then you know we changed
         * the OPERATION to OP_WRITE. This means that one of these keys in the iterator will return
         * a channel that is writable (key.isWritable()). The write() method will explain further.
         */
        if (key.isWritable()) {
            System.out.println("Writing...");
            write(key);
        }
        /*
         * If you already read the comments in the write method then you understand that we registered
         * the OPERATION OP_READ. That means that on the next Selector.select(), there is probably a key
         * that is ready to read (key.isReadable()). The read() method will explain further.
         */
        if (key.isReadable()) {
            System.out.println("Reading connection");
            read(key);
        }
    }
}

您可以在此處找到實施源

注意:異步服務器

作為非阻塞實現的替代方案,我們可以部署異步服務器。 例如,您可以使用AsynchronousServerSocketChannel類,它為面向流的偵聽套接字提供異步通道。

要使用它,首先執行其靜態open()方法,然后bind()到特定端口 接下來,您將執行其accept()方法,並向其傳遞一個實現CompletionHandler接口的類。 通常,您會發現將處理程序創建為匿名內部類

從這個AsynchronousServerSocketChannel對象,你調用accept()告訴它開始偵聽連接,並向它傳遞一個自定義的CompletionHandler實例。 當我們調用accept() ,它會立即返回。 請注意,這與傳統的阻塞方法不同; accept()方法被阻塞,直到客戶端連接到它AsynchronousServerSocketChannel accept()方法為您處理它。

這里有一個例子:

public class NioSocketServer
{
    public NioSocketServer()
    {
        try {
            // Create an AsynchronousServerSocketChannel that will listen on port 5000
            final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel
                    .open()
                    .bind(new InetSocketAddress(5000));

            // Listen for a new request
            listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>()
            {
                @Override
                public void completed(AsynchronousSocketChannel ch, Void att)
                {
                    // Accept the next connection
                    listener.accept(null, this);

                    // Greet the client
                    ch.write(ByteBuffer.wrap("Hello, I am Echo Server 2020, let's have an engaging conversation!\n".getBytes()));

                    // Allocate a byte buffer (4K) to read from the client
                    ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
                    try {
                        // Read the first line
                        int bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS);

                        boolean running = true;
                        while (bytesRead != -1 && running) {
                            System.out.println("bytes read: " + bytesRead);

                            // Make sure that we have data to read
                            if (byteBuffer.position() > 2) {
                                // Make the buffer ready to read
                                byteBuffer.flip();

                                // Convert the buffer into a line
                                byte[] lineBytes = new byte[bytesRead];
                                byteBuffer.get(lineBytes, 0, bytesRead);
                                String line = new String(lineBytes);

                                // Debug
                                System.out.println("Message: " + line);

                                // Echo back to the caller
                                ch.write(ByteBuffer.wrap(line.getBytes()));

                                // Make the buffer ready to write
                                byteBuffer.clear();

                                // Read the next line
                                bytesRead = ch.read(byteBuffer).get(20, TimeUnit.SECONDS);
                            } else {
                                // An empty line signifies the end of the conversation in our protocol
                                running = false;
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        // The user exceeded the 20 second timeout, so close the connection
                        ch.write(ByteBuffer.wrap("Good Bye\n".getBytes()));
                        System.out.println("Connection timed out, closing connection");
                    }

                    System.out.println("End of conversation");
                    try {
                        // Close the connection if we need to
                        if (ch.isOpen()) {
                            ch.close();
                        }
                    } catch (I/OException e1)
                    {
                        e1.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, Void att)
                {
                    ///...
                }
            });
        } catch (I/OException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        NioSocketServer server = new NioSocketServer();
        try {
            Thread.sleep(60000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

你可以在這里找到完整的代碼

在Java中實現非阻塞套接字的最佳方法是什么?

只有一種方法。 SocketChannel.configureBlocking(false)

請注意,其中一些答案不正確。 SocketChannel.configureBlocking(false)將其置於非阻塞模式。 您不需要Selector來執行此操作。 您只需要一個Selector來實現超時或帶有非阻塞套接字的多路復用 I / O.

除了使用非阻塞IO之外,您可能會發現為連接創建一個寫入線程要簡單得多。

注意:如果您只需要幾千個連接,則每個連接一到兩個線程更簡單。 如果每台服務器有大約一萬或更多的連接,則需要使用選擇器的NIO。

java.nio包提供了Selector的工作方式,就像在C中一樣。

我剛寫了這段代碼。 它運作良好。 這是上面答案中提到的Java NIO的一個例子,但在這里我發布了代碼。

ServerSocketChannel ssc = null;
try {
    ssc = ServerSocketChannel.open();
    ssc.socket().bind(new InetSocketAddress(port));
    ssc.configureBlocking(false);
    while (true) {
        SocketChannel sc = ssc.accept();
        if (sc == null) {
            // No connections came .
        } else {
            // You got a connection. Do something
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM