在上一篇中我们使用同步非阻塞的思想 集合NIO中的管道 选择器 还有缓冲区实现了多个客户端与一个服务端的通信。这次我们来一个更复杂的应用 多人群聊系统:
- 编写一个NIO群聊系统,实现客户端和客户端的通信(非阻塞)
- 服务端:可以检测用户上线 离线 并实现转发功能
- 客户端:可以无阻塞的发丝哦那个消息给其他客户端用户们,同时接受其他客户端用户发来的消息
听着是不是很有意思??
那我们开始把!
首先我们梳理一下 这个群里系统的 核心设计点:
在上一篇中我们实现 客户端和服务端通信的时候, 客户端首先创建了一个管道 然后绑定给 selector 选择器 发送数据。
服务端这边 监听,首先selector 轮询 先判断接受请求 再处理读取数据的请求然后收到数据了。
而现在 情况发送了一点小小的改变:
- 有多个客户端(之前也支持多客户端)意味着有多个管道来访问
- 服务端原来是收到消息 现在它收到消息之后要把这个消息发给其他的客户端,这样就有了群聊效果
- 客户端原来只是发消息 现在它还要兼顾收群聊系统里面其他客户端发来的消息
ok我们一步一步来:
服务端
public class Server {
//定义一些选择器 通道 端口
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private static final int PORT = 9999;
public Server(){
//接受选择器
try {
//接受选择器
selector =Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
定义好需要的东西 然后初始化 这些都是前几篇里面相同的内容
public static void main(String[] args) {
Server server = new Server();
server.listen();
}
private void listen() {
try{
while (selector.select()>0){
//获取所有准备好的事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey selectionKey =it.next();
//判断一下这个事件是什么
if(selectionKey.isAcceptable()){
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable())
{
//处理客户端的消息 把它转发给其他客户端
readClientData(selectionKey);
}
it.remove();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
在主方法中我们处于监听的状态,上面的监听代码和上一篇中几乎一模一样,
在上一篇中已经详细的描述了 这里就不多赘述了。
唯一改变的就是 readClientData(selectionKey);
之前通信的时候 这里服务端收到消息之后打出来就ok了 , 现在我们需要把它们转发到其他客户端的管道中去:
/**
* 接受当前客户端通道的信息,转发给其他客户端
* @param selectionKey
*/
private void readClientData(SelectionKey selectionKey) {
SocketChannel clientChannel = null;
try {
clientChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = clientChannel.read(buffer);
if (count>0){
buffer.flip();
String msg = new String(buffer.array(),0,count);
System.out.println("接收到客户端消息"+msg);
sentToAllClient(msg,clientChannel);
}
} catch (Exception e) {
try {
System.out.println("有人离线了"+clientChannel.getRemoteAddress());
selectionKey.cancel();
serverSocketChannel.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
我们从当前管道中 读取出消息 转换为字符串
然后把它发送给其他的客户端:
private void sentToAllClient(String msg, SocketChannel clientChannel) throws IOException{
System.out.println("--服务端转发当前客户端消息给其他所有在线注册的客户端--");
System.out.println("当前处理线程为:"+Thread.currentThread().getName());
for (SelectionKey key:selector.keys()){
Channel channel = key.channel();
//不要发数据发给自己: 判断遍历所有的通道时 当前通道是不是当前客户端的同一个通道
if (channel instanceof SocketChannel && channel != clientChannel){
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
((SocketChannel)channel).write(buffer);
}
}
}
这个方法要注意 首先你想不想一个客户端发给群聊的所有客户端之后 要不要再发给它自己
这里我们选择不发自己:
所以我们要判断一下 这里获取到选择器里面所有的管道之后:
判断 第一它不是服务端管道(因为服务端也有一个channel)其次它不是自己当前的channel。
然后我们在for循环中 把这条消息 挨个发给所有的的channel。就实现了群聊的核心功能。
客户端
/**
* 群聊系统客户端
*/
public class Client {
private Selector selector;
private SocketChannel socketChannel;
private static final int PORT = 9999;
public Client(){
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("当前客户端就绪:");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Client client = new Client();
//定义一个线程专门负责监听服务端发过来的读消息事件
new Thread(new Runnable() {
@Override
public void run() {
try {
client.readFromServer();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("主线程发消息-请说:");
String msg = scanner.nextLine();
client.sendToServer(msg);
}
}
private void sendToServer(String msg) {
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void readFromServer() throws IOException {
System.out.println("副线程收消息:");
//监听其他客户端发来的消息
while (selector.select()>0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey selectionKey =it.next();
//判断一下这个事件是什么
if (selectionKey.isReadable()){
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len;
while ((len = clientChannel.read(buffer))>0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
it.remove();
}
}
}
}
客户端很简单 发消息的流程和上一篇中一模一样 照抄过来就好了
额外增加的部分:
就是我们要收其他客户端发来的消息,所以我们在客户端中要单开一个线程出来专门收消息。
完工 我们来测试一下:
启动服务器和三个客户端:
client1 发消息
当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你好
主线程发消息-请说:
你总是问我爱你值不值得
主线程发消息-请说:
但是你应该明白
主线程发消息-请说:
爱 就是不问 值得不值得
client2 收消息
当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你总是问我爱你值不值得
但是你应该明白
爱 就是不问 值得不值得
client1 收消息
当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你总是问我爱你值不值得
但是你应该明白
爱 就是不问 值得不值得
接收到客户端消息你好
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息你总是问我爱你值不值得
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息但是你应该明白
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息爱 就是不问 值得不值得
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
Process finished with exit code 130
通过这样的实践能更好的体会NIO通信模式和多路复用机制。
往后我们会遇到很多应用比如redis tomcat nginx等等他们的底层都有多路复用的设计思想。
这个专栏就到此结束啦!
希望能给大家一点启发和帮助