上班聊天,摸鱼神器,手写一款即时通讯工具(附源码!!!)
文章目錄
- 即時通訊工具
- 客戶端
- 服務端
- 1、鏈接
- 2、登錄
- 3、其他方法
- 3.1、讀取客戶端的消息
- 3.2、給客戶端發送消息
- 3.3、日志記錄
- 3.4、工具集合
- 3.5、ChatSocket
- 服務端部署
- 優點與缺點
- 最后
認真工作不叫做賺錢,那叫做用勞動換取報酬,上班摸魚才是真的賺錢。
即時通訊工具
如果上班有空閑時間,最喜歡做的事情自然是和熟悉的朋友一起聊聊天,互相吐槽工作中遇到的人和事,緩解工作的壓力。
如果直接在桌面上打開 QQ 或者微信,那目標無疑是巨大的,QQ 和微信的桌面客戶端明晃晃地占據整個電腦桌面,只要有同事或者領導從你身邊經過,或是在你后面看一眼,就立刻能夠知道你在上班摸魚,那場面不亞于公開處刑…… (@_@)
領導:看來工作還是不飽和啊 ┑( ̄Д  ̄)┍
針對這種情況,技術人自然不甘落后,總是可以想出各種方法躲避同事和領導發現你在上班摸魚 ≡ω≡
思量再三,最終還是放棄了 IDEA 的各種插件,轉而決定還是自己手寫一款簡易的即時通信工具。
既然要自己動手,那自然也要先對這款即時通訊工具做個簡單的規劃。
- 這款即時通訊工具分為客戶端和服務端的,每個用戶可以使用客戶端進行即時通訊。
- 通訊工具盡可能簡單,只依賴于 JDK,即完全使用 Java 網絡編程功能實現,不依賴其他的第三方庫。
- 通訊工具不需要桌面,使用 Java 自帶的 Scanner 控制臺輸入即可。
這樣一款基于 Java 網絡編程的即時通訊工具,只要在 IDEA 運行客戶端代碼,即可在控制臺與其他朋友快樂地聊天。只要不是同事或者領導貼著你的電腦屏幕觀看,他絕對想不到你是在使用 IDEA 上班摸魚聊天。
客戶端
客戶端是給使用這款即時通訊工具的用戶使用的,從安全和用戶體驗的角度上來說,客戶端應該盡可能精簡,只負責發送和接受數據即可。
因為這是一款即時通訊工具,客戶端需要做的有兩件事:
- 監聽客戶端的輸入和發送。
- 監聽服務端發送過來的消息。
因為我們使用 JDK 自帶的 Scanner 類來進行客戶端的輸入,而這個輸入是一個阻塞的操作,所以我們需要創建一條額外的線程來進行服務端的監聽工作。
客戶端需要兩條用戶線程:
- main 線程用來監聽客戶端的輸入和發送。
- 另外創建一條線程用來監聽服務端的消息發送。
思路已經設計好了,可以使用代碼來實現了:
/*** 聊天室客戶端** @author herenpeng* @since 2021-07-09 12:00:00*/ public class ChatClient {public static void main(String[] args) {try (Socket socket = new Socket("127.0.0.1", 12345)) {// 讀取服務端發的消息new Thread(() -> readMsg(socket)).start();OutputStream os = socket.getOutputStream();Scanner scanner = new Scanner(System.in);System.out.println("請輸入您的聊天室昵稱:");while (true) {String chat = scanner.next();System.out.println("---------------------------");os.write(chat.getBytes());}} catch (Exception e) {System.out.println("【系統消息】聊天室炸了,BUG之神降臨了");e.printStackTrace();System.exit(0);}}private static void readMsg(Socket socket) {try {while (true) {InputStream is = socket.getInputStream();byte[] bytes = new byte[1024];int len = is.read(bytes);System.out.println(new String(bytes, 0, len));}} catch (Exception e) {System.out.println("【系統消息】你已退出聊天室,開始認真工作吧");System.exit(0);}}}服務端
服務端的設計比客戶端要困難很多,為了便于開發和理解,我直接使用了 Java 阻塞式的網絡 IO 來進行實現,即每一個客戶端連接都創建一個線程來進行處理。
這種阻塞式的網絡 IO 的好處在于便于理解和開發,而缺點也非常明顯,因為這是一個通訊工具,即每一個鏈接都是長鏈接。
即每個客戶端用戶鏈接服務端后,都會在服務端專門有一個線程處理這個客戶端相關的網絡 IO 操作。如果用戶量少的情況下還比較好,但是用戶一旦多了起來,服務端將會創建 N 多個線程,而且在客戶端不主動斷開的情況下,服務器這些線程會一直占用服務器資源,服務器將會消費非常大的資源,而且很容易崩潰。
基于這種情況,我后面也實現了一個 Java NIO 版本的客戶端和服務端,在文章末尾也會一起附上源碼。
我將服務端的操作分為兩個步驟:
1、鏈接
服務端阻塞地等待客戶端的鏈接請求。
/*** 聊天室服務端** @author herenpeng* @since 2021-07-09 12:00:00*/ public class ChatServer {/*** 啟動類** @param args 啟動參數* @throws IOException 拋出IO異常*/public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(12345);new Thread(() -> start(server)).start();// 加載配置CHAT_CFG_RELOAD_PASSWORD = UUID.randomUUID().toString();logInfo("【系統消息】聊天室配置加載密鑰:" + CHAT_CFG_RELOAD_PASSWORD);reloadChatCfg(args.length == 1 ? args[0] : null, null);logInfo("【系統消息】聊天室啟動成功了!");}/*** 服務開始方法** @param server 服務對象*/private static void start(ServerSocket server) {try {while (true) {// 鏈接操作ChatSocket chatSocket = connection(server);// 登錄操作login(chatSocket);}} catch (Exception e) {logInfo("【系統消息】聊天室發生了異?!?#34;);e.printStackTrace();} finally {logInfo("【系統消息】正在關閉聊天室資源……");close(server);}}/*** 鏈接客戶端** @param server 服務對象* @return ChatSocket 對象* @throws IOException 拋出異常*/private static ChatSocket connection(ServerSocket server) throws IOException {Socket socket = server.accept();ChatSocket chatSocket = new ChatSocket(socket);userDB.add(chatSocket);sendMsgToUser(socket, "============================\n" +"1、本聊天室僅為娛樂,請勿在該聊天室內談論敏感內容,比如涉政,涉黃,賬號密碼等等!\n" +"2、聊天室內容明文傳輸,聊天信息泄露本聊天室概不負責!\n" +"3、本聊天室內容后臺不做任何存儲,聊天信息如果需要請自行保留!\n" +"4、最終解釋權歸本聊天室所有!\n" +"============================");return chatSocket;}/*** 保存所有用戶socket的集合*/private static final List<ChatSocket> userDB = new LinkedList<>();}2、登錄
服務端獲取到客戶端請求后,將 Socket 包裝為我們自定義的 ChatSocket,便于我們進行登錄操作。
/*** 用戶登錄方法** @param chatSocket ChatSocket 對象*/ private static void login(ChatSocket chatSocket) {// 給每個用戶一個線程處理new Thread(() -> {Socket socket = chatSocket.getSocket();String username = null;try {InputStream is = socket.getInputStream();byte[] bytes = new byte[1024];int len = readMsg(is, bytes);if (len == -1) {logout(chatSocket);return;}username = new String(bytes, 0, len);chatSocket.setUsername(username);// 刷新配置if (CHAT_CFG_RELOAD_PASSWORD.equals(username)) {reloadChatCfg(is, bytes, socket);return;}loginTip(username, socket);// 機器人歡迎robotWelcome(username);while (true) {len = readMsg(is, bytes);if (len == -1) {logout(chatSocket);return;}String msg = new String(bytes, 0, len);sendMsgToOtherUser(username, socket, msg);// 機器人回復消息randomRobotReply(msg);}} catch (IOException e) {try {logout(chatSocket);} catch (Exception ex) {remove(socket);ex.printStackTrace();}e.printStackTrace();}}).start(); }3、其他方法
3.1、讀取客戶端的消息
/*** 讀取消息的方法** @param is 輸入流* @param bytes 字節數組* @return 讀取的長度*/ private static int readMsg(InputStream is, byte[] bytes) {int len;try {len = is.read(bytes);} catch (Exception e) {return -1;}return len; }3.2、給客戶端發送消息
/*** 給所有的用戶發送系統消息** @param msg 系統消息* @throws IOException 拋出異常*/ private static void sendSysMsg(String msg) throws IOException {for (ChatSocket chatSocket : userDB) {String sysMsg = getCurrentTime() + "\n" + msg + "\n" + chatSeparate;Socket socket = chatSocket.getSocket();sendMsgToUser(socket, sysMsg);} }/*** 發送消息給其他用戶** @param username 消息發送用戶名稱* @param self 消息發送的用戶socket* @param msg 消息* @throws IOException 拋出異常*/ private static void sendMsgToOtherUser(String username, Socket self, String msg) throws IOException {for (ChatSocket chatSocket : userDB) {Socket socket = chatSocket.getSocket();if (socket.equals(self)) {continue;}String sendMsg = "(" + username + ") " + getCurrentTime() + "\n" + msg + "\n" + chatSeparate;sendMsgToUser(socket, sendMsg);} }/*** 給指定的用戶發送消息,文本消息** @param socket 消息發送的用戶socket* @param sendMsg 消息* @throws IOException 拋出異常*/ private static void sendMsgToUser(Socket socket, String sendMsg) throws IOException {OutputStream os = socket.getOutputStream();os.write(sendMsg.getBytes()); }3.3、日志記錄
雖然服務端不需要記錄用戶的聊天信息,但是還是需要記錄一些服務器的日志信息。
/*** 打印日志** @param message 日志信息*/ private static void logInfo(String message) {System.out.println(getCurrentDateTime() + " " + message); }3.4、工具集合
/*** 獲取當前在線的所有玩家名稱** @return 當前在線的所有玩家名稱*/ private static List<String> getLoginUsernames() {return userDB.stream().map(ChatSocket::getUsername).filter(Objects::nonNull).collect(Collectors.toList()); }/*** 判斷時間是否是 11:00 - 04:59 晚上** @return 是返回true,否則返回false*/ private static boolean isNight() {Calendar calendar = Calendar.getInstance();int hour = calendar.get(Calendar.HOUR_OF_DAY);return hour >= 23 || hour <= 4; }/*** 時間格式化對象*/ private static final SimpleDateFormat timeSdf = new SimpleDateFormat("HH:mm:ss"); private static final SimpleDateFormat DateTimeSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");/*** 獲取當前的時間的格式化字符串** @return 當前的時間的格式化字符串*/ private static synchronized String getCurrentTime() {return timeSdf.format(new Date()); }/*** 獲取當前的日期時間的格式化字符串** @return 當前的日期時間的格式化字符串*/ private static synchronized String getCurrentDateTime() {return DateTimeSdf.format(new Date()); }/*** 判斷一個字符串是否為空** @param string 字符串* @return 為空返回true,否則返回false*/ private static boolean isEmpty(String string) {return string == null || string.length() == 0; }/*** 判斷一個字符串是否不為空** @param string 字符串* @return 不為空返回true,否則返回false*/ private static boolean isNotEmpty(String string) {return !isEmpty(string); }3.5、ChatSocket
/*** 封裝的 ChatSocket*/ private static class ChatSocket {private final Socket socket;private String username;public ChatSocket(Socket socket) {this.socket = socket;}public Socket getSocket() {return socket;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;} }服務端部署
為了方便在服務器上運行服務端代碼,我還特意寫了一個 Shell 腳本用來處理服務端代碼的運行、停止、重啟、查找等操作。
CHAT_SERVER_DIR=/usr/app/chat CHAT_SERVER=ChatServer CHAT_LOG_FILE=${CHAT_SERVER_DIR}/chat.log # 聊天室啟動參數 CHAT_CFG=${2}help() {echo "=================="echo "start 啟動服務"echo "stop 停止服務"echo "restart 重啟服務"echo "find 查找服務"echo "help 幫助"echo "==================" }start() {javac -encoding UTF-8 ${CHAT_SERVER}\.javanohup java -Dfile.encoding=UTF-8 ${CHAT_SERVER} ${CHAT_CFG} >>${CHAT_LOG_FILE} 2>&1 &echo "服務${CHAT_SERVER}已啟動" }stop() {PID=$(ps -ef | grep java | grep ChatServer | awk '{print $2}')if [ "${PID}" == "" ]thenecho "服務${CHAT_SERVER}已停止"elsekill ${PID}echo "服務${CHAT_SERVER}已停止"fi }restart() {stopsleep 3startecho "服務${CHAT_SERVER}已重啟" }find() {PID=$(ps -ef | grep java | grep ChatServer | awk '{print $2}')if [ "${PID}" == "" ]thenecho "服務${CHAT_SERVER}已停止"elseecho "服務${CHAT_SERVER}正在運行:PID=${PID}"fi }case ${1} in"")echo "=== 參數錯誤 ===";;start)start;;stop)stop;;restart)restart;;find)find;;*)help;; esacexit 0優點與缺點
優點
- 簡單便捷,無論是客戶端還是服務端,都只依賴了 JDK 的環境,沒有任何第三方依賴,客戶端的代碼只在復制到有 JDK 環境的電腦上即可運行,方便快捷。
- 足夠隱蔽,客戶端在 CMD 或者 IDEA 環境下都可以運行,這樣你身邊的同事只要不仔細觀察你的電腦屏幕,絕對想不到你是在和朋友聊天,只以為你是在認真工作。
缺點
- 服務端基于阻塞式網絡 IO 開發,服務端只能夠承受有限個的客戶端鏈接。(Java NIO 版本可以解決這個缺點)
- 太過簡陋,因為只是單純地進行網絡 IO 的寫入和讀取,所以對于一些復雜的網絡環境問題都沒有進行處理,比如網絡黏包的問題,在客戶端連接較多的情況下,可能會發生網絡黏包的問題,導致一些消息粘黏在一起,發送給客戶端。
- 需要一個服務器,因為這款即使通訊工具是 CS 模式,需要一個服務器運行服務端代碼。
最后
雖然這款即時通訊工具確實還不夠完善,但是如果只是用于幾個朋友之間簡單地進行聊天,這款即時通訊工具還是非常給力的。
這款即時通訊工具的源代碼已經被托管到了 GitHub 上,同時還附帶了這款即時通訊工具的 Java NIO 版本,有興趣的同學的可以訪問我的 GitHub 下載源碼。
GitHub:https://github.com/herenpeng/chat.git
同時,我還在 Gitee 上提供了倉庫鏡像。
Gitee:https://gitee.com/herenpeng/chat.git
如果你喜歡這款即時通訊工具,希望各位同學可以給我的 GitHub 或者 Gitee 倉庫點一個 Star,非常感謝!
總結
以上是生活随笔為你收集整理的上班聊天,摸鱼神器,手写一款即时通讯工具(附源码!!!)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用opencv从USB摄像头获取图片
- 下一篇: 怎样才算是好程序员?关于好程序员与好代码