目录
之前手写了同步阻塞的socket通信聊天室:Java实现ChatRoom_zhangm2020的博客-CSDN博客
NIO是怎么实现的?那跟同步阻塞有什么区别呢?
NIO编程
三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动。Selector选择器用于监听多个通道的事件(连接请求,数据到达等)因此使用单个线程就可以监听多个客户端通道
BIO与NIO区别
- BIO以流的方式处理数据;NIO以缓冲区的方式处理数据
- BIO是阻塞的;NIO是非阻塞的
- BIO基于字节流和字符流进行操作;NIO基于Channel通道和Buffer缓冲区进行操作
基于channel通道实现通信
场景:一个服务端一个客户端(不涉及Selector)
服务端代码
public class NIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
//1. 打开一个服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 绑定对应的端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//3. 通道默认是阻塞的,需要设置为非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服务端启动成功");
while (true) {
//4. 检查是否有客户端连接 有客户端连接会返回对应的通道
SocketChannel socketChannel = serverSocketChannel.accept();
if (null == socketChannel) {
System.out.println("没有客户连接,非阻塞我去干其他事情");
Thread.sleep(2000);
continue;
}
//5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
String message = new String(allocate.array(), 0, read);
System.out.println("读到客户端消息:" + message);
//6. 给客户端回写数据
socketChannel.write(ByteBuffer.wrap("我是服务端,我收到了你的消息".getBytes()));
//7. 释放资源
socketChannel.close();
}
}
}
注:java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)返回值
//正数: 表示本次读到的有效字节个数. //0 : 表示本次没有读到有效字节. //-1 : 表示读到了末尾
该方法是阻塞的,如果客户端连接后一直没有发数据,那么这个方法会阻塞等待客户端信息,所以会造成服务端资源的浪费,场景2会涉及到Selector选择器,可以避免这个问题
客户端代码
public class NIOClient {
public static void main(String[] args) throws IOException {
//1. 打开通道
SocketChannel socketChannel = SocketChannel.open();
//2. 设置连接IP和端口号
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
//3. 写出数据
socketChannel.write(ByteBuffer.wrap("你好,我是客户端".getBytes()));
//4. 读取服务器写回的数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("获取到服务端消息:" + new String(byteBuffer.array(), 0, read));
//5. 释放资源
socketChannel.close();
}
}
}
通信结果
Channel通道与流的区别
- 通道可读可写;流是单向的(只能读或者写,输入流输出流)
- 通道可以异步读写(非阻塞)
- 通道是基于缓冲区buffer来读写的
基于Selector选择器服务端实现通信
场景:一个服务端多个客户端
服务端代码
public class NIOSelectorServer {
public static void main(String[] args) throws IOException {
//1. 打开一个服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 绑定对应的端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//3. 通道默认是阻塞的,需要设置为非阻塞
serverSocketChannel.configureBlocking(false);
//4. 创建选择器
Selector selector = Selector.open();
//5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功");
while (true) {
//6. 检查选择器是否有事件
//返回值:事件个数
int select = selector.select(2000);
if (0 == select) {
System.out.println("没有事件");
continue;
}
//7. 获取事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
if (selectionKey.isAcceptable()) {
System.out.println("获取到连接继续事件");
//9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端已连接" + socketChannel);
//通道必须设置为非阻塞,selector是轮询监听,所以不能被阻塞到某个通道上
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
//10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
if (selectionKey.isReadable()) {
System.out.println("获取到读就绪事件");
//11. 得到客户端通道,读取数据到缓冲区
//获取selector监听到的通道事件
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("读到客户端消息:" + new String(byteBuffer.array(), 0, read));
//12. 给客户端回写数据
socketChannel.write(ByteBuffer.wrap("我是服务端,我收到了你的消息".getBytes()));
socketChannel.close();
}
}
//13. 从集合中删除对应的事件, 因为防止二次处理
iterator.remove();
}
}
}
}
java.nio.channels.Selector#select(long)返回值:事件个数
客户端代码不变
通信结果
开启两个客户端:客户端0,客户端1