NIO学习笔记1

这是第一篇NIO学习笔记,至于会不会有第二篇到时候再说

最近也是刚刚开始接触NIO,主要用于替换ServerSocket

备忘:cat</dev/tcp/ip/port可以直接创建tcp连接,不过只能显示服务器的返回信息,把小于号改成大于号就可以向服务器发送消息

双向的话还是用telnet比较好。当然这是没来得及写客户端情况下的应付手段

首先是ServerSocketChannel的例子
首先是开启服务器
Java代码 复制代码 收藏代码
  1. //创建一个选择器
  2. selector = Selector.open();
  3. //创建ServerSocketChannel并将其绑定在端口上
  4. ServerSocketChannel server = ServerSocketChannel.open();
  5. server.bind(new InetSocketAddress(PORT));
  6. //设置为非阻塞模式
  7. server.configureBlocking(false);
  8. //注册这个channel
  9. server.register(selector, SelectionKey.OP_ACCEPT);

这里我刚开始的时候犯了个错误,bind的时候new InetSocketAddress我输入了localhost和port两个参数,结果造成服务器只能通过localhost访问
我从路由器设置到selinux到iptables来回检查了无数便,最后netstat -na发现了真相。顿时有种吐血的感觉。

之后就进入一个while true循环获取事件了
Java代码 复制代码 收藏代码
  1. while(true){
  2. // select操作会把发生关注事件的Key加入到selectionKeys中(只管加不管减)
  3. if(selector.select()==0){
  4. continue;
  5. }
  6. //获取发生了关注时间的KEY集合
  7. //selector.keys()会返回所有的SelectionKey(包括没有发生事件的)
  8. Set<SelectionKey> keys = selector.selectedKeys();
  9. for(SelectionKey key:keys){
  10. process(key);
  11. //移除处理完成的key
  12. keys.remove(key);
  13. }
  14. }


这里我设置了一个Hashtable保存每个连接和key的对应关系,便于应对服务器需要主动向某个地址发送数据的情况
Java代码 复制代码 收藏代码
  1. private void process(SelectionKey key) throws IOException{
  2. //OP_ACCEPT事件,这个只有ServerSocketChannel会触发
  3. if(key.isAcceptable()){
  4. SocketChannel channel = ((ServerSocketChannel)key.channel()).accept();
  5. //非阻塞
  6. channel.configureBlocking(false);
  7. //取得刚注册的key保存到连接map中
  8. SelectionKey nkey = channel.register(key.selector(),SelectionKey.OP_READ,ByteBuffer.allocate(1024));
  9. // get address
  10. InetSocketAddress addr = (InetSocketAddress)channel.getRemoteAddress();
  11. targetMap.put(addr.toString(), nkey);
  12. }
  13. //OP_READ事件,可读取的数据
  14. if(key.isReadable()){
  15. TCPReader.processRead(key);
  16. }
  17. //OP_WRITE事件,写数据
  18. if(key.isWritable()){
  19. TCPWriter.processWrite(key);
  20. }
  21. }

【主动通信】
这个虽然是后做的,不过既然提到了还是做一个说明吧
说实话这方面我没有在网上查找到任何可用的资料,完全是自己摸索出来的
首先,连接到服务器的TCP连接的channel可以通过getRemoteAddress()方法取得来源地址,强制转换未InetSocketAddress后可以直接调用.toString()方式,得到的结果是:/ip:port 形式的字符串,通过这个可以很容易区分开每个SelectionKey。刚好可以作为Hashtable的Key

然后就是这个SelectionKey的获取问题了,最开始以为产生OP_ACCEPT事件的key直接就能拿走,测试后发现这是服务器端的Key,如果需要取得对应客户端连接的Key则需要多一个步骤。

我注意到
Java代码 复制代码 收藏代码
  1. channel.register(key.selector(),SelectionKey.OP_READ,ByteBuffer.allocate(1024));

这个注册方法返回值就是一个SelectionKey,取出测试后发现就是和客户端连接的Key。
初步目标达成!

不过单单取得SelectionKey没用,还需要完成发送流程,这里我在网上看到有一种设想是把要发送的对象attach到key上然后在Write流程中通过attachment取出,实测后发现此方式数据极容易在Read流程中被当作ByteBuffer读取,出现强制转换类型错误。即使使用ByteBuffer作为介质也可能被Read流程当作输入数据取出。传递方面还是用其他方式吧。

常规的流程先检测Read状态后检测Write状态,但是实测发现直接修改key.interestOps为OP_WRITE状态并没有被检测器捕获,还是要自己另外实现channel的write流程。不过要注意一个channel的write一定要放在同一个线程中,不然极可能将要发送的数据插入到正在发送的数据包中造成数据混乱无法读取。这个大家都是老手就不赘述了。

注意取得channel并调用write方法前一定要把key设置为OP_WRITE模式,否则会报异常

【数据包分离】
TCP数据流一个很大的特点是实际上并不清楚数据包的长度大小,也不清楚何时结束。所有的数据都按顺序叠在一起。因此发送的数据包可能因为过大而被分割成几个小部分发过来,也可能前后两个包首尾相连一起发过来,将数据流按协议一个个剥离出来是一个必须妥善解决的问题。
在此有必要列出我使用的字节流协议详情:
字节序号 内容
0 数据包的类型(视频流、图片、RSA的public key申请、视频流终止信号等)
1-4 4个字节组成int值,表示数据的目标用户id(target)
5-9 4个字节组成的int值,表示正文数据的长度(length)
10-(length+9) 正文数据



读取线程采用此算法(UML学的不好,能看懂最好。。。哈哈),使用了2个ByteArrayOutputStream双缓冲来缓存数据。留有一个填加数据的方法。使用一个static Hashtable管理线程池,在static方法中传入一个SelectionKey,通过key的地址寻找目标线程并将ByteBuffer中的byte[]数据输入stream1缓冲流

而线程中则将stream1中的数据转入stream2中,随后对数据进行读取和处理。处理完一个包后如果还有剩余数据则放回stream2继续处理。线程末尾检查stream1是否有新的数据,如果有则循环,没有就结束线程并从线程池中删除对象。

今天暂时到这里,由于是自学,个人见解难免错漏,也在此抛砖引玉,也希望有高人能给出更有效的处理方法 
原文地址:https://www.cnblogs.com/bjanzhuo/p/3576050.html