网络聊天室的分析与实现

前言

  本文基于多线程实现网络聊天室,采用一个服务器端、多个客户端的软件结构,实现多个客户端之间的群聊,以及私聊的功能。软件项目中融入了观察者模式、单例模式,使项目更加易于维护。
  本文的主要目的是通过实现网络聊天室,熟练掌握多线程、网络、设计模式,面向对象的编程技能。

一、需求分析

  网络聊天室由一个服务器与多个客服端组成,客服端可以随时加入,也能随时退出,而不影响其他客户端的正常运作。客服端无任何限制发送或接受的条件,达到及时发送及时接收的功能。服务器作为唯一后台运行程序,为客户端之间的互聊提供服务。
  网络聊天室主要有两个功能:
  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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章