• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • 【Socket案例】原生java實現基于TCP的多人聊天室

    標簽: # Socket網絡編程進階與實戰  socket  聊天室  多線程  tcp

    效果圖

     

    功能

    從項目代碼結構來,代碼主要分為簡單的服務端和客戶端。當運行服務端后,可運行多個客戶端連接到服務端。某個客戶端發送消息,都會經由服務端轉發到除了自己的其他客戶端。

    代碼雖然不多,而且直接使用原生java手寫,但是卻很大程度接近聊天室的功能,這比網上很多關于Socket的入門案例都要有含金量。甚至你可以對應代碼打包成jar包。如下圖,并將server.jar放到云服務器上運行。那么其他人都可以通過cmd的方式運行client.jar來加入聊天室,并進行群聊天。

    體驗

    https://www.ysqorz.top/uploaded/file/client.jar

    想試一下效果的同學,可通過上述鏈接,下載client.jar。然后在jar包所在目錄,以管理員身份運行cmd。通過命令:java -jar client.jar 運行jar包。即可連接到服務器。如果有其他人也運行jar包,連接到服務器,那么就可以群聊了。當然,你可以運行多個cmd來模擬多人的情況。

    實現思路

    1、多個客戶端與服務端建立起TCP連接,等待連接的過程是阻塞狀態。因此需要一個獨立的線程Connector來專門處理客戶端的連接,并且該線程只負責這件事,也就是說這個線程一旦等到有客戶端連接,就建立其連接,之后就不管了。客戶端的消息收發右其他線程完成。

    2、對于每一個單獨的客戶端,它的消息的收發需要交由專門的類ClientHandler來完成。顧名思義,ClientHandler意為客戶端處理者。每一個ClientHandler專門負責一個客戶端的消息收發。因此有多少個建立連接的客戶端,服務端就持有多少個ClientHandler。因此服務端需要持有并維護一個ClientHandler列表。

    3、對于每個客戶端進行讀寫分離。這一點是非常有必要的。在read客戶端消息時,會處于阻塞狀態,如果不分離,就不能對客戶端進行寫操作了。網上很多案例都是,read的時候阻塞,然后讀到客戶端的消息后,再自動回送消息。要進行讀寫分離,客戶端的讀寫操作就必須由單獨的線程來負責。因此每一個ClientHandler里面都持有讀、寫兩個線程。

    上面分析了實現聊天室案例的難點和主體思路,更多坑點和細節,參照源碼!該案例涉及Socket的使用、多線程、Java面向對象的思想,對功能職責進行劃分。

    轉載請注明出處  

    完整代碼

    Package server

    package server;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.InetAddress;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import utils.CloseUtil;
    
    /**
     * 客戶端處理者
     * @author	passerbyYSQ
     * @date	2020-9-23 9:18:31
     */
    public class ClientHandler {
        private Socket socket;
        private ClientInfo clientInfo;
        
        // 對客戶端進行讀寫分離
        // 負責從客戶端讀的單獨線程
        private ReadHandler reader;
        // 負責向客戶端寫的單獨線程
        private WriteHandler writer;
        
        // 事件回調。當某個客戶端的事件觸發時,回調給TcpServer
        private ClientEventCallback callback;
    
        public ClientHandler(Socket socket, ClientEventCallback callback) throws IOException {
            this.socket = socket;
            this.callback = callback;
            clientInfo = new ClientInfo(socket.getLocalAddress(), socket.getPort());
            
            System.out.println("新客戶端鏈接:" + clientInfo);
            
            reader = new ReadHandler(socket.getInputStream());
            reader.start();
            writer = new WriteHandler(socket.getOutputStream());
        }
    
        public void sendMsg(String msg) {
            writer.send(msg);
        }
    
        public void exit() {
            reader.exit();
            writer.exit();
            CloseUtil.close(socket);
        }
    
        public ClientInfo getClientInfo() {
            return clientInfo;
        }
    
        interface ClientEventCallback {
            void onMsgReceived(ClientHandler handler, String msg);
    
            void onClientExit(ClientHandler handler);
        }
    
        class ClientInfo {
            InetAddress inetAddr;
            Integer port;
    
            ClientInfo(InetAddress inetAddr, Integer port) {
                this.inetAddr = inetAddr;
                this.port = port;
            }
    
            @Override
            public String toString() {
                return "client [ip=" + this.inetAddr.getHostAddress() + ", port=" + this.port + "]";
            }
        }
    
        class ReadHandler extends Thread {
            DataInputStream in;
            boolean running = true;
    
            ReadHandler(InputStream inputStream) {
                in = new DataInputStream(inputStream);
            }
    
            public void run() {
                while(running) {
                    try {
                        String msg = in.readUTF();
                        if ("bye".equalsIgnoreCase(msg)) {
                        	exit();
                        	callback.onClientExit(ClientHandler.this);
                            break;
                        }
                        
                        callback.onMsgReceived(ClientHandler.this, msg);
                        
                    } catch (IOException e) {
                        System.out.println(clientInfo.toString() + "關閉:" + 
                        		"異常-" + e.getCause() + ",信息-" + e.getMessage());
                        exit();
                    }
               }
            }
    
            void exit() {
                running = false;
                CloseUtil.close(in);
            }
        }
    
        /**
         * 負責向客戶端寫的單獨線程。
         * 不繼承Thread類,而是巧妙地采用一個單例線程池來發送消息
         */
        class WriteHandler {
            DataOutputStream out;
            ExecutorService executor;
    
            WriteHandler(OutputStream outputStream) {
                out = new DataOutputStream(outputStream);
                executor = Executors.newSingleThreadExecutor();
            }
    
            void send(final String msg) {
                executor.execute(new Runnable() {
                    public void run() {
                        try {
                            out.writeUTF(msg);
                            out.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
    
                    }
                });
            }
    
            void exit() {
                CloseUtil.close(out);
                executor.shutdownNow();
            }
        }
    }
    
    package server;
    
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.List;
    
    import server.ClientHandler.ClientEventCallback;
    import utils.CloseUtil;
    
    public class TcpServer implements ClientEventCallback {
        private static ServerSocket serverSocket;
        // 客戶端處理者的列表
        private List<ClientHandler> clientHandlerList = new ArrayList<>();
        private Connector connector;
    
        public void setup() throws IOException {
            serverSocket = new ServerSocket(8096);
            connector = new Connector();
            connector.start();
            
            System.out.println("服務器啟動成功,服務器信息:ip-" + 
            		serverSocket.getInetAddress().getHostAddress() + 
            		", port-" + serverSocket.getLocalPort());
        }
    
        public void exit() {
            exitAllClients();
            connector.exit();
            CloseUtil.close(serverSocket);
        }
    
        public void exitAllClients() {
            System.out.println("客戶端數量:" + clientHandlerList.size());
            for (ClientHandler handler : clientHandlerList) {
            	handler.exit();
            }
            clientHandlerList.clear();
        }
    
        /**
         * 消息到達時的轉發
         */
        @Override
        public void onMsgReceived(ClientHandler handler, String msg) {
            msg = handler.getClientInfo().toString() + ":" + msg;
            System.out.println(msg);
            broadcast(handler, msg);
        }
    
        /**
         * 客戶端退出時的回調
         */
        @Override
        public void onClientExit(ClientHandler handler) {
        	handler.exit();
            clientHandlerList.remove(handler);
            
        	String msg = handler.getClientInfo() + 
        			"已退出群聊。當前客戶端數量:" + clientHandlerList.size();
        	broadcast(handler, msg);
        	
            System.out.println(msg);
            
            
        }
    
        /**
         * 轉發消息
         * @param handler	消息所來自的客戶端
         * @param msg
         */
        private void broadcast(ClientHandler handler, String msg) {
        	for (ClientHandler clientHandler : clientHandlerList) {
        		// 轉發消息時跳過自己
        		if (clientHandler == handler) {
                    continue;
                }
        		clientHandler.sendMsg(msg);
            }
        }
    
        /**
         * 負責處理客戶端連接的單獨線程
         */
        class Connector extends Thread {
            boolean running = true;
    
            public Connector() {
                super("負責處理客戶端連接的單獨線程");
                this.setPriority(MAX_PRIORITY);
            }
    
            public void run() {
                while(running) {
                    try {
                        Socket socket = serverSocket.accept();
                        ClientHandler handler = new ClientHandler(socket, TcpServer.this);
                        clientHandlerList.add(handler);
                        
                        broadcast(handler, handler.getClientInfo() + 
                        		"加入群聊。當前客戶端數量:" + clientHandlerList.size());
                    } catch (IOException e) {
                        System.out.println("ServerSocket異常關閉:異常-" + 
                        		e.getCause() + ",信息-" + e.getMessage());
                        exit();
                    }
                }
    
            }
    
            void exit() {
                running = false;
            }
        }
    }
    
    package server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class Server {
    	
        public static void main(String[] args) throws IOException {
            TcpServer tcpServer = new TcpServer();
            tcpServer.setup();
            
            BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in));
            boolean flag = true;
    
            do {
                String command = bufReader.readLine();
                if (command == null) {
                    break;
                }
    
                switch (command.toLowerCase()) {
    	            case "exit clients": {
    	            	tcpServer.exitAllClients();
    	            	break;
    	            }
    	            case "exit": {
    	            	tcpServer.exit();
    	            	flag = false;
    	            	break;
    	            }
    	            default: {
    	            	System.out.println("Unsupport commond!");
    	            }
                }
            } while(flag);
    
            tcpServer.exit();
        }
    }
    

    Package client

    package client;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import utils.CloseUtil;
    
    public class TcpClient {
        public static final String SERVER_IP = "119.45.164.115";
        public static final int SERVER_PORT = 8096;
        private Socket socket = new Socket(SERVER_IP, SERVER_PORT);
        private ReadHandler reader;
        private WriteHandler writer;
    
        public TcpClient() throws IOException {
            System.out.println("連接服務器成功,服務器信息:ip-" + 
            		socket.getInetAddress().getHostAddress() + 
            		", port-" + socket.getPort());
            System.out.println("客戶端信息:ip-" + socket.getLocalAddress() + 
            		", port-" + socket.getLocalPort());
            
            reader = new TcpClient.ReadHandler(socket.getInputStream());
            reader.start();
            writer = new TcpClient.WriteHandler(socket.getOutputStream());
        }
    
        public void exit() {
            reader.exit();
            writer.exit();
            CloseUtil.close(socket);
        }
    
        public void send(String msg) {
            writer.send(msg);
        }
    
        class ReadHandler extends Thread {
            DataInputStream in;
            boolean running = true;
    
            ReadHandler(InputStream inputStream) {
                this.in = new DataInputStream(inputStream);
            }
    
            public void run() {
                while(running) {
                    try {
                        String msg = this.in.readUTF();
                        System.out.println(msg);
                         
                    } catch (IOException e) {
                        System.out.println("客戶端關閉:異常-" + e.getCause() + 
                        		",信息-" + e.getMessage());
                        exit();
                    }
                }
            }
    
            void exit() {
                running = false;
                CloseUtil.close(in);
            }
        }
    
        class WriteHandler {
            DataOutputStream out;
            ExecutorService executor;
    
            WriteHandler(OutputStream outputStream) {
                out = new DataOutputStream(outputStream);
                executor = Executors.newSingleThreadExecutor();
            }
    
            void send(final String msg) {
                executor.execute(new Runnable() {
                    public void run() {
                        try {
                            out.writeUTF(msg);
                            out.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
    
                    }
                });
            }
    
            void exit() {
                CloseUtil.close(out);
                executor.shutdownNow();
            }
        }
    }
    package client;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class Client {
    	
        public static void main(String[] args) throws IOException {
            TcpClient tcpClient = new TcpClient();
            
            BufferedReader bufReader = new BufferedReader(
            		new InputStreamReader(System.in));
            
            boolean isClosed = false;
    
            while(true) {
                String msg = bufReader.readLine();
                // socket異常斷開,可能造成msg為null,引發下面的空指針異常
                if (msg == null) {
                    break;
                }
                // 空字符串不發送
                if (msg.length() == 0) {
                	continue;
                }
                // 退出客戶端
                if ("exit".equalsIgnoreCase(msg)) {
            		break;
            	}
                
                // 連接未斷開時,才發送
                if (!isClosed) {
                	// 斷開連接
                    if ("bye".equalsIgnoreCase(msg)) {
                        System.out.println("已請求服務器斷開連接,輸入exit退出客戶端!");
                        isClosed = true;
                    } 
                	tcpClient.send(msg);
                }
            }
    
            tcpClient.exit();
        }
    }
    

    Package utils

    package utils;
    
    import java.io.Closeable;
    import java.io.IOException;
    
    /**
     * 關閉資源的工具類
     * @author	passerbyYSQ
     * @date	2020-9-23 8:55:33
     */
    public class CloseUtil {
    	
        public static void close(Closeable... closeableArr) {
        	if (closeableArr == null) {
        		return;
        	}
            for (Closeable closeable : closeableArr) {
            	try {
    				closeable.close();
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
            }
        }
    }
    

     

    版權聲明:本文為qq_43290318原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/qq_43290318/article/details/108760556

    智能推薦

    基于C++的TCP聊天室案例

        TCPServer.cpp:   TCPClient.cpp:  ...

    node案例:Node.js + express +socket實現在線實時多人聊天室

    文件結構如下: 前端部分: 登錄頁面Login部分: login.html login.css login.js index.html index.css index.js server.js 最后 終端輸入 瀏覽器地址欄輸入...

    Java網絡編程-Socket編程初涉二(基于BIO模型的簡易多人聊天室)

    Java網絡編程-Socket編程初涉二(基于BIO模型的簡易多人聊天室) 要求 我們要實現一個基于BIO模型的簡易多人聊天室,不能像上一個版本一樣(Java網絡編程-Socket編程初涉一(簡易客戶端-服務器)),當服務器與某個客戶端連接成功后,它們在進行數據交互過程中,其他客戶端的連接請求,服務器是不會響應的,我們這里要使用BIO模型來改善這一點。 所謂多人聊天室,就是某個用戶發送的消息,其他...

    java基于線程的socket的簡單聊天室案例

    服務端: 客戶端: 結果:...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    猜你喜歡

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    精品国产乱码久久久久久蜜桃不卡