当前位置: 首页 > news >正文

通信网络编程2.0——JAVA

一、传统阻塞式 I/O 模型

实现简易多人聊天系统:服务端与客户端

服务端

public class ChatServer {int port = 6666;// 定义服务器端口号为 6666ServerSocket ss;// 定义一个 ServerSocket 对象用于监听客户端连接//List<Socket> clientSockets = new ArrayList<>();// 定义一个列表用于存储已连接的客户端 Socket 对象List<Socket> clientSockets = new CopyOnWriteArrayList<>();//迭代时会复制整个底层数组,因此在遍历过程中其他线程对集合的修改不会影响当前遍历,// 有效避免了 ConcurrentModificationException 异常。
}

定义了 ChatServer 类,指定服务器端口为 6666 ,创建 ServerSocket 对象用于监听客户端连接,采用 CopyOnWriteArrayList 存储已连接的客户端 Socket 对象。相较于普通列表,CopyOnWriteArrayList 在迭代时会复制整个底层数组,确保在遍历过程中其他线程对集合的修改不会影响当前遍历,有效避免并发修改异常。

public void initServer() {// 初始化服务器的方法try {ss = new ServerSocket(port);// 创建 ServerSocket 对象并绑定到指定端口System.out.println("服务器启动,等待客户端连接...");} catch (IOException e) {throw new RuntimeException(e);}}

 initServer 方法用于创建 ServerSocket 对象并绑定到指定端口,启动服务器端,等待客户端连接。

public void listenerConnection() {// 监听客户端连接的方法,返回连接的 Socket 对象new Thread(()->{while(true){try {Socket socket = ss.accept();// 调用 accept() 方法等待客户端连接//clientSockets.add(socket);synchronized (clientSockets) {// 同步操作确保线程安全clientSockets.add(socket);// 将连接的客户端 Socket 对象添加到列表中}System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress());// 输出客户端连接成功提示信息及客户端 IP 地址} catch (IOException e) {throw new RuntimeException(e);}}}).start();}

listenerConnection 方法启动一个线程,通过 ServerSocketaccept() 方法等待客户端连接。当有客户端连接时,将其 Socket 对象添加到客户端列表中,并输出客户端 IP 地址。

public void readMsg(List<Socket> clientSockets, JTextArea msgShow) {// 读取客户端消息的方法//System.out.println("clientSockets size: " + clientSockets.size()); // 检查列表大小synchronized (clientSockets) {// 对客户端列表进行同步操作Thread tt = new Thread(() -> {// 创建一个线程用于读取并处理客户端消息//System.out.println("开始读取客户端发送的消息");while (true) {// 无限循环持续读取消息InputStream is;// 定义输入流对象用于读取客户端消息Socket socket = null;try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}for (Socket cSocket : clientSockets) {// 遍历每个客户端 Socket//System.out.println("循环每个socket");socket = cSocket;if(socket == null){continue;}try {is = socket.getInputStream();// 获取客户端 Socket 对象的输入流} catch (IOException e) {throw new RuntimeException(e);}try {int idLen = is.read();// 读取消息中发送方名称长度的字节if(idLen == 0){continue;}byte[] id = new byte[idLen];// 根据读取的长度创建字节数组存储发送方名称is.read(id);// 读取发送方名称字节数组int msgLen = is.read();// 读取消息内容长度的字节if(msgLen == 0){continue;}byte[] msg = new byte[msgLen];// 根据读取的长度创建字节数组存储消息内容is.read(msg);// 读取消息内容字节数组System.out.println(new String(id) + "发送的消息:" + new String(msg));// 将字节数组转换为字符串并输出消息内容msgShow.append(new String(id) + "说:" + new String(msg) + "\n");// 转发信息给所有其他客户端for (Socket clientSocket : clientSockets) {// 遍历所有已连接的客户端 Socket 对象if (clientSocket == socket) {// 如果是当前发送消息的客户端continue;}OutputStream os = null;// 定义输出流对象用于向其他客户端发送消息os = clientSocket.getOutputStream();// 获取客户端 Socket 对象的输出流os.write(id.length);// 发送发送方名称长度os.write(id);// 发送发送方名称字节数组os.write(msg.length);// 发送消息内容长度os.write(msg);// 发送消息内容字节数组os.flush();// 刷新输出流确保数据发送完成}} catch (IOException e) {throw new RuntimeException(e);}}}});tt.start();}}

 readMsg 方法用于读取客户端发送的消息。创建一个线程,持续读取每个客户端的输入流。读取消息时,先读取发送方名称长度、名称字节数组,再读取消息内容长度、内容字节数组,将其转换为字符串并显示在消息区域。然后将消息转发给所有其他客户端。

public void start() {// 启动服务器的方法initServer();// 调用初始化服务器的方法//new Thread(()->{//startSend();// 启动服务端从控制台向所有客户端发送消息的线程//}).start();ChatUI ui = new ChatUI("服务端", clientSockets);ui.setVisible(true); // 确保 UI 可见listenerConnection();// 调用监听客户端连接的方法readMsg(clientSockets,ui.msgShow);// 调用读取消息的方法}

start 方法初始化服务器,创建服务端界面,启动监听客户端连接和读取消息的功能。

客户端

public class Client {Socket socket;// 定义 Socket 对象用于与服务器建立连接String ip;// 定义服务器 IP 地址int port;// 定义服务器端口号InputStream in;// 定义输入流对象用于读取服务器发送的消息OutputStream out;// 定义输出流对象用于向服务器发送消息public Client(String ip, int port) {// 构造方法,初始化客户端 IP 地址和端口号this.ip = ip;this.port = port;}
}

 定义了 Client 类,包含 Socket 对象用于连接服务器,以及服务器的 IP 地址和端口号。构造方法用于初始化客户端 IP 地址和端口号。

public void connectServer(String userName) {// 连接服务器的方法try {socket = new Socket(ip, port);// 创建 Socket 对象连接到指定 IPin = socket.getInputStream();// 获取 Socket 对象的输入流用于读取消息out = socket.getOutputStream();// 获取 Socket 对象的输出流用于发送消息try {out.write(userName.length());out.write(userName.getBytes());out.flush();} catch (IOException e) {throw new RuntimeException(e);}System.out.println("连接服务器成功");} catch (IOException e) {throw new RuntimeException(e);}}

connectServer 方法用于连接服务器。创建 Socket 对象连接到指定 IP 地址和端口号的服务器,获取输入流和输出流。然后向服务器发送用户名长度和用户名字节数组,完成连接。

public void readMsg(JTextArea msgShow) {// 读取服务器发送的消息的方法new Thread(() -> {// 创建一个线程用于读取并处理服务器消息try {System.out.println("开始读取消息");while (true) { // 无限循环持续读取消息int senderNameLength = in.read();// 读取发送方名称长度的字节byte[] senderNameBytes = new byte[senderNameLength];// 根据读取的长度创建字节数组存储发送方名称in.read(senderNameBytes);// 读取发送方名称字节数组int msgLength = in.read();// 读取消息内容长度的字节byte[] msgBytes = new byte[msgLength];// 根据读取的长度创建字节数组存储消息内容in.read(msgBytes);// 读取消息内容字节数组System.out.println(new String(senderNameBytes) + "发送的消息:" + new String(msgBytes));// 将字节数组转换为字符串并输出消息内容msgShow.append(new String(senderNameBytes) +"说:" + new String(msgBytes) + "\n");}} catch (IOException e) {throw new RuntimeException(e);}}).start();}

 readMsg 方法用于读取服务器发送的消息。创建一个线程,读取发送方名称长度、名称字节数组,消息内容长度、内容字节数组,将其转换为字符串并显示在消息区域。

public void startClient() {// 启动客户端的方法String userName = JOptionPane.showInputDialog("请输入用户名:");connectServer(userName);// 调用连接服务器的方法ChatUI ui = new ChatUI(userName, out);readMsg(ui.msgShow);// 调用读取消息的方法//startSend();// 调用发送消息的方法try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread() {public void run() {while (true) {try {out.write(0);out.flush();Thread.sleep(500);} catch (IOException e) {throw new RuntimeException(e);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}.start();}

startClient 方法启动客户端。通过对话框获取用户名,连接服务器,创建客户端界面,启动读取消息的功能。同时创建一个线程,定期向服务器发送心跳包(0),保持连接。

图形界面

服务端界面

public ChatUI(String title, List<Socket> clientSockets) {// 服务器端构造方法super(title);// 设置窗口标题setSize(500, 500);// 设置窗口大小setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置关闭操作JScrollPane scrollPane = new JScrollPane(msgShow);// 创建滚动面板包括消息显示区域scrollPane.setPreferredSize(new Dimension(0, 350));add(scrollPane, BorderLayout.NORTH);// 添加到窗口北部// 创建消息输入面板及组件JPanel msgInput = new JPanel();JTextArea msg = new JTextArea();JScrollPane scrollPane1 = new JScrollPane(msg);scrollPane1.setPreferredSize(new Dimension(480, 80));msgInput.add(scrollPane1);JButton send = new JButton("发送");msgInput.add(send);msgInput.setPreferredSize(new Dimension(0, 120));add(msgInput, BorderLayout.SOUTH);// 添加到窗口南部setVisible(true);ChatListener cl = new ChatListener();// 创建事件监听器send.addActionListener(cl);// 为发送按钮添加监听器cl.showMsg = msgShow;// 传递消息显示组件cl.msgInput = msg;cl.userName = title;cl.clientSockets = clientSockets;}

 服务端界面包含显示消息的文本区域和消息输入面板。文本区域用于展示聊天记录,输入面板包含一个文本区域用于输入消息,一个发送按钮用于发送消息。为发送按钮添加事件监听器,当点击按钮时,触发发送消息的操作。

客户端界面

public ChatUI(String title, OutputStream out) {// 客户端构造方法super(title);setSize(500, 500);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JScrollPane scrollPane = new JScrollPane(msgShow);scrollPane.setPreferredSize(new Dimension(0, 350));add(scrollPane, BorderLayout.NORTH);JPanel msgInput = new JPanel();JTextArea msg = new JTextArea();JScrollPane scrollPane1 = new JScrollPane(msg);scrollPane1.setPreferredSize(new Dimension(480, 80));msgInput.add(scrollPane1);JButton send = new JButton("发送");msgInput.add(send);msgInput.setPreferredSize(new Dimension(0, 120));add(msgInput, BorderLayout.SOUTH);setVisible(true);clientListener cl = new clientListener();send.addActionListener(cl);cl.showMsg = msgShow;cl.msgInput = msg;cl.userName = title;cl.out = out;}

客户端界面与服务端界面结构类似,用于展示聊天记录和输入消息。为发送按钮添加客户端事件监听器,当点击按钮时,将消息发送给服务器。

事件监听器

服务端事件监听器

public class ChatListener implements ActionListener {public List<Socket> clientSockets;// 客户端 Socket 列表JTextArea showMsg;// 消息显示区域JTextArea msgInput;// 消息输入区域String userName;// 用户名OutputStream out;// 输出流public void actionPerformed(ActionEvent e) {// 处理发送按钮点击事件String text = msgInput.getText();// 获取输入的消息文本showMsg.append(userName + ": " + text + "\n");// 在显示区域追加消息for (Socket cSocket : clientSockets) {// 遍历所有客户端Socket socket = cSocket;try {out = socket.getOutputStream();// 获取客户端输出流out.write(userName.getBytes().length);// 发送用户名长度out.write(userName.getBytes());// 发送用户名out.write(text.getBytes().length);// 发送消息内容长度out.write(text.getBytes());// 发送消息内容out.flush();// 刷新输出流} catch (IOException ex) {throw new RuntimeException(ex);}}}}

 服务端事件监听器实现了 ActionListener 接口。当点击发送按钮时,获取输入的消息文本,将其添加到显示区域。然后遍历所有客户端 Socket,获取每个客户端的输出流,发送用户名长度、用户名、消息内容长度、消息内容给每个客户端。

客户端事件监听器

public class clientListener implements ActionListener {JTextArea showMsg;// 消息显示区域JTextArea msgInput;// 消息输入区域String userName;// 用户名OutputStream out;// 输出流public void actionPerformed(ActionEvent e) {// 处理发送按钮点击String text = msgInput.getText();// 获取输入消息showMsg.append(userName + ": " + text + "\n");// 显示消息try {out.write(userName.getBytes().length);// 发送用户名长度out.write(userName.getBytes());// 发送用户名out.write(text.getBytes().length);// 发送消息长度out.write(text.getBytes());// 发送消息内容out.flush();// 刷新输出流//msgInput.setText(""); // 清空输入框} catch (IOException ex) {throw new RuntimeException(ex);}}
}

客户端事件监听器实现了 ActionListener 接口。当点击发送按钮时,获取输入的消息文本,将其添加到显示区域。然后通过客户端的输出流向服务器发送用户名长度、用户名、消息内容长度、消息内容。

 

运行效果

二、NIO 模型

聊天服务器

public class NIOChatServer {private static final int PORT = 8080;private static final Charset charset = Charset.forName("UTF-8");private static Set<SocketChannel> clients = new HashSet<>();public static void main(String[] args) throws IOException {Selector selector = null;try {selector = Selector.open();ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.bind(new InetSocketAddress(PORT));serverChannel.configureBlocking(false);serverChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {throw new RuntimeException(e);}System.out.println("Server started on port " + PORT);while (true){try {selector.select();} catch (IOException e) {throw new RuntimeException(e);}Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()){SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()){handleAccept(key,selector);}else if (key.isReadable()){handleRead(key);}}}}
}

在服务器端代码中,首先创建一个选择器(Selector), 它是 NIO 中用于监听多个通道事件的核心组件。然后打开一个服务器套接字通道(ServerSocketChannel),绑定到指定端口(8080),并将其设置为非阻塞模式。接着将通道注册到选择器上,监听连接接受事件(SelectionKey.OP_ACCEPT)。启动一个无限循环,调用选择器的 select() 方法,该方法会阻塞直到有通道事件发生,然后迭代处理每个事件。

private static void handleAccept(SelectionKey key, Selector selector) throws IOException{ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);clients.add(clientChannel);System.out.println("New Client connected to " + clientChannel.getRemoteAddress());}

当检测到客户端连接事件时,服务器通道(ServerSocketChannel)调用 accept() 方法接受新连接,返回新的客户端套接字通道(SocketChannel)。将客户端通道设置为非阻塞模式,并注册到选择器上,监听读事件(SelectionKey.OP_READ),以便后续接收该客户端的消息。同时将客户端通道添加到 clients 集合中,用于后续广播消息。

private static void handleRead(SelectionKey key) throws IOException {SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = 0;try {bytesRead = clientChannel.read(buffer);if (bytesRead == -1){disconnectClient(clientChannel);return;}buffer.flip();String message = charset.decode(buffer).toString();System.out.println("Received: " + message);broadcastMessage(message,clientChannel);} catch (IOException e) {disconnectClient(clientChannel);}}

 当检测到读事件时,表示客户端有数据可读。创建一个缓冲区(ByteBuffer)用于存储从客户端读取的数据。调用客户端通道的 read() 方法将数据读取到缓冲区。如果返回值为 -1,表示客户端断开连接,调用 disconnectClient() 方法处理断开事件。否则,将缓冲区翻转(flip),解码缓冲区中的字节数据为字符串,并调用 broadcastMessage() 方法将消息广播给其他客户端。

private static void broadcastMessage(String message, SocketChannel sender) throws IOException {ByteBuffer buffer = charset.encode(message);for (SocketChannel client : clients) {if(client != sender && client.isConnected()){client.write(buffer);buffer.rewind();}}}

将消息字符串编码为字节缓冲区,然后遍历所有客户端通道。对于每个客户端通道(除了发送消息的客户端),如果通道处于连接状态,就将缓冲区中的数据写入通道,实现消息的广播。写完后调用 rewind() 方法重置缓冲区的位置,以便下次写操作从头开始。

private static void disconnectClient(SocketChannel client) throws IOException {clients.remove(client);System.out.println("Client disconnected from " + client.getRemoteAddress());client.close();}

 从 clients 集合中移除断开连接的客户端通道,关闭该通道,并打印断开连接的日志。

聊天客户端

public class BlockingChatClient {public static void main(String[] args) {Socket socket = null;try {socket = new Socket("localhost", 8080);System.out.println("Connected");// 创建输入输出流BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));// 启动一个线程来读取服务器发送的消息new Thread(() -> {String message;try {while ((message = reader.readLine()) != null) {System.out.println("[Server] " + message);}} catch (IOException e) {System.out.println("Disconnected from server");}}).start();// 从控制台读取用户输入并发送到服务器BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));String input;while ((input = consoleReader.readLine()) != null) {writer.write(input);writer.newLine();writer.flush();if ("/exit".equalsIgnoreCase(input)) {break;}}} catch (IOException e) {e.printStackTrace();} finally {try {if (socket != null) {socket.close();}} catch (IOException e) {e.printStackTrace();}}}
}

客户端代码使用传统的阻塞式 I/O。启动时连接到服务器,建立 Socket 连接。然后获取输入流和输出流。启动一个线程读取服务器发送的消息并打印到控制台。在主线程中,从控制台读取用户输入,发送到服务器。当输入 "/exit" 时,退出客户端。最后确保关闭 Socket 连接。 

运行效果

三、两种模型对比

I/O模型

  • 资源消耗大 :每个客户端都需要一个独立线程,大量客户端连接会导致线程数量剧增,增加系统资源消耗和线程切换开销。

  • 扩展性差 :线程数量受限于系统资源,难以处理高并发场景。

NIO 模型

  • 高并发支持 :一个线程可管理多个客户端连接,显著降低资源消耗,提升系统并发能力。

  • 高效复用 :通过选择器复用线程,减少线程创建和销毁的开销。

对比方面传统阻塞式 I/ONIO 模型
线程模型一客户端一线程,资源占用大,线程切换频繁一线程多客户端,资源占用小,线程切换少
并发能力并发能力受限于线程数量支持高并发,可处理大量客户端连接
阻塞处理依赖线程隔离防止阻塞通过非阻塞 I/O 和多路复用防止阻塞
适用场景适合客户端数量较少,对响应时间要求不高的场景适合高并发场景,如即时通讯、在线游戏等

http://www.xdnf.cn/news/992953.html

相关文章:

  • HALCON第五讲-> 形状匹配
  • 每日八股文6.12
  • 蓝桥杯20112 不同的总分值
  • 网页怎么调用字体ttf文件?
  • Go 语言安装指南:并解决 `url.JoinPath` 及 `Exec format error` 问题
  • [论文阅读] 系统架构 | 零售 IT 中的微服务与实时处理:开源工具链与部署策略综述
  • MySQL数据库:关系型数据库的基石
  • AVL树的平衡艺术:用C++写出会“站立”的二叉树(未完待续)
  • 【SAS求解多元回归方程】REG多元回归分析-多元一次回归
  • windows基线配置
  • ss928v100模型的导出、量化和转换
  • 中科院1区|IF6.7:基于PCA/OPLS-DA和KEGG通路分析的多组学整合,揭示沙棘-水飞蓟复方改善高脂血症的分子基础
  • C语言:指针进阶(下)
  • OpenAI推出专业级大模型o3-pro:为高精度任务而生
  • 【技术追踪】纵向 MRI 生成和弥漫性胶质瘤生长预测的治疗感知扩散概率模型(TMI-2025)
  • 商标注册小类怎么选?业务+战略双维度匹配
  • 离线部署openstack 2024.1 nova
  • C++实现文本编辑功能
  • cocosCreator 2.4 使用 flavor 配置安卓多渠道
  • OpneLayers 创建地图卷帘
  • 系统设计基本功:流量与存储需求估算
  • 40 C 语言日期与时间函数详解:time、ctime、difftime、clock(含 UTC/本地时间转换)
  • PostGIS实现波段添加导入【ST_AddBand】
  • Linux相关问题整理
  • 如何利用智能助手提升工作效率:从入门到实践
  • C语言学习20250611
  • Docker容器技术介绍,应用场景,安装应用以及项目部署
  • AUTOSAR图解==>AUTOSAR_TR_ModelingShowCases
  • D. Plus Minus Permutation
  • day28/60