服务端程序启动一个服务线程负责监听客户端连接,接收新连接后,使用负载均衡从8个后台负责通信的线程中选一个将其注册到该线程,由该线程负责和该客户端通道的通信工作。每个线程一个selector。
服务端代码
class NioReactorChatServer {
ExecutorService workPool; // 任务线程,业务使用
private final static int WORKER_NUMBER = 8;
private ReactorThread[] acceptThreads;
private ReactorThread[] workThreads;
public NioReactorChatServer() {
this.workThreads = new ReactorThread[WORKER_NUMBER];
this.acceptThreads = new ReactorThread[1]; // 也可设置一个
workPool = Executors.newCachedThreadPool();
}
// 调用入口,监听服务开启
public void startAccept() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8001)); // 默认IP是0.0.0.0
acceptThreads[0] = newAcceptWorker();
ReactorThread acceptWorker = acceptThreads[0];
acceptWorker.start(); // 先启动,因为下面语句基本都是先于run方法执行的
acceptWorker.register(serverSocketChannel, SelectionKey.OP_ACCEPT);
}
// 创建服务端监听工作线程
private ReactorThread newAcceptWorker() throws IOException {
return new ReactorThread() {
@Override
public void handle(SelectableChannel channel) throws Exception {
ServerSocketChannel server = (ServerSocketChannel) channel;
SocketChannel client = server.accept();
client.configureBlocking(false);
// 将该client负载均衡到workThread中去
ReactorThread worker = getWorker();
worker.start(); // 启动worker,重复启动只是保证启动
worker.register(client, SelectionKey.OP_READ);
}
};
}
// 选择一个工作线程负责该客户端通信,计算负载均衡,选择负载最小的那个,如果线程数没达到上限则创建新工作线程负责
private ReactorThread getWorker() throws IOException {
int leastLoad = Integer.MAX_VALUE;
int selected = 0;
for (int i = 0; i < WORKER_NUMBER; i++) {
ReactorThread worker = workThreads[i];
if (worker == null) {
worker = newWorker();
workThreads[i] = worker;
return worker;
}
if (worker.getCount() < leastLoad) {
leastLoad = worker.getCount();
if (leastLoad == 0) {
return worker; // 如果有负载为0的直接使用,也就是工作线程负责的管道关闭后被清理后降为0直接使用免得创建新的
}
selected = i;
}
}
return workThreads[selected];
}
// 创建一个新的工作线程
private ReactorThread newWorker() throws IOException {
return new ReactorThread() {
@Override
public void handle(SelectableChannel channel) throws Exception {
SocketChannel client = (SocketChannel) channel;
StringBuilder content = new StringBuilder();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readLength = client.read(buffer);
while (client.isOpen() && readLength > 0) {
// 这里可以自定义协议接收,此时readLength>0改为!=-1,因为等于0通道可能继续发送,判断结束是协议遇到结束标识
content.append(new String(buffer.array(), 0, readLength));
buffer.clear();
readLength = client.read(buffer);
}
if (readLength == -1) {
client.close();
}
if (client.isOpen()) {
System.out.println(getName() + " receive: " + content.toString());
workPool.submit(() -> {
// todo 业务操作,数据库,接口等
});
// 读取完回应数据,如果client的isOpen为false则下面的写入会再次打开
buffer = ByteBuffer.wrap("response hello world".getBytes());
while (buffer.hasRemaining()) {
client.write(buffer);
}
}
}
};
}
// 负责和客户端通讯
abstract class ReactorThread extends Thread {
// 新连接的注册任务放在selector工作线程中,降低争夺锁的可能性。
// 虽然taskQueue是同步的,但因为register和select方法同步,select内部轮询方式进行,等待时更容易导致register争夺锁。
private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(); // 分配到该工作线程的新连接任务
private Selector selector;
private final AtomicInteger count = new AtomicInteger(0); // 负载计数,指负责的channel数量,使用原子计数
private int keyCount = 0; // select中的keys的大小
private boolean running = false;
@Override
public synchronized void start() {
// 拦截,重复调用只启动一个线程,running一旦设为true将不再设为false,该线程interrupt之后不能再启动,需重新创建
if (!running) {
super.start();
running = true;
}
}
/**
* 创建一个Selector的工作线程负责注册到其上的通道通信工作,子类实现的读写数据处理工作。
*
* @throws IOException IO异常
*/
public ReactorThread() throws IOException {
this.selector = Selector.open();
}
@Override
public void run() {
while (!isInterrupted()) {
try {
Runnable task;
while ((task = keyCount > 0 ? taskQueue.poll() : taskQueue.take()) != null) {
task.run();
}
} catch (InterruptedException ex) {
continue;
}
try {
// 有通道时最长等待1秒,如果等待中有Ready事件不管有没到1秒都立即返回,到时限没有就绪事件返回0,没有通道时设最小时长。
selector.select(keyCount > 0 ? 1000 : 1); // keyCount为0,select参数为0仍然阻塞到有就绪事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
try {
handle(key.channel()); // 由使用者处理这个就绪事件
if (!key.channel().isOpen()) {
key.cancel(); // 关闭的管道取消订阅
decrementKeyCount();
}
} catch (Exception ex) {
key.cancel(); // 关闭的管道取消订阅
decrementKeyCount();
}
}
} catch (IOException exc) {
// empty
}
}
}
/**
* 处理具体的读写请求
*
* @param channel 连接通道,这里指客户端通道对象
* @throws Exception 异常
*/
public abstract void handle(SelectableChannel channel) throws Exception;
/**
* 将新的通道,一般是客户端通道,注册到该线程的选择器上
*
* @param channel 要注册的通道
* @param ops 对该通道的关心事件,包括读写等,用于该通道接下来的通信
*/
public void register(SelectableChannel channel, int ops) {
this.taskQueue.add(() -> {
try {
channel.register(this.selector, ops);
keyCount++;
} catch (ClosedChannelException exc) {
// An empty catch block,客户端刚连接后中断
count.decrementAndGet();
}
});
this.count.incrementAndGet();
}
// 更新keys中的数量同时更新负载数量
private void decrementKeyCount() {
keyCount--;
count.decrementAndGet();
}
/**
* 取得该线程的订阅的Key数量,即管道数量
*/
public int getCount() {
return count.get();
}
}
}
客户端代码
class NioChatClient {
private Scanner scanner = new Scanner(System.in);
void start() throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("localhost", 8001));
// 注册到选择器(多路复用器)
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
do {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 取得就绪状态的SelectionKey
Iterator<SelectionKey> keys = selectionKeys.iterator(); // 生成迭代器
while (keys.hasNext()) {
SelectionKey key = keys.next();
if (key.isConnectable()) {
System.out.println("connect loop.");
channel.finishConnect();
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isReadable()) {
System.out.println("connect loop.");
read(key);
} else if (key.isWritable()) {
System.out.println("connect loop.");
write(key);
}
keys.remove();
}
System.out.println("select one time.");
} while (!getInput("exit? ").startsWith("y"));
channel.close();
this.scanner.close();
}
private void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int number = channel.read(buffer); // 读取一次一般要求一次发送的内容不超过buffer长度
String message = new String(buffer.array(), 0, number);
System.out.println("receive message: " + message);
// channel.register(selector, SelectionKey.OP_WRITE); // 读取完,注册写操作,下次轮询直接进入写
}
private void write(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
String message = getInput("send message: ");
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// channel.register(selector, SelectionKey.OP_READ); // 写完,注册读操作,等待读
}
// 读取标准输入内容
private String getInput(String prompt) {
System.out.print(prompt);
return this.scanner.nextLine();
// scanner.close()将同时关闭System.in,调用后in关闭,下次再输入时将无法使用
}
}
如果有什么建议欢迎提出。