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("当选择键是可读的处理逻辑");//

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 Java NIO 来实现服务的开,同时使用普通的 Socket 来实现客户的开。下面是一个简单的示例: 服务代码: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; public class NIOServer { public static void main(String[] args) throws IOException { // 创建 ServerSocketChannel 对象 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 绑定监听口 serverSocketChannel.bind(new InetSocketAddress(8888)); // 设置为阻塞模式 serverSocketChannel.configureBlocking(false); while (true) { // 接受客户连接 SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel != null) { // 收到客户连接 System.out.println("接收到客户连接:" + socketChannel.getRemoteAddress()); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取客户送的数据 int bytesRead = socketChannel.read(buffer); if (bytesRead > 0) { // 切换缓冲区为读模式 buffer.flip(); // 解码数据 String message = StandardCharsets.UTF_8.decode(buffer).toString(); System.out.println("收到客户消息:" + message); // 回复客户 buffer.clear(); buffer.put("Hello, Client!".getBytes(StandardCharsets.UTF_8)); buffer.flip(); socketChannel.write(buffer); } } } } } ``` 客户代码: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class SocketClient { public static void main(String[] args) throws IOException { // 创建 Socket 对象 Socket socket = new Socket(); // 连接服务socket.connect(new InetSocketAddress("localhost", 8888)); // 消息服务器 String message = "Hello, Server!"; socket.getOutputStream().write(message.getBytes(StandardCharsets.UTF_8)); // 接收服务器回复的消息 byte[] buffer = new byte[1024]; int bytesRead = socket.getInputStream().read(buffer); if (bytesRead > 0) { String reply = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8); System.out.println("收到服务消息:" + reply); } // 关闭连接 socket.close(); } } ``` 在上面的示例中,服务使用 `ServerSocketChannel` 来监听客户连接,并使用 `SocketChannel` 来读取客户送的数据,并回复消息客户使用普通的 `Socket` 来连接服务,并消息服务,并接收服务回复的消息

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值