【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();
}
}
}
}
智能推薦
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模型來改善這一點。 所謂多人聊天室,就是某個用戶發送的消息,其他...
猜你喜歡
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_...