Initial commit
This commit is contained in:
29
NIOChatServ/.gitignore
vendored
Normal file
29
NIOChatServ/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
### IntelliJ IDEA ###
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
10
NIOChatServ/.idea/.gitignore
generated
vendored
Normal file
10
NIOChatServ/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 依赖于环境的 Maven 主目录路径
|
||||
/mavenHomeManager.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
NIOChatServ/.idea/misc.xml
generated
Normal file
6
NIOChatServ/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
NIOChatServ/.idea/modules.xml
generated
Normal file
8
NIOChatServ/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/NIOChatServ.iml" filepath="$PROJECT_DIR$/NIOChatServ.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
4
NIOChatServ/.idea/vcs.xml
generated
Normal file
4
NIOChatServ/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings" defaultProject="true" />
|
||||
</project>
|
||||
11
NIOChatServ/NIOChatServ.iml
Normal file
11
NIOChatServ/NIOChatServ.iml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
255
NIOChatServ/src/client/NioChatClient.java
Normal file
255
NIOChatServ/src/client/NioChatClient.java
Normal file
@@ -0,0 +1,255 @@
|
||||
package client;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class NioChatClient extends JFrame {
|
||||
|
||||
// UI 组件
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JButton connectButton;
|
||||
|
||||
private JTextArea messageArea; // 显示聊天内容
|
||||
private JTextField inputField; // 输入框
|
||||
private JButton sendButton; // 发送按钮
|
||||
|
||||
// NIO相关
|
||||
private volatile SocketChannel socketChannel;
|
||||
private volatile Selector selector;
|
||||
private volatile boolean running = false; // 用于控制读线程
|
||||
|
||||
private Thread readThread; // 后台读取服务器消息的线程
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
NioChatClient client = new NioChatClient();
|
||||
client.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造UI
|
||||
*/
|
||||
public NioChatClient() {
|
||||
super("NIO Chat Client");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(600, 400);
|
||||
|
||||
// 主机、端口输入区域
|
||||
JPanel connectPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
connectPanel.add(new JLabel("服务器主机:"));
|
||||
hostField = new JTextField("127.0.0.1", 10);
|
||||
connectPanel.add(hostField);
|
||||
|
||||
connectPanel.add(new JLabel("端口:"));
|
||||
portField = new JTextField("8888", 5);
|
||||
connectPanel.add(portField);
|
||||
|
||||
connectButton = new JButton("连接");
|
||||
connectPanel.add(connectButton);
|
||||
|
||||
// 聊天显示区域
|
||||
messageArea = new JTextArea();
|
||||
messageArea.setEditable(false);
|
||||
JScrollPane scrollPane = new JScrollPane(messageArea);
|
||||
|
||||
// 发送消息区域
|
||||
JPanel inputPanel = new JPanel(new BorderLayout());
|
||||
inputField = new JTextField();
|
||||
sendButton = new JButton("发送");
|
||||
|
||||
inputPanel.add(inputField, BorderLayout.CENTER);
|
||||
inputPanel.add(sendButton, BorderLayout.EAST);
|
||||
|
||||
// 布局
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(connectPanel, BorderLayout.NORTH);
|
||||
getContentPane().add(scrollPane, BorderLayout.CENTER);
|
||||
getContentPane().add(inputPanel, BorderLayout.SOUTH);
|
||||
|
||||
// 按钮事件绑定
|
||||
connectButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (!running) {
|
||||
connectToServer();
|
||||
} else {
|
||||
disconnectFromServer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sendButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
sendMessage(inputField.getText().trim());
|
||||
inputField.setText("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
private void connectToServer() {
|
||||
final String host = hostField.getText().trim();
|
||||
final int port = Integer.parseInt(portField.getText().trim());
|
||||
|
||||
try {
|
||||
// 打开Selector和SocketChannel
|
||||
selector = Selector.open();
|
||||
socketChannel = SocketChannel.open();
|
||||
socketChannel.configureBlocking(false);
|
||||
socketChannel.connect(new InetSocketAddress(host, port));
|
||||
socketChannel.register(selector, SelectionKey.OP_CONNECT);
|
||||
|
||||
// 启动后台线程,处理服务器消息
|
||||
running = true;
|
||||
readThread = new Thread(this::readLoop, "NioClient-ReadThread");
|
||||
readThread.start();
|
||||
|
||||
// 更新UI状态
|
||||
connectButton.setText("断开");
|
||||
appendMessage("正在连接到服务器 " + host + ":" + port + "...\n");
|
||||
} catch (IOException e) {
|
||||
appendMessage("连接失败: " + e.getMessage() + "\n");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开与服务器的连接
|
||||
*/
|
||||
private void disconnectFromServer() {
|
||||
running = false;
|
||||
try {
|
||||
if (socketChannel != null) {
|
||||
socketChannel.close();
|
||||
}
|
||||
if (selector != null) {
|
||||
selector.wakeup();
|
||||
selector.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
socketChannel = null;
|
||||
selector = null;
|
||||
connectButton.setText("连接");
|
||||
appendMessage("已断开连接。\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给服务器
|
||||
*/
|
||||
private void sendMessage(String msg) {
|
||||
if (!running || socketChannel == null || !socketChannel.isOpen()) {
|
||||
appendMessage("未连接服务器,无法发送消息。\n");
|
||||
return;
|
||||
}
|
||||
if (msg.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socketChannel.write(ByteBuffer.wrap((msg + "\n").getBytes()));
|
||||
} catch (IOException e) {
|
||||
appendMessage("发送失败: " + e.getMessage() + "\n");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取服务器消息的循环线程
|
||||
*/
|
||||
private void readLoop() {
|
||||
try {
|
||||
while (running && selector != null && selector.isOpen()) {
|
||||
// 阻塞等待事件
|
||||
selector.select();
|
||||
|
||||
// 处理所有就绪事件
|
||||
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
|
||||
while (it.hasNext()) {
|
||||
SelectionKey key = it.next();
|
||||
it.remove();
|
||||
|
||||
if (!key.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理连接完成
|
||||
if (key.isConnectable()) {
|
||||
handleConnect(key);
|
||||
}
|
||||
|
||||
// 处理可读
|
||||
if (key.isReadable()) {
|
||||
handleRead(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
appendMessage("读取服务器消息时出现异常: " + e.getMessage() + "\n");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
// 读线程结束时进行清理
|
||||
disconnectFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接完成
|
||||
*/
|
||||
private void handleConnect(SelectionKey key) throws IOException {
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
if (sc.isConnectionPending()) {
|
||||
sc.finishConnect();
|
||||
}
|
||||
sc.configureBlocking(false);
|
||||
sc.register(selector, SelectionKey.OP_READ);
|
||||
appendMessage("已连接到服务器。\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可读事件:从服务器接收数据
|
||||
*/
|
||||
private void handleRead(SelectionKey key) throws IOException {
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
|
||||
int readBytes = sc.read(buffer);
|
||||
if (readBytes == -1) {
|
||||
// 服务器端关闭
|
||||
appendMessage("服务器已断开连接。\n");
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (readBytes > 0) {
|
||||
buffer.flip();
|
||||
// 按照简单文本协议,读到换行符即可作为一条消息
|
||||
String msg = new String(buffer.array(), 0, buffer.limit()).trim();
|
||||
appendMessage(msg + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在UI文本框中追加消息 (线程安全)
|
||||
*/
|
||||
private void appendMessage(String msg) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
messageArea.append(msg);
|
||||
// 自动滚动到末尾
|
||||
messageArea.setCaretPosition(messageArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
}
|
||||
153
NIOChatServ/src/server/NioChatServer.java
Normal file
153
NIOChatServ/src/server/NioChatServer.java
Normal file
@@ -0,0 +1,153 @@
|
||||
package server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class NioChatServer {
|
||||
|
||||
// 维护所有已连接的 SocketChannel
|
||||
private static final ConcurrentHashMap<SocketChannel, String> clientMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 端口可根据需要进行修改
|
||||
int port = 8888;
|
||||
try (Selector selector = Selector.open();
|
||||
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
|
||||
|
||||
// 配置为非阻塞
|
||||
serverSocketChannel.configureBlocking(false);
|
||||
// 绑定端口
|
||||
serverSocketChannel.bind(new InetSocketAddress(port));
|
||||
// 注册到 selector,监听 "接收新连接" 事件
|
||||
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
System.out.println("服务器已启动,监听端口: " + port);
|
||||
|
||||
// 事件循环
|
||||
while (true) {
|
||||
// 阻塞等待就绪的通道
|
||||
selector.select();
|
||||
// 获取就绪的事件集合
|
||||
Set<SelectionKey> selectionKeys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> iterator = selectionKeys.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
SelectionKey key = iterator.next();
|
||||
iterator.remove(); // 每次处理完一个事件后必须移除,避免重复处理
|
||||
|
||||
try {
|
||||
// 处理新连接
|
||||
if (key.isAcceptable()) {
|
||||
handleAccept(key, selector);
|
||||
}
|
||||
// 处理可读事件
|
||||
if (key.isReadable()) {
|
||||
handleRead(key, selector);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 发生异常时关闭连接并清理
|
||||
closeChannel(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新客户端连接
|
||||
*/
|
||||
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
|
||||
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
|
||||
SocketChannel sc = ssc.accept();
|
||||
if (sc == null) {
|
||||
return;
|
||||
}
|
||||
// 配置为非阻塞
|
||||
sc.configureBlocking(false);
|
||||
// 注册到选择器中,关注读事件
|
||||
sc.register(selector, SelectionKey.OP_READ);
|
||||
// 将此客户端加入管理集合(如果需要,可以存储客户端的名称/ID等信息)
|
||||
clientMap.put(sc, sc.getRemoteAddress().toString());
|
||||
System.out.println("客户端连接: " + sc.getRemoteAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端发送的数据
|
||||
*/
|
||||
private static void handleRead(SelectionKey key, Selector selector) throws IOException {
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
|
||||
int readBytes = sc.read(buffer);
|
||||
// 正常关闭或异常
|
||||
if (readBytes == -1) {
|
||||
closeChannel(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有数据
|
||||
if (readBytes > 0) {
|
||||
buffer.flip();
|
||||
String msg = new String(buffer.array(), 0, buffer.limit()).trim();
|
||||
|
||||
System.out.println("收到消息: " + msg + " 来自: " + clientMap.get(sc));
|
||||
|
||||
// 识别特殊命令
|
||||
if ("EXIT".equalsIgnoreCase(msg)) {
|
||||
// 关闭连接
|
||||
System.out.println("客户端请求断开连接: " + clientMap.get(sc));
|
||||
closeChannel(key);
|
||||
} else if ("PING".equalsIgnoreCase(msg)) {
|
||||
// 回复 PONG
|
||||
String pong = "PONG\n";
|
||||
sc.write(ByteBuffer.wrap(pong.getBytes()));
|
||||
} else {
|
||||
// 普通消息进行广播
|
||||
broadcastMessage(sc, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向其他所有客户端广播消息
|
||||
*/
|
||||
private static void broadcastMessage(SocketChannel sender, String msg) {
|
||||
// 构造广播内容
|
||||
String senderInfo = clientMap.get(sender);
|
||||
String broadcastMsg = "[" + senderInfo + "] " + msg + "\n";
|
||||
|
||||
// 遍历所有连接的客户端, 写入消息
|
||||
clientMap.forEach((channel, info) -> {
|
||||
if (channel != sender && channel.isOpen()) {
|
||||
try {
|
||||
channel.write(ByteBuffer.wrap(broadcastMsg.getBytes()));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭通道,清理资源
|
||||
*/
|
||||
private static void closeChannel(SelectionKey key) {
|
||||
try {
|
||||
SocketChannel sc = (SocketChannel) key.channel();
|
||||
System.out.println("连接关闭: " + clientMap.get(sc));
|
||||
clientMap.remove(sc);
|
||||
key.cancel();
|
||||
sc.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user