1.背景

通过JavaNio实现一个简单的网络模块,有点像Netty的线程模型,一个线程(AcceptThread)建立新连接,把新连接绑定到某个SelectorThread,SelectorThread处理读/写。

  • AcceptThread:拥有一个Selector,上面只注册了一个ServerSocketChannel,监听客户端新连接,当接收到新连接时,把新连接注册到SelectorThread上。
  • SelectorThread:拥有一个Selector,用于注册客户端连接,并且处理读/写事件。

2.代码

public class NonblockingServer {
/** 处理客户端读写 */
private SelectorThread[] selectorThreads;
/** 建立新连接 */
private AcceptThread acceptThread;
/** 选择器 */
private SelectNextSelectorThread nextSelectorThread;
/** 默认selectorThread数量 */
private static int defaultSelectorNum = 3; public NonblockingServer() {
this(defaultSelectorNum);
} public NonblockingServer(int selectorThreadNum) {
if (selectorThreadNum < 1) {
throw new IllegalArgumentException("SelectorThread线程数量不能低于1");
}
selectorThreads = new SelectorThread[selectorThreadNum];
try {
for (int i = 0; i < selectorThreads.length; i++) {
selectorThreads[i] = new SelectorThread("selector-thread-" + i);
}
acceptThread = new AcceptThread("accept-thread");
nextSelectorThread = new SelectNextSelectorThread(Arrays.asList(selectorThreads));
} catch (Exception e) {
throw new RuntimeException(e);
}
} public void listen(String host, int port) {
try {
acceptThread.listen(host, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
} /**
* 处理连接事件,把新连接注册到一个SelectorThread上
*/
class AcceptThread extends Thread {
private Selector selector;
private ServerSocketChannel serverChannel; AcceptThread(String name) throws IOException {
super(name);
selector = Selector.open();
} public void listen(String host, int port) throws IOException {
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
serverChannel.bind(new InetSocketAddress(host, port));
this.start();
} @Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
int select = selector.select();
if (select == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handAccept();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} private void handAccept() throws IOException {
SocketChannel clientChannel = serverChannel.accept();
SelectorThread selectorThread = nextSelectorThread.nextThread();
// 懒加载
if (!selectorThread.isStart()) {
selectorThread.start();
}
selectorThread.register(clientChannel);
}
} /**
* 处理客户端连接的读/写事件
*/
class SelectorThread extends Thread {
private Selector selector;
private volatile boolean start = false; SelectorThread(String name) throws IOException {
super(name);
selector = Selector.open();
} @Override
public void run() {
while (start) {
try {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (start && iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
if (key.isReadable()) {
handRead(channel);
} else if (key.isWritable()) {
handWrite(channel);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} /**
* 这里简单的把输出打印出来
*/
private void handRead(SocketChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read == -1) {
System.out.println(channel.getRemoteAddress() + " 断开连接");
channel.close();
} else {
byte[] bytes = new byte[read];
buffer.flip();
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
} private void handWrite(SocketChannel channel) {
// nothing
} /**
* 把新连接注册到本线程
*/
public void register(SocketChannel clientChannel) throws IOException {
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} @Override
public synchronized void start() {
start = true;
super.start();
} public void shutdown() {
this.start = false;
this.interrupt();
} public boolean isStart() {
return start;
}
} static class SelectNextSelectorThread {
private final Collection<? extends SelectorThread> threads;
private Iterator<? extends SelectorThread> iterator; public <T extends SelectorThread> SelectNextSelectorThread(Collection<T> threads) {
this.threads = threads;
iterator = this.threads.iterator();
} /**
* 选择下一个SelectorThread,这里为轮询
*
* @return SelectorThread
*/
public SelectorThread nextThread() {
if (!iterator.hasNext()) {
iterator = threads.iterator();
}
return iterator.next();
}
}
}

NonblockingServer

3.问题

当用客户端连接几次后,发现AcceptThread被卡死了!新连接无法建立。
用jvisualVM可以看到AcceptThread状态为监视,说明此时的AcceptThread在等待某个锁。


通过IDEA的dump threads也可以看到:

观察SelectThread线程:

SelectorImpl.lockAndDoSelect代码:

    private int lockAndDoSelect(long var1) throws IOException {
synchronized(this) {
if (!this.isOpen()) {
throw new ClosedSelectorException();
} else {
int var10000;
synchronized(this.publicKeys) {
synchronized(this.publicSelectedKeys) {
var10000 = this.doSelect(var1);
}
} return var10000;
}
}
}

SelectorImpl.register代码:

    protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
synchronized(this.publicKeys) {
this.implRegister(var4);
} var4.interestOps(var2);
return var4;
}
}

这两个方法都需要获取this.publicKeys的锁,经过调试发现,SeletorThread在获取Selector的产生的事件时selector.select(),一直在不断的循环执行SelectorImpl.lockAndDoSelect方法,一直在不停的获取this.publicKeys的锁,所以这里的register方法很难有机会拿到this.publicKeys的锁,AcceptThread就卡死在获取锁的过程中了。

4.解决方法

Netty在注册新连接时,是把这个注册过程封装成一个任务,交给SelectorThread执行的,就不会发生线程冲突。

Thrift是把新连接放到SelecttorThread中的BlockingQueue中,也是由SelectorThread执行的。

5.修正后的代码

public class NonblockingServer {
/** 处理客户端读写 */
private SelectorThread[] selectorThreads;
/** 建立新连接 */
private AcceptThread acceptThread;
/** 选择器 */
private SelectNextSelectorThread nextSelectorThread;
/** 默认selectorThread数量 */
private static int defaultSelectorNum = 3; public NonblockingServer() {
this(defaultSelectorNum);
} public NonblockingServer(int selectorThreadNum) {
if (selectorThreadNum < 1) {
throw new IllegalArgumentException("SelectorThread线程数量不能低于1");
}
selectorThreads = new SelectorThread[selectorThreadNum];
try {
for (int i = 0; i < selectorThreads.length; i++) {
selectorThreads[i] = new SelectorThread("selector-thread-" + i);
}
acceptThread = new AcceptThread("accept-thread");
nextSelectorThread = new SelectNextSelectorThread(Arrays.asList(selectorThreads));
} catch (Exception e) {
throw new RuntimeException(e);
}
} public void listen(String host, int port) {
try {
acceptThread.listen(host, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
} /**
* 处理连接事件,把新连接注册到一个SelectorThread上
*/
class AcceptThread extends Thread {
private Selector selector;
private ServerSocketChannel serverChannel; AcceptThread(String name) throws IOException {
super(name);
selector = Selector.open();
} public void listen(String host, int port) throws IOException {
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
serverChannel.bind(new InetSocketAddress(host, port));
this.start();
} @Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
int select = selector.select();
if (select == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
handAccept();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} private void handAccept() throws IOException {
SocketChannel clientChannel = serverChannel.accept();
SelectorThread selectorThread = nextSelectorThread.nextThread();
// 懒加载
if (!selectorThread.isStart()) {
selectorThread.start();
}
selectorThread.register(clientChannel);
}
} /**
* 处理客户端连接的读/写事件
*/
class SelectorThread extends Thread {
private Selector selector;
private BlockingQueue<Runnable> tasks;
private volatile boolean start = false; SelectorThread(String name) throws IOException {
super(name);
selector = Selector.open();
tasks = new LinkedBlockingQueue<>();
} @Override
public void run() {
while (start) {
try {
selector.select();
handTask();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (start && iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
if (key.isReadable()) {
handRead(channel);
} else if (key.isWritable()) {
handWrite(channel);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} private void handTask() {
Runnable task;
while (start && (task = tasks.poll()) != null) {
task.run();
}
} /**
* 这里简单的把输出打印出来
*/
private void handRead(SocketChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read == -1) {
System.out.println(channel.getRemoteAddress() + " 断开连接");
channel.close();
} else {
byte[] bytes = new byte[read];
buffer.flip();
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
} private void handWrite(SocketChannel channel) {
// nothing
} /**
* 把新连接注册到本线程
*/
public void register(SocketChannel clientChannel) {
submit(() -> {
try {
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} catch (Exception e) {
try {
clientChannel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
selector.wakeup();
} public void submit(Runnable task) {
tasks.offer(task);
} @Override
public synchronized void start() {
start = true;
super.start();
} public void shutdown() {
this.start = false;
this.interrupt();
} public boolean isStart() {
return start;
}
} static class SelectNextSelectorThread {
private final Collection<? extends SelectorThread> threads;
private Iterator<? extends SelectorThread> iterator; public <T extends SelectorThread> SelectNextSelectorThread(Collection<T> threads) {
this.threads = threads;
iterator = this.threads.iterator();
} /**
* 选择下一个SelectorThread,这里为轮询
*
* @return SelectorThread
*/
public SelectorThread nextThread() {
if (!iterator.hasNext()) {
iterator = threads.iterator();
}
return iterator.next();
}
}
}

NonblockingServer

【记录】Java NIO实现网络模块遇到的BUG的更多相关文章

  1. JAVA NIO学习记录1-buffer和channel

    什么是NIO? Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不 ...

  2. Java NIO学习与记录(八): Reactor两种多线程模型的实现

    Reactor两种多线程模型的实现 注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现 紧接着上篇Reactor单线程模型的例子来,假设Handler的 ...

  3. java nio的一个严重BUG

    java nio的一个严重BUG Posted on 2009-09-28 19:27 dennis 阅读(4588) 评论(5)  编辑  收藏 所属分类: java .源码解读      这个BU ...

  4. Java nio 空轮询bug到底是什么

    编者注:Java nio 空轮询bug也就是Java nio在Linux系统下的epoll空轮询问题. epoll机制是Linux下一种高效的IO复用方式,相较于select和poll机制来说.其高效 ...

  5. JAVA NIO学习记录2-非阻塞式网络通信

    一.阻塞与非阻塞 传统的IO 流都是阻塞式的.也就是说,当一个线程调用read() 或write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务.因此,在完成网络通信 ...

  6. Java NIO学习与记录(五): 操作系统的I/O模型

    操作系统的I/O模型 在开始介绍NIO Reactor模式之前,先来介绍下操作系统的五种I/O模型,了解了这些模型,对理解java nio会有不小的帮助. 先来看下一个服务端处理一次网络请求的流程图: ...

  7. Java NIO 入门

    本文主要记录 Java 中  NIO 相关的基础知识点,以及基本的使用方式. 一.回顾传统的 I/O 刚接触 Java 中的 I/O 时,使用的传统的 BIO 的 API.由于 BIO 设计的类实在太 ...

  8. 源码分析netty服务器创建过程vs java nio服务器创建

    1.Java NIO服务端创建 首先,我们通过一个时序图来看下如何创建一个NIO服务端并启动监听,接收多个客户端的连接,进行消息的异步读写. 示例代码(参考文献[2]): import java.io ...

  9. java nio系列文章

    java nio系列教程 基于NIO的Client/Server程序实践 (推荐) java nio与并发编程相关电子书籍   (访问密码 48dd) 理解NIO nio学习记录 图解ByteBuff ...

随机推荐

  1. nyoj 78:圈水池 【凸包入门】

    题目链接 将所有点按从左至右顺序排序,然后将所有点先从左到右扫描再从右到左扫描,逐渐将凸包轮廓“勾勒”出来 (凸包轮廓满足,轮廓上连续的三个点按先后顺序给出的话呈逆时针方向) 最后删去一个重复的起(终 ...

  2. 禁止打开 F12 开发者工具

    禁止F12 window.onkeydown = window.onkeyup = window.onkeypress = function (event) { // 判断是否按下F12,F12键码为 ...

  3. Linux系统重要文件

    Linux系统重要文件介绍与配置 一网卡配置文件 (一)网卡配置文件参数介绍 文件路径信息:/ec/sysconfig/network-scripts/ifcfg-eth0 文件作用说明:配置网卡地址 ...

  4. dubbo-go 的开发、设计与功能介绍

    dubbo-go 的前世今生 dubbo-go 是目前 Dubbo 多语言生态最火热的项目.dubbo-go 最早的版本应该要追溯到 2016 年,由社区于雨同学编写 dubbo-go 的初版.当时很 ...

  5. gitHub那些优秀的库和想要实现的效果

    1. 轮播库SDCycleScrollView 2. 自动布局库SDAutoLayout 3. 类似支付宝福卡滑动切换的效果 https://github.com/huangxuan518/HXCar ...

  6. html 的一些基础操作

    花了一天学了点html语言..不记下来的话又白学了 基础中的基础格式 <!DOCTYPE html> <html> <head> <!-- 字符集的选择 ut ...

  7. 建站手册-浏览器信息:挪威的 Opera 浏览器

    ylbtech-建站手册-浏览器信息:挪威的 Opera 浏览器 1.返回顶部 1. http://www.w3school.com.cn/browsers/browsers_opera.asp 2. ...

  8. Elasticsearch后台运行步骤

    Elasticsearch后台运行步骤 1.cmd 到elasticsearch 中bin目录下 2.elasticsearch-service 出现  3.安装服务 elasticsearch-se ...

  9. appium常见问题05_修改Android手机运行环境(adb指令修改hosts)

    自动化测试过程中,手机有时会跳网,怎样保持手机测试的环境稳定性,可以通过adb指令修改android手机hosts,保持手机运行在hosts中配置的环境中: 修改方法如下: 前提条件:已安装andro ...

  10. mybatis的一些小细节

    Mybatis要解决的问题: 1. 将sql语句硬编码到java代码中,如果修改sql语句,需要修改java代码,重新编译.系统可维护性不高. 设想如何解决? 能否将sql单独配置在配置文件中. 2. ...