网络聊天室的分析与实现
前言
本文基于多线程实现网络聊天室,采用一个服务器端、多个客户端的软件结构,实现多个客户端之间的群聊,以及私聊的功能。软件项目中融入了观察者模式、单例模式,使项目更加易于维护。
本文的主要目的是通过实现网络聊天室,熟练掌握多线程、网络、设计模式,面向对象的编程技能。
一、需求分析
网络聊天室由一个服务器与多个客服端组成,客服端可以随时加入,也能随时退出,而不影响其他客户端的正常运作。客服端无任何限制发送或接受的条件,达到及时发送及时接收的功能。服务器作为唯一后台运行程序,为客户端之间的互聊提供服务。
网络聊天室主要有两个功能:
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、客户端下线
钢铁侠客户端下线,不影响其他两个客户端的正常聊天功能。
五、项目总结
项目在工作之余历时两天完成了。基本功能全部实现,合理运用了单例模式、观察者模式。在设计的过程中不断发现问题优化软件结构,不注重业务功能逻辑,重在设计模式、多线程、网络编程的合理运用。
本项目的改进之处:可将释放资源行为封装成一个类;进一步优化代码,达到易于重构和扩展;优化性能问题。
后期会通过不断的学习,持续优化该项目。