通信網絡編程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.pswp.cn/pingmian/84540.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/84540.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/84540.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

(轉)什么是DockerCompose?它有什么作用?

一、什么是DockerCompose? DockerCompose可以基于Compose文件幫我們快速的部署分布式應用&#xff0c;而無需手動一個個創建和運行容器。 Compose文件是一個文本文件&#xff0c;通過指令定義集群中的每個容器如何運行。 DockerCompose就是把DockerFile轉換成指令去運行。 …

Python打卡第51天

浙大疏錦行 作業&#xff1a; day43的時候我們安排大家對自己找的數據集用簡單cnn訓練&#xff0c;現在可以嘗試下借助這幾天的知識來實現精度的進一步提高 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from tor…

Notepad++ 官方下載

https://notepad-plus-plus.org/downloads/ 下載官網 1、https://github.com/notepad-plus-plus/notepad-plus-plus/releases 2、https://notepad-plus-plus.org/news/v881-we-are-with-ukraine/

運維之十個問題--2

目錄 1. 如果有ip惡意刷流量怎么辦 2. 標準端口范圍 3.內存16G&#xff0c;交換分區多大 4.請簡述非對稱加密算法&#xff0c;ping命令通過什么協議實現&#xff0c;icmp是什么協議 5.客戶訪問網站速度慢原因 6. 進程和線程的區別 7.zabbix監控是你搭建的嗎&#xff0c;平…

vue前端面試題——記錄一次面試當中遇到的題(1)

1.v-if和v-show的區別 v-if和v-show都是Vue中用于條件渲染的指令&#xff0c;但它們的實現機制和適用場景有所不同&#xff1a; v-if是真正的條件渲染&#xff0c;在條件切換時會銷毀和重建DOM元素&#xff0c;適合運行時條件變化不頻繁的場景&#xff1b; v-show只是通過CS…

【QT面試題】(三)

文章目錄 Qt信號槽的優點及缺點Qt中的文件流和數據流區別&#xff1f;Qt中show和exec區別QT多線程使用的方法 (4種)QString與基本數據類型如何轉換&#xff1f;QT保證多線程安全事件與信號的區別connect函數的連接方式&#xff1f;信號與槽的多種用法Qt的事件過濾器有哪些同步和…

Vscode下Go語言環境配置

前言 本文介紹了vscode下Go語言開發環境的快速配置&#xff0c;為新手小白快速上手Go語言提供幫助。 1.下載官方Vscode 這步比較基礎&#xff0c;已經安裝好的同學可以直接快進到第二步 官方安裝包地址&#xff1a;https://code.visualstudio.com/ 雙擊一直點擊下一步即可,記…

HTML 文本省略號

目錄 HTML 文本省略號超行省略號如何實現1. 單行文本溢出顯示省略號2. 多行文本溢出顯示省略號方法一&#xff1a;使用 -webkit-line-clamp&#xff08;推薦&#xff09;方法二&#xff1a;使用偽元素&#xff08;兼容性好&#xff09;方法三&#xff1a;使用 JavaScript 動態監…

Spring Boot 實現流式響應(兼容 2.7.x)

在實際開發中&#xff0c;我們可能會遇到一些流式數據處理的場景&#xff0c;比如接收來自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 內容&#xff0c;并將其原樣中轉給前端頁面或客戶端。這種情況下&#xff0c;傳統的 RestTemplate 緩存機制會…

ffmpeg 新版本轉碼設置幀率上限

ffmpeg 新版本轉碼設置幀率上限 ffmpeg 在老版本比如 4.3的時候&#xff0c;轉碼設置幀率上限是通過vsync控制 # 設置動態控制最大幀率60 "-vsync 2 -r 60" 新版本這個參數沒辦法動態判斷控制幀率了 替換為使用filter中的fps進行設置 # 設置動態幀率最大60幀 -…

Qt繪制電池圖標源碼分享

一、效果展示 二、源碼分享 cell.h #ifndef CELL_WIDGET_H #define CELL_WIDGET_H #include <QWidget> #include <QPainter> #include <QPaintEngine> #include <QPaintEvent>/* 電池控件類 */ class CellWidget : public QWidget {Q_OBJECTQ_PROPERTY…

安卓基礎(生成APK)

??生成調試版&#xff08;Debug&#xff09;?? Build → Build Bundle(s)/APK(s) → Build APK輸出路徑&#xff1a;app/build/outputs/apk/debug/app-debug.apk ??生成發布版&#xff08;Release&#xff09;?? Build → Generate Signed Bundle/APK → 選擇 ??APK?…

如何在 TypeScript 中使用類型保護

前言 類型保護是一種 TypeScript 技術&#xff0c;用于獲取變量類型的信息&#xff0c;通常用于條件塊中。類型保護是返回布爾值的常規函數??&#xff0c;它接受一個類型并告知 TypeScript 是否可以將其縮小到更具體的值。類型保護具有獨特的屬性&#xff0c;可以根據返回的…

山東大學軟件學院項目實訓-基于大模型的模擬面試系統-面試對話標題自動總結

面試對話標題自動總結 主要實現思路&#xff1a;每當AI回復用戶之后&#xff0c;調用方法查看當前對話是否大于三條&#xff0c;如果大于則將用戶的兩條和AI回復的一條對話傳給DeepSeek讓其進行總結&#xff08;后端&#xff09;&#xff0c;總結后調用updateChatTopic進行更新…

Spring Cloud與Alibaba微服務架構全解析

Spring Cloud與Spring Cloud Alibaba微服務架構解析 1. Spring Boot概念 Spring Boot并不是新技術&#xff0c;而是基于Spring框架下“約定優于配置”理念的產物。它幫助開發者更容易、更快速地創建獨立運行和產品級別的基于Spring框架的應用。Spring Boot中并沒有引入新技術…

AI 賦能 Java 開發:從通宵達旦到高效交付的蛻變之路

作為一名深耕 Java 開發領域多年的從業者&#xff0c;相信很多同行都與我有過相似的經歷&#xff1a;在 “996” 甚至 “007” 的高壓模式下&#xff0c;被反復修改的需求、復雜的架構設計、無休止的代碼編寫&#xff0c;以及部署時層出不窮的問題折磨得疲憊不堪。長期以來&…

06. C#入門系列【自定義類型】:從青銅到王者的進階之路

C#入門系列【自定義類型】&#xff1a;從青銅到王者的進階之路 一、引言&#xff1a;為什么需要自定義類型&#xff1f; 在C#的世界里&#xff0c;系統自帶的類型&#xff08;如int、string、bool&#xff09;就像是基礎武器&#xff0c;能解決一些簡單問題。但當你面對復雜的…

使用 PyTorch 和 TensorBoard 實時可視化模型訓練

在這個教程中&#xff0c;我們將使用 PyTorch 訓練一個簡單的多層感知機&#xff08;MLP&#xff09;模型來解決 MNIST 手寫數字分類問題&#xff0c;并且使用 TensorBoard 來可視化訓練過程中的不同信息&#xff0c;如損失、準確度、圖像、參數分布和學習率變化。 步驟 1&…

第十五章 15.OSPF(CCNA)

第十五章 15.OSPF(CCNA) 介紹了大家都能用的OSPF動態路由協議 注釋&#xff1a; 學習資源是B站的CCNA by Sean_Ning CCNA 最新CCNA 200-301 視頻教程(含免費實驗環境&#xff09; PS&#xff1a;喜歡的可以去買下他的課程&#xff0c;不貴&#xff0c;講的很細 To be cont…

手機連接windows遇到的問題及解決方法

文章目錄 寫在前面一、手機與windows 連接以后 無法在win端打開手機屏幕,提示801方法零、檢查連接方法一、系統修復方法二、斷開重連方法三、軟件更新方法四、關閉防火墻 寫在前面 本文主要記錄所遇到的問題以及解決方案&#xff0c;以備后用。 所用機型&#xff1a;win11 專業…