使用NIO的一个最大优势就是客户端于服务器自己的不再是阻塞式的,也就意味着服务器无需通过为每个客户端的链接而开启一个线程。而是通过一个叫Selector的轮循器来不断的检测那个Channel有消息处理。
简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的Set集合,进行后续的I/O操作。
由于select操作只管对selectedKeys的集合进行添加而不负责移除,所以当某个消息被处理后我们需要从该集合里去掉。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

下面,我们通过NIO编程的序列图和源码分析来熟悉相关的概念,以便巩固我们前面所学的NIO基础知识。

è¿éåå¾çæè¿°

下面,我们对NIO服务端的主要创建过程进行讲解和说明,作为NIO的基础入门,我们将忽略掉一些在生产环境中部署所需要的一些特性和功能(比如TCP半包等问题)。

步骤一:打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,代码示例如下。

ServerSocketChannel server = ServerSocketChannel.open();
步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码如下。

server.socket().bind(new InetSocketAddress(7777),1024);
// 设置为非阻塞模式, 这个非常重要
server.configureBlocking(false);
步骤三:创建Reactor线程,创建多路复用器并启动线程,代码如下。(即 selector)

Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
步骤四:将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件,代码如下。

server.register(selector, SelectionKey.OP_ACCEPT);
步骤五:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,代码如下。

while(true){
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = (SelectKey)it.next();
//处理io
}
}
步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路,代码示例如下。

// 得到与客户端的套接字通道
SocketChannel channel = ssc.accept();
步骤七:设置客户端链路为非阻塞模式,示例代码如下。

channel.configureBlocking(false);
步骤八:将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,用来读取客户端发送的网络消息,代码如下。

channel.register(selector, SelectionKey.OP_READ);
步骤九:异步读取客户端请求消息到缓冲区,示例代码如下。

int readBytes = channel.read(byteBuffer);
步骤十:对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排,示例代码如下。

Object message = null;
while (buffer.hasRemain()) {
buffer.mark();
message = decode(buffer);
if(message == null){
buffer.reset();
break;
}
}
if(!buffer.hasRemain()){
buffer.clear();
}else {
buffer.compact();
}

//业务线程处理message
步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端,示例代码如下。

socketChannel.write(byteBuffer);
注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓冲区,此处不赘述,后续会详细分析Netty的处理策略。

  1. package chanel;
  2.  
  3. import java.io.IOException;
  4. import java.net.InetSocketAddress;
  5. import java.nio.ByteBuffer;
  6. import java.nio.channels.SelectionKey;
  7. import java.nio.channels.Selector;
  8. import java.nio.channels.ServerSocketChannel;
  9. import java.nio.channels.SocketChannel;
  10. import java.nio.charset.Charset;
  11. import java.util.Iterator;
  12. import java.util.Set;
  13.  
  14. public class NioService {
  15. public static void main(String[] args) {
  16. try {
  17. NioService server = new NioService();
  18. server.init();
  19.  
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. public void init() throws IOException {
  25. Charset charset = Charset.forName("UTF-8");
  26. // 创建一个选择器,可用close()关闭,isOpen()表示是否处于打开状态,他不隶属于当前线程
  27. Selector selector = Selector.open();
  28. // 创建ServerSocketChannel,并把它绑定到指定端口上
  29. ServerSocketChannel server = ServerSocketChannel.open();
  30. server.socket().bind(new InetSocketAddress(7777),1024);
  31. // 设置为非阻塞模式, 这个非常重要
  32. server.configureBlocking(false);
  33. // 在选择器里面注册关注这个服务器套接字通道的accept事件
  34. // ServerSocketChannel只有OP_ACCEPT可用,OP_CONNECT,OP_READ,OP_WRITE用于SocketChannel
  35. server.register(selector, SelectionKey.OP_ACCEPT);
  36.  
  37. while (true) {
  38. //休眠时间为1s,无论是否有读写等事件发生,selector每隔1s都被唤醒一次
  39. selector.select(1000);
  40. Set<SelectionKey> keys = selector.selectedKeys();
  41. Iterator<SelectionKey> it = keys.iterator();
  42. SelectionKey key = null;
  43. while (it.hasNext()) {
  44. //如果key对应的Channel包含客户端的链接请求
  45. // OP_ACCEPT 这个只有ServerSocketChannel才有可能触发
  46. key=it.next();
  47. // 由于select操作只管对selectedKeys进行添加,所以key处理后我们需要从里面把key去掉
  48. it.remove();
  49. if (key.isAcceptable()) {
  50. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  51. // 得到与客户端的套接字通道
  52. //ServerSocketChannel的accept接收客户端的连接请求并创建SocketChannel实例,完成上述操作后,相当于完成了TCP的三次握手,TCP物理链路正式建立。
  53. //我们需要将新创建的SocketChannel设置为异步非阻塞,同时也可以对其TCP参数进行设置,例如TCP接收和发送缓冲区的大小等。此处省掉
  54. SocketChannel channel = ssc.accept();
  55. channel.configureBlocking(false);
  56. channel.register(selector, SelectionKey.OP_READ);
  57. //将key对应Channel设置为准备接受其他请求
  58. key.interestOps(SelectionKey.OP_ACCEPT);
  59. }
  60. if (key.isReadable()) {
  61. SocketChannel channel = (SocketChannel) key.channel();
  62. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  63. String content = "";
  64. try {
  65. int readBytes = channel.read(byteBuffer);
  66. if (readBytes > 0) {
  67. byteBuffer.flip(); //为write()准备
  68. byte[] bytes = new byte[byteBuffer.remaining()];
  69. byteBuffer.get(bytes);
  70. content+=new String(bytes);
  71. System.out.println(content);
  72. //回应客户端
  73. doWrite(channel);
  74. }
  75. // 写完就把状态关注去掉,否则会一直触发写事件(改变自身关注事件)
  76. key.interestOps(SelectionKey.OP_READ);
  77. } catch (IOException i) {
  78. //如果捕获到该SelectionKey对应的Channel时出现了异常,即表明该Channel对于的Client出现了问题
  79. //所以从Selector中取消该SelectionKey的注册
  80. key.cancel();
  81. if (key.channel() != null) {
  82. key.channel().close();
  83. }
  84. }
  85. }
  86. }
  87. }
  88. }
  89. private void doWrite(SocketChannel sc) throws IOException{
  90. byte[] req ="服务器已接受aaa".getBytes();
  91. ByteBuffer byteBuffer = ByteBuffer.allocate(req.length);
  92. byteBuffer.put(req);
  93. byteBuffer.flip();
  94. sc.write(byteBuffer);
  95. if(!byteBuffer.hasRemaining()){
  96. System.out.println("Send 2 Service successed");
  97. }
  98. }
  99. }

现在我们来看编写客户端的流程:

è¿éåå¾çæè¿°

步骤一:打开SocketChannel,绑定客户端本地地址(可选,默认系统会随机分配一个可用的本地地址),示例代码如下。

SocketChannel channel = SocketChannel.open();
步骤二:设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数,示例代码如下。

channel.configureBlocking(false);
步骤三:异步连接服务端,示例代码如下。

步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立),示例代码如下。

if(channel.connect(new InetSocketAddress("127.0.0.1",7777))){
channel.register(selector, SelectionKey.OP_READ);
//发送消息
doWrite(channel, "66666666");
}else {
channel.register(selector, SelectionKey.OP_CONNECT);
}
步骤五:向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答,示例代码如下。

channel.register(selector, SelectionKey.OP_CONNECT);
步骤六:创建Reactor线程,创建多路复用器并启动线程,代码如下。

selector = Selector.open();
new Thread(new ReactorTask()).start();
步骤七:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,代码如下。

while (!stop){
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()){
}
}
步骤八:接收connect事件进行处理并判断是否链接成功,示例代码如下。

if (key.isConnectable()){
if (channel.finishConnect()) {
}
}
步骤九:注册读事件到多路复用器,示例代码如下。

channel.register(selector, SelectionKey.OP_READ);
步骤十:异步读客户端请求消息到缓冲区,示例代码如下。

int readBytes = channel.read(byteBuffer);
步骤十一:对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排,示例代码如下。

Object message = null;
while (buffer.hasRemain()) {
buffer.mark();
message = decode(buffer);
if(message == null){
buffer.reset();
break;
}
}
if(!buffer.hasRemain()){
buffer.clear();
}else {
buffer.compact();
}

//业务线程处理message
步骤十二:将发生对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端,示例代码如下。

socketChannel.write(byteBuffer);

  1. package chanel;
  2.  
  3. import java.io.IOException;
  4. import java.net.InetSocketAddress;
  5. import java.nio.ByteBuffer;
  6. import java.nio.channels.SelectionKey;
  7. import java.nio.channels.Selector;
  8. import java.nio.channels.SocketChannel;
  9. import java.nio.charset.Charset;
  10. import java.util.Iterator;
  11. import java.util.Set;
  12. import java.util.concurrent.ArrayBlockingQueue;
  13.  
  14. public class NioClient {
  15. public static void main(String[] args) {
  16. try {
  17. NioClient client = new NioClient();
  18. client.init();
  19.  
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24.  
  25. // 创建一个套接字通道,注意这里必须使用无参形式
  26. private Selector selector = null;
  27. static Charset charset = Charset.forName("UTF-8");
  28. private volatile boolean stop = false;
  29. public ArrayBlockingQueue<String> arrayQueue = new ArrayBlockingQueue<String>(8);
  30.  
  31. public void init() throws IOException {
  32. selector = Selector.open();
  33. SocketChannel channel = SocketChannel.open();
  34. // 设置为非阻塞模式,这个方法必须在实际连接之前调用(所以open的时候不能提供服务器地址,否则会自动连接)
  35. channel.configureBlocking(false);
  36. if (channel.connect(new InetSocketAddress("127.0.0.1", 7777))) {
  37. channel.register(selector, SelectionKey.OP_READ);
  38. //发送消息
  39. doWrite(channel, "66666666");
  40. } else {
  41. channel.register(selector, SelectionKey.OP_CONNECT);
  42. }
  43.  
  44. //启动一个接受服务器反馈的线程
  45. // new Thread(new ReceiverInfo()).start();
  46.  
  47. while (!stop) {
  48. selector.select(1000);
  49. Set<SelectionKey> keys = selector.selectedKeys();
  50. Iterator<SelectionKey> it = keys.iterator();
  51. SelectionKey key = null;
  52. while (it.hasNext()) {
  53. key = it.next();
  54. it.remove();
  55. SocketChannel sc = (SocketChannel) key.channel();
  56. // OP_CONNECT 两种情况,链接成功或失败这个方法都会返回true
  57. if (key.isConnectable()) {
  58. // 由于非阻塞模式,connect只管发起连接请求,finishConnect()方法会阻塞到链接结束并返回是否成功
  59. // 另外还有一个isConnectionPending()返回的是是否处于正在连接状态(还在三次握手中)
  60. if (channel.finishConnect()) {
  61. /* System.out.println("准备发送数据");
  62. // 链接成功了可以做一些自己的处理
  63. channel.write(charset.encode("I am Coming"));
  64. // 处理完后必须吧OP_CONNECT关注去掉,改为关注OP_READ
  65. key.interestOps(SelectionKey.OP_READ);*/
  66. sc.register(selector, SelectionKey.OP_READ);
  67. // new Thread(new DoWrite(channel)).start();
  68. doWrite(channel, "66666666");
  69. } else {
  70. //链接失败,进程推出或直接抛出IOException
  71. System.exit(1);
  72. }
  73. }
  74. if (key.isReadable()) {
  75. //读取服务端的响应
  76. ByteBuffer buffer = ByteBuffer.allocate(1024);
  77. int readBytes = sc.read(buffer);
  78. String content = "";
  79. if (readBytes > 0) {
  80. buffer.flip();
  81. byte[] bytes = new byte[buffer.remaining()];
  82. buffer.get(bytes);
  83. content += new String(bytes);
  84. stop = true;
  85. } else if (readBytes < 0) {
  86. //对端链路关闭
  87. key.channel();
  88. sc.close();
  89. }
  90. System.out.println(content);
  91. key.interestOps(SelectionKey.OP_READ);
  92. }
  93. }
  94. }
  95. }
  96.  
  97. private void doWrite(SocketChannel sc, String data) throws IOException {
  98. byte[] req = data.getBytes();
  99. ByteBuffer byteBuffer = ByteBuffer.allocate(req.length);
  100. byteBuffer.put(req);
  101. byteBuffer.flip();
  102. sc.write(byteBuffer);
  103. if (!byteBuffer.hasRemaining()) {
  104. System.out.println("Send 2 client successed");
  105. }
  106. }
  107. }

启动客户端类的init()方法之后就会向服务器发送一个字符串,服务器接受到之后会向客户端回应一个。运行如上代码,结果正确。

通过源码对比分析,我们发现NIO编程难度确实比同步阻塞BIO大很多,我们的NIO例程并没有考虑“半包读”和“半包写”,如果加上这些,代码将会更加复杂。NIO代码既然这么复杂,为什么它的应用却越来越广泛呢,使用NIO编程的优点总结如下。

(1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。

(2)SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

3)线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。

JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0,引人注目的是,Java正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动I/O对应的AIO,后续我们学习下如何利用NIO2.0编写AIO程序,还是以客户端服务器通信为例进行讲解。

Nio使用Selector客户端与服务器的通信的更多相关文章

  1. HttpClient实现客户端与服务器的通信

    本篇主要讲解了利用HttpClient实现 windows主机与linux服务器的通信与传递数据 HttpClient代码,服务器端配置 系统和安装软件 1)ubuntu 14.04 64位系统 2) ...

  2. 用java语言构建一个网络服务器,实现客户端和服务器之间通信,实现客户端拥有独立线程,互不干扰

    服务器: 1.与客户端的交流手段多是I/O流的方式 2.对接的方式是Socket套接字,套接字通过IP地址和端口号来建立连接 3.(曾经十分影响理解的点)服务器发出的输出流的所有信息都会成为客户端的输 ...

  3. motan源码分析六:客户端与服务器的通信层分析

    本章将分析motan的序列化和底层通信相关部分的代码. 1.在上一章中,有一个getrefers的操作,来获取所有服务器的引用,每个服务器的引用都是由DefaultRpcReferer来创建的 pub ...

  4. 客户端与服务器之间通信收不到信息——readLine()

    写服务器端和客户端之间通信,结果一直读取不到信息,在https://blog.csdn.net/yiluxiangqian7715/article/details/50173573 上找到了原因:使用 ...

  5. HttpWebRequest 基础连接已经关闭: 接收时发生错误 GetRequestStream 因为算法不同,客户端和服务器无法通信。

    在代码行 HttpWebRequest objRequest = (HttpWebRequest)HttpWebRequest.Create(sUrl 前面加上 ServicePointManager ...

  6. Java网络编程客户端和服务器通信

    在java网络编程中,客户端和服务器的通信例子: 先来服务器监听的代码 package com.server; import java.io.IOException; import java.io.O ...

  7. Java模拟客户端向服务器上传文件

    先来了解一下客户端与服务器Tcp通信的基本步骤: 服务器端先启动,然后启动客户端向服务器端发送数据. 服务器端收到客户端发送的数据,服务器端会响应应客户端,向客户端发送响应结果. 客户端读取服务器发送 ...

  8. 利用NIO的Selector处理服务器-客户端模型

    package NIOTEST; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocket ...

  9. Socket与SocketServer结合多线程实现多客户端与服务器通信

    需求说明:实现多客户端用户登录,实现多客户端登录一般都需要使用线程技术: (1)创建服务器端线程类,run()方法中实现对一个请求的响应处理: (2)修改服务器端代码,实现循环监听状态: (3)服务器 ...

随机推荐

  1. js批量上传文件

    html代码: <input type="file" id='upload' name="upload" multiple="multiple& ...

  2. Docker小白从零入门实战

    环境:Centos 6.9 0.查看是否满足安装需求. 先检查服务器环境,docker要求操作系统CentOS6以上,kernel 版本必须2.6.32-431或更高,即>=CentOS 6.5 ...

  3. windows下安装cygwin及配置(转)

    reference:https://cygwin.com/install.html 对比:MinGW vs. CygWin    https://www.cnblogs.com/findumars/p ...

  4. 杭电1004 ac code

    #include <stdio.h> #include <string.h> #include <stdlib.h> #define STR_LEN 256 str ...

  5. OpenStack之queens版本创建负载均衡器时报错问题!

    采用kolla-ansible部署完毕后,创建负载均衡器时会提示如下的报错 解决办法: 修改网络节点的neutron-lbaas-agent容器 进入lbaas容器里 [root@openstack0 ...

  6. Sass 混合宏、继承、占位符 详解

    混合宏-声明混合宏如果你的整个网站中有几处小样式类似,比如颜色,字体等,在 Sass 可以使用变量来统一处理,那么这种选择还是不错的.但当你的样式变得越来越复杂,需要重复使用大段的样式时,使用变量就无 ...

  7. python-django优缺点

    [Django]是利用[Python]语言从事[Web]开发的首选框架.如果你以后想从事[python web]开发工作,就必需了解其优缺点.这些都可能会是你将来的面试题哦. [Django]的优点 ...

  8. Wrapper

    开放封闭原则: 开放对扩展 封闭修改源代码 改变了人家调用方式 装饰器结构 """ 默认结构为三层!!!每层返回下一层内存地址就可以进行执行函数, 传参:语法糖中的传参可 ...

  9. winform 与百度搜索智能提示

    private void textBox1_TextChanged(object sender, EventArgs e) { listBox1.Items.Clear(); if (string.I ...

  10. java学习笔记20(Arraylist复习,Collection接口方法,迭代器,增强型for循环)

    集合:集合是Java提供的一种容器,可以用来存储多个数据: 集合与数组的区别:集合的长度是可变的,数组的长度是固定的 集合中存储的数据必须是引用类型数据: ArrayList回顾: public cl ...