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的生成和解析工具

<!-- 用于生成或解析json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<!-- 用于在单元测试类可以使用javabean注解-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

(3)服务端【源码里写了很详细的注释,我懒得再写一篇】

服务端实现类源码

package com.example.javabaisc.nio.mysocket;

import com.example.javabaisc.nio.mysocket.service.EatService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; @RunWith(SpringRunner.class)
@SpringBootTest
public class ServerSocket { //main方法启动
// public static void main(String[] args) throws IOException {
// //配置选择器
// selector();
// //监听
// listen();
// }
@Test
//单元测试方法启动
public void serverSocket() throws IOException {
//配置选择器
selector();
//监听
listen();
} //服务层接口
@Autowired
private EatService eatService; //选择器作为全局属性
private Selector selector = null; //存储信道对象 ,静态公用的全局变量
//key是ip和端口,如 /127.0.0.1:64578
//value 是 储信道对象
private static final ConcurrentMap<String, SocketChannel> socketChannelMap = new ConcurrentHashMap<>();
//存储ip和端口
// key 是用户名
// value 是ip和端口,如 /127.0.0.1:64578
private static final ConcurrentMap<String, String> ipMap = new ConcurrentHashMap<>();
//存储用户名
// key 是ip和端口
// value 是用户名
private static final ConcurrentMap<String, String> usernameMap = new ConcurrentHashMap<>(); /**
* 配置选择器
* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错
*/
private void selector() throws IOException {
//服务信道
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() throws IOException {
//进入无限循环遍历
while (true) {
//这个方法是阻塞的,是用来收集有io操作通道的注册事件【也就是选择键】,需要收到一个以上才会往下面执行,否则一直等待到超时,超时时间是可以设置的,
//直接输入参数数字即可,单位毫秒 ,如果超时后仍然没有收到注册信息,那么将会返回0 ,然后往下面执行一次后又循环回来
//不写事件将一直阻塞下去
// selector.select();
//这里设置超时时间为3000毫秒
if (selector.select(3000) == 0) {
//如果超时后返回结果0,则跳过这次循环
continue;
}
//使用迭代器遍历选择器里的所有选择键
Iterator<SelectionKey> 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) throws IOException {
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) throws IOException {
SocketChannel sc = null;
/*
每当客户端强制关闭了连接,就会发送一条数据过来这里说
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
因此需要这里销毁连接,即关闭该socket通道即可
*/
try {
System.out.println("当选择键是可读的处理逻辑");
//获取信道,需要强转
sc = (SocketChannel) key.channel();
//key 获取 通道 关联的 缓冲区【这里使用是报错,read读操作报空指针异常,奇了怪了】
// ByteBuffer buffer = (ByteBuffer) key.attachment();
//只能自定义了
ByteBuffer buffer = ByteBuffer.allocate(1024);
//获取选择器
Selector mselector = key.selector();
//信道做读操作 ,返回读取数据的字节长度
long mreadSize;
//存储从心得解析出来的字符串
String jsonstr = "";
//当字节长度大于零,说明还有信息没有读完
while ((mreadSize = sc.read(buffer)) > 0) {
System.out.println("=======");
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//获取该索引间的数据 ,buffer.get()返回的是节数
byte[] b = buffer.array();
//指定编码将字节流转字符串
jsonstr = new String(b, StandardCharsets.UTF_8);
//打印
System.out.println(jsonstr);
}
//当字节长度为-1时,也就是没有数据可读取了,那么就关闭信道
if (mreadSize == -1) {
sc.close();
}
//检查字符串是否为空
if (!jsonstr.isEmpty()) {
//数据发送过来不为空
//进入业务层
eatService.food(mselector, sc, buffer, jsonstr, ipMap, usernameMap, socketChannelMap);
} } catch (Exception e) {
// e.printStackTrace();
System.out.println("//远程客户端强迫关闭了连接。关闭客户端已经关闭,服务端继续运行"); //发生异常才关闭
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());
//移除
socketChannelMap.remove(ra.toString());
System.out.println(socketChannelMap);
//
String username = usernameMap.get(ra.toString());
System.out.println("用户名叫:" + username + " 的客户端下线");
usernameMap.remove(ra.toString());
ipMap.remove(username);
//
System.out.println("当前在线人数:" + socketChannelMap.size());
//
System.out.println("打印当前用户信息");
System.out.println(ipMap);
System.out.println(usernameMap);
//
sc.close();
}
//取消该选择键
key.channel();
}
} //当选择键是可写的且是有效的处理逻辑
private void writeHandler(SelectionKey key) throws IOException {
System.out.println("当选择键是可写的且是有效的处理逻辑 ,我被自己通知来写东西啦,虽然不知道为什么要分开读写");
//获取信道,需要强转
SocketChannel sc = (SocketChannel) key.channel();
//设置字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//
String str = "我要写东西,你看到了吗" + System.currentTimeMillis();
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//将字符转成字节流放入缓冲中
buffer.put(str.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//信道做写操作
sc.write(buffer);
}
//整理索引【即position定位到缓冲区未读的数据末尾 ,capacity = limit】
buffer.compact();
//获取选择器
Selector mselector = key.selector();
//注册读就绪事件
sc.register(mselector, SelectionKey.OP_READ); } }

服务层接口

package com.example.javabaisc.nio.mysocket.service;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ConcurrentMap; public interface EatService {
public void food(Selector mselector, SocketChannel sc, ByteBuffer buffer, String jsonstr,
ConcurrentMap<String, String> ipMap,
ConcurrentMap<String, String> usernameMap,ConcurrentMap<String, SocketChannel> socketChannelMap) throws IOException;
}

服务层接口实现类

package com.example.javabaisc.nio.mysocket.service;

import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Service; import java.io.IOException;
import java.net.SocketAddress;
import java.nio.Buffer;
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.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentMap; /**
* 业务层
*/
@Service
public class EatServiceImpl implements EatService {
@Override
public void food(Selector mselector, SocketChannel sc, ByteBuffer buffer, String jsonstr,
ConcurrentMap<String, String> ipMap,
ConcurrentMap<String, String> usernameMap, ConcurrentMap<String, SocketChannel> socketChannelMap) throws IOException { //解析json串成map
Map<String, Object> map = JSON.parseObject(jsonstr);
System.out.println(map);
int type = (Integer) map.get("type");
if (type == 1) {
//返回结果
String res = "apple,好好吃,我好饿";
Map<String, Object> map2 = new HashMap<>();
map2.put("r-type", 1);
map2.put("data", res);
String jsonStr = JSON.toJSONString(map2);
//
System.out.println(jsonStr);
//
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//指定编码将符串转字字节流
buffer.put(jsonStr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//写操作
sc.write(buffer);
}
//整理索引【即position定位到缓冲区未读的数据末尾 ,capacity = limit】
//这里用不到
//buffer.compact();
//注册读就绪事件,【让客户端读】
sc.register(mselector, SelectionKey.OP_READ);
} else if (type == 3) {
try {
//客户端回应
System.out.println(" //客户端回应 业务类型3,//到了这里不再传输数据,懒得写,以免无限循环");
//获取远程ip地址与端口号
SocketAddress ra = sc.getRemoteAddress();
//获取该客户端的用户名
String username = usernameMap.get(ra.toString());
//懒得写新接口,直接判断如果该客户端如果是cen,则向yue发送信息
if (username.equals("cen")) {
//判断guo是否在线
////获取guo的ip
String ip = ipMap.get("guo");
System.out.println("guo 的ip:" + ip);
if (ip == null || ip.isEmpty()) {
System.out.println("guo 不存在,未上线");
return;
}
System.out.println("向 guo 发送信息");
//存在
//
SocketChannel mchannel = socketChannelMap.get(ip);
String res = "我是cen,我向guo发送消息,看到了吗" + new Date();
//
System.out.println(res);
//
Map<String, Object> map3 = new HashMap<>();
map3.put("r-type", 6);
map3.put("data", res);
String jsonStr = JSON.toJSONString(map3);
//清除索引信息【即position = 0 ;capacity = limit】
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
buffer2.clear();
//指定编码将符串转字字节流
buffer2.put(jsonStr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer2.flip();
System.out.println(new String(buffer2.array()));
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer2.hasRemaining()) {
//写操作
mchannel.write(buffer2);
}
//整理索引【即position定位到缓冲区未读的数据末尾 ,capacity = limit】
//这里用不到
// buffer.compact();
//注册读就绪事件,【让客户端读】
sc.register(mselector, SelectionKey.OP_READ);
System.out.println("发送成功"); }
} catch (Exception e) {
e.printStackTrace();
} } else if (type == 0) {
System.out.println("type是0");
String username = (String) map.get("username");
System.out.println("用户名叫:" + username + " 的客户端上线");
//注册用户信息[根据用户名获取ip]
ipMap.put(username, sc.getRemoteAddress().toString());
//注册用户信息[根据ip获取用户名]
usernameMap.put(sc.getRemoteAddress().toString(), username);
System.out.println("打印当前用户信息");
System.out.println(ipMap);
System.out.println(usernameMap);
//向选择器注册写就绪事件,是通知自己写东西【让自己写】,一般不会注册OP_WRITE,为了展示用法我才这样写
sc.register(mselector, SelectionKey.OP_WRITE);
}
}
}

分别做了两个客户端,与服务端的代码很相似,但是再小的区别也不能粗心大意,不然直接报错

源码一样,区别是用户名不同【用于测试两个客户端之间发送消息】

用户名为 cen 的客户端

package com.example.javabaisc.nio.mysocket;

import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test; 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.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; public class ClientSocket { //用户名
String username = "cen"; @Test
//单元测试方法启动
public void clientSocket() throws IOException {
//配置选择器
selector();
//监听
listen();
} //选择器作为全局属性
private Selector selector = null; /**
* 配置选择器
* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错
*/
private void selector() throws IOException {
//信道
SocketChannel channel = null;
//开启选择器
selector = Selector.open();
//开启信道
channel = SocketChannel.open();
//把该channel设置成非阻塞的,【需要手动设置为false】
channel.configureBlocking(false);
//管道连接互联网socket地址,输入ip和端口号
channel.connect(new InetSocketAddress("localhost", 8080));
//管道向选择器注册信息----连接就绪
channel.register(selector, SelectionKey.OP_CONNECT);
} /**
* 写了监听事件的处理逻辑
*/
private void listen() throws IOException {
out:
//进入无限循环遍历
while (true) {
//这个方法是阻塞的,是用来收集有io操作通道的注册事件【也就是选择键】,需要收到一个以上才会往下面执行,否则一直等待到超时,超时时间是可以设置的,
//直接输入参数数字即可,单位毫秒 ,如果超时后仍然没有收到注册信息,那么将会返回0 ,然后往下面执行一次后又循环回来
//不写事件将一直阻塞下去
// selector.select();
//这里设置超时时间为3000毫秒
if (selector.select(3000) == 0) {
//如果超时后返回结果0,则跳过这次循环
continue;
}
//使用迭代器遍历选择器里的所有选择键
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
//当迭代器指针指向下一个有元素是才执行内部代码块
while (ite.hasNext()) {
//获取选择键
SelectionKey key = ite.next();
//选择键操作完成后,必须删除该元素【选择键】,否则仍然存在选择器里面,将会在下一轮遍历再执行一次,形成了脏数据,因此必须删除
ite.remove();
//当选择键是可连接的
if (key.isConnectable()) {
if ( connectableHandler(key)) {
System.out.println("//远程主机未上线,退出循环,关闭客户端");
//退出循环,关闭客户端
break out;
}
}
//当选择键是可读的
else if (key.isReadable()) {
if (readHandler(key)) {
System.out.println("//远程主机强迫关闭了连接。退出循环,关闭客户端");
//退出循环,关闭客户端
break out;
}
}
//当选择键是可写的且是有效的【其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,
// 因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放在这里写啊】
//为了演示我还是写了
else if (key.isWritable() && key.isValid()) {
writeHandler(key);
}
}
}
} //当选择键是可连接的处理逻辑
//static静态,可用可不用
private boolean connectableHandler(SelectionKey key) throws IOException {
System.out.println("当选择键是可连接的处理逻辑");
SocketChannel sc =null;
/*
每当连接远程主机发现未上线,则会在这里报异常
java.net.ConnectException: Connection refused: no further information
*/
try {
sc = (SocketChannel) key.channel();
//如果管道是连接悬挂
if (sc.isConnectionPending()) {
//管道结束连接
sc.finishConnect();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//=============
Map<String, Object> map = new HashMap<>();
map.put("type", 0);
map.put("date", "socket首次握手成功,你好");
map.put("username", username);
String jsonstr = JSON.toJSONString(map);
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//指定编码将符串转字字节流
buffer.put(jsonstr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//写操作
sc.write(buffer);
}
sc.register(selector, SelectionKey.OP_READ);
}
return false;
}catch (Exception e){
// e.printStackTrace();
//取消该选择键
key.channel();
//发生异常才关闭
if (sc != null) {
sc.close();
}
return true;
} } //当选择键是可读的处理逻辑
private boolean readHandler(SelectionKey key) throws IOException {
SocketChannel sc = null;
/*
每当服务端强制关闭了连接,就会发送一条数据过来这里说
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
因此需要这里销毁连接,即关闭该socket通道即可
*/
try {
System.out.println("当选择键是可读的处理逻辑");
//获取信道,需要强转
sc = (SocketChannel) key.channel();
//key 获取 通道 关联的 缓冲区【这里使用是报错,read读操作报空指针异常,奇了怪了】
// ByteBuffer buffer = (ByteBuffer) key.attachment();
//只能自定义了
ByteBuffer buffer = ByteBuffer.allocate(1024);
//获取选择器
Selector mselector = key.selector();
//信道做读操作 ,返回读取数据的字节长度
long mreadSize;
//存储从心得解析出来的字符串
String jsonstr = "";
//当字节长度大于零,说明还有信息没有读完
while ((mreadSize = sc.read(buffer)) > 0) {
System.out.println("=======");
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//获取该索引间的数据 ,buffer.get()返回的是节数
byte[] b = buffer.array();
//指定编码将字节流转字符串
jsonstr = new String(b, StandardCharsets.UTF_8);
//打印
System.out.println(jsonstr);
}
//当字节长度为-1时,也就是没有数据可读取了,那么就关闭信道
if (mreadSize == -1) {
sc.close();
}
//检查字符串是否为空
if (!jsonstr.isEmpty()) {
//数据发送过来不为空
//进入业务层 【与服务端的一样写法,我这里就演示服务层了】
// eatService.food(mselector, sc, buffer, jsonstr);
//为了演示响应,我直接用做写就绪事件响应
//注册写就绪事件 ,这句话等同于 直接调用 writeHandler(SelectionKey key)
sc.register(mselector, SelectionKey.OP_WRITE); } return false;
} catch (Exception e) {
// e.printStackTrace();
//取消该选择键
key.channel();
//发生异常才关闭
if (sc != null) {
sc.close();
}
//关闭客户端
return true;
}
} //当选择键是可写的且是有效的处理逻辑
private void writeHandler(SelectionKey key) throws IOException {
System.out.println("当选择键是可写的且是有效的处理逻辑 ,我被自己通知来写东西啦,虽然不知道为什么要分开读写");
//获取信道,需要强转
SocketChannel sc = (SocketChannel) key.channel();
//设置字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//=============
Map<String, Object> map = new HashMap<>();
map.put("type", 3);
map.put("date", "我要写东西,你看到了吗" + System.currentTimeMillis());
String jsonstr = JSON.toJSONString(map);
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//将字符转成字节流放入缓冲中
buffer.put(jsonstr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//信道做写操作
sc.write(buffer);
}
//整理索引【即position定位到缓冲区未读的数据末尾 ,capacity = limit】
buffer.compact();
//获取选择器
Selector mselector = key.selector();
//注册读就绪事件
sc.register(mselector, SelectionKey.OP_READ); } }

用户名为 guo 的客户端

package com.example.javabaisc.nio.mysocket;

import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test; 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.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; public class ClientSocket2 { //用户名
String username = "guo"; @Test
//单元测试方法启动
public void clientSocket() throws IOException {
//配置选择器
selector();
//监听
listen();
} //选择器作为全局属性
private Selector selector = null; /**
* 配置选择器
* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错
*/
private void selector() throws IOException {
//信道
SocketChannel channel = null;
//开启选择器
selector = Selector.open();
//开启信道
channel = SocketChannel.open();
//把该channel设置成非阻塞的,【需要手动设置为false】
channel.configureBlocking(false);
//管道连接互联网socket地址,输入ip和端口号
channel.connect(new InetSocketAddress("localhost", 8080));
//管道向选择器注册信息----连接就绪
channel.register(selector, SelectionKey.OP_CONNECT);
} /**
* 写了监听事件的处理逻辑
*/
private void listen() throws IOException {
out:
//进入无限循环遍历
while (true) {
//这个方法是阻塞的,是用来收集有io操作通道的注册事件【也就是选择键】,需要收到一个以上才会往下面执行,否则一直等待到超时,超时时间是可以设置的,
//直接输入参数数字即可,单位毫秒 ,如果超时后仍然没有收到注册信息,那么将会返回0 ,然后往下面执行一次后又循环回来
//不写事件将一直阻塞下去
// selector.select();
//这里设置超时时间为3000毫秒
if (selector.select(3000) == 0) {
//如果超时后返回结果0,则跳过这次循环
continue;
}
//使用迭代器遍历选择器里的所有选择键
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
//当迭代器指针指向下一个有元素是才执行内部代码块
while (ite.hasNext()) {
//获取选择键
SelectionKey key = ite.next();
//选择键操作完成后,必须删除该元素【选择键】,否则仍然存在选择器里面,将会在下一轮遍历再执行一次,形成了脏数据,因此必须删除
ite.remove();
//当选择键是可连接的
if (key.isConnectable()) {
if ( connectableHandler(key)) {
System.out.println("//远程主机未上线,退出循环,关闭客户端");
//退出循环,关闭客户端
break out;
}
}
//当选择键是可读的
else if (key.isReadable()) {
if (readHandler(key)) {
System.out.println("//远程主机强迫关闭了连接。退出循环,关闭客户端");
//退出循环,关闭客户端
break out;
}
}
//当选择键是可写的且是有效的【其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,
// 因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放在这里写啊】
//为了演示我还是写了
else if (key.isWritable() && key.isValid()) {
writeHandler(key);
}
}
}
} //当选择键是可连接的处理逻辑
//static静态,可用可不用
private boolean connectableHandler(SelectionKey key) throws IOException {
System.out.println("当选择键是可连接的处理逻辑");
SocketChannel sc =null;
/*
每当连接远程主机发现未上线,则会在这里报异常
java.net.ConnectException: Connection refused: no further information
*/
try {
sc = (SocketChannel) key.channel();
//如果管道是连接悬挂
if (sc.isConnectionPending()) {
//管道结束连接
sc.finishConnect();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//=============
Map<String, Object> map = new HashMap<>();
map.put("type", 0);
map.put("date", "socket首次握手成功,你好");
map.put("username", username);
String jsonstr = JSON.toJSONString(map);
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//指定编码将符串转字字节流
buffer.put(jsonstr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//写操作
sc.write(buffer);
}
sc.register(selector, SelectionKey.OP_READ);
}
return false;
}catch (Exception e){
// e.printStackTrace();
//取消该选择键
key.channel();
//发生异常才关闭
if (sc != null) {
sc.close();
}
return true;
} } //当选择键是可读的处理逻辑
private boolean readHandler(SelectionKey key) throws IOException {
SocketChannel sc = null;
/*
每当服务端强制关闭了连接,就会发送一条数据过来这里说
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
因此需要这里销毁连接,即关闭该socket通道即可
*/
try {
System.out.println("当选择键是可读的处理逻辑");
//获取信道,需要强转
sc = (SocketChannel) key.channel();
//key 获取 通道 关联的 缓冲区【这里使用是报错,read读操作报空指针异常,奇了怪了】
// ByteBuffer buffer = (ByteBuffer) key.attachment();
//只能自定义了
ByteBuffer buffer = ByteBuffer.allocate(1024);
//获取选择器
Selector mselector = key.selector();
//信道做读操作 ,返回读取数据的字节长度
long mreadSize;
//存储从心得解析出来的字符串
String jsonstr = "";
//当字节长度大于零,说明还有信息没有读完
while ((mreadSize = sc.read(buffer)) > 0) {
System.out.println("=======");
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//获取该索引间的数据 ,buffer.get()返回的是节数
byte[] b = buffer.array();
//指定编码将字节流转字符串
jsonstr = new String(b, StandardCharsets.UTF_8);
//打印
System.out.println(jsonstr);
}
//当字节长度为-1时,也就是没有数据可读取了,那么就关闭信道
if (mreadSize == -1) {
sc.close();
}
//检查字符串是否为空
if (!jsonstr.isEmpty()) {
//数据发送过来不为空
//进入业务层 【与服务端的一样写法,我这里就演示服务层了】
// eatService.food(mselector, sc, buffer, jsonstr);
//为了演示响应,我直接用做写就绪事件响应
//注册写就绪事件 ,这句话等同于 直接调用 writeHandler(SelectionKey key)
sc.register(mselector, SelectionKey.OP_WRITE); } return false;
} catch (Exception e) {
// e.printStackTrace();
//取消该选择键
key.channel();
//发生异常才关闭
if (sc != null) {
sc.close();
}
//关闭客户端
return true;
}
} //当选择键是可写的且是有效的处理逻辑
private void writeHandler(SelectionKey key) throws IOException {
System.out.println("当选择键是可写的且是有效的处理逻辑 ,我被自己通知来写东西啦,虽然不知道为什么要分开读写");
//获取信道,需要强转
SocketChannel sc = (SocketChannel) key.channel();
//设置字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//=============
Map<String, Object> map = new HashMap<>();
map.put("type", 3);
map.put("date", "我要写东西,你看到了吗" + System.currentTimeMillis());
String jsonstr = JSON.toJSONString(map);
//清除索引信息【即position = 0 ;capacity = limit】
buffer.clear();
//将字符转成字节流放入缓冲中
buffer.put(jsonstr.getBytes(StandardCharsets.UTF_8));
//定位到有效字符的索引【即limit = position ,position = 0 ,capacity 不变】
buffer.flip();
//如果 position < limit ,即仍有缓冲区的数据未写到信道中
while (buffer.hasRemaining()) {
//信道做写操作
sc.write(buffer);
}
//整理索引【即position定位到缓冲区未读的数据末尾 ,capacity = limit】
buffer.compact();
//获取选择器
Selector mselector = key.selector();
//注册读就绪事件
sc.register(mselector, SelectionKey.OP_READ); } }

3.测试

(1)分别启动服务端、cen客户端、guo客户端

服务端控制台打印

cen客户端

guo客户端

因为我在服务层设置了如果guo客户端不在线,则不发消息,

不如在线,cen客户端 会发消息给guo客户端,因为cen先上线,因此当时guo还没上线

(2)现在换过来

分别启动服务端、guo客户端,【先不启动cen客户端】

服务端控制台打印

guo客户端

好了,现在启动cen客户端,

希望向guo客户端发送消息

【cen客户端控制台打印没什么变化,因为本来就没做什么业务】

现在看看guo控制台打印,出来了,多出了几句话,包括cen客户端发来的数据

现在查看服务端控制台打印

【源码里面的注释够详细了,我懒得再说什么】

(3)现在测试客户端下线,服务端捕获的效果

关闭cen客户端

查看服务端控制台

可见,捕获客户端下线成功

源码位置截图 【是在 “当选择键是可读的处理逻辑 ” 方法处捕获的】

为什么写在这里?

我在源码的注释详细说明了原因,这里懒得写

(4)反过来,如果服务端突然关闭,客户端会如何

分别开启服务端、cen客户端, 然后在关闭服务端

查看cen客户端控制台打印 【是在 客户端 “当选择键是可读的处理逻辑 ” 方法处捕获的】

可检测出服务端关闭了,客户端关闭了【我故意设计的,当服务关闭,客户端也会跟着关闭,其实也可以不关闭,改一下就好了】

源码截图 【是在 客户端 “当选择键是可读的处理逻辑 ” 方法处捕获的】

(5)测试当服务端未开启,客户端请求连接会如何

关闭服务端,开启cen客户端

查看cen客户端控制台打印 【是在 客户端 “当选择键是可连接的处理逻辑 ” 方法处捕获的】

------------------------

参考博文原址:

https://www.cnblogs.com/fswhq/p/9788008.html#_label0_2

https://blog.csdn.net/shulianghan/article/details/106411546

https://www.jianshu.com/p/119b11ff837a

https://blog.csdn.net/zhanglong_4444/article/details/89002242

NIO【同步非阻塞io模型】关于 NIO socket 的详细总结【Java客户端+Java服务端 + 业务层】【可以客户端间发消息】的更多相关文章

  1. NIO【同步非阻塞io模型】关于 文件io 的总结

    1.前言 这一篇随笔是写 NIO 关于文件输入输出的总结 /* 总结: 1.io操作包括 socket io ,file io ; 2.在nio模型,file io使用fileChannel 管道 , ...

  2. 同步异步阻塞非阻塞Reactor模式和Proactor模式 (目前JAVA的NIO就属于同步非阻塞IO)

    在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作. 在比较这两个模式之前,我们首先的搞明白 ...

  3. Python之阻塞IO模型与非阻塞IO模型

    Python之阻塞IO模型与非阻塞IO模型 IO模型 1 阻塞IO: 全程阻塞 2 非阻塞IO: 发送多次系统调用: 优点:wait for data时无阻塞 缺点:1 系统调用太多 2 数据不是实时 ...

  4. 网络IO模型 非阻塞IO模型

    网络IO模型 非阻塞IO模型 同步 一件事做完后再做另一件事情 异步 同时做多件事情 相对论 多线程 多进程 协程 异步的程序 宏观角度:异步 并发聊天 阻塞IO 阻塞IO的问题 一旦阻塞就不能做其他 ...

  5. 多路复用 阻塞/非阻塞IO模型 网络IO两个阶段

    1.网络IO的两个阶段 waitdata copydata send 先经历:copydata阶段 recv 先经历:waitdata阶段 再经历 copydata阶段 2.阻塞的IO模型 之前写的都 ...

  6. python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)

    9.16 非阻塞IO模型 cpu占用率过高 服务端: from socket import * import time s = socket() s.bind(('127.0.0.1',8080)) ...

  7. IO模型,非阻塞IO模型,select实现多路复用

    1. IO阻塞模型 IO问题: 输入输出 我要一个用户名用来执行登陆操作,问题用户名需要用户输入,输入需要耗时, 如果输入没有完成,后续逻辑无法继续,所以默认的处理方式就是 等 将当前进程阻塞住,切换 ...

  8. 同步IO与同步非阻塞IO的理解

    本文图片均来自网络 一.同步IO---Blocking IO 在Blocking IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,直到数据准备好,并且将数据从 ...

  9. python 并发编程 非阻塞IO模型

    非阻塞IO(non-blocking IO) Linux下,可以通过设置socket使其变为non-blocking.当对一个non-blocking socket执行读操作时,流程是这个样子: 从图 ...

随机推荐

  1. 【阿菜做实践】利用go语言写一个简单的Pow样例

    本篇博客的主要内容是用go写一个简单的Proof-of-Work共识机制,不涉及到网络通信环节,只是一个本地的简单demo.开发IDE用的是JB Golang. 整个项目的文件结构如下: PoWdem ...

  2. xtra+binlog增量备份脚本

    目录 一.备份原理 innobackupex原理 binlog原理 特点 备份策略 二.环境准备 开启binlog 创建授权用户 安装innobackupex 三.添加脚本 全量备份 增量备份 bin ...

  3. Mysql资料 查询条件

    目录 一.计算 二.比较 三.逻辑运算符 四.位运算符 五.优先顺序 一.计算 二.比较 三.逻辑运算符 四.位运算符 五.优先顺序 实际上,很少有人能将这些优先级熟练记忆,很多情况下我们都是用&qu ...

  4. Gitlab更改项目间的fork提交关系

    目录 一.前情提要 二.实际操作 一.前情提要 1.dzsw/cgd_xx项目,通过fork按钮在dzsw_dev组下面同步了一个项目 2.但是现在dzsw/cgd_xx项目因为没法提交合并请求,一点 ...

  5. Fiddler抓包ios设备

    Fiddler绝对称得上是"抓包神器", Fiddler不但能截获各种浏览器发出的HTTP请求, 也可以截获各种智能手机发出的HTTP/HTTPS请求. Fiddler能捕获ISO ...

  6. set env export区别

    set env export区别 set,env和export这三个命令都可以用来显示shell变量,区别[root@localhost root]# aaa=bbb[root@localhost r ...

  7. java多线程9:线程池

    线程池 线程池的优点 我们知道线程的创建和上下文的切换也是需要消耗CPU资源的,所以在多线程任务下,使用线程池的优点就有: 第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗. ...

  8. ffmpeg 系列博客

    https://www.ffmpeg.org/download.html#build-macffmpeg 系列博文https://me.csdn.net/blog/leixiaohua1020http ...

  9. [C# Expression] 之动态创建表达式

    上一篇中说到了 Expression 的一些概念性东西,其实也是为了这一篇做知识准备.为了实现 EFCore 的多条件.连表查询,简化查询代码编写,也就有了这篇文章.   在一些管理后台中,对数据进行 ...

  10. ubuntu ffmpeg 转码错误

    [aac @ 0xde2400] The encoder 'aac' is experimental but experimental codecs are not enabled, add '-st ...