【Java】基於TCP協議多線程服務器-客戶端交互控制檯聊天室簡例

      前兩天想到一個手機APP項目,使用到藍牙,發現BluetoothSocket和J2EE網絡變成的Socket差不多,使用之餘順手寫一個多線程服務器與客戶端交互實現聊天室的一個小例子,方便新人學習網絡編程模塊,期間使用到多線程和IO輸入輸出流的操作,有點兒不明白的過後我會有一些個人使用心得總結,敬請期待哈!

      源碼內容十分簡單,我工程文件我存在下面的地址上去了,方便大家下載,0積分,爲了方便大家學習了。

下載地址請移步

      Server.Java文件,主要是服務端管理,通過多線程接收各用戶發送消息以及消息轉送等處理。

package cn.com.dnyy.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Server {

    // 用以存放客戶端Socket的管理Map集合
    private Map<String, ClientManager> ClientManagers = null;

    public static void main(String[] args) {
        new Server().startServer();// 啓動服務端
    }

    // 開啓服務器方法
    private void startServer(){
        ServerSocket ss = null;// 服務器Socket
        Socket s = null;// 與客戶端交互的Socket
        try {
            ss = new ServerSocket(8888);// 創建服務器,端口爲8888
            ClientManagers = new HashMap<String, Server.ClientManager>();// 創建管理集合
            System.out.println("創建服務器成功!");
            while(true){
                s = ss.accept();// 接收客戶端請求Socket管道
                ClientManager cm = new ClientManager(s);// 新建線程Runnable
                new Thread(cm).start();// 構建新線程管理該客戶端Socket
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(ss != null) ss.close();// 關閉服務器
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 管理客戶端Socket線程內部類
    private class ClientManager implements Runnable {

        private Socket clientSocket;// 客戶端Socket
        private BufferedReader br;// 基於客戶端Socket的輸入流(從客戶端輸入服務端)
        private PrintWriter pw;// 基於客戶端Socket的輸出流(服務端輸出到客戶端)
        private boolean flag;// 線程可用標識
        private String physicalAddress;// 客戶端名
        private String userName;// 用戶名

        // 構造方法
        public ClientManager(Socket s) throws IOException{
            this.clientSocket = s;// 獲取客戶端Socket
            br = new BufferedReader(new InputStreamReader(s.getInputStream()));// 基於客戶端Socket創建輸入流
            pw = new PrintWriter(s.getOutputStream(), true);// 基於客戶端Socket創建輸出流
            physicalAddress = s.getInetAddress().getHostAddress()+":"+s.getPort();// 獲取客戶端Socket物理地址:端口
            userName = br.readLine();// 獲取用戶名
            flag = true;// 標識可用
            System.out.println(userName+"["+physicalAddress+"]"+"已經成功連接到服務器!");// 客戶端連接服務器成功消息發送給所有在線用戶
            sendMessageForAll("已經成功連接到服務器!");// 客戶端連接服務器成功消息發送給所有在線用戶
            ClientManagers.put(userName, this);// 將自身加入全局客戶端線程管理中
            sendOnlineList(false);// 發送當前所有在線用戶的列表
        }

        /* 
         * 發送當前所有在線用戶的列表
         * 參數IsOnly-->true:發送給當前用戶;false:發送給所有在線用戶;
         */
        private void sendOnlineList(boolean IsOnly) {
            if(IsOnly){
                StringBuilder sb = new StringBuilder(KeyWords.ONLINE_CLIENTS_LIST + "所有人");// 在線用戶列表前綴
                for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍歷在線用戶Socket集合
                    if(userName.equals(item.getKey())) continue;// 不加入自身
                    sb.append("," + item.getKey());// 添加其它在線用戶
                }
                pw.println(sb);// 發送列表給當前用戶
            }else{
                for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍歷在線用戶Socket集合
                    StringBuilder sb = new StringBuilder(KeyWords.ONLINE_CLIENTS_LIST);// 在線用戶列表前綴
                    sb.append(mapKeyToStringBesidesItem(ClientManagers, item.getKey()));// 題頭基礎上加上各項
                    item.getValue().pw.println(sb);// 發送列表給所有用戶
                }
            }
        }

        // 輸入Map<String, ClientManager>後輸出除了指定字符串外其餘的用","連接的字符串
        public String mapKeyToStringBesidesItem(Map<String, ClientManager> stringMap, String besides){
            if (stringMap==null) {// 如果輸入的Map集合爲空,則不進行操作
                return null;
            }

            StringBuilder result = new StringBuilder();// 結果字符串Builder
            boolean flag = false;// 是否使用逗號拼接標記
            for (Map.Entry<String, ClientManager> item : stringMap.entrySet()) {// 遍歷Map集合
                if(item.getKey().equals(besides)) continue;// 如果存在排除的字符串則不進行添加操作
                if (flag) {// 使用逗號進行拼接(不爲第一項)
                    result.append(",");
                }else {// 不適用逗號拼接(第一項標記)
                    flag=true;
                }
                result.append(item.getKey());// 拼接字符串
            }
            return result.toString();// 輸出字符串
        }

        // 發送消息給所有在線用戶
        private void sendMessageForAll(String msg) {
            for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍歷在線用戶Socket集合
                if(userName.equals(item.getKey())) continue;// 不發送給自身
                item.getValue().pw.println(userName+"["+physicalAddress+"]:"+msg);// 加上消息頭髮送
            }
        }

        // 發送消息給指定用戶
        private void sendMessageForSomeOne(List<String> userList, String msg){
            for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍歷在線用戶Socket集合
                if(userList.contains(item.getKey())) item.getValue().pw.println(userName+"["+physicalAddress+"]:"+msg);// 加上消息頭髮送
            }
        }

        // TODO 接收信息方法
        private void receiveMessage() throws IOException{
            String str = null;// 臨時接收信息變量
            while ((str = br.readLine()) != null){// 循環接收客戶端信息
                if(str.equals(KeyWords.CLIENT_CLOSE_POST)){// 客戶端請求斷開連接
                    stopThread();// 停止線程
                    pw.println(KeyWords.CLIENT_CLOSE_POST_RETURN);// 返回客戶端斷開確認
                    break;// 結束循環
                } else if(str.startsWith(KeyWords.SEND_TO_TARGET_START)){// 接收消息對象列表前綴

                    // 分割出發送對象列表
                    String[] tempSendTo = str.substring(KeyWords.SEND_TO_TARGET_START.length(), str.indexOf(KeyWords.SEND_TO_TARGET_END)).split(",");
                    List<String> sendTo = Arrays.asList(tempSendTo);

                    // 獲取發送的消息
                    String msg = str.substring(str.indexOf(KeyWords.SEND_TO_TARGET_END) + KeyWords.SEND_TO_TARGET_END.length());

                    if(Integer.valueOf(1).equals(tempSendTo.length)){// 如果發送對象只有一個
                        if("所有人".equals(tempSendTo[0])){// 發送給所有人
                            sendMessageForAll(msg);
                        }else{// 真的只有一個發送目標
                            sendMessageForSomeOne(sendTo, msg);
                        }
                    }else{// 發送目標不止一個
                        sendMessageForSomeOne(sendTo, msg);
                    }
                } else if(str.startsWith(KeyWords.GET_ONLINE_CLIENTS_LIST)){// 請求獲取在線客戶端列表前綴
                    sendOnlineList(true);
                }
                System.out.println("收到"+userName+"["+physicalAddress+"]"+"發來的消息:"+str);// 客戶端消息傳遞日誌
            }
            System.out.println(userName+"["+physicalAddress+"]"+"已經斷開與服務器的連接!");// 客戶端斷開連接日誌
            stopThread();// 斷開了連接則需要將此線程移除
        }

        // 停止該進程的方法
        private void stopThread(){
            flag = false;// 標識爲空
        }

        @Override
        public void run() {
            try {
                while (true) {
                    if(!flag) break;// 如果標識爲空則結束循環
                    receiveMessage();// 調用接收消息方法
                }
            } catch (SocketException e){
                stopThread();// 停止線程
                System.out.println(userName+"["+physicalAddress+"]"+"通過非常規方式斷開了與服務器的連接!");// 客戶端強制關閉日誌
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    sendMessageForAll("已經斷開與服務器的連接!");// 客戶端斷開服務器的連接消息發送給所有在線用戶
                    if(clientSocket != null) clientSocket.close();// 關閉客戶端連接
                    ClientManagers.remove(userName);// 將線程自身從全局客戶端線程管理中移除
                    sendOnlineList(false);// 發送當前所有在線用戶的列表
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

      Client.Java爲客戶端部分,輸入與接收分線程實現,從而輸入之餘能夠顯示聊天信息交互,以及一些特殊操作關鍵詞過濾等功能。

package cn.com.dnyy.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

public class Client {

    private Socket linkServerSocket;// 與服務器通信的Socket管道
    private BufferedReader br;// 接收服務端輸入
    private PrintWriter pw;// 對服務端輸出
    private BufferedReader ubr = null;// 接收用戶控制檯輸入
    private String userName = null;// 用戶名
    private boolean flag = true;// 接收線程可用標識
    private List<String> onlineUserName;// 在線用戶列表
    private List<String> sendToList;// 發送的目標用戶列表

    public static void main(String[] args) {
        new Client().startUp();// 啓動客戶端
    }

    // 啓動客戶端
    private void startUp(){
        try {
            linkServerSocket = new Socket("127.0.0.1", 8888);// 連接服務器IP和端口
            ubr = new BufferedReader(new InputStreamReader(System.in));// 通過InputStreamReader轉換流從字節輸入流InputStream轉換爲BufferedReader
            br = new BufferedReader(new InputStreamReader(linkServerSocket.getInputStream()));// 通過服務器Socket創建輸入流
            pw = new PrintWriter(linkServerSocket.getOutputStream(), true);// 通過服務器Socket創建輸出流
            onlineUserName = new ArrayList<String>();// 構建新用戶列表
            sendToList = new ArrayList<String>();// 構建新的發送目標用戶列表
            System.out.println("連接服務器通信正常!");// 提示客戶端連接正常
            System.out.println("=========請輸入您的用戶名:=========");// 提示用戶輸入用戶名信息
            userName = ubr.readLine();// 獲取用戶輸入的用戶名
            pw.println(userName);// 發送用戶名給服務器
            System.out.println("=========我們歡迎您的到來!=========");
            System.out.println("輸入\"-M\"顯示菜單功能提示");
            System.out.println("輸入消息並按回車鍵進行消息發送");
            System.out.println("=========預祝您使用愉快!!=========");

            new Thread(new ReceiveMessage()).start();// 啓動接收線程
            String str = null;// 記錄讀取到的用戶輸入的字符內容
            while ((str = ubr.readLine()) != null) {// TODO 循環讀取用戶輸入數據
                if(!flag) break;
                if("-M".equals(str)){// 顯示菜單
                    showMainMenu();
                }else if("-U".equals(str)){// 顯示用戶列表
                    showUserList();
                }else if("-Q".equals(str)){// 斷開連接
                    pw.println(KeyWords.CLIENT_CLOSE_POST);
                }else if("-A".equals(str)){// 設置消息接收人爲所有人
                    sendToList.clear();// 清空消息接收人
                    sendToList.add("所有人");// 設置消息接收人爲所有人
                }else if("-I".equals(str)){// 查看當前接收人列表
                    showReceiverList();
                }else if(str.startsWith("-S,")){// 添加接收人
                    String tempAddName = str.substring(str.indexOf(",") + 1);// 獲取要添加的用戶名
                    if(onlineUserName.contains(tempAddName)) {// 如果存在該用戶
                        sendToList.add(tempAddName);// 添加到接收名單中
                        System.out.println("接收人[" + tempAddName + "]添加成功!");
                        if(sendToList.contains("所有人")) sendToList.remove("所有人");// 如果存在“所有人”選項,則去掉
                    }
                }else{// 發送消息
                    StringBuilder sendTo = new StringBuilder(KeyWords.SEND_TO_TARGET_START);// 消息對象列表前綴
                    if(sendToList.size() < 1){// 如果發送目標列表小於1,即無發送列表
                        sendToList.add("所有人");// 羣聊
                    }
                    for (int i = 0; i < sendToList.size(); i++) {// 加入接收用戶列表
                        if(i < sendToList.size() - 1){
                            sendTo.append(sendToList.get(i)+",");
                        }else{
                            sendTo.append(sendToList.get(i));
                        }
                    }
                    sendTo.append(KeyWords.SEND_TO_TARGET_END+str);// 接收用戶列表後綴+內容
                    pw.println(sendTo);// 傳輸用戶寫入的數據到服務器
                }
            }
        } catch (SocketException e){
            flag = false;
            System.out.println("服務器正在維護中!請稍後重試登陸!");
            System.out.println("=========請按回車關閉程序!=========");
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(linkServerSocket != null) linkServerSocket.close();// 關閉服務端連接
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(ubr != null) ubr.close();// 關閉用戶輸入流
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 顯示主菜單
    private void showMainMenu() {
        System.out.println("======================================");
        System.out.println("=|[-U]用戶列表|[-I]接收人列表|[-Q]安全退出|=");
    }

    // 顯示用戶列表
    private void showUserList() {
        StringBuilder sb = new StringBuilder("==============用戶列表=============");// 分隔符
        sb.append("\n");// 換行
        for(int i = 0; i < onlineUserName.size(); i++){// 循環遍歷在想用戶列表
            sb.append(onlineUserName.get(i));// 輸出名字
            sb.append("\n");// 換行
        }
        sb.append("=================================");// 分隔符
        sb.append("輸入\"-A\"清空接收人並設置爲發送給所有人\n");
        sb.append("輸入\"-S,接收人暱稱\"添加消息接收人");
        System.out.println(sb);// 輸出列表到控制檯
    }

    // 查看當前接收人列表
    private void showReceiverList() {
        StringBuilder sb = new StringBuilder("==============接收列表=============");// 分隔符
        sb.append("\n");// 換行
        for(int i = 0; i < sendToList.size(); i++){// 循環遍歷在想用戶列表
            sb.append(sendToList.get(i));// 輸出名字
            sb.append("\n");// 換行
        }
        sb.append("=================================");// 分隔符
        sb.append("輸入\"-A\"清空接收人並設置爲發送給所有人\n");
        sb.append("輸入\"-S,接收人暱稱\"添加消息接收人");
        System.out.println(sb);// 輸出列表到控制檯
    }

    // TODO 接收消息
    private void receive() {
        try {
            String str = br.readLine();// 讀取服務器消息
            if(str.equals(KeyWords.CLIENT_CLOSE_POST_RETURN)){// 允許關閉程序標識
                flag = false;// 如果收到可以關閉程序標識,則關閉程序
                System.out.println("=========請按回車關閉程序!=========");
                return;
            } else if(str.startsWith(KeyWords.ONLINE_CLIENTS_LIST)){// 在線用戶列表前綴標識
                onlineUserName.clear();// 同步在線用戶前清空之前存在的在線用戶列表
                String[] tempUserNameList = str.substring(KeyWords.ONLINE_CLIENTS_LIST.length()).split(",");// 將去掉前綴後的字符串進行分割
                for(String item : tempUserNameList){// 遍歷用戶列表
                    onlineUserName.add(item);// 將遍歷項添加到用戶列表中
                }
                for(int i = 0; i < sendToList.size(); i++){// 刪除發送目標中不存在的在線用戶
                    if(!onlineUserName.contains(sendToList.get(i))) sendToList.remove(i);
                }
                if(sendToList.size() < 1){// 如果發送目標列表小於1,即無發送列表
                    sendToList.add("所有人");// 羣聊
                }
            } else {// 獲取消息
                System.out.println(str);// 輸出消息
            }
        } catch (SocketException e){
            flag = false;
            System.out.println("服務器正在維護中!請稍後重試登陸!");
            System.out.println("=========請按回車關閉程序!=========");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 接收服務器信息線程
    private class ReceiveMessage implements Runnable {
        @Override
        public void run() {
            while(true){
                if(!flag) break;
                receive();
            }
        }
    }
}

      KeyWords.Java文件主要是服務器和客戶端交互的關鍵字記錄文件。

package cn.com.dnyy.tcp;

public class KeyWords {
    public final static String CLIENT_CLOSE_POST = "quit:";// 客戶端請求斷開連接
    public final static String CLIENT_CLOSE_POST_RETURN = "disconnect:";// 客戶端請求斷開連接返回值
    public final static String GET_ONLINE_CLIENTS_LIST = "getonline:";// 向服務端請求在線客戶端列表前綴
    public final static String ONLINE_CLIENTS_LIST = "onlinelist:";// 服務端發送在線客戶端列表前綴
    public final static String SEND_TO_TARGET_START = "sendto:";// 消息中接收目標列表前綴
    public final static String SEND_TO_TARGET_END = ":end";// 消息中接收目標列表後綴
}

      文件就這3個,內容也非常之簡單,註釋我也寫得十分的詳盡,如果大家有什麼疑問的話,敬請在下方留言,我會一一給大家答疑清楚的,喜歡的話請關注點贊,謝謝支持啦~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章