java NIO-我们到底能走多远系列(39)

时间:2024-11-30 13:05:13

  献给各位:

Satisfied Mind
Red Hayes and Jack Rhodes
How many times have you heard someone say,
"If I had his money I would do things my way."
How little they know, well, it's so hard to find,
One rich man in ten with a satisfied mind.
Oh, once I was waiting for fortune and fame,
Had everything that I needed to get a start in life's game.
Then suddenly it happened, I lost every dime,
But I'm richer by far with a satisfied mind.
No money can buy back your youth when you're old,
Or a friend when you're lonely or a love that's grown cold.
And the world's richest person is a pauper at times,
Compared to the one with a satisfied mind.
When my life is over and my time has run out,
All my friends and my loved ones I'm gonna leave them no doubt.
But there's one thing for certain, when it comes my time,
I'm gonna leave this old world with a satisfied mind.

  

主题:

一直想研读netty的框架,看了些后发现的确能学到好些东西,又牵涉到各种知识,所以回过头来再复习一下NIO的几个细节。

1,非阻塞 和 阻塞区别:

也许会在面试的时候会被问道吧,理解为需要做一件事不能立即得到完成后的应答,需要等待,那就阻塞了,否则就可以理解为非阻塞。

最常举的例子就是socket的例子了:

传统的serversocket阻塞模式:

public class ServerSocketApp {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8989);
ss.accept(); //等待
System.out.println(1);
}
}

运行这个程序 为什么没有输出1 ?
因为ServerSocket 是阻塞模式的 ,在没有任何连接之前,accept方法一直在那里阻塞着,直到有connection来继续往下执行,所以在运行程序的时候,并没输出1,若要输出 telnet一下就可以了

nio中的 非阻塞:
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(8989));
// 设置成非阻塞
ssc.configureBlocking(false);
ssc.accept(); // 不需要等待
System.out.println(1);
}

运行这个程序 有1 输出!!
这就是因为 它是非阻塞模式的。

2,readiness selection的设计

  事实上,socket的底层读写操作会阻塞,前面的第一个例子就是这个原因导致的,第二个例子只是忽略了这个原因都返回而已。

弄明白为什么socket的read/write会阻塞:

  write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。当kernel的该socket的发送缓冲区已满时,write操作就会被阻塞了,那么调用这个write的线程就会阻塞。read调用阻塞,通常是发送端的数据没有到达。
传统的解决方案是:每一个socket对应一个线程,在read的时候阻塞,事实上就是不断在问准备好没有,一旦准备好之后就由这个线程执行逻辑。这样就把每一个线程当做监听器,来监听每一个socket的就绪状态。这个其实就是非阻塞模式
这样就又来个readiness selection的解决方案:
  在操作socket的时候,我们只主动方,读,写,不管是否可以读了,是否可以写了,也就是上面提到的缓冲区是不是有空间啊,数据是不是已经穿过来了这些判断。
这儿有牵涉到一个好莱坞原则: “不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”

  因为我们去read/write的时候并不知道是否会线程阻塞,所以我们把主被换一下,当我们知道已经符合read/write条件时,通知线程来进行read/write的真正操作。
有了上面的解释,关于readiness selection的设计其实已经差不多明了了。
把各个channel注册到Selecotor上,每一个channel绑定一个SelectionKey,selectionKey上包括了一个这个channel感兴趣的key,然后访问Selecotor上的keys来查看已经符合就绪条件的channel,然后无阻塞的操作。

结构图:
java NIO-我们到底能走多远系列(39)
readiness selection的设计和实现中,有三个重要角色SelectableChannel、SelectionKey和Selecotor。 
它们之间的关系和使用方法,可以参考它们的源码~
1,Selecotor也只是封装了调用select( ), poll( ) ,等其他本地操作而已。管理这所有的key,内部封装了3个key set,来维护注册的key,就绪的key,删除的key:
Registered key set
Selected key set
Cancelled key set
2,SelectionKey类似是chennel的指针
3,SelectableChannel 带有Selectable属性的channel
具体的使用代码:
public class NIOServer {

    /* 标识数字 */
private int flag = 0;
/* 缓冲区大小 */
private int BLOCK = 4096;
/* 接受数据缓冲区 */
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/* 发送数据缓冲区 */
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector; public NIOServer(int port) throws IOException {
// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);
// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));
// 通过open()方法找到Selector
selector = Selector.open();
// 注册到selector,等待连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----8888:");
} // 监听
private void listen() throws IOException {
while (true) {
// 选择一组键,并且相应的通道已经打开
selector.select();
// 返回此选择器的已选择键集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
} // 处理请求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count = 0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {
// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();
// 配置为非阻塞
client.configureBlocking(false);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_WRITE);
} else if (selectionKey.isReadable()) {
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
// 将缓冲区清空以备下次读取
receivebuffer.clear();
// 读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0, count);
System.out.println("服务器端接受客户端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
// 将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
sendText = "message from server--" + flag++;
// 向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
// 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
// 输出到通道
client.write(sendbuffer);
System.out.println("服务器端向客户端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_WRITE);
}
} /**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 10000;
NIOServer server = new NIOServer(port);
server.listen();
}
}
client端:
public class ClientTest {

    /* 标识数字 */
private static int flag = 0;
/* 缓冲区大小 */
private static int BLOCK = 4096;
/* 接受数据缓冲区 */
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/* 发送数据缓冲区 */
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/* 服务器端地址 */
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 10000); public static void main(String[] args) throws IOException {
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS); Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count = 0; while (true) {
// 选择一组键,其相应的通道已为 I/O 操作准备就绪。
// 此方法执行处于阻塞模式的选择操作。
selector.select();
// 返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
// System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
// 将缓冲区清空以备下次读取
receivebuffer.clear();
// 读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0,
count);
System.out.println("客户端接受服务器端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_READ);
} } else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
// 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}

3,Reactor-反应堆模式

参考Doug Lea的多线程反应堆模式的实现代码如下,详细可以参考他的ppt:摸我

结构图:

java NIO-我们到底能走多远系列(39)

class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverChannel;
static final int WORKER_POOL_SIZE = 10;
static ExecutorService workerPool; Reactor(int port) throws IOException {
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false); // Register the server socket channel with interest-set set to ACCEPT operation
SelectionKey sk = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
} public void run() {
try {
while (true) { selector.select();
Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) {
SelectionKey sk = (SelectionKey) it.next();
it.remove();
Runnable r = (Runnable) sk.attachment();
if (r != null)
r.run();
}
}
}
catch (IOException ex) {
ex.printStackTrace();
}
} class Acceptor implements Runnable {
public void run() {
try {
SocketChannel channel = serverChannel.accept();
if (channel != null)
new Handler(selector, channel);
}
catch (IOException ex) {
ex.printStackTrace();
}
}
} public static void main(String[] args) {
workerPool = Executors.newFixedThreadPool(WORKER_POOL_SIZE); try {
new Thread(new Reactor(9090)).start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
class Handler implements Runnable {
final SocketChannel channel;
final SelectionKey selKey; static final int READ_BUF_SIZE = 1024;
static final int WRiTE_BUF_SIZE = 1024;
ByteBuffer readBuf = ByteBuffer.allocate(READ_BUF_SIZE);
ByteBuffer writeBuf = ByteBuffer.allocate(WRiTE_BUF_SIZE); Handler(Selector sel, SocketChannel sc) throws IOException {
channel = sc;
channel.configureBlocking(false); // Register the socket channel with interest-set set to READ operation
selKey = channel.register(sel, SelectionKey.OP_READ);
selKey.attach(this);
selKey.interestOps(SelectionKey.OP_READ);
sel.wakeup();
} public void run() {
try {
if (selKey.isReadable())
read();
else if (selKey.isWritable())
write();
}
catch (IOException ex) {
ex.printStackTrace();
}
} // Process data by echoing input to output
synchronized void process() {
byte[] bytes; readBuf.flip();
bytes = new byte[readBuf.remaining()];
readBuf.get(bytes, 0, bytes.length);
System.out.print("process(): " + new String(bytes, Charset.forName("ISO-8859-1"))); writeBuf = ByteBuffer.wrap(bytes); // Set the key's interest to WRITE operation
selKey.interestOps(SelectionKey.OP_WRITE);
selKey.selector().wakeup();
} synchronized void read() throws IOException {
int numBytes; try {
numBytes = channel.read(readBuf);
System.out.println("read(): #bytes read into 'readBuf' buffer = " + numBytes); if (numBytes == -1) {
selKey.cancel();
channel.close();
System.out.println("read(): client connection might have been dropped!");
}
else {
Reactor.workerPool.execute(new Runnable() {
public void run() {
process();
}
});
}
}
catch (IOException ex) {
ex.printStackTrace();
return;
}
} void write() throws IOException {
int numBytes = 0; try {
numBytes = channel.write(writeBuf);
System.out.println("write(): #bytes read from 'writeBuf' buffer = " + numBytes); if (numBytes > 0) {
readBuf.clear();
writeBuf.clear(); // Set the key's interest-set back to READ operation
selKey.interestOps(SelectionKey.OP_READ);
selKey.selector().wakeup();
}
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}

注意:clent端可以参考前面的例子,可以通用。

那么这个所谓的反应堆模式其实已经比较接近netty的结构了,为了构造出高性能的服务端前人做了很多的努力~,在进入netty的世界前打好基础先。

总结:

1,在学习NIO的时候牵涉到很多jvm,甚至操作系统底层的原理知识,发现自己对计算机底层原理的理解几乎是不及格的,在大学的时候没有打好基础,这部分需要补充。

2,牵涉到很多位置的知识领域只能死啃的办法,多看书,多实验。

让我们继续前行

----------------------------------------------------------------------

努力不一定成功,但不努力肯定不会成功。