前言
最近在学习B站尚硅谷的netty的网络编程课程,非常感谢硅谷,硅谷威武!感兴趣的朋友,也可以去看一下。
学习netty之前,学习了BIO ,NIO,给后续netty课程打下扎实基础,现分享一下NIO 的群聊的一个案例 ,案例仅作为自己笔记
需要对NIO 的 三大组件有些了解哦,通道,选择器,buffer(缓冲区)
服务端代码流程:
客户端发起连接,服务端进行监听,当有事件发生时,服务器根据不同事件做不同处理
连接事件:服务端会为每个连接的客户端创建对应的通道,并注册到选择器,
读取事件:服务端会将通道中的数据读到buffer中,然后再进行转发,转发排除自己
客户端代码流程:
客户端发起连接,并开启一个线程循环去读取服务端转发过来的数据,主线程则阻塞,等待用户输入数据,然后发送,将数据写入通道,服务器端再从通道内获取数据
下面我贴上代码
服务端
package com.bio.demo;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @author yanjun.liu
* @date 2021/4/29--10:42
* <p>
* GroupChatServer 服务端的职责:
* 1.初始化 selector ,serverSocketChannel 并且进行绑定ip 端口,并且将自己注册到selector上,指定监听的事件(连接事件)
* 2.开启监听,并且处理selector 上,发送的事件(根据不同事件做不同处理)
* 如果是客户端连接事件 需要通过连接获取客户端socketChannel,然后设置非阻塞,并且将这个客户端socketChannel注册在selector,并指定监听的事件
* 如果发送事件的户端是发送数据,那么就是读事件,serverSocketChannel需要读取客户端的数据
*/
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private static final int PORT = 8888;
/**
* 构造器初始化工作
*
* @throws Exception
*/
public GroupChatServer() throws Exception {
//创建初始化selector
selector = Selector.open();
//创建初始化serverSocketChannel
serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//绑定ip端口
serverSocketChannel.bind(new InetSocketAddress(PORT));
//将serverSocketChannel 注册到 selector上,SelectionKey.OP_ACCEPT 客户端连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 服务端需要监听selector 上发送的事件,根据不同事件,做出不同处理
*/
public void listen() throws Exception {
while (true) {
//Selector 进行监听 select 方法,返回有事件发生的通道的个数,不传毫秒值会阻塞,直到拿到至少一个通道返回
// 重载方法,此方法还可以传入一个毫秒值,表示等待多少毫秒之后返回
int select = selector.select();
//说明有事件发送,进行处理
if (select > 0) {
//获取selector 上发送事件的SelectionKey,这里注意!SelectionKey 和通道是有绑定关系的,
//也可以说 获取selector 上发送事件的通道 socketChannel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//取出每个有事件发送的 SelectionKey
SelectionKey key = iterator.next();
//如果是客户端连接事件,监听建立连接,并且拿到对应的socketChannel,
// 设置非阻塞,并且将此 socketChannel注册在selector 上并设置监听事件
if (key.isAcceptable()) {
//serverSocketChannel.accept(); 此方法本身是阻塞的,但是代码进入if,就说明有客户端连接,因此这个方法必定会往下执行,不阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("获取到客户端的socketChannel:" + socketChannel.hashCode());
socketChannel.configureBlocking(false);
//设置读事件,
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + ":上线了");
}
//读事件,将数据从通道读取到buffer中
if (key.isReadable()) {
redDate(key);
}
//进入while,处理完这个事件之后,需要将这个事件从selectedKeys 数组中,将当前selectedKey 删除
iterator.remove();
}
} else {
System.out.println("客户端没有事件发生,继续循环等待");
}
}
}
public void redDate(SelectionKey key) throws IOException {
//通过SelectionKey 反向获取 SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = 0;
try {
//读取 从通道读取到buffer,返回读取到的长度
count = socketChannel.read(buffer);
if (count > 0) {
String msg = new String(buffer.array());
System.out.println("服务器收到客户端->" + socketChannel.getRemoteAddress() + "-发送的消息msg:" + msg);
System.out.println("服务器开始转发消息===============================");
//重点!服务端接收到消息了,还需要将消息转发给其客户端,当然要排除发消息的这个客户端哦
//获取所有注册在此selector 上通道 对应的SelectionKey
Set<SelectionKey> keys = selector.keys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey next = it.next();
Channel channel = (Channel) next.channel();
//不是他自己,服务端就转发消息给客户端 socketChannel 为发消息的客户端
if (channel instanceof SocketChannel && channel != socketChannel) {
ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());
((SocketChannel) channel).write(wrap);
}
}
}
} catch (IOException e) {
System.out.println(socketChannel.getRemoteAddress() + ": 离线了===============================");
//取消注册关闭通道
key.cancel();
socketChannel.close();
}
}
public static void main(String[] args) throws Exception {
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
客户端
package com.bio.demo;
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.util.Iterator;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author yanjun.liu
* @date 2021/4/29--17:10
*/
public class GroupChatClient {
private SocketChannel socketChannel;
private Selector selector;
private String userName;
/**
* 客户端类 初始化
*
* @throws IOException
*/
public GroupChatClient() throws IOException {
//客户端进行连接服务端
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//创建selector
selector = Selector.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//将客户端此通道 注册在 selector上,并指定监听的事件为读 read
socketChannel.register(selector, SelectionKey.OP_READ);
//初始化客户端名称
userName = socketChannel.getRemoteAddress().toString().substring(1);
System.out.println("客户端 is ok");
}
/**
* 客户端发送消息
*
* @param massage
* @throws Exception
*/
public void sendMessage(String massage) throws Exception {
String msg = userName + "说: " + massage;
ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());
//通过buff 中的数据 ,写入客户端通道,(服务端会监听,得到此通道,并读取此通道内的数据)
socketChannel.write(wrap);
}
/**
* 客户端也要读取服务端 转发 过来的数据,并显示
*
* @throws IOException
*/
public void readMassage() throws IOException {
//获取此 selector 有事件发生的 通道个数,如果不删除执行过的key,这里不会阻塞,而是返回0
int select = selector.select();
if (select > 0) {
//获取此selector 发生事件的 SelectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//处理读事件
if (key.isReadable()) {
//通过SelectionKey 反现获取对应的通道
SocketChannel channel = (SocketChannel) key.channel();
System.out.println("客户端的通道对象为:" + channel);
ByteBuffer buff = ByteBuffer.allocate(1024);
//将通道里面的数据读取到buff中
channel.read(buff);
String msg = new String(buff.array());
System.out.println(msg);
//删除当前事件
iterator.remove();
}
}
} else {
System.out.println("没有可用的通道");
}
}
public static void main(String[] args) throws Exception {
GroupChatClient groupChatClient = new GroupChatClient();
//启动一个线程去读取数据
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(new Runnable() {
@Override
public void run() {
//一直尝试读取服务端发送过来的数据,每割两秒
while (true) {
try {
groupChatClient.readMassage();
Thread.currentThread().sleep(2000);
} catch (Exception e) {
e.getStackTrace();
}
}
}
});
//主线程等待用户输入(scanner 阻塞)
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
groupChatClient.sendMessage(s);
}
}
}
运行
依次启动服务端,然后启动客户端即可,进行测试