網絡聊天室的分析與實現

前言

  本文基於多線程實現網絡聊天室,採用一個服務器端、多個客戶端的軟件結構,實現多個客戶端之間的羣聊,以及私聊的功能。軟件項目中融入了觀察者模式、單例模式,使項目更加易於維護。
  本文的主要目的是通過實現網絡聊天室,熟練掌握多線程、網絡、設計模式,面向對象的編程技能。

一、需求分析

  網絡聊天室由一個服務器與多個客服端組成,客服端可以隨時加入,也能隨時退出,而不影響其他客戶端的正常運作。客服端無任何限制發送或接受的條件,達到及時發送及時接收的功能。服務器作爲唯一後臺運行程序,爲客戶端之間的互聊提供服務。
  網絡聊天室主要有兩個功能:
  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、客戶端下線
  鋼鐵俠客戶端下線,不影響其他兩個客戶端的正常聊天功能。
在這裏插入圖片描述

五、項目總結

  項目在工作之餘歷時兩天完成了。基本功能全部實現,合理運用了單例模式、觀察者模式。在設計的過程中不斷髮現問題優化軟件結構,不注重業務功能邏輯,重在設計模式、多線程、網絡編程的合理運用。
  本項目的改進之處:可將釋放資源行爲封裝成一個類;進一步優化代碼,達到易於重構和擴展;優化性能問題。
  後期會通過不斷的學習,持續優化該項目。

發佈了21 篇原創文章 · 獲贊 0 · 訪問量 1818
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章