细节重点!
Selector.select()只对这个方法被调用前注册进来的事件才会被监听。如果在这个方法阻塞后,有其他线程往这个selector注册事件,不会被监听到!
这个带来的影响直接影响多线程多Selector的程序设计!!!
初步引入netty原理图,帮助理解上述细节
下图中,select->processSelectedKeys->runAllTasks在while(true)中不断循环(loop),select从阻塞返回,处理相应的keys后,调用runAllTasks方法,将select阻塞过程中新注册进来的事件添加到selector中,这样再次返回到select阻塞方法后,新注册的事件就可以被selector监听到了!!!
使用多selector多线程模式
多路复用器线程类,同时也是一个ThreadLocal在生成对象前已经完成对selector的初始化。主要负责接收并监听新的事件请求。
public class SelectorThread extends ThreadLocal<LinkedBlockingQueue<Channel>> implements Runnable{
// 一个线程对应一个selector,
// 每个客户端,只绑定到其中一个selector
Selector selector = null;
LinkedBlockingQueue<Channel> lbq = get(); //获取initialValue的值
SelectorThreadGroup stg;
@Override
protected LinkedBlockingQueue<Channel> initialValue() {
return new LinkedBlockingQueue<>();//初始化与当前线程绑定的ThreadLocal的值,通过get可以获取
}
SelectorThread(SelectorThreadGroup stg){
try {
this.stg = stg;
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//Loop
while (true){
try {
int nums = selector.select(); //阻塞,wakeup()可以让select()立即返回
//2,处理selectkeys
if(nums>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){ //线程处理的过程
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){ //复杂,接受客户端的过程(接收之后,要注册,多线程下,新的客户端,注册到那里呢?)
acceptHandler(key);
}else if(key.isReadable()){
readHander(key);
}else if(key.isWritable()){
}
}
}
//3,处理一些task : listen client
if(!lbq.isEmpty()){ //如果队列不为空,表示有新的连接要注册到selector
Channel c = lbq.take();
if(c instanceof ServerSocketChannel){
ServerSocketChannel server = (ServerSocketChannel) c;
server.register(selector,SelectionKey.OP_ACCEPT);
System.out.println(Thread.currentThread().getName()+" register listen");
}else if(c instanceof SocketChannel){
SocketChannel client = (SocketChannel) c;
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println(Thread.currentThread().getName()+" register client: " + client.getRemoteAddress());
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void readHander(SelectionKey key) {
System.out.println(Thread.currentThread().getName()+" read......");
ByteBuffer buffer = (ByteBuffer)key.attachment();
SocketChannel client = (SocketChannel)key.channel();
buffer.clear();
while(true){
try {
int num = client.read(buffer);
if(num > 0){
buffer.flip(); //将读到的内容翻转,然后直接写出
while(buffer.hasRemaining()){
client.write(buffer);
}
buffer.clear();
}else if(num == 0){
break;
}else if(num < 0 ){
//客户端断开了
System.out.println("client: " + client.getRemoteAddress()+"closed......");
key.cancel();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void acceptHandler(SelectionKey key) {
System.out.println(Thread.currentThread().getName()+" acceptHandler......");
ServerSocketChannel server = (ServerSocketChannel)key.channel();
try {
SocketChannel client = server.accept();
client.configureBlocking(false);
stg.nextSelectorV3(client);
} catch (IOException e) {
e.printStackTrace();
}
}
// 给当前多路复用器设置工作者线程
public void setWorker(SelectorThreadGroup stgWorker) {
this.stg = stgWorker;
}
}
线程组:包含多个SelectorThread,使用多个多路复用器,可以同时接收多个事件连接。
public class SelectorThreadGroup {
ServerSocketChannel server=null;
SelectorThread[] sts;
AtomicInteger xid = new AtomicInteger(0);
SelectorThreadGroup stg = this;// 默认是boss
// 可以选择设置worker,处理客户端读写请求
public void setWorker(SelectorThreadGroup stg){
this.stg = stg;
}
SelectorThreadGroup(int num){
//num 线程数
sts = new SelectorThread[num];
for (int i = 0; i < num; i++) {
sts[i] = new SelectorThread(this);
new Thread(sts[i]).start();
}
}
public void bind(int port) {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//注册到某个selector
nextSelectorV3(server);
} catch (IOException e) {
e.printStackTrace();
}
}
public void nextSelectorV3(Channel c) {
try {
if(c instanceof ServerSocketChannel){
SelectorThread st = nextBossThread(); //listen 选择了 boss组中的一个线程后,要更新这个线程的work组
st.lbq.put(c);
st.setWorker(stg);
st.selector.wakeup();// 使得这个事件在线程st的selector中生效
}else {
SelectorThread st = nextWorkerThread(); //在 main线程种,取到堆里的selectorThread对象
//1,通过队列传递数据 消息
st.lbq.add(c);
//2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
st.selector.wakeup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 返回boss的一个线程
private SelectorThread nextBossThread() {
int index = xid.incrementAndGet() % sts.length;
return sts[index];
}
// 返回worker的一个线程
private SelectorThread nextWorkerThread() {
int index = xid.incrementAndGet() % stg.sts.length;
return stg.sts[index];
}
}
启动
public class MainThread {
public static void main(String[] args) {
//1,创建 IO Thread (一个或者多个)
//boss有自己的线程组
SelectorThreadGroup boss = new SelectorThreadGroup(3); //创建slector线程组并启动线程,一个线程一个selector
//worker有自己的线程组
SelectorThreadGroup worker = new SelectorThreadGroup(3);
// boss线程组拥有worker线程组,因为boss一旦accept得到client后得去worker中 next出一个线程分配
boss.setWorker(worker);
// boss线程组可以同时监听多个端口
boss.bind(9999);
boss.bind(8888);
boss.bind(6666);
boss.bind(7777);
}
}
总结
以上实现的功能:
1. 同一个端口进来的连接交给同一个boss线程处理,不同的连接会轮询boss线程组,交由不同的boss线程处理。
2. 不同的boss线程处理accept请求后,剩下的其它事件都交由同一个worker线程组处理。
这样就完成了boss只需要负责监听accept连接,后续的read/write事件交由worker去执行。