实例要求:
(1)编写一个NIO多人群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
(2)服务器端:可以监测用户上线、离线,并实现消息转发功能
(3)客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(都是由服务器转发得到)
(4)目的:进一步理解NIO非阻塞网络编程机制
- 服务器
package nio.groupchat;
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 Mushroom
* @date 2020-03-15 20:52
*/
public class GroupChatServer {
//定义相关属性
private Selector selector; //多路复用选择器
private ServerSocketChannel listenChannel;//服务器监听
private static final int PORT = 6666;//端口
/**
* 初始化工作
*/
public GroupChatServer() throws IOException {
//实例化多路复用选择器
selector = Selector.open();
//实例化服务器监听
listenChannel = ServerSocketChannel.open();
//绑定服务器端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞
listenChannel.configureBlocking(false);
//注册ServerSocketChannel到Selector,并监听连接事件
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已启动.....");
}
/**
* 监听
*/
private void listen() throws IOException {
while (true) {
//阻塞,直到通道中有事件发生
selector.select();
//通道中有事件发生,得到SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历SelectionKey集合,根据事件的类型,执行相应的操作
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//通道发生连接事件
if (selectionKey.isAcceptable()) {
//得到SocketChannel
SocketChannel socketChannel = listenChannel.accept();
//设置非阻塞
socketChannel.configureBlocking(false);
//注册SocketChannel到Selector,监听读取事件
socketChannel.register(selector, SelectionKey.OP_READ);
//展示用户上线信息
System.out.println(socketChannel.getRemoteAddress() + "用户上线啦......");
}
//通道发生读取事件
if (selectionKey.isReadable()) {
//读取客户端消息
readData(selectionKey);
}
//删除当前处理完SelectionKey,防止多线程下重复操作
iterator.remove();
}
}
}
/**
* 读取客户端消息
*/
private void readData(SelectionKey selectionKey) {
SocketChannel socketChannel = null;
try {
//通过SelectionKey反向得到对应的SocketChannel
socketChannel = (SocketChannel) selectionKey.channel();
//创建缓冲区Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将通道中的数据读入缓冲区
int readCount = socketChannel.read(byteBuffer);
//有读取到字节,如果没有读取到字节可能是客户端下线或者未发送消息,需要处理客户端下线的异常
if (readCount > 0) {
//将缓冲区的数据转换成字符串
String msg = new String(byteBuffer.array());
//显示读取到的数据
System.out.println(msg);
//转发消息
sendInfoToOtherClients(msg, socketChannel);
}
//用户下线的相应处理
} catch (IOException e) {
try {
//显示用户下线的信息
System.out.println(socketChannel.getRemoteAddress() + "已下线.....");
//取消注册
selectionKey.cancel();
//关闭通道
socketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 向客户端(通道)转发消息
* 实际上就是向通道写入信息
*/
private void sendInfoToOtherClients(String msg, SocketChannel currentChannel) throws IOException {
System.out.println("服务器开始转发消息.....");
//得到所有注册到Selector上的通道,注意selector.selectedKeys()区分开,后者是得到所有有事件发生的SelectionKey
Set<SelectionKey> keys = selector.keys();
//遍历SelectionKey集合
for (SelectionKey selectionKey : keys) {
//得到对应的Channel,这里同时含有ServerSocketChannel,因此不能直接强转为SocketChannel
Channel targetChannel = selectionKey.channel();
//排除掉发送该消息的客户端,排除掉ServerSocketChannel
if (targetChannel instanceof SocketChannel && targetChannel != currentChannel) {
//创建缓冲区并写入数据
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//强转为SocketChannel
SocketChannel socketChannel = (SocketChannel) targetChannel;
//把缓冲区的数据写入通道
socketChannel.write(byteBuffer);
}
}
}
public static void main(String[] args) {
try {
//启动服务器
GroupChatServer groupChatServer = new GroupChatServer();
//开启监听,当存在客户端的消息就读取并转发
groupChatServer.listen();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动异常......");
}
}
}
- 客户端
package nio.groupchat;
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;
/**
* 群聊系统---客户端
* @author Mushroom
* @date 2020-03-16 10:48
*/
public class GroupChatClient {
//定义相关属性
private final String HOST = "127.0.0.1";//服务器ip地址
private final int PORT = 6666;//服务器端口号
private Selector selector;//多路复用选择器
private SocketChannel socketChannel;//客户端通道
private String username;//客户端名称
/**
* 初始化工作
*/
public GroupChatClient() throws IOException {
//实例化多路复用选择器
selector = Selector.open();
//实例化客户端通道并绑定服务器地址和端口
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//注册socketChannel到Selector,并监听读取事件
socketChannel.register(selector, SelectionKey.OP_READ);
//设置客户端名称
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("客户端已启动......");
}
/**
* 向服务器发送消息
*/
private void sendInfo(String msg) throws IOException {
msg = username + "说:" + msg;
//把缓冲区的数据写入通道
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
/**
* 读取向服务器转发的消息
*/
private void readInfo() throws IOException {
//一直阻塞,直到有事件发生后往下执行
selector.select();
//得到SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历SelectionKey集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//得到含有事件的SelectionKey
SelectionKey selectionKey = iterator.next();
//对读取事件作出相应处理
if (selectionKey.isReadable()) {
//反向获得通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//设置非阻塞
socketChannel.configureBlocking(false);
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将通道的数据读到缓冲区
socketChannel.read(byteBuffer);
//输出缓冲区中得到的消息
System.out.println(new String(byteBuffer.array()));
}
//删除当前处理完的SelectionKey
iterator.remove();
}
}
public static void main(String[] args) {
try {
//启动客户端
final GroupChatClient groupChatClient = new GroupChatClient();
//启动一个线程,循环读取服务器转发的消息
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
//读取服务器转发的消息
groupChatClient.readInfo();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
//用于向服务器发送信息
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("请输入发送的信息:");
//获得用户发送的消息
String msg = scanner.nextLine();
//将消息发送给服务器
groupChatClient.sendInfo(msg);
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}