網絡聊天室的分析與實現
前言
本文基於多線程實現網絡聊天室,採用一個服務器端、多個客戶端的軟件結構,實現多個客戶端之間的羣聊,以及私聊的功能。軟件項目中融入了觀察者模式、單例模式,使項目更加易於維護。
本文的主要目的是通過實現網絡聊天室,熟練掌握多線程、網絡、設計模式,面向對象的編程技能。
一、需求分析
網絡聊天室由一個服務器與多個客服端組成,客服端可以隨時加入,也能隨時退出,而不影響其他客戶端的正常運作。客服端無任何限制發送或接受的條件,達到及時發送及時接收的功能。服務器作爲唯一後臺運行程序,爲客戶端之間的互聊提供服務。
網絡聊天室主要有兩個功能:
1、客戶端之間實現羣聊。多個客戶端在線時,其中某個客戶端發送普通消息時,其他在線客戶端都可以接受該消息。
2、客戶端之間實現私聊;多個客戶端在線時,其中某個客戶端可以通過在消息末尾加“@用戶名”字串,只有對應客戶端能收到該消息。
二、程序設計
通過網絡聊天室的需求分析可知:
1、服務器端應採用單例模式,達到服務器作爲唯一後臺運行程序;
2、服務器端在線等待多客服端的連接和退出,需要採用多線程實現連接動作;
3、服務器端是先接收消息,後轉發消息,實現羣發消息;
4、羣發消息是觀察者模式,服務器作爲主題,客戶端作爲觀察者,客戶端的創建需要註冊到服務器,服務器有改動時,通知客戶端。
5、客戶端發送消息給服務端,有服務端負責轉達到其他客戶端;
6、客戶端隨時發送,隨時接收消息,需要採用多線程實現輸入輸出;
7、客戶端通過在消息末尾加“@用戶名”字串,實現私聊功能;
程序的結構如圖所示:
三、程序編碼
程序代碼根據上圖結構而設計,將對應的部分設計爲一個單獨的類,以便相關功能的維護。代碼目錄如下:
1、TCPServer.java
2、TCPClient.java
3、Channel.java
4、MsgSend.java
5、MsgReceive.java
具體代碼如下:
TCPServer.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 1、用單例模式實現Server端
* 2、用觀察者模式完成Channel對象的註冊及通知
* 3、完成羣聊功能
* 4、完成私聊功能
* @author 編符俠
* 2019-07-12
*/
public class TCPServer {
//用於創建TCP服務端
private ServerSocket serverSocket;
private int port;
//Channel集合
private CopyOnWriteArrayList<Channel> channelList;
//等待TCP客服端的連接標誌位
private boolean flag;
//TCP服務端實例的單例模式, 使用volatile排除指令重排
private static volatile TCPServer tcpServer=null;
//構造器
public TCPServer(int port) {
super();
this.port = port;
init();
}
//初始化成員變量
public void init() {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
release();
}
flag = true;
channelList = new CopyOnWriteArrayList();
}
//TCP服務端實例的單例模式
public static TCPServer getInstance(int port) {
//使判斷實例存在的流程直接通過
if(tcpServer != null)
return tcpServer;
//同步訪問tcpServer
synchronized(tcpServer.class){
//過濾掉少數因指令重排進入的流程
if(tcpServer != null)
return tcpServer;
tcpServer = new TCPServer(port);
}
return tcpServer;
}
public CopyOnWriteArrayList<Channel> getChannelList() {
return channelList;
}
public void setChannelList(CopyOnWriteArrayList<Channel> channelList) {
this.channelList = channelList;
//更新channelList集合後要通知各channel對象
notifyChannels();
}
//啓動TCP服務端工作
public void begin() {
while(flag) {
try {
Socket socket = serverSocket.accept();
Channel channel = new Channel(socket, this);
new Thread(channel).start();
} catch (IOException e) {
flag = false;
release();
}
}
}
//觀察者模式,註冊一個channel對象
public void registerObserver(Channel channel) {
channelList.add(channel);
notifyChannels();
}
//觀察者模式,通知每個channel對象所關心的channelList集合
public void notifyChannels() {
for(Channel channel : channelList) {
channel.updateChannelList(channelList);
}
}
//釋放資源
public void release() {
if(serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(channelList != null) {
channelList.clear();
}
}
public static void main(String[] args) {
System.out.println("----------- Server ------------");
new TCPServer(6666).begin();
}
}
TCPClient.java
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* TCP客戶端
* 1、輸入方式單獨一個線程
* 2、輸出方式單獨一個線程
* @author 編符俠
* 2019-07-12
*/
public class TCPClient {
//TCP客戶端
private Socket socket;
//發送端
private MsgSend send;
//接收端
private MsgReceive receive;
//客戶端名字
private String name;
public TCPClient(String url, int port, String name) {
super();
try {
socket = new Socket(url, port);
} catch (UnknownHostException e) {
release();
} catch (IOException e) {
release();
}
this.name = name;
send = new MsgSend(socket);
receive = new MsgReceive(socket);
}
//啓動客戶端,啓動輸入、輸出線程
public void begin() {
new Thread(send, name).start();
new Thread(receive, name).start();
}
//釋放資源
public void release() {
if(socket != null) {
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("Please input your name: ");
String name = new Scanner(System.in).nextLine();
System.out.println("----------- "+name+" ------------");
TCPClient client = new TCPClient("localhost", 6666, name);
client.begin();
}
}
Channel.java
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;
/**
*
* TCP服務端的Channel對象
* 1、採用觀察者模式
* @author 編符俠
* 2019-07-12
*/
public class Channel implements Runnable{
//保存TCP服務端
private TCPServer serverDelegate;
//輸入、輸出端
private MsgSend send;
private MsgReceive receive;
//Channel集合
private CopyOnWriteArrayList<Channel> channelList;
private boolean flag;
public Channel(Socket socket, TCPServer serverDelegate) {
super();
this.serverDelegate = serverDelegate;
send = new MsgSend(socket);
receive = new MsgReceive(socket);
//觀察者模式,註冊channel對象
serverDelegate.registerObserver(this);
flag = true;
}
//獲取Channel對象的輸出端
public MsgSend getSend() {
return send;
}
//獲取Channel對象的輸入端
public MsgReceive getReceive() {
return receive;
}
//觀察者模式,用於TCP服務端(主題)通知Channel對象(觀察者)更新channelList列表
public void updateChannelList(CopyOnWriteArrayList<Channel> channelList) {
this.channelList = channelList;
}
@Override
public void run() {
while(flag) {
String msg = receive.receiveMsg();
//當接收到msg爲null時,代表對應的Client已退出,則可以關閉對應的Channel
if(msg == null) {
flag = false;
channelList.remove(this);
serverDelegate.setChannelList(channelList);
continue;
}
//通知除this對象的其他Channel對象
for(Channel channel : channelList) {
if(channel == this)
continue;
channel.getSend().sendMsg(msg);
}
}
}
}
MsgSend.java
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
/**
* 輸出端
* @author 編符俠
* 2019-07-12
*/
public class MsgSend implements Runnable{
//輸出流
private DataOutputStream outStream;
//輸入流
private Scanner scan;
private Socket socket;
private boolean flag;
public MsgSend(Socket socket) {
super();
this.socket = socket;
try {
outStream = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
release();
}
flag = true;
scan = new Scanner(System.in);
}
//封裝的發送方法
public void sendMsg(String msg) {
if(msg == null)
return;
try {
outStream.writeUTF(msg);
outStream.flush();
} catch (IOException e) {
flag = false;
release();
System.out.println("MsgSend::sendMsg from "+Thread.currentThread().getName());
}
}
@Override
public void run() {
while(flag) {
//客戶端輸入一行字符串
String msg = scan.nextLine();
String name = Thread.currentThread().getName();
sendMsg(name+" : "+msg);
//若輸入的字符串爲"bye",則關閉發送端
if(msg.equals("bye")) {
flag = false;
release();
}
}
}
//釋放資源
public void release() {
if(outStream != null) {
try {
outStream.close();
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
MsgReceive.java
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 接收端
* @author 編符俠
* 2019-07-12
*/
public class MsgReceive implements Runnable{
//數據輸入流
private DataInputStream inStream;
//客戶端socket
private Socket socket;
private boolean flag;
public MsgReceive(Socket socket) {
super();
this.flag = true;
this.socket = socket;
try {
inStream = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
flag = false;
release();
}
}
//封裝的接收數據方法
public String receiveMsg() {
String msg=null;
try {
msg = inStream.readUTF();
} catch (IOException e) {
flag = false;
release();
System.out.println("[ERROR]=>MsgReceive::receiveMsg from "+Thread.currentThread().getName());
}
return msg;
}
@Override
public void run() {
while(flag) {
String msg = receiveMsg();
//篩選合法字符
if(msg==null || msg.equals("")) {
flag = false;
break;
}
//分段含有“@”的私密消息
String[] tmp = msg.split("@");
if(tmp.length > 1) {
//含有“@”的私密消息,判斷“@”的字符是否爲本線程對象
if(tmp[1].equals(Thread.currentThread().getName())) {
System.out.println("私密消息==>"+tmp[0]);
}
}
else {
//不含有“@”的私密消息
System.out.println("收到消息==>"+msg);
}
}
}
//釋放資源
public void release() {
if(inStream != null) {
try {
inStream.close();
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
四、程序測試
本節只作簡單的功能測試。一個服務器啓動TCPServer.java,一個客服端啓動一次TCPClient.java,通過打開多個console實現對話框效果進行互聊。
1、客戶端登錄
啓動三個客戶端,應邀輸入各自客戶端的聊天暱稱。
2、羣聊消息
編符俠客戶端發送消息,其他兩個客戶端接收消息。
3、私密消息
編符俠客戶端發送私密消息給鋼鐵俠,只有鋼鐵俠客戶端接收消息,鋼鐵俠客戶端發送私密消息給編符俠,只有編符俠接收消息。
4、客戶端下線
鋼鐵俠客戶端下線,不影響其他兩個客戶端的正常聊天功能。
五、項目總結
項目在工作之餘歷時兩天完成了。基本功能全部實現,合理運用了單例模式、觀察者模式。在設計的過程中不斷髮現問題優化軟件結構,不注重業務功能邏輯,重在設計模式、多線程、網絡編程的合理運用。
本項目的改進之處:可將釋放資源行爲封裝成一個類;進一步優化代碼,達到易於重構和擴展;優化性能問題。
後期會通過不斷的學習,持續優化該項目。