[转]NIO Client程序片断
程序员文章站
2022-06-14 16:10:09
...
The client
Before getting started it's worth mentioning that the client implementation is going to end up looking a lot like the server implementation we just finished writing. There's going to be a lot of common functionality that can be factored out and shared between the client and server implementation. For the most part I'm going to totally ignore that and write the client as a standalone piece of code to avoid clouding the picture with layer upon layer of abstraction. I'll leave that as an exercise to the reader. However, there are one or two little bits of code we've already written that will be shared between the implementations, because not doing so may reduce clarity. It's a judgement call on my part so if you disagree, forgive me and try not to let it bother you too much.
That said, let's get on with the client code. Once again, we're going to need some infrastructure. We can actually borrow a lot from the server code, since once a connection is accepted/established there's not a lot of difference between a client and server. A client still needs to read and write data, so at the very least we're going to need that logic. Given the similarities I'm going to build the client implementation by starting with the server implementation and tweaking it as needed.
First, let's remove some bits we don't need. The server socket channel can go, we won't be needing that. We can also remove the accept() method and the logic in the run() method that invokes it.
Next we can throw out most of the logic in the initSelector() method. In fact, we only need the first line, leaving us with a method that looks like this.
We're still missing the most important piece of client-side logic: establishing a connection. This usually goes hand in hand with wanting to send some data. There are scenarios where that's not true: you might, for example, want to establish a bunch of connections to a remote server up front and then send all traffic over those connections. I'm not going to take that approach but it's not hard to get there from here. While we're on the topic of what I'm not going to do, I'm also not going to implement connection pooling. I'm not going to implement a client that talks to a bunch of remote servers on different addresses; the client here will talk to one remote server (albeit using multiple simultaneous connections). What I am going to implement is a client that establishes a new connection, sends a message, waits for a response and then disconnects (oh, and I'm not going to handle the case where a response isn't received). Anything beyond that adds very little beyond more code to confuse the issue.
Phew, that paragraph really got away from me. Back to the code! What we'll need is a send() method that requests a new connection and queues data to be sent on that connection when it comes up. We'll also need some way of notifying the caller when the response is received, but one step at a time.
But before we can put together the send() method we really need a method for initiating a new connection. The send() method will make more sense if we introduce initiateConnection() first.
In a nutshell, initiating a connection means creating a new non-blocking socket channel, initiating a (non-blocking) connection and registering an interest in finishing the connection. The only wrinkle is that, since the calling thread is not the selecting thread, the last step must be deferred. We don't wake the selecting thread up because the method that calls this will want to queue some data to be written when the connection is completed. Waking the selecting thread up here opens us up to a race condition where the connection is completed by the selecting thread before the calling thread queues the data and OP_WRITE is never set on the channel's selection key.
Given the requirements we discussed earlier and the initiateConnection() above, our send() method needs to look something like this (with an additional instance member which will be clarified later).
It's important to note that nowhere in the two methods just introduced do we request that the OP_CONNECT flag be set on the socket channel's selection key. If we did that we'd overwrite the OP_CONNECT flag and never complete the connection. And if we combined them then we'd run the risk of trying to write on an unconnected channel (or at least having to deal with that case). Instead what we'll do is set the OP_WRITE flag when the connection is established (we could do this based on whether or not data was queued but for our scenario it's acceptable to do it this way).
Which brings us to the second change to our run() method. Remember that we started out with the server implementation, so we still have the logic for handling a pending change to the interest operation set for a selection key. To that we need to handle a pending channel registration. Voila...
And, of course, we'll need to handle the connectable event...
// Check what event is available and deal with it
if (key.isConnectable()) {
this.finishConnection(key);
} else if (key.isReadable()) {
this.read(key);
} else if (key.isWritable()) {
this.write(key);
}
And of course we need an implementation for finishConnection().
As I mentioned before, once the connection is complete we immediately register an interest in writing on the channel. Data has already been queued (or we wouldn't be establishing a connection in the first place). Since the current thread in this case is the selecting thread we're free to modify the selection key directly.
One last change before we're ready to put it all together. Our server implementation's read() handed the data off to a worker thread (that just echoed it back). For the client side we really want to hand the data (the response) back to whoever initiated the original send (the request). Scroll back up the page until you find mention of a RspHandler (it was passed as a parameter to send()). That's our conduit back to the original caller and it replaces our call to the EchoServer in the read() method. So the last line looks like this:
Which begs an implementation of handleResponse. All this method has to do is look up the handler we stashed for this channel and pass it the data we've read. We let the handler indicate whether or not it's seen enough. If it has we'll close the connection.
The response handler itself is pretty simple but for it to make complete sense we need to see how all of this hangs together in the form of our client's main() method.
The response handler implementation is shown below. I waited until last so that the waitForResponse() would make sense, along with some of the synchronization logic. It's entirely possible to pull this logic into the send() method, effectively making it a synchronous call. But this tutorial is illustrating asynchronous I/O so that seems kind of silly. It's not as silly as it sounds, mind you, since you might want to handle a large number of active connections even if the client is forced to use them one at a time. But this illustrates how to perform the send asynchronously. Everything else is merely details.
Quoted from: http://www.javafaq.nu/java-article1103.html
Before getting started it's worth mentioning that the client implementation is going to end up looking a lot like the server implementation we just finished writing. There's going to be a lot of common functionality that can be factored out and shared between the client and server implementation. For the most part I'm going to totally ignore that and write the client as a standalone piece of code to avoid clouding the picture with layer upon layer of abstraction. I'll leave that as an exercise to the reader. However, there are one or two little bits of code we've already written that will be shared between the implementations, because not doing so may reduce clarity. It's a judgement call on my part so if you disagree, forgive me and try not to let it bother you too much.
That said, let's get on with the client code. Once again, we're going to need some infrastructure. We can actually borrow a lot from the server code, since once a connection is accepted/established there's not a lot of difference between a client and server. A client still needs to read and write data, so at the very least we're going to need that logic. Given the similarities I'm going to build the client implementation by starting with the server implementation and tweaking it as needed.
First, let's remove some bits we don't need. The server socket channel can go, we won't be needing that. We can also remove the accept() method and the logic in the run() method that invokes it.
Next we can throw out most of the logic in the initSelector() method. In fact, we only need the first line, leaving us with a method that looks like this.
private Selector initSelector() throws IOException { // Create a new selector return SelectorProvider.provider().openSelector(); }
We're still missing the most important piece of client-side logic: establishing a connection. This usually goes hand in hand with wanting to send some data. There are scenarios where that's not true: you might, for example, want to establish a bunch of connections to a remote server up front and then send all traffic over those connections. I'm not going to take that approach but it's not hard to get there from here. While we're on the topic of what I'm not going to do, I'm also not going to implement connection pooling. I'm not going to implement a client that talks to a bunch of remote servers on different addresses; the client here will talk to one remote server (albeit using multiple simultaneous connections). What I am going to implement is a client that establishes a new connection, sends a message, waits for a response and then disconnects (oh, and I'm not going to handle the case where a response isn't received). Anything beyond that adds very little beyond more code to confuse the issue.
Phew, that paragraph really got away from me. Back to the code! What we'll need is a send() method that requests a new connection and queues data to be sent on that connection when it comes up. We'll also need some way of notifying the caller when the response is received, but one step at a time.
But before we can put together the send() method we really need a method for initiating a new connection. The send() method will make more sense if we introduce initiateConnection() first.
private SocketChannel initiateConnection() throws IOException { // Create a non-blocking socket channel SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // Kick off connection establishment socketChannel.connect(new InetSocketAddress(this.hostAddress, this.port)); // Queue a channel registration since the caller is not the // selecting thread. As part of the registration we'll register // an interest in connection events. These are raised when a channel // is ready to complete connection establishment. synchronized(this.pendingChanges) { this.pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER, SelectionKey.OP_CONNECT)); } return socketChannel; }
In a nutshell, initiating a connection means creating a new non-blocking socket channel, initiating a (non-blocking) connection and registering an interest in finishing the connection. The only wrinkle is that, since the calling thread is not the selecting thread, the last step must be deferred. We don't wake the selecting thread up because the method that calls this will want to queue some data to be written when the connection is completed. Waking the selecting thread up here opens us up to a race condition where the connection is completed by the selecting thread before the calling thread queues the data and OP_WRITE is never set on the channel's selection key.
Given the requirements we discussed earlier and the initiateConnection() above, our send() method needs to look something like this (with an additional instance member which will be clarified later).
// Maps a SocketChannel to a RspHandler private Map rspHandlers = Collections.synchronizedMap(new HashMap()); public void send(byte[] data, RspHandler handler) throws IOException { // Start a new connection SocketChannel socket = this.initiateConnection(); // Register the response handler this.rspHandlers.put(socket, handler); // And queue the data we want written synchronized (this.pendingData) { List queue = (List) this.pendingData.get(socket); if (queue == null) { queue = new ArrayList(); this.pendingData.put(socket, queue); } queue.add(ByteBuffer.wrap(data)); } // Finally, wake up our selecting thread so it can make the required changes this.selector.wakeup(); }
It's important to note that nowhere in the two methods just introduced do we request that the OP_CONNECT flag be set on the socket channel's selection key. If we did that we'd overwrite the OP_CONNECT flag and never complete the connection. And if we combined them then we'd run the risk of trying to write on an unconnected channel (or at least having to deal with that case). Instead what we'll do is set the OP_WRITE flag when the connection is established (we could do this based on whether or not data was queued but for our scenario it's acceptable to do it this way).
Which brings us to the second change to our run() method. Remember that we started out with the server implementation, so we still have the logic for handling a pending change to the interest operation set for a selection key. To that we need to handle a pending channel registration. Voila...
switch (change.type) { case ChangeRequest.CHANGEOPS: SelectionKey key = change.socket.keyFor(this.selector); key.interestOps(change.ops); break; case ChangeRequest.REGISTER: change.socket.register(this.selector, change.ops); break; }
And, of course, we'll need to handle the connectable event...
// Check what event is available and deal with it
if (key.isConnectable()) {
this.finishConnection(key);
} else if (key.isReadable()) {
this.read(key);
} else if (key.isWritable()) {
this.write(key);
}
And of course we need an implementation for finishConnection().
private void finishConnection(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); // Finish the connection. If the connection operation failed // this will raise an IOException. try { socketChannel.finishConnection(); } catch (IOException e) { // Cancel the channel's registration with our selector key.cancel(); return; } // Register an interest in writing on this channel key.interestOps(SelectionKey.OP_WRITE); }
As I mentioned before, once the connection is complete we immediately register an interest in writing on the channel. Data has already been queued (or we wouldn't be establishing a connection in the first place). Since the current thread in this case is the selecting thread we're free to modify the selection key directly.
One last change before we're ready to put it all together. Our server implementation's read() handed the data off to a worker thread (that just echoed it back). For the client side we really want to hand the data (the response) back to whoever initiated the original send (the request). Scroll back up the page until you find mention of a RspHandler (it was passed as a parameter to send()). That's our conduit back to the original caller and it replaces our call to the EchoServer in the read() method. So the last line looks like this:
// Handle the response this.handleResponse(socketChannel, this.readBuffer.array(), numRead);
Which begs an implementation of handleResponse. All this method has to do is look up the handler we stashed for this channel and pass it the data we've read. We let the handler indicate whether or not it's seen enough. If it has we'll close the connection.
private void handleResponse(SocketChannel socketChannel, byte[] data, int numRead) throws IOException { // Make a correctly sized copy of the data before handing it // to the client byte[] rspData = new byte[numRead]; System.arraycopy(data, 0, rspData, 0, numRead); // Look up the handler for this channel RspHandler handler = (RspHandler) this.rspHandlers.get(socketChannel); // And pass the response to it if (handler.handleResponse(rspData)) { // The handler has seen enough, close the connection socketChannel.close(); socketChannel.keyFor(this.selector).cancel(); } }
The response handler itself is pretty simple but for it to make complete sense we need to see how all of this hangs together in the form of our client's main() method.
public static void main(String[] args) { try { NioClient client = new NioClient(InetAddress.getByName("localhost"), 9090); Thread t = new Thread(client); t.setDaemon(true); t.start(); RspHandler handler = new RspHandler(); client.send("Hello World".getBytes(), handler); handler.waitForResponse(); } catch (Exception e) { e.printStackTrace(); } }
The response handler implementation is shown below. I waited until last so that the waitForResponse() would make sense, along with some of the synchronization logic. It's entirely possible to pull this logic into the send() method, effectively making it a synchronous call. But this tutorial is illustrating asynchronous I/O so that seems kind of silly. It's not as silly as it sounds, mind you, since you might want to handle a large number of active connections even if the client is forced to use them one at a time. But this illustrates how to perform the send asynchronously. Everything else is merely details.
Quoted from: http://www.javafaq.nu/java-article1103.html
推荐阅读