NIO非阻塞通信服务端部分代码

来源:互联网 发布:国安数据 编辑:程序博客网 时间:2024/05/22 00:15

第一篇自己写的博客,之前总觉得不把东西做出个大概就写出来自己会不爽所以一直没写,今天终于厚着脸皮写了第一篇,希望我的脸皮今后越来越厚。

之前自己一直在搞PC端和手机端通信的软件,中途受了百度云推送(BaiduPush)的诱惑去做了两三个月基于百度云推送的聊天软件,结果在我的真机上跑不动,后台服务死都不出来,让我备受打击。几周前回归局域网通信,那时还是ServerSocket、Socket的阻塞式IO通信,感觉很不顺手,前几天换了NIO非阻塞通信,感觉舒爽多了,于是跑来这里分享一下成果。


NIO原理

NIO与IO的区别

传统IO也叫作BIO,字母“B”即Block,阻塞式IO;NIO即非阻塞IO(异步的AIO这里就不说了)。
Java IO操作围绕流(stream),流中没有数据,程序就卡在read()或write()的一行上,直到流中有数据读取或者数据完全写入,程序才继续向下运行,这种现象就是阻塞。
Java NIO操作围绕通道(channel),从通道请求读目前可用数据,没有数据则什么都读不到而不是一直阻塞;向通道写数据,也不需要等待直到完全写入;由于对通道的读写是非阻塞式的,线程通常可以同时处理多个通道。

Buffer

即一个缓冲区,固定数据容量的容器,一般来说倾向于存储字节(ByteBuffer)。它是向通道(channel)写的数据的直接来源,也是从通道读取的数据的直接容器,通俗的说,Buffer里的数据直接写到channel里,从channel读的数据首先会存到Buffer里。

Channel

即通道,一种途径,通道的两端借助通道传输数据。直观的表示:写数据端 -> ByteBuffer -> Channel -> ByteBuffer -> 读数据端。通道的两端可以是网络的两端,也可以是文件和文件操作程序等。

Selector

选择器,提供选择执行已经就绪的任务的能力。selector充当一个监视者,一个或多个创建的Channel注册到Selector中,返回一个表示通道和选择器的键(Key),它记录了这个通道。调用Selector的select()方法时会刷新所有key并检查注册的通道,可以获取key的集合进而遍历得到就绪的通道,对就绪的通道进行读写操作。形象的比喻:Selector是一条走廊,Channel都是走廊两边的房间,要打开门进房间就要先获得钥匙(Key),要获得钥匙(Key)就要先去管理员那登记入住信息(Register注册)。

Key

表示对应的注册关系,要获得通道Channel需要先获得Key,Key可以被取消,获得对应关系是否有效的状态等。


以上是我对于这些个基本概念的个人见解,比较浅薄,如果有不对的欢迎评论指正。

代码分享

下面这一部分是Java服务器端一个线程LinkServer的run()方法,循环对客户端通信通道进行处理、接收数据需要读取的请求就新建一个线程处理请求。

public void run() {try {// 打开一个选择器selector = Selector.open();// 服务端通道ServerSocketChannel server = ServerSocketChannel.open();// 地址,系统自动给ip,端口为指定的9090InetSocketAddress addr = new InetSocketAddress(PORT);// 绑定地址server.bind(addr);// 设定非阻塞server.configureBlocking(false);// 向此选择器注册给定的通道server.register(selector, SelectionKey.OP_ACCEPT);// 更新界面,GetLocalLanIP()是通过DOS指令获取本机局域网IP的方法ui.tf_IP.setText(GetLocalLanIP());ui.tf_port.setText(PORT + "");while (true) {selector.select(); // 循环到这里会阻塞,直到至少有一个SelectionKeySystem.out.println("接入数量:" + selector.selectedKeys().size());// SelectionKey是一个注册标记或选择键,跟通道一一对应,可以通过它操作通道for (SelectionKey key : selector.selectedKeys()) {// 从selector上的已选择key集中删除正在处理的SelectionKeyselector.selectedKeys().remove(key);// 后边的操作中有些会将key取消、将通道关闭掉,但是key不会立即被移除// 它会被放到选择器的已取消键集里,标为无效,以便之后移除// 所以这个判断是跳过 已无效但未移除的keyif (!key.isValid()) {continue;}if (key.isAcceptable()) { // 接到客户端的连接请求// 接到一个和客户端通信的通道SocketChannel client = server.accept();// 设为非阻塞client.configureBlocking(false);// 注册通道,初始操作为 读client.register(selector, SelectionKey.OP_READ);// key的interest 集合设置为给定值// interest 集合 确定了下一次调用某个选择器的选择方法时,将测试哪类操作的准备就绪信息。key.interestOps(SelectionKey.OP_ACCEPT);// 附加一个客户端地址的字符串的对象,是个对象都可以附加key.attach(client.getRemoteAddress());// 打印一下客户端地址System.out.println(client.getRemoteAddress());}if (key.isReadable()) { // 数据需要读取的请求// ArrayList<SelectionKey> keyDing// 某个key如果存在于keyDing表示它正在被某个线程处理if (!keyDing.contains(key)) {// 如果这个key当前没有线程处理,新建线程处理它keyDing.add(key);// 线程处理完毕后会把key从keyDing里踢掉new SysMsgReadThread(key).start();}}}// 给一点时间SysMsgReadThread作为缓冲,// key很多的时候可以不给时间,后期可以动态优化Thread.sleep(5);}} catch (Exception e) {e.printStackTrace();}// 更新界面ui.ta_state.append("退出线程LinkServer\n");}


下面这部分代码是数据读取请求处理线程SysMsgReadThread的run()方法,通道没有数据就结束读取,客户端断开就取消SelectionKey并关闭通道。

对读取的字符串处理的方法ParseSysMsg(String SysMsgStr)我就不贴出来了,不同的人有不同的处理方式。

public void run() {// long n = getId();获取线程的idSystem.out.println("线程 " + n + " 启动");// 通过key获得通道client = (SocketChannel) key.channel();// 缓存ByteBuffer buff = ByteBuffer.allocate(1024);// 接收的字符串String SysMsgStr = "";try {while (true) {// 读取,read(buff)返回读取的字节数,如果没数据就一直返回0// 如果客户端已经断开、进(线)程被杀掉、强退等,就一直返回-1int num = client.read(buff);if (num > 0) {// 这表示通道里读取到了数据System.out.println("线程 " + n + " 读到" + num + "字节");buff.flip();SysMsgStr += charset.decode(buff);} else {// 这表示通道里没数据if (num == -1) {// 这进一步表示客户端断开了System.out.println("线程 " + n + " 读到了-1");// 这里对key进行cancel对通道进行关闭// 但由于key不会立即移除所以之前要判断isValid()来跳过取消的keykey.cancel();client.close();}break;}}System.out.println("线程 " + n + " 读取的数据(长 " + SysMsgStr.length() + "):" + SysMsgStr);// 这个判断是由于上几行取消了key,不判断的话这里会异常if (key.isValid()) {key.interestOps(SelectionKey.OP_READ);// 为下一次读取做准备}} catch (IOException ex) {key.cancel();try {client.close();} catch (IOException e) {e.printStackTrace();}}if (SysMsgStr.length() > 0) { // 聊天信息不为空ParseSysMsg(SysMsgStr); // 调用方法对读取到的消息进行处理}super.run();System.out.println("线程 " + n + " 结束");keyDing.remove(key);// 线程处理这个key结束后就把key从队列里踢掉}


最后这部分代码是通过命令行获取本机在局域网内的IP地址的方法。可能我这个方法比较low,但是知道更好的方法之前就用这个好了。

public static String GetLocalLanIP() {try {String command = "cmd.exe /c ipconfig";Process p = Runtime.getRuntime().exec(command);BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), Charset.forName("GBK")));ArrayList<String> lines = new ArrayList<String>();String line = null;while ((line = br.readLine()) != null) {lines.add(line);if (line.indexOf("默认网关") == 3 && line.length() > 40) {String result = lines.get(lines.size() - 3);result = result.substring(result.lastIndexOf(":") + 2, result.length());return result;}}br.close();p.destroy();} catch (IOException e) {e.printStackTrace();}return null;}

以上就是我目前PC(Java)服务器端和手机(Android)客户端中服务器端比较关键的代码了,注释挺多的便于理解,客户端代码过一段时间之后会有分享。

代码虽然low,但是以后会越改越好,期待与路过的各位相互交流。

Buffer

即一个缓冲区,固定数据容量的容器,一般来说倾向于存储字节(ByteBuffer)。它是向通道(channel)写的数据的直接来源,也是从通道读取的数据的直接容器,通俗的说,Buffer里的数据直接写到channel里,从channel读的数据首先会存到Buffer里。