如果你看完了上一篇日志,相信你已经万事俱备,搞明白通道Channel、数据包装Buffer和选择器Selector。那么就来用上这些组件,搭建一个非阻塞式的,更高性能的服务器端和客户端吧!
首先来看看最核心的服务器部分:
package com.justin.nioclient;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOclient implements Runnable {
private Selector selector;
protected void initClient(int port, InetAddress ipAddress) throws IOException {
// 配置要连接的远程服务器地址和端口号
InetSocketAddress address = new InetSocketAddress(ipAddress, port);
SocketChannel channel = SocketChannel.open(); //打开通道
channel.configureBlocking(false); // 设置为非阻塞式
channel.connect(address); // connect()方法连接远程服务器
this.selector = Selector.open(); // 创建选择器
channel.register(selector, SelectionKey.OP_CONNECT); // 注册连接事件
}
protected void connectServer() throws IOException {
while (true) {
if(selector.isOpen() == false) {
break;
}
selector.select(); //查找连接事件的通道
Set connectKeySet = selector.selectedKeys();
Iterator iterator = connectKeySet.iterator(); //迭代器遍历通道集合
while (iterator.hasNext()) {
SelectionKey availableKey = (SelectionKey) iterator.next();
iterator.remove();
if(availableKey.isConnectable()) {
System.out.println("Search connectable channel success!");
new clientConnect().Connect(selector, availableKey);
} else if(availableKey.isValid() && availableKey.isWritable()) {
System.out.println("Search writable channel success!");
new clientWrite().Write(selector, availableKey);
} else if(availableKey.isValid() && availableKey.isReadable()) {
System.out.println("Search readable channel ssuccess!");
new clientRead().Read(selector, availableKey);
}
}
}
}
@Override
public void run() {
System.out.println("Start client.");
NIOclient client = new NIOclient();
try {
client.initClient(5000, InetAddress.getLocalHost());
client.connectServer();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws UnknownHostException, IOException {
Thread clientThread = new Thread(new NIOclient());
clientThread.start();
}
}
代码第7行,打开一个ServerSocketChannel,这是一个可以监听TCP连接的通道,相当于IO中的ServerSocket,在第8行设置这个通道为非阻塞式后,第12行我们会给它绑定主机地址和端口号。最后在第14行为该通道Channel注册接收客户端连接事件,并让selector来管理这个通道,可以看到,register()的返回值是一个SelectionKey对象,上一篇日志说了,对象里面存放了通道,被管理的选择器和通道的注册事件等。
接着就可以不断监听这个端口,等待新的客户端连接进来。和ServerSocket一样,ServerSocketChannel也有一个accept()方法,调用accept()方法后,不同的是,ServerSocket的accept()方法会一直阻塞,知道有新的连接请求,而ServerSocketChannel可以设置成非阻塞式的,上面我们已经设置了。非阻塞式的ServerScketChannel调用accept()方法,会立刻返回,如果没有新的连接请求,则返回null。
监听到客户端的请求,之后第17行就可以调用Selector.select()方法,来返回获得那些对服务器端或客户端想要执行的事件已就绪的通道,不过要注意的是,select()方法是阻塞的,假设服务器端需要接收事件的通道,则select()方法会一直阻塞,直到有一个符合的就绪通道为止,当然也有非阻塞的select()方法,就是selectNow()。获得这些通道后,下一行再调用Selector.selectedKeys()方法,来访问这些通道,它们是SelectionKey对象,由于符合要求的通道可能不止一个,所以用集合存放。
获得就绪通道的集合后,我们用迭代器来遍历这个SelectionKey对象集合,选择我们需要的事件的通道。注意,每得到一个SelectionKey对象后,使用完通道,第33行最后都要把这个对象从迭代器中移除,防止重复使用同一个SelectionKey。
回看上面,迭代器每获得一个键后,判断该键是否有效和注册的事件是否符合我们需要,来执行相应的接受连接,写操作或读操作。首先来看接受连接事件,如果SelectionKey键集中有接受连接事件的就绪通道,表明服务器可以接受客户端发来的连接请求了:
package com.justin.nioserver;
import java.net.InetAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class serverAccept {
protected void Accept(Selector selector, SelectionKey availableKey) {
// 首先获得服务器通道
ServerSocketChannel server = (ServerSocketChannel) availableKey.channel();
SocketChannel clientChannel; // 客户端连接
try {
clientChannel = server.accept(); // 监听客户端连接请求,返回的通道就是客户端和服务器通信的通道
clientChannel.configureBlocking(false); // 设置为非阻塞式
// 这个客户端连接的感兴趣事件是读事件
SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
// 为每一个客户端连接分配一个通信用的数据队列
DataQueue dataQueue = new DataQueue();
if(clientChannel != null) {
clientKey.attach(dataQueue); // 通信数据队列附加到这个客户端上
}
InetAddress clientAddress = clientChannel.socket().getInetAddress();
System.out.println("Accepted connection from " + clientAddress.getHostAddress() + ".");
// availableKey.cancel();
} catch (Exception e) {
e.printStackTrace();
}
}
}
接受连接方法,为每一个接入连接的客户端实例化一个SocketChannel对象,它和Socket类似,是一个连接TCP网络的套接字通道,不同的是SocketChannle可以设置为非阻塞式的(第18行)。 建立与服务器端的连接后,我们让客户端把这个通道注册为读事件,让服务器端去读取客户端发来的数据。第24行我们为这个连接分配了一个通信数据交换队列后,把这个队列用SelectionKey.attach()方法附加到键上,即服务器端和客户端通过这个数据交换队列来通信,这是允许的,因为在SelectionKey对象中,除了包含通道,被管理的选择器,interest集合,ready集合外,还有一个“附加对象”,它允许你把一个其他的对象附加到这个SelectionKey键中,例如我们可以自定义一个数据通道附加给它,也可以直接实例化一个ByteBuffer给它:
package com.justin.nioserver;
import java.nio.ByteBuffer;
import java.util.LinkedList;
public class DataQueue {
private LinkedList dataQueue;
DataQueue() {
dataQueue = new LinkedList();
}
// 数据队列写入
public void writeByteBuffer(ByteBuffer data) {
dataQueue.addFirst(data);
}
// 获取数据队列
public LinkedList getQueue() {
return dataQueue;
}
}
第24行,对通道操作前,首先判断该SocketChannel是否为null,为什么要这么作?因为在服务器端,我们的SeverSocketChannel设置成了非阻塞式,所以在调用accept()方法时,如果没有新的客户端连接请求,那么会立刻返回null,所以,每次都要先检查这个客户端SocketChannle是否不为null,再对它进行操作。
package com.justin.nioclient;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOclient implements Runnable {
private Selector selector;
protected void initClient(int port, InetAddress ipAddress) throws IOException {
// 配置要连接的远程服务器地址和端口号
InetSocketAddress address = new InetSocketAddress(ipAddress, port);
SocketChannel channel = SocketChannel.open(); //打开通道
channel.configureBlocking(false); // 设置为非阻塞式
channel.connect(address); // connect()方法连接远程服务器
this.selector = Selector.open(); // 创建选择器
channel.register(selector, SelectionKey.OP_CONNECT); // 注册连接事件
}
protected void connectServer() throws IOException {
while (true) {
if(selector.isOpen() == false) {
break;
}
selector.select(); //查找连接事件的通道
Set connectKeySet = selector.selectedKeys();
Iterator iterator = connectKeySet.iterator(); //迭代器遍历通道集合
while (iterator.hasNext()) {
SelectionKey availableKey = (SelectionKey) iterator.next();
iterator.remove();
if(availableKey.isConnectable()) {
System.out.println("Search connectable channel success!");
new clientConnect().Connect(selector, availableKey);
} else if(availableKey.isValid() && availableKey.isWritable()) {
System.out.println("Search writable channel success!");
new clientWrite().Write(selector, availableKey);
} else if(availableKey.isValid() && availableKey.isReadable()) {
System.out.println("Search readable channel ssuccess!");
new clientRead().Read(selector, availableKey);
}
}
}
}
@Override
public void run() {
System.out.println("Start client.");
NIOclient client = new NIOclient();
try {
client.initClient(5000, InetAddress.getLocalHost());
client.connectServer();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws UnknownHostException, IOException {
Thread clientThread = new Thread(new NIOclient());
clientThread.start();
}
}
首先是初始化,在每一个客户端连接中都实例化一个SocketChannel通道,并把它设置为非阻塞式的通道,事件注册为连接事件。之后的做法与服务器端类似,获得就需通道的键集后,遍历这个集合,获取例如连接就绪的通道,然后执行连接操作(第40行),来看看客户端连接操作:
package com.justin.nioclient;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class clientConnect {
protected void Connect(Selector selector, SelectionKey availableKey) throws IOException {
SocketChannel channel = (SocketChannel) availableKey.channel();
if(channel.isConnectionPending()) {
System.out.println("connecting....");
channel.finishConnect();
}
if(channel.isConnected()) {
System.out.println("Successful connection!");
}
channel.configureBlocking(false);
availableKey.interestOps(SelectionKey.OP_CONNECT); //移除连接事件
channel.register(selector, SelectionKey.OP_WRITE); //注册写事件,向服务器发送信息
// availableKey.cancel();
}
}
连接操作很简单,不过先要判断是否已经建立连接,完成连接后,把通道的连接事件移除,修改为写事件,让客户端通过该通道向服务器端发送数据。之后在客户端线程,Selector选择器会发现这个写事件的就需通道,然后执行相应的写操作。
客户端和服务器端建立连接后,从上面代码可以看到,服务器端由OP_ACCEPT接收事件变为OP_READ读事件,而客户端由OP_CONNECT连接事件变为OP_WRITE写事件,服务器端会去读客户端发来的数据,首先看客户端的写事件:
package com.justin.nioclient;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class clientWrite {
protected void Write(Selector selector, SelectionKey availableKey) throws IOException {
SocketChannel channel = (SocketChannel) availableKey.channel();
ByteBuffer dataByte = ByteBuffer.allocate(1024);
dataByte.clear();
dataByte.put(ByteBuffer.wrap(new String("Hello server!").getBytes("UTF-8")));
dataByte.flip();
if(channel.isConnected()) {
channel.configureBlocking(false);
System.out.println("Send information to the server...");
while(dataByte.hasRemaining()) {
channel.write(dataByte);
}
availableKey.interestOps(SelectionKey.OP_WRITE); //完成写操作后,移除写事件
channel.register(selector, SelectionKey.OP_READ); //注册读事件,接收服务器发回来的信息
// availableKey.cancel();
}
}
}
第11行首先获得这个写事件通道,然后实例化ByteBuffer缓冲区包装我们需要发送的数据。第17行,还是先判断连接是否已建立,因为非阻塞式的SocketChannel可能会得到一个null。之后就可以往通道理写入数据,用while循环,直到ByteBuffer里的数据全部写入为止。最后,第23-24行把通道写事件移除,改为读事件,让客户端发送完数据后,准备接收来自服务器端的回复信息。好了,客户端发送完数据了,服务器端也已经设置成读事件,来看看服务器端的读事件代码:
package com.justin.nioserver;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class serverRead {
protected void Read(Selector selector, SelectionKey availableKey) throws IOException {
SocketChannel channel = (SocketChannel) availableKey.channel();
ByteBuffer dataByte = ByteBuffer.allocate(1024);
System.out.print("Accept messages from client:\r\n");
try {
int dataLength = channel.read(dataByte);
while (dataLength != 0) {
System.out.println("content length: " + dataLength);
dataByte.flip(); // 方法改变读写指针的位置,从0开始
// hasRemaining()方法返回Buffer中剩余的可用长度
while (dataByte.hasRemaining()) {
System.out.print((char)dataByte.get());
}
System.out.println("\r\n");
dataByte.clear(); //清空缓冲区
dataLength = channel.read(dataByte);
}
availableKey.interestOps(SelectionKey.OP_READ); //读完数据后,将读事件移除,防止一直重复读
channel.register(selector, SelectionKey.OP_WRITE); //注册写事件
// availableKey.cancel();
} catch (Exception e) {
System.out.println("Error to read.");
e.printStackTrace();
availableKey.channel().close();
return;
}
}
}
读事件同样先获得就绪通道,然后实例化一个ByteBuffer缓冲区来存读到的数据,缓冲区分配1024字节的大小。这里的读事件操作只是简单地把读到的数据输出,实际情况中服务器通常要把客户端发来的数据做一些处理,只要在这部分中自行添加数据处理方法即可。数据读完/处理完后,把通道的读事件移除,修改为写事件,模拟实际情况中的服务器端对客户端数据做完处理后,返回给客户端。
来看看服务器端的写事件:
package com.justin.nioserver;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class serverWrite {
protected void Write(Selector selector, SelectionKey availableKey) throws IOException {
SocketChannel channel = (SocketChannel) availableKey.channel(); // 获得通道
/* 你可以通过获得客户端和服务器端的通信数据交换队列来进行数据传输
*DataQueue dataQueue = (DataQueue) availableKey.attachment(); // 获得通讯队列
*LinkedList queue = dataQueue.getQueue();
*/
LinkedList queue = new LinkedList();
queue.add(ByteBuffer.wrap(new String("Hello client!").getBytes("UTF-8")));
ByteBuffer dataByte = queue.getLast();
/* 你也可以用ByteBuffer直接给客户端发送数据
*ByteBuffer dataByte = ByteBuffer.allocate(1024);
*dataByte.clear();
*dataByte.put(ByteBuffer.wrap(new String("Hello client!").getBytes("UTF-8")));
*dataByte.flip();
*/
System.out.println("Send messages to the client...");
try {
int dataLength = channel.write(dataByte); //写入通道
if(dataLength == -1) {
availableKey.channel().close();
return;
}
if(dataByte.remaining() == 0) {
// Buffer缓冲区内的数据全部写完了
queue.removeLast();
}
} catch (Exception e) {
System.out.println("Error to write.");
e.printStackTrace();
availableKey.channel().close();
return;
}
if(queue.size() == 0) {
availableKey.interestOps(SelectionKey.OP_WRITE); // 所有数据写完后,将写事件移除
channel.close();
availableKey.cancel();
}
}
}
因为我们前面为每一个服务器端-客户端连接都绑定了一个数据交换队列,所以其实我们可以通过这个队列做通讯,当然也可以直接用ByteBuffer。当所有数据写入完成后,服务器端就可以把这个通道关闭了,移除通道的写事件,同时第38行,把通信队列中的数据也清空,清除缓存。
最后一步,就是客户端接受服务器端发来的回应数据:
package com.justin.nioclient;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class clientRead {
protected void Read(Selector selector, SelectionKey availableKey) throws IOException {
SocketChannel channel = (SocketChannel) availableKey.channel();
ByteBuffer dataByte = ByteBuffer.allocate(1024);
//读数据到缓冲区Buffer中
int readData = channel.read(dataByte);
while (readData != -1) {
System.out.println("Accept messages from server: " + readData);
dataByte.flip(); // 方法改变读写指针的位置,从0开始
while (dataByte.hasRemaining()) {
System.out.print((char)dataByte.get());
}
System.out.println("\r\n");
dataByte.clear(); //清空缓冲区
readData = channel.read(dataByte);
}
availableKey.interestOps(SelectionKey.OP_READ);
channel.close();
availableKey.cancel();
}
}
客户端的读事件和服务器端基本一样,获得读事件通道后,使用ByteBuffer缓冲区存放读到的数据并输出,所有数据读取完毕后,把通道的读事件移除,关闭通道即可。
来看看它们的交互结果:
完整实现代码已上传GitHub:
https://github.com/justinzengtm/Java-Multithreading/tree/master/Java-NIO/NIO-NETWORK/NIO-network