[英]Non-blocking sockets
在Java中實現非阻塞套接字的最佳方法是什么?
還是有這樣的事情? 我有一個通過套接字與服務器通信的程序,但是如果數據/連接出現問題,我不希望套接字調用阻塞/導致延遲。
Java 2 Standard Edition 1.4中引入的Java 非阻塞套接字允許應用程序之間的網絡通信,而不會阻止使用套接字的進程。 但是什么是非阻塞套接字,它在哪些上下文中有用,以及它是如何工作的?
非阻塞套接字允許在通道上進行I / O操作,而不會阻止使用它的進程。 這意味着,我們可以使用單個線程來處理多個並發連接並獲得“異步高性能”讀/寫操作(有些人可能不同意)
好的, 在哪些情況下它可能有用?
假設您希望實現接受不同客戶端連接的服務器。 同時假設您希望服務器能夠同時處理多個請求。 使用傳統方式,您有兩種選擇來開發這樣的服務器:
這兩種解決方案都有效,但是采用第一種解決方案來開發整個線程管理解決方案,具有相關的並發性和沖突問題。 第二種解決方案使應用程序依賴於非JDK外部模塊,可能您必須使庫適應您的需求。 通過非阻塞套接字,您可以實現非阻塞服務器,而無需直接管理線程或使用外部模塊。
在詳細介紹之前,您需要了解的術語很少:
Java NIO有一個名為Selector
的類,它允許單個線程檢查多個通道上的I / O事件。 這怎么可能? 好吧, selector
可以檢查通道的“准備就緒” ,例如客戶端嘗試連接或讀/寫操作。 也就是說, Selector
每個實例都可以監視更多的套接字通道 ,從而監視更多的連接。 現在,當通道上發生某些事件(發生事件)時, selector
通知應用程序處理請求 。 selector
通過創建事件鍵 (或選擇鍵)來完成它,它們是SelectionKey
類的實例。 每個key
包含有關發出請求的人以及請求的 類型的信息 ,如圖1所示。
服務器實現由無限循環組成,其中selector
等待事件並創建事件密鑰。 密鑰有四種可能的類型:
通常在服務器端創建acceptable
密鑰。 實際上,這種密鑰只是簡單地通知服務器客戶端需要連接,然后服務器將套接字通道個性化並將其與選擇器相關聯以進行讀/寫操作。 在此之后,當接受的客戶端讀取或寫入某些內容時,選擇器將為該客戶端創建readable
或writeable
密鑰。
現在,您已准備好按照提議的算法用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.