搭建一個一臺服務端多臺客戶端連接的聊天室
首先梳理一下核心點:
服務端有一個本地Map,記錄了所有連接的信息,以達到給所有人發送的效果
編碼時的公共流程:
1.先打開各自的channel和selector,然後channel註冊到selector上,註冊事件C:CONNECT;S:ACCEPT
2.然後進行一個死循環,第一行是selector.select()該事件會阻塞自己知道感興趣的事件發生。
3.事件發生後進行selector.selectedKeys(),返回一個Set<SelectionKey>,因爲可能同時發生多個感興趣的事件。
4.進行各自的操作
5.在操作完之後需要清空調用clear()對set集合清空。
客戶端:
isConnectable()與isReadable()是處於同一級的,分別監聽連接和讀取
連接:isisConnectable->isConnectionPending->finishConnect纔算正式建立好連接;再把建立連接成功的信息寫到通道里.write(buffer);在開啓一個線程一直監聽本地鍵盤輸入;最後把讀事件註冊到selector上
讀取時:從管道中.read(buffer)即可讀取到buffer中
服務端:
例子監聽了Accept和Read事件:
當有連接的時候:吧該管道感興趣的時間設爲讀並放進本地map中
當管道有數據可讀的時候:先從管道讀數據,然後遍歷map向所有連接端寫數據
源碼:
server.java
package com.rikka.nio;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.*;
/**
* @Author Yan1less
* @Date 2019/4/21 12:55
* @Description 實現分發給所有客戶端
**/
public class NioServer {
private static Map<String,SocketChannel> clientMap = new HashMap<>();
//服務器端只有一個線程
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8899));
Selector selector = Selector.open();
//把channel註冊到selector上
//ServerSocketChannel關注連接事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
try {
//這個方法會一直阻塞,直到發生了其感興趣的事件
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(selectionKey -> {
//SocketChannel關注讀取事件
final SocketChannel client;
try{
if(selectionKey.isAcceptable()){
//這裏爲什麼能強行向下轉型呢?是因爲上面只把serverSocketChannel註冊到了OP_ACCEPT事件上
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
//到這一步ServerSocketChannel的任務完成了,我們接下來只需要從client進行操作
client = server.accept();
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
String key = "【"+UUID.randomUUID().toString() +"】";
clientMap.put(key,client);
}else if(selectionKey.isReadable()){
client = (SocketChannel)selectionKey.channel();
//一個不夠用咋整
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if(count>0){
readBuffer.flip();
Charset charset = Charset.forName("utf-8");
String receivedMessage = String.valueOf(charset.decode(readBuffer).array());
System.out.println(client + ": " + receivedMessage);
String senderKey = null;
for(Map.Entry<String,SocketChannel> entry : clientMap.entrySet()){
if(client == entry.getValue()){
senderKey = entry.getKey();
break;
}
}
for(Map.Entry<String,SocketChannel> entry : clientMap.entrySet()){
SocketChannel value = entry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((senderKey+" : "+receivedMessage).getBytes());
//把數據放到buffer裏稱爲讀
writeBuffer.flip();
value.write(writeBuffer);
}
}
}
}catch (Exception e){
e.printStackTrace();
}
});
selectionKeys.clear();
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
client.java
package com.rikka.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author Yan1less
* @Date 2019/4/21 13:49
* @Description TODO
**/
public class NioClient {
public static void main(String[] args) throws Exception {
try{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector,SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("127.0.0.1",8899));
while (true){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(selectionKey -> {
if(selectionKey.isConnectable()){
SocketChannel client = (SocketChannel) selectionKey.channel();
//判斷這個連接是不是正在進行的狀態
if(client.isConnectionPending()){
try {
//這樣連接纔算建立好了
client.finishConnect();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now()+"連接成功").getBytes());
writeBuffer.flip();
client.write(writeBuffer);
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
executorService.submit(()->{
while(true){
try{
writeBuffer.clear();
InputStreamReader input = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(input);
String sendMessage = br.readLine();
writeBuffer.put(sendMessage.getBytes());
writeBuffer.flip();
client.write(writeBuffer);
}catch (Exception ex){
ex.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
try {
client.register(selector,SelectionKey.OP_READ);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}else if(selectionKey.isReadable()){
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
try {
int read = client.read(readBuffer);
if(read>0){
String receivedMesage = new String(readBuffer.array(),0,read);
System.out.println(receivedMesage);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
selectionKeys.clear();
}
}catch (Exception ex){
}
}
}
Tips:
1.每次新建一個channel的時候都要設置其configureBlocking(false)爲非阻塞
2.無論是C或是S通過SocketChannel.open()方法獲得的channel通道都只是爲了鏈接,真正的數據傳輸通道都是通過selectionKey.channel()獲得的(對於server來說需要在進行accept()方法來獲得實際操作channel,注:ServerSocketChannel的作用也是爲了獲得SocketChannel),該方法需要強制類型轉換,轉換成SocketChannel
3.channel.read(buffer)是把buffer讀取出來