實例要求:
(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();
}
}
}