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

本文详细介绍了Java NIO的同步非阻塞IO模型在Socket通信中的应用,包括四个关键事件:OP_ACCEPT、OP_CONNECT、OP_READ和OP_WRITE。通过实例展示了服务器端和服务端的代码实现,以及客户端的交互过程,解释了为何可以使用NIO进行高效率的文件I/O和Socket通信。此外,还讨论了在实际操作中的一些疑问,如为何需要分开写事件和读事件的处理。
摘要由CSDN通过智能技术生成

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)目录结构

d35664afb3c8f7f949025ca5ab0966f9.png

只需要红框部分 其他可有可无,这是个maven工程,在测试类实现,之所以使用maven是因为导入依赖包很方便

(2)导入依赖包,需要使用json的生成和解析工具

com.alibaba

fastjson

1.2.56

org.springframework.boot

spring-boot-starter-test

test

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

服务端实现类源码

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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("当选择键是可读的处理逻辑");//获取信道,需要强转

sc =(SocketChannel) key.channel();//key 获取 通道 关联的 缓冲区【这里使用是报错,read读操作报空指针异常,奇了怪了】//ByteBuffer buffer = (ByteBuffer) key.attachment();//只能自定义了

ByteBuffer buffer = ByteBuffer.allocate(1024);//获取选择器

Selector mselector =key.selector();//信道做读操作 ,返回读取数据的字节长度

longmreadSize;//存储从心得解析出来的字符串

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 = newString(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) throwsIOException {

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);

}

}

View Code

服务层接口

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagecom.example.javabaisc.nio.mysocket.service;importjava.io.IOException;importjava.nio.ByteBuffer;importjava.nio.channels.Selector;importjava.nio.channels.SocketChannel;importjava.util.concurrent.ConcurrentMap;public interfaceEatService {public voidfood(Selector mselector, SocketChannel sc, ByteBuffer buffer, String jsonstr,

ConcurrentMapipMap,

ConcurrentMap usernameMap,ConcurrentMap socketChannelMap) throwsIOException;

}

View Code

服务层接口实现类

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagecom.example.javabaisc.nio.mysocket.service;importcom.alibaba.fastjson.JSON;importorg.springframework.stereotype.Service;importjava.io.IOException;importjava.net.SocketAddress;importjava.nio.Buffer;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.Date;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.ConcurrentMap;/*** 业务层*/@Servicepublic class EatServiceImpl implementsEatService {

@Overridepublic voidfood(Selector mselector, SocketChannel sc, ByteBuffer buffer, String jsonstr,

ConcurrentMapipMap,

ConcurrentMap usernameMap, ConcurrentMap socketChannelMap) throwsIOException {//解析json串成map

Map map =JSON.parseObject(jsonstr);

System.out.println(map);int type = (Integer) map.get("type");if (type == 1) {//返回结果

String res = "apple,好好吃,我好饿";

Map 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发送消息,看到了吗" + newDate();//System.out.println(res);// Map 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(newString(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);

}

}

}

View Code

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

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

用户名为 cen 的客户端

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagecom.example.javabaisc.nio.mysocket;importcom.alibaba.fastjson.JSON;importorg.junit.jupiter.api.Test;importjava.io.IOException;importjava.net.InetSocketAddress;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.HashMap;importjava.util.Iterator;importjava.util.Map;public classClientSocket {//用户名

String username = "cen";

@Test//单元测试方法启动

public void clientSocket() throwsIOException {//配置选择器

selector();//监听

listen();

}//选择器作为全局属性

private Selector selector = null;/*** 配置选择器

* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错*/

private void selector() throwsIOException {//信道

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() throwsIOException {

out://进入无限循环遍历

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.isConnectable()) {if( connectableHandler(key)) {

System.out.println("//远程主机未上线,退出循环,关闭客户端");//退出循环,关闭客户端

breakout;

}

}//当选择键是可读的

else if(key.isReadable()) {if(readHandler(key)) {

System.out.println("//远程主机强迫关闭了连接。退出循环,关闭客户端");//退出循环,关闭客户端

breakout;

}

}//当选择键是可写的且是有效的【其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,//因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放在这里写啊】//为了演示我还是写了

else if (key.isWritable() &&key.isValid()) {

writeHandler(key);

}

}

}

}//当选择键是可连接的处理逻辑//static静态,可用可不用

private boolean connectableHandler(SelectionKey key) throwsIOException {

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 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) throwsIOException {

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();//信道做读操作 ,返回读取数据的字节长度

longmreadSize;//存储从心得解析出来的字符串

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 = newString(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) throwsIOException {

System.out.println("当选择键是可写的且是有效的处理逻辑 ,我被自己通知来写东西啦,虽然不知道为什么要分开读写");//获取信道,需要强转

SocketChannel sc =(SocketChannel) key.channel();//设置字节缓冲区

ByteBuffer buffer = ByteBuffer.allocate(1024);//=============

Map 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);

}

}

View Code

e6caebc390432feaab48395c4b53ea7c.png

用户名为 guo 的客户端

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagecom.example.javabaisc.nio.mysocket;importcom.alibaba.fastjson.JSON;importorg.junit.jupiter.api.Test;importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.SocketChannel;importjava.nio.charset.StandardCharsets;importjava.util.HashMap;importjava.util.Iterator;importjava.util.Map;public classClientSocket2 {//用户名

String username = "guo";

@Test//单元测试方法启动

public void clientSocket() throwsIOException {//配置选择器

selector();//监听

listen();

}//选择器作为全局属性

private Selector selector = null;/*** 配置选择器

* 如果使用 main 启动 ,那么 selector() 需要设为静态,因为main 函数是static的,都在报错*/

private void selector() throwsIOException {//信道

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() throwsIOException {

out://进入无限循环遍历

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.isConnectable()) {if( connectableHandler(key)) {

System.out.println("//远程主机未上线,退出循环,关闭客户端");//退出循环,关闭客户端

breakout;

}

}//当选择键是可读的

else if(key.isReadable()) {if(readHandler(key)) {

System.out.println("//远程主机强迫关闭了连接。退出循环,关闭客户端");//退出循环,关闭客户端

breakout;

}

}//当选择键是可写的且是有效的【其实是自己通知自己触发的,我不明白为什么要有一个分开的感兴趣事件,//因为响应客户端直接在读操作后直接做写操作,然后注册读就绪事件就行了,没必要分开放在这里写啊】//为了演示我还是写了

else if (key.isWritable() &&key.isValid()) {

writeHandler(key);

}

}

}

}//当选择键是可连接的处理逻辑//static静态,可用可不用

private boolean connectableHandler(SelectionKey key) throwsIOException {

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 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) throwsIOException {

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();//信道做读操作 ,返回读取数据的字节长度

longmreadSize;//存储从心得解析出来的字符串

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 = newString(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) throwsIOException {

System.out.println("当选择键是可写的且是有效的处理逻辑 ,我被自己通知来写东西啦,虽然不知道为什么要分开读写");//获取信道,需要强转

SocketChannel sc =(SocketChannel) key.channel();//设置字节缓冲区

ByteBuffer buffer = ByteBuffer.allocate(1024);//=============

Map 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);

}

}

View Code

651634dd960c30f4f395e068896cb5ac.png

3.测试

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

服务端控制台打印

19dda07e2a9c6b991d166efaf2cbc8c2.png

cen客户端

acbf194efd31afcfc3ea25a8763de606.png

guo客户端

819243082639ddd3008fe87c7279795c.png

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

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

1b0e3f8bbd198713a14cffb018092fc3.png

(2)现在换过来

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

服务端控制台打印

1bb8b5ac48b333c130675a7441a9db3c.png

guo客户端

c13e7af287590f792fdd2b33ce2a4eb2.png

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

希望向guo客户端发送消息

6bf6065ebf592913f961457d26e6a257.png

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

c6b7d595f57f222a3a7a31f9623baaef.png

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

f0869c377dde3e33b54e967c54f2a597.png

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

7767ce5e337b6aa968bec390561dd176.png

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

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

关闭cen客户端

查看服务端控制台

6cddbe2b99c00f68ef1a307458f11951.png

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

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

a0c2ddbb8c7341bcb7b0420148457b78.png

为什么写在这里?

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

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

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

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

014d0f3d73c61d76fc088b732f35173b.png

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

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

9a97202595b7984875a81f56fb5c5fff.png

c717a6f338398c1f66280c6ac74d3c3a.png

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

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

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

c7e3db179f272666b3e039407586be41.png

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

参考博文原址:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值