手写JAVA NIO实现Socket通信及其过程中注意的问题
2017-07-26 13:33
645 查看
当然现在不需要自己手写NIO实现socket,都是在需要建立TCP/IP连接的程序中直接使用mina框架,或者netty框架, 后者使用的更多。趁着在用mina框架解析网关协议之际,本文手写NIO实现socket的服务端,找一找学习NIO中遇到的问题,以及在调试的过程中学习对某些API的理解,文中只写了服务端,客户端用SocketTools这个工具充当,测试。
服务端代码实现:(运行起来只需要导入log4j和slf4j的jar包,看启动日志需要加上log4j.properties)
本来是同一个文件的东西,我也不知道系统为什么给自动分到两个部分去了
学习的过程中对一些概念的理解、常见的问题以及网上一些相关NIO程序中的问题总结:
1. Selector的select方法在没有准备好的IO操作时,一直处于阻塞状态,直到有可操作的IO准备好。这里的准备好不是指有通道注册在Selector上,而是指在所注册的通道里已经有了相关请求(例如有客户端的连接,客户端输入了数据等)。
2. 一旦通道注册在Selector上,就是一直注册在其上,除非关闭此通道(调用channe的close方法)。每次select方法返回大于0后,会调用selectionKeys()方法找出所有准备好的选择键,对SelectionKeys的遍历过程中,必须删除SelectionKeys集合中单个SelectionKey,此处与通道的注册毫无关系,注册在通道上的channel仍旧在通道上,删除的只是当前select大于0的单次的准备好的SelectionKey.(显然对SelectionKey的处理是以select返回为周期的,返回一次处理一次,而且每次处理必须把返回的SelectionKey清理干净(放在一个Set中的,由selectionKeys()方法返回))。
3. SelectionKey的interestOps()只是改变已经注册在Selector上那个的通道感兴趣的事件,覆盖原来调用register时注册的感兴趣事件,这里并不是新增注册或者其他,仅仅是对原有状态的改变(疯狂java讲义中,NIO实现非阻塞socket通信中对此方法的使用显然是多余的,不用使用此方法设置成准备下次连接/读 ,照样是可以被连接或者读的,因为本来注册的时候就是对读/连接的事件感兴趣)。
4.网上有些程序例子在调用完isReadable()方法后,将SelectionKey取出的channel注册为可写register(selector,OP_WRITE),显然这样处理以后,对所注册通道感兴趣事件修改为 了可写,不可以与当前客户端进行下次读了。此通道先前是注册的可读,现在被修改为了可写,是同一个通道,如果正常操作,此处注册为可读|可写,当然调用interestOps修改也是可以的,和注册是同一个意思(因为是一个已经注册的通道)
5. 一旦注册了可写通道,select方法返回值永远大于0,因为至少有一个可写的通道注册在上面,所以一直陷入了IsWritable的死循环中,其实也不能叫死循环,其他请求来照样可以处理,只是每次必然会走这个判断,下同
6.不对socket连接断开的情况做处理的话,一旦连接服务器的客户端TCP/IP连接断开,会使得服务器select方法一直返回值大于0,因为里面一直有一个可读的请求,这样每次都会进入isReadable的判断里,陷入一个可读的死循环。具体需要处理断开的方法是对这个读请求捕获,捕获到以后将此通道关闭。(判断Socketchannel的read方法返回值,如果是-1,表示已经断开,取消当前SelectionKey .cancel,关闭当前通道 .close())
服务端代码实现:(运行起来只需要导入log4j和slf4j的jar包,看启动日志需要加上log4j.properties)
本来是同一个文件的东西,我也不知道系统为什么给自动分到两个部分去了
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author yanzh * */ public class NIOServer { private Logger LOG = LoggerFactory.getLogger(NIOServer.class); private ServerSocketChannel server; private ByteBuffer sendBuffer; private ByteBuffer recvBuffer; private Selector selector; private int port = 9012; //初始化服务器 NIOServer(int port){ this.port = port; try{ recvBuffer = ByteBuffer.allocate(1024); sendBuffer = ByteBuffer.allocate(1024); server = ServerSocketChannel.open(); server.socket().bind(new InetSocketAddress(port)); server.configureBlocking(false); selector = Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT); LOG.info("服务器已启动,监控端口号:{}", this.port); }catch(IOException e){ LOG.error("连接时出现IO异常"); } } NIOServer(){ this(9012); } //初始化,监听端口 public void start(){ try { listener(); } catch (IOException e) { LOG.info("监听端口IO异常"); } } //一个死循环一直在监听,处理端口事件 public void listener() throws IOException{ while(true){ LOG.info("-----------------------------------------------------"); LOG.info("1.selectedKeys的值:{}", selector.selectedKeys().size()); LOG.info("1.registe的值:{}", selector.keys().size()); int n = selector.select(); LOG.info("----------------------fengexian1---------------------"); LOG.info("2.select返回值:{}", n); LOG.info("2.selectedKeys的值:{}", selector.selectedKeys().size()); LOG.info("2.registe的值:{}", selector.keys().size()); LOG.info("-------------------------------------------------------"); //没有准备好的通道,其实我觉得根本不会到这里,因为如果没有通道准备好, //应该select函数一直阻塞着。 if(n == 0){ continue; } Set<SelectionKey> eventKeys = selector.selectedKeys(); Iterator<SelectionKey> it = eventKeys.iterator(); while(it.hasNext()){ SelectionKey eventKey = it.next(); it.remove(); //准备好的通道中取得了通道和选择器的对应关系,利用此关系可以得到通道或者选择器。 //开始具体处理通道相关内容,连接,读,写等; handleKey(eventKey); } } } //处理IO口连接,读写等函数 public void handleKey(SelectionKey eventKey) throws IOException{ if(eventKey.isAcceptable()){ SocketChannel sc = server.accept(); LOG.info("新的客户端已经连接成功"); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); } if(eventKey.isReadable()){ SocketChannel sc = (SocketChannel)eventKey.channel(); String content = ""; int n; recvBuffer.clear(); try{ while((n = sc.read(recvBuffer)) > 0){ content = content + new String(recvBuffer.array(), 0, n); } }catch(IOException e){ eventKey.cancel(); sc.close(); return; } if(n == -1){ SocketChannel scc = (SocketChannel)eventKey.channel(); eventKey.channel().close(); eventKey.cancel(); LOG.info("客户端{}已经关闭。", scc.socket().getRemoteSocketAddress()); return; } LOG.info("receive client input Stirng : {}", content); //content = "yanzh"; if(content.length() > 0){ sendBuffer.clear(); sendBuffer.put(content.getBytes()); sendBuffer.flip(); sc.write(sendBuffer); }
//sc.configureBlocking(false); //sc.register(selector, SelectionKey.OP_WRITE); //eventKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } if(eventKey.isWritable()){ LOG.info("sendBuffer可写"); SocketChannel sc = (SocketChannel)eventKey.channel(); if(sendBuffer.remaining()>0){ sc.write(sendBuffer); LOG.info("sendBuffer剩余大小:{}", sendBuffer.remaining()); } } } //主函数启动服务 public static void main(String[] args) { new NIOServer().start(); } }
学习的过程中对一些概念的理解、常见的问题以及网上一些相关NIO程序中的问题总结:
1. Selector的select方法在没有准备好的IO操作时,一直处于阻塞状态,直到有可操作的IO准备好。这里的准备好不是指有通道注册在Selector上,而是指在所注册的通道里已经有了相关请求(例如有客户端的连接,客户端输入了数据等)。
2. 一旦通道注册在Selector上,就是一直注册在其上,除非关闭此通道(调用channe的close方法)。每次select方法返回大于0后,会调用selectionKeys()方法找出所有准备好的选择键,对SelectionKeys的遍历过程中,必须删除SelectionKeys集合中单个SelectionKey,此处与通道的注册毫无关系,注册在通道上的channel仍旧在通道上,删除的只是当前select大于0的单次的准备好的SelectionKey.(显然对SelectionKey的处理是以select返回为周期的,返回一次处理一次,而且每次处理必须把返回的SelectionKey清理干净(放在一个Set中的,由selectionKeys()方法返回))。
3. SelectionKey的interestOps()只是改变已经注册在Selector上那个的通道感兴趣的事件,覆盖原来调用register时注册的感兴趣事件,这里并不是新增注册或者其他,仅仅是对原有状态的改变(疯狂java讲义中,NIO实现非阻塞socket通信中对此方法的使用显然是多余的,不用使用此方法设置成准备下次连接/读 ,照样是可以被连接或者读的,因为本来注册的时候就是对读/连接的事件感兴趣)。
4.网上有些程序例子在调用完isReadable()方法后,将SelectionKey取出的channel注册为可写register(selector,OP_WRITE),显然这样处理以后,对所注册通道感兴趣事件修改为 了可写,不可以与当前客户端进行下次读了。此通道先前是注册的可读,现在被修改为了可写,是同一个通道,如果正常操作,此处注册为可读|可写,当然调用interestOps修改也是可以的,和注册是同一个意思(因为是一个已经注册的通道)
5. 一旦注册了可写通道,select方法返回值永远大于0,因为至少有一个可写的通道注册在上面,所以一直陷入了IsWritable的死循环中,其实也不能叫死循环,其他请求来照样可以处理,只是每次必然会走这个判断,下同
6.不对socket连接断开的情况做处理的话,一旦连接服务器的客户端TCP/IP连接断开,会使得服务器select方法一直返回值大于0,因为里面一直有一个可读的请求,这样每次都会进入isReadable的判断里,陷入一个可读的死循环。具体需要处理断开的方法是对这个读请求捕获,捕获到以后将此通道关闭。(判断Socketchannel的read方法返回值,如果是-1,表示已经断开,取消当前SelectionKey .cancel,关闭当前通道 .close())
相关文章推荐
- MyEclipse6.5整合flex实现与java简单通信过程中遇到的问题和注意事项
- socket通信需要注意的问题
- java nio实现非阻塞Socket通信实例
- BufferReader与BufferWriter实现socket通信注意事项
- TCP/IP详解(卷2实现)学习笔记(一)udp的socket通信过程底层实现概述(1)
- 八数码问题的过程表示及其实现
- Linux socket编程入门及客户端服务器端通信实现 – 提高篇:TCP连接过程分析
- Java Socket 通信中传递Object对象注意的问题
- java nio实现非阻塞Socket通信实例
- 解决swift实现的websocket与后台通信问题:websocket is disconnected: masked and rev data is not currently supported
- 在WCF中实现双向通信, Callback 注意问题
- SQL Server实现CLR步骤及其需要配置注意的问题(转)
- 如何干净的在服务中实现socket长链接与服务器通信并处理相应的线程问题(有更新)
- [Ruby]Autoit实现过程中遇到的问题或是要注意的内容
- c++和java在socket通信过程中发送和接收函数的问题
- socket通信需要注意的问题
- SQL Server实现CLR步骤及其需要配置注意的问题(转)
- 在Ubuntu下实现本地套接字(socket)通信以及遇到的问题!
- TQ2440与西门子S7-200 PLC自由口通信实现过程中问题总结
- qwebsocket使用wss通信时的应注意的问题