NIO客户端创建序列图如下:
步骤一:打开SocketChannel,绑定客户端本地地址(可选,默认系统会随机分配一个可用的本地地址),示例代码如下。
SocketChannel clientChannel = SocketChannel.open();
步骤二:设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数,示例如下。
clientChannel。configureBlocking(false);
clientChannel.socket().setReuseAddress(true);//启用设置socket的TCP参数
clientChannel.socket().setReceiveBufferSize(BUFFER_SIZE);
clientChannel.socket().setSendBufferSize(BUFFER_SIZE);
步骤三:异步连接服务端,示例代码如下。
boolean connected = clientChannel.connect(new InetSocketAddress("ip",port));
步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立),示例代码如下。
if(connected){
clientChannel.register(selector,SelectionKey.OP_READ,ioHandler);
}else{
clientChannel.register(selector,SelectionKey.OPCONNECT,ioHandler);
}
步骤五:向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答,示例代码如下。
clientChannel.register(selector,SelectionKey.OP_CONNECT,ioHandler);
步骤六:创建Reactor线程,创建多路复用器并启动线程,代码如下。
Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
步骤七:多路复用器在线程run方法的无限循环体内准备就绪的Key,代码如下。
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = SelectedKeys.iterator();
while(it.hasNest()){
SelectionKey key = (SelectionKey)it.next();
......
}
步骤八:接收connect事件进行处理,示例代码如下。
if(key.isConnectable())
//handlerConnect();
步骤九:判断连接结果,如果连接成功,注册读事件到多路复用器,示例代码如下。
if(channel.finishConnect())
registerRead();
步骤十:注册读事件到多路复用器,示例代码如下。
clientChannel.register(selector,SelectionKey.OP_READ,ioHandler);
步骤十一:异步读客户端请求消息到缓冲区,示例代码如下。
int readNumber = channel.read(receivedBuffer);
步骤十二:对ByteBuffer进行编解码,如果有半包消息接收缓冲区Reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排。示例代码如下:
Object message = null;
while(buffer.hanRemain()){
byteBuffer.mark();
Object message = decode(byteBuffer);
if (message == null) {
byteBuffer.reset();
break;
}
messageList.add(message);
}
if(!byteBuffer.hasRemaing()){
byteBuffer.clear();
}else{
byteBuffer.compact();
}
if(messageList != null & !messageList.isEmpty()){
for(Object messageE : messageList)
handlerTask(messageE);
}
步骤十三:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端。示例代码如下:
socketChannel.write(buffer);
NIO创建的TimeClient源码分析
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
//
}
}
new Thread(new TimeClientHandle("127.0.0.1",port),"TimeClient-001").start();
}
}
与之前唯一不同的地方在于通过创建TimeClientHandle线程来处理异步连接和读写操作,由于TimeClient非常简单且变更不大,这里重点分析TimeClientHandle,代码如下。
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run(){
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectedKyes = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKyes.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
//多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//判断连接是否成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
}else {
System.exit(1);//连接失败,进程退出
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is:" + body);
this.stop = true;
} else if (readBytes < 0) {
//对端链路关闭
key.cancel();
sc.close();
}else {
;//读到0字节忽略
}
}
}
}
private void doConnect() throws IOException {
//如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
socketChannel.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("Send order 2 server succeed.");
}
}
}
别的没有好说的,就说下handleInput方法这块,我们首先对SelectionKey进行判断,看它处于什么状态。如果是处于连接状态,说明服务端已经返回ACK应答消息。这是我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法。如果返回值为true,说明客户端连接成功;如果返回值为false或者直接抛出IOException,说明连接失败。在本例程中,返回值为true,说明连接成功。将SocketChannel注册到多路复用器上,注册Selection.OP_READ操作位,监听网络读操作,然后发送请求消息给服务器。