1.前言
以前使用 websocket来实现双向通信,如今深入了解了 NIO 同步非阻塞io模型 ,
优势是 处理效率很高,吞吐量巨大,能很快处理大文件,不仅可以 做 文件io操作,
还可以做socket通信 、收发UDP包、Pipe线程单向数据连接。
这一篇随笔专门讲解 NIO socket通信具体操作
注意:这是重点!!!
兴趣集合有4个事件,
分别是:
SelectionKey.OP_ACCEPT 【接收连接就绪,专用于服务端】
SelectionKey.OP_CONNECT 【连接就绪 , 专用于客户端】
SelectionKey.OP_READ 【读就绪 ,通知 对面端 读做读操作】
SelectionKey.OP_WRITE 【写就绪 , 通知自己端 做写操作】
当信道向选择器注册感兴趣事件SelectionKey.OP_WRITE 时
即源码
sc.register(mselector, SelectionKey.OP_WRITE);
让自己的选择器 触发自己的 key.isWritable()&&key.isValid()
然后是让自己做一个写操作,
最后再注册 读就绪事件,用来通知 对方端【可能是客户端 或 服务端,因为是相互的】
做读事件,即让对面的选择器触发 key.isReadable()。
-----------------------------------------------------------
我觉得这就是脱裤子放屁操作。。。
其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,
因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放写啊
2.操作
(1)目录结构
只需要红框部分 其他可有可无,这是个maven工程,在测试类实现,之所以使用maven是因为导入依赖包很方便
(2)导入依赖包,需要使用json的生成和解析工具
com.alibaba
fastjson
1.2.56
org.springframework.boot
spring-boot-starter-test
test
(3)服务端【源码里写了很详细的注释,我懒得再写一篇】
服务端实现类源码
packagecom.example.javabaisc.nio.mysocket;importcom.example.javabaisc.nio.mysocket.service.EatService;importorg.junit.jupiter.api.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.junit4.SpringRunner;importjava.io.IOException;importjava.net.InetSocketAddress;importjava.net.Socket;importjava.net.SocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.ServerSocketChannel;importjava.nio.channels.SocketChannel;importjava.nio.charset.StandardCharsets;importjava.util.Iterator;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;
@RunWith(SpringRunner.class)
@SpringBootTestpublic classServerSocket {//main方法启动//public static void main(String[] args) throws IOException {// //配置选择器//selector();// //监听//listen();//}
@Test//单元测试方法启动
public void serverSocket() throwsIOException {//配置选择器
selector();//监听
listen();
}//服务层接口
@AutowiredprivateEatService eatService;//选择器作为全局属性
private Selector selector = null;//存储信道对象 ,静态公用的全局变量//key是ip和端口,如 /127.0.0.1:64578//value 是 储信道对象
private static final ConcurrentMap socketChannelMap = new ConcurrentHashMap<>();//存储ip和端口//key 是用户名//value 是ip和端口,如 /127.0.0.1:64578
private static final ConcurrentMap ipMap = new ConcurrentHashMap<>();//存储用户名//key 是ip和端口//value 是用户名
private static final ConcurrentMap usernameMap = new ConcurrentHashMap<>();/*** 配置选择器
* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错*/
private void selector() throwsIOException {//服务信道
ServerSocketChannel channel = null;//开启选择器
selector =Selector.open();//开启服务信道
channel =ServerSocketChannel.open();//把该channel设置成非阻塞的,【需要手动设置为false】
channel.configureBlocking(false);//开启socket 服务,由信道开启,绑定端口 8080
channel.socket().bind(new InetSocketAddress(8080));//管道向选择器注册信息----接收连接就绪
channel.register(selector, SelectionKey.OP_ACCEPT);
}/*** 写了监听事件的处理逻辑*/
private void listen() throwsIOException {//进入无限循环遍历
while (true) {//这个方法是阻塞的,是用来收集有io操作通道的注册事件【也就是选择键】,需要收到一个以上才会往下面执行,否则一直等待到超时,超时时间是可以设置的,//直接输入参数数字即可,单位毫秒 ,如果超时后仍然没有收到注册信息,那么将会返回0 ,然后往下面执行一次后又循环回来//不写事件将一直阻塞下去//selector.select();//这里设置超时时间为3000毫秒
if (selector.select(3000) == 0) {//如果超时后返回结果0,则跳过这次循环
continue;
}//使用迭代器遍历选择器里的所有选择键
Iterator ite =selector.selectedKeys().iterator();//当迭代器指针指向下一个有元素是才执行内部代码块
while(ite.hasNext()) {//获取选择键
SelectionKey key =ite.next();//选择键操作完成后,必须删除该元素【选择键】,否则仍然存在选择器里面,将会在下一轮遍历再执行一次,形成了脏数据,因此必须删除
ite.remove();//当选择键是可接受的
if(key.isAcceptable()) {
acceptableHandler(key);
}//当选择键是可读的
else if(key.isReadable()) {
readHandler(key);
}//当选择键是可写的且是有效的【其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,//因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放在这里写啊】//为了演示我还是写了
else if (key.isWritable() &&key.isValid()) {
writeHandler(key);
}//当选择键是可连接的【其实这个是在客户端才会被触发,为了演示这里也可以写,我才写的】
else if(key.isConnectable()) {
System.out.println("选择键是可连接的,key.isConnectable() 是 true");
}
}
}
}//当选择键是可接受的处理逻辑//static静态,可用可不用
private void acceptableHandler(SelectionKey key) throwsIOException {
System.out.println("当选择键是可接受的处理逻辑");//从选择键获取服务信道,需要强转
ServerSocketChannel serverSocketChannel =(ServerSocketChannel) key.channel();//服务信道监听新进来的连接,返回一个信道
SocketChannel sc =serverSocketChannel.accept();//信道不为空才执行
if (sc != null) {//到了这里说明连接成功//
//获取本地ip地址与端口号//SocketAddress socketAddress = sc.getLocalAddress();//System.out.println(socketAddress.toString());///127.0.0.1:8080//获取远程ip地址与端口号
SocketAddress ra =sc.getRemoteAddress();
System.out.println(ra.toString());///127.0.0.1:64513//存储信道对象
socketChannelMap.put(ra.toString(), sc);
System.out.println("当前在线人数:" +socketChannelMap.size());//将该信道设置为非阻塞
sc.configureBlocking(false);//获取选择器
Selector mselector =key.selector();//信道注册到选择器 ---- 读操作就绪
sc.register(mselector, SelectionKey.OP_READ);//在这里设置字节缓冲区的 关联关系,但是我设置会在读操作报空指针异常,原因未知//sc.register(mselector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
}//当选择键是可读的处理逻辑
private void readHandler(SelectionKey key) throwsIOException {
SocketChannel sc= null;/*每当客户端强制关闭了连接,就会发送一条数据过来这里说
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
因此需要这里销毁连接,即关闭该socket通道即可*/
try{
System.out.println("当选择键是可读的处理逻辑");//