前言
在前面我们详细讲解了BIO、NIO,本次我们将使用NIO来完成一个群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞);
群聊系统基本要求:
- 服务器端可监测客户端上线、离线、并转发客户端消息;
- 客户端可群发消息给其它用户,也可接收其它客户发送的消息;
代码示例
服务端:
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public GroupChatServer() {
}
public GroupChatServer(String host, int port) {
try {
// 创建选择器
selector = Selector.open();
// 创建ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(host, port));
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel注册到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() throws Exception {
while (true) {
if (selector.select() > 0) { // 监控是否有事件发生
// 获取有事件发生的SelectionKey
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIterator = selectionKeySet.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
if (selectionKey.isAcceptable()) { // 连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + "上线啦~");
} else if (selectionKey.isReadable()) { // 读取事件
// 反向获取Channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
if (socketChannel.read(byteBuffer) > 0) {
// 读取到的消息
String msg = new String(byteBuffer.array());
System.out.println("接收到[" + socketChannel.getRemoteAddress() + "]发送的消息:" + msg);
// 群发消息
this.sendMsgToOtherClient(msg, socketChannel);
}
} catch (IOException e) {
System.out.println(socketChannel.getRemoteAddress() + "离线啦~");
selectionKey.channel(); // 取消此通道注册
socketChannel.close(); // 关闭此通道
}
}
selectionKeyIterator.remove();
}
}
}
}
/**
* 转发消息给其它通道
*
* @param msg 转发的消息
* @param self 当前通道,也就是群发排除的通道
* @throws IOException
*/
private void sendMsgToOtherClient(String msg, SocketChannel self) throws IOException {
// 向其它客户端群发消息
System.out.println("服务端开始转发消息。。。");
// 获取所有注册到selector上的SocketChannel
Set<SelectionKey> keySet = selector.keys();
for (SelectionKey key : keySet) {
Channel keyChannel = key.channel();
// 群发时排除自己
if (keyChannel instanceof SocketChannel && keyChannel != self) {
SocketChannel targetChannel = (SocketChannel) keyChannel;
// 将读取到的消息写入到SocketChannel
targetChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
}
}
public static void main(String[] args) throws Exception {
new GroupChatServer("127.0.0.1", 8899).listen();
}
}
客户端:
public class GroupChatClient {
private Selector selector;
private SocketChannel socketChannel;
private String clientName;
public GroupChatClient() {
}
public GroupChatClient(String serverHost, int serverPort) {
try {
// 创建一个selector
selector = Selector.open();
// 连接服务端
socketChannel = SocketChannel.open(new InetSocketAddress(serverHost, serverPort));
// 设置非阻塞
socketChannel.configureBlocking(false);
// 将客户端通道注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
clientName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(clientName + "准备就绪~");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送消息
*
* @param msg 消息
* @throws IOException
*/
public void sendMsg(String msg) throws IOException {
msg = clientName + "说 :" + msg;
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
/**
* 读取消息
*/
public void readMsg() throws IOException {
if (selector.select() > 0) { // 有事件发生的通道
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIterator = selectionKeySet.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
if (selectionKey.isReadable()) { // 读取事件
// 反向获取channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 创建一个buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 读取消息
socketChannel.read(byteBuffer);
System.out.println("收到一条消息:" + new String(byteBuffer.array()));
}
selectionKeyIterator.remove();
}
}
}
public static void main(String[] args) throws Exception {
GroupChatClient groupChatClient = new GroupChatClient("127.0.0.1", 8899);
// 独立一个线程进行消息读取
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
groupChatClient.readMsg();
Thread.currentThread().sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
// 获取控制台输入
String inputMsg = scanner.nextLine();
groupChatClient.sendMsg(inputMsg);
}
}
}
通讯演示:
服务端:
/127.0.0.1:10481上线啦~
/127.0.0.1:10499上线啦~
/127.0.0.1:10537上线啦~
接收到[/127.0.0.1:10481]发送的消息:127.0.0.1:10481说 :client1
服务端开始转发消息。。。
接收到[/127.0.0.1:10499]发送的消息:127.0.0.1:10499说 :client2
服务端开始转发消息。。。
接收到[/127.0.0.1:10537]发送的消息:127.0.0.1:10537说 :client3
服务端开始转发消息。。。
/127.0.0.1:10481离线啦~
/127.0.0.1:10499离线啦~
/127.0.0.1:10537离线啦~
client1:
127.0.0.1:10481准备就绪~
client1
收到一条消息:127.0.0.1:10499说 :client2
收到一条消息:127.0.0.1:10537说 :client3
client2:
127.0.0.1:10499准备就绪~
收到一条消息:127.0.0.1:10481说 :client1
client2
收到一条消息:127.0.0.1:10537说 :client3
client3:
127.0.0.1:10537准备就绪~
收到一条消息:127.0.0.1:10481说 :client1
收到一条消息:127.0.0.1:10499说 :client2
client3