选择器服务器端代码

上一篇文章毫无条理地讲了很多和选择器相关的知识点,下面进入实战,看一下如何写和使用选择器实现服务端Socket数据接收的程序,这也是NIO中最核心、最精华的部分。

看一下代码:

 public class SelectorServer
{
private static int PORT = 1234; public static void main(String[] args) throws Exception
{
// 先确定端口号
int port = PORT;
if (args != null && args.length > 0)
{
port = Integer.parseInt(args[0]);
}
// 打开一个ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
// 获取ServerSocketChannel绑定的Socket
ServerSocket ss = ssc.socket();
// 设置ServerSocket监听的端口
ss.bind(new InetSocketAddress(port));
// 设置ServerSocketChannel为非阻塞模式
ssc.configureBlocking(false);
// 打开一个选择器
Selector selector = Selector.open();
// 将ServerSocketChannel注册到选择器上去并监听accept事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true)
{
// 这里会发生阻塞,等待就绪的通道
int n = selector.select();
// 没有就绪的通道则什么也不做
if (n == 0)
{
continue;
}
// 获取SelectionKeys上已经就绪的通道的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 遍历每一个Key
while (iterator.hasNext())
{
SelectionKey sk = iterator.next();
// 通道上是否有可接受的连接
if (sk.isAcceptable())
{
ServerSocketChannel ssc1 = (ServerSocketChannel)sk.channel();
SocketChannel sc = ssc1.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
// 通道上是否有数据可读
else if (sk.isReadable())
{
readDataFromSocket(sk);
}
iterator.remove();
}
}
} private static ByteBuffer bb = ByteBuffer.allocate(1024); // 从通道中读取数据
protected static void readDataFromSocket(SelectionKey sk) throws Exception
{
SocketChannel sc = (SocketChannel)sk.channel();
bb.clear();
while (sc.read(bb) > 0)
{
bb.flip();
while (bb.hasRemaining())
{
System.out.print((char)bb.get());
}
System.out.println();
bb.clear();
}
}
}

代码中已经有了相关的注释,这里继续解释一下:

(1)第8行~第12行,确定要监听的端口号,这里是1234

(2)第13行~第20行,由于选择器管理的是通道(Channel),因此首先要有通道。这里是服务器的程序,因此获取ServerSocketChannel,同时获取它所对应的ServerSocket,设置服务端的Channel为非阻塞模式,并绑定之前确定的端口号1234

(3)第21行~第24行,打开一个选择器,并注册当前通道感兴趣的时间为accept时间,即监听来自客户端的Socket数据

(4)第25行~第28行,调用select()方法等待来自客户端的Socket数据。程序会阻塞在这儿不会往下走,直到客户端有Socket数据的到来为止,所以严格意义上来说,NIO并不是一种非阻塞IO,因为NIO会阻塞在Selector的select()方法上

(5)第29行~第33行,没有什么好说的,如果select()方法获取的数据是0的话,下面的代码都没必要走,当然这是有可能发生的

(6)第34行~第39行,获取到已经就绪的通道的迭代器进行迭代,泛型是选择键SelectionKey,前文讲过,选择键用于封装特定的通道

(7)第40行~第52行,这里是一个关键点、核心点,这里做了两件事情:

  a)满足isAcceptable()则表示该通道上有数据到来了,此时我们做的事情不是获取该通道->创建一个线程来读取该通道上的数据,这么做就和前面一直讲的阻塞IO没有区别了,也无法发挥出NIO的优势来。我们做的事情只是简单地将对应的SocketChannel注册到选择器上,通过传入OP_READ标记,告诉选择器我们关心新的Socket通道什么时候可以准备好读数据

  b)满足isReadable()则表示新注册的Socket通道已经可以读取数据了,此时调用readDataFromSocket方法读取SocketChannel中的数据,读取数据的方法前面通道的文章中已经详细讲过了,就不讲了

(8)第53行,将键移除,这一行很重要也是容易忘记的一步操作。加入不remove,将会导致45行中出现空指针异常,原因不难理解,可以自己思考一下。

选择器客户端代码

选择器客户端的代码,没什么要求,只要向服务器端发送数据就可以了。这里选用的是Java NIO4:Socket通道一文中,最后一部分开五个线程向服务端发送数据的程序:

 public class SelectorClient
{
private static final String STR = "Hello World!";
private static final String REMOTE_IP = "127.0.0.1";
private static final int THREAD_COUNT = 5; private static class NonBlockingSocketThread extends Thread
{
public void run()
{
try
{
int port = 1234;
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(REMOTE_IP, port));
while (!sc.finishConnect())
{
System.out.println("同" + REMOTE_IP + "的连接正在建立,请稍等!");
Thread.sleep(10);
}
System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());
String writeStr = STR + this.getName();
ByteBuffer bb = ByteBuffer.allocate(writeStr.length());
bb.put(writeStr.getBytes());
bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)
sc.write(bb);
bb.clear();
sc.close();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
} public static void main(String[] args) throws Exception
{
NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i] = new NonBlockingSocketThread();
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].start();
// 一定要join保证线程代码先于sc.close()运行,否则会有AsynchronousCloseException
for (int i = 0; i < THREAD_COUNT; i++)
nbsts[i].join();
}
}

代码执行结果

先运行服务端程序:

空白,很正常,因为在监听客户端数据的到来,此时并没有数据。接着运行客户端程序:

看到5个线程的数据已经发送,此时服务端的执行情况是:

数据全部接收到并打印,看到右边的方框还是红色的,说明这5个线程的数据接收、打印完毕之后,再继续等待着客户端的数据的到来。

总结一下Selector的执行两个关键点:

1、注册一个ServerSocketChannel到selector中,这个通道的作用只是为了监听客户端是否有数据到来(这里注意一下有数据到来,意思是假如需要接收100个字节,如果到来了1个字节就算数据到来了),只要有数据到来,就把特定通道注册到selector中,并指定其事件为读事件

2、ServerSocketChannel和SocketChannel(通道里面的是客户端的数据)共同存在在Selector中,只要有注册的事件到来,Selector取消阻塞状态,遍历SelectionKey集合,继续注册读取数据的通道,或者是从通道中读取数据。

选择过程的可扩展性

从上面的代码以及之前对于Selector的解读可以看到,Selector可以简化用单线程同时管理多个可选择通道的实现。使用一个线程来为多个通道提供服务,通过消除管理各个线程的额外开销,可能会降低复杂性并可能大幅提升性能。但只使用一个线程来服务所有可选择的通道是不是一个好主意呢?这要看情况。

对单核CPU的系统而言这可能是一个好主意,因为在任何情况下都只有一个线程能够运行。通过消除在线程之间进行上下文切换带来的额外开销,总吞吐量可以提高。但对于一个多核CPU的系统而言呢?字啊一个有n个CPU的系统上,当一个单一的线程线性轮流地处理每一个线程时,可能有(n-1)个CPU处于空闲状态。

一种可行的解决办法是使用多个选择器。但是请尽量不要这么做,在大量通道上执行就绪选择并不会有很大的开销,大多数工作是由底层操作系统完成的,管理多个选择器并随机地将通道分派给它们当中的一个并不是这个问题的合理的解决方案。

一种更好的解决方案是对所有的可选择通道使用同一个选择器,并将对就绪选择通道的服务委托给其他线程。开发者只使用一个线程监控通道的就绪状态,至于通道处于就绪状态之后又如何做,有两种可行的做法:

1、使用一个协调好的工作线程池来处理接收到的数据,当然线程池的大小是可以调整的

2、通道根据功能由不同的工作线程来处理,它们可能是日志线程、命令/控制线程、状态请求线程等

Java NIO6:选择器2---代码篇的更多相关文章

  1. [Android&amp;Java]浅谈设计模式-代码篇:观察者模式Observer

    观察者,就如同一个人,对非常多东西都感兴趣,就好像音乐.电子产品.Game.股票等,这些东西的变化都能引起爱好者们的注意并时刻关注他们.在代码中.我们也有这种一种方式来设计一些好玩的思想来.今天就写个 ...

  2. 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

    作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...

  3. Java 性能优化之 String 篇

    原文:http://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/ Java 性能优化之 String 篇 String 方法用于文本分析 ...

  4. Java基础加强之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  5. [转]深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)

    以下内容转自: 作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-l ...

  6. 基于Java的数据采集(终结篇)

    关于写过关于JAVA采集入库的三篇文章: 基于Java数据采集入库(一):http://www.cnblogs.com/lichenwei/p/3904715.html 基于Java数据采集入库(二) ...

  7. Java入门到精通——基础篇之多线程实现简单的PV操作的进程同步

    Java入门到精通——基础篇之多线程实现简单的PV操作的进程同步 一.概述     PV操作是对信号量进行的操作.     进程同步是指在并发进程之间存在一种制约关系,一个进程的执行依赖另一个进程的消 ...

  8. [转][JAVA]定时任务之-Quartz使用篇

    [BAT][JAVA]定时任务之-Quartz使用篇 定时任务之-Quartz使用篇 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与 ...

  9. 【Java面试】基础知识篇

    [Java面试]基础知识篇 Java基础知识总结,主要包括数据类型,string类,集合,线程,时间,正则,流,jdk5--8各个版本的新特性,等等.不足的地方,欢迎大家补充.源码分享见个人公告.Ja ...

随机推荐

  1. 用python实现一个不排序的列表功能

    #!/usr/bin/env python # -*- coding: utf-8 -*- # learn <<Problem Solving with Algorithms and Da ...

  2. 用python DIY一个图片转pdf工具并打包成exe

    最近因为想要看漫画,无奈下载的漫画是jpg的格式,网上的转换器还没一个好用的,于是乎就打算用python自己DIY一下: 这里主要用了reportlab.开始打算随便写几行,结果为若干坑纠结了挺久,于 ...

  3. x

    笔记 { 计算机 { 底层 { 程序运行 } 信息学 { 网络 { 网络信息安全 } 算法 { 算法 拟合 编译解释词法分析 } 编程语言 { C语言 嵌入式C++ } 黑客 } } 安卓 { 软件 ...

  4. java内存模型(待完善)

    JMM 1.内存模型的抽象. 本地内存是JMM的一个抽象概念,并不是真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化. 2.内存可见性问题? ? 3.重排序  编译器优化重排序   ...

  5. WebGL入门教程(三)-webgl动画

    前面文章: WebGL入门教程(一)-初识webgl WebGL入门教程(二)-webgl绘制三角形 WebGL动画有移动.旋转和缩放,我们将移动.旋转和缩放图形,然后将其绘制到屏幕上,称为变换(tr ...

  6. Linux学习笔记(9)-守护进程

    明天学这个!! ---------------------------------------------------------- 守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终 ...

  7. nodejs复习03

    文件系统fs 重命名 fs.rename() fs.renameSync 优先选择异步,可以进行异常判断 打开关闭文件 fd = fs.openSync(file, flags) fs.closeSy ...

  8. 简述JavaScript对象、数组对象与类数组对象

    问题引出 在上图给出的文档中,用JavaScript获取那个a标签,要用什么办法呢?相信第一反应一定是使用document.getElementsByTagName('a')[0]来获取.同样的,在使 ...

  9. 【Redis】:Jedis 使用

    Redis 支持很多语言, 例如C#,RUBY,JAVA 等, Jedis是redis的java版本的客户端实现 一个简单的Jedis使用 依赖第三方包jedis-2.7.2.jar commons- ...

  10. block 从B界面向A界面传值

    最近在改公司外包项目的代码,发现了一种block传值的用法很有意思,记录一下 A.B两个页面 在B界面.h中定义 @property (nonatomic,strong) void(^block)(N ...