目 录
1 绪论 1
1.1 项目概述 1
1.2 项目意义 1
2 开发工具和相关技术简介 2
2.1 MyEclipse简介 2
2.2 Java语言介绍 2
2.3 GUI简介 2
3 系统需求分析 4
3.1 系统可行性分析 4
3.2 系统需求分析 4
3.3 组内成员分工 4
3.4 项目进度安排 5
4 系统设计 6
4.1 系统设计 6
5 系统实现 8
6 结论和心得 11
1 绪论
1.1 项目概述
综合应用Java的GUI编程和网络编程,实现一个能够支持多组用户同时使用的聊天室软件。该聊天室共分为客户端和服务器端两部分,服务器端程序主要负责侦听客户端发来的消息,客户端需要登录到服务端才可以实现正常的聊天功能。服务器端的主要实现在特定端口上进行侦听,等待客户端连接,默认端口设置为9999。客户端的主要功能为,连接到已经开启服务的服务端。具有比较友好的GUI界面,并使用C/S模式,支持多个用户同时使用,用户可以自己选择加入或者创建房间,和房间内的其他用户互发信息,互发表情。
1.2 项目意义
根据当前网络的要求,网络聊天越来越受各种网民所青睐。因此开发网络聊天是相当有必要的。随着网络的快速发展,网络引燃已经成为了我们了解世界和关注世界各地新动态的一个主要途径。因此伴随着网络而必然产生的一些软件也越来越多,而能够实现即时通信的聊天软件也是随之诞生,且深受用户们喜爱。设计聊天室,实现聊天功能可以让我们简单的了解到他们是怎么做到的,深切的体会感受,从而增加自己的网络设计能力。
2 开发工具和相关技术简介
本项目是《聊天室小程序》。开发环境:My eclipse;开发语言:Java语言;开发技术:GUI。本章将对开发工具和相关技术进行简单介绍。
2.1 MyEclipse简介
MyEclipse,是在Eclipse 基础上加上自己的插件开发而成的功能强大的企业级集成开发环境,主要用于Java、Java EE以及移动应用的开发。在最新版本的MyEclipse中,配合CodeMix使用支持也十分广泛,尤其是对各种开源产品和主流开发框架的支持相当不错。目前已支持PHP、Python、Vue、Angular、React等语言和框架开发。
MyEclipse 是一个十分优秀的用于开发Java, J2EE的 Eclipse 插件集合,MyEclipse的功能非常强大,支持也十分广泛,尤其是对各种开源产品的支持十分不错。MyEclipse可以支持Java Servlet,AJAX,JSP,JSF,Struts,Spring,Hibernate,EJB3,JDBC数据库链接工具等多项功能。可以说MyEclipse是几乎囊括了所有主流开源产品的专属eclipse开发工具。
2.2 Java语言介绍
Java是由Sun Microsystems公司推出的Java面向对象程序设计语言(以下简称Java语言)和Java平台的总称。由James Gosling和同事们共同研发,并在1995年正式推出。Java最初被称为Oak,是1991年为消费类电子产品的嵌入式芯片而设计的。1995年更名为Java,并重新设计用于开发Internet应用程序。用Java实现的HotJava浏览器(支持Java applet)显示了Java的魅力:跨平台、动态Web、Internet计算。从此,Java被广泛接受并推动了Web的迅速发展,常用的浏览器均支持Javaapplet。另一方面,Java技术也不断更新。Java自面世后就非常流行,发展迅速,对C++语言形成有力冲击。在全球云计算和移动互联网的产业环境下,Java更具备了显著优势和广阔前景。
java语言的特点:
(1)简单;
(2)跨平台性;
(3)面向对象;
(4)健壮性;
(5)多线程;
(6)安全性;
(7)动态的。
2.3 GUI简介
图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。
图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。图形用户界面由窗口、下拉菜单、对话框及其相应的控制机制构成,在各种新式应用程序中都是标准化的,即相同的操作总是以同样的方式来完成,在图形用户界面,用户看到和操作的都是图形对象,应用的是计算机图形学的技术。
3 系统需求分析
3.1 系统可行性分析
从技术方面来看,Java语言的优点主要表现在简单,面向对象,多线程,安全性等方面。Java实现了自动的功能收集,简化了内存管理的工作,使程序设计更简便,同时减少了出错的可能。Java提供了简单的类机制和动态的架构,模型对象中封装了它的状态变量和方法。Java同时支持继承特性,Java的类可以从其他类中继承行为。Java支持界面界面允许程序员定义方法,但不立即实现一个类可以实现多个界面。利用界面可以得到更直观的理解。
3.2 系统需求分析
客户端的功能主要包括如下的功能:
- 选择连上服务端
- 显示当前房间列表(包括房间号和房间名称)
- 选择房间进入
- 多个用户在线群聊
- 可以发送表情(用本地的,实际上发送只发送表情的代码)
- 退出房间
- 选择创建房间
- 房间里没人(房主退出),导致房间解散
- 显示系统提示消息
- 显示用户消息
- 构造标准的消息结构发送
- 维护GUI所需的数据模型
服务端的功能主要包括:
- 维护用户信息和房间信息
- 处理用户发送来的消息选择转发或者回复处理结果
- 构造标准的消息结构发送
架构
整个程序采用C/S设计架构,分为一个服务端和多个客户端。服务端开放一个端口给所有开客户端,客户端连接该端口并收发信息,服务端在内部维护客户端的组,并对每一个客户端都用一个子线程来收发信息。
功能模块图:
聊天室功能模块图
3.3 组内成员分工
表3-1 组内成员分工情况表
序号 |
姓名 |
组内角色 |
小组分工 |
备注 |
1 |
温艳珍 |
项目经理 |
编写room类,user类 |
共同整合测试 |
2 |
张倩 |
架构师,编码 |
编写server类 |
共同整合测试 |
3 |
李佳路 |
架构师,编码 |
编写client类 |
共同整合测试 |
3.4 进度安排
进度安排如表3-2所示。
表3-2 进度安排表
阶段 |
持续时间 |
阶段描述 |
输出 |
构思阶段 |
两小时 |
需求分析 |
需求说明,功能模块图 |
设计阶段 |
四小时 |
系统设计 |
设计说明-可以画流程图;数据库设计 |
是现阶段 |
两天 |
编写代码 |
项目工程源代码 |
三小时 |
系统测试 |
功能测试-测试说明 |
|
运行阶段 |
一天 |
部署、运行 |
系统使用说明、运维报告-答辩 |
4 系统设计
4.1 系统设计
服务端:
主要功能描述:1.服务器端聊天程序要在待定的端口上等待来自聊天客户的连接请求;
- 服务器聊天程序要随时监控连接的状态并显示出来;
- 要随时准备好接受来自客户端的信息,随时把接受到的信息显示出来;
Server类:通过port号来构造服务器端对象,维护一个总的用户列表和一个房间列表。
public Server(int port) throws Exception {
allUsers = new ArrayList<>();
rooms = new RoomList();
this.port=port;
unusedUserID=1;
ss = new ServerSocket(port);
System.out.println("Server is builded!");
}
获得下一个可用的用户id,
private long getNextUserID(){
if(unusedUserID < MAX_USERS)
return unusedUserID++;
else
return -1;
}
开始监听,当接受到新的用户连接,就创建一个新的用户,并添加到用户列表中,然后创建一个新的服务线程用于收发该用户的消息,
public void startListen() throws Exception{
while(true){
Socket socket = ss.accept();
long id = getNextUserID();
if(id != -1){
User user = new User("User"+id, id, socket);
System.out.println(user.getName() + " is login...");
allUsers.add(user);
ServerThread thread = new ServerThread(user, allUsers, rooms);
thread.start();
}else{
System.out.println("Server is full!");
socket.close();
}
}
}
测试用main方法设置侦听窗口为9999,并开始监听
public static void main(String[] args) {
try {
Server server = new Server(9999);
server.startListen();
} catch (Exception e) {
e.printStackTrace();
}
}
ServerThread类:通过用户的对象实例、全局的用户列表、房间列表进行构造。
public ServerThread(User user,
ArrayList<User> userList, RoomList map){
this.user=user;
this.userList=userList;
this.map=map;
pw=null;
roomId = -1;
}
线程运行部分,持续读取用户socket发送来的数据,并解析。
public void run(){
try{
while (true) {
String msg=user.getBr().readLine();
System.out.println(msg); /*解析用户的数据格式*/
parseMsg(msg);
}
}catch (SocketException se) { /*处理用户断开的异常*/
System.out.println("user "+user.getName()+" logout.");
}catch (Exception e) { /*处理其他异常*/
e.printStackTrace();
}finally {
try {
/*
* 用户断开或者退出,需要把该用户移除
* 并关闭socket
*/
remove(user);
user.getBr().close();
user.getSocket().close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
用正则表达式匹配数据的格式,根据不同的指令类型,来调用相应的方法处理,
private void parseMsg(String msg){
String code = null;
String message=null;
if(msg.length()>0){
/*匹配指令类型部分的字符串*/
Pattern pattern = Pattern.compile("<code>(.*)</code>");
Matcher matcher = pattern.matcher(msg);
if(matcher.find()){
code = matcher.group(1);
}
/*匹配消息部分的字符串*/
pattern = Pattern.compile("<msg>(.*)</msg>");
matcher = pattern.matcher(msg);
if(matcher.find()){
message = matcher.group(1);
}
switch (code) {
case "join":
// add to the room
// code = 1, 直接显示在textArea中
// code = 11, 在list中加入
// code = 21, 把当前房间里的所有用户返回给client
if(roomId == -1){
roomId = Long.parseLong(message);
map.join(user, roomId);
sendRoomMsgExceptSelf(buildCodeWithMsg("<name>"+user.getName()+"</name><id>"+user.getId()+"</id>", 11));
// 这个消息需要加入房间里已有用户的列表
returnMsg(buildCodeWithMsg("你加入了房间:" + map.getRoom(roomId).getName(), 1));
returnMsg(buildCodeWithMsg(getMembersInRoom(), 21));
}else{
map.esc(user, roomId);
sendRoomMsg(buildCodeWithMsg(""+user.getId(), 12));
long oldRoomId = roomId;
roomId = Long.parseLong(message);
map.join(user, roomId);
sendRoomMsgExceptSelf(buildCodeWithMsg("<name>"+user.getName()+"</name><id>"+user.getId()+"</id>", 11));
returnMsg(buildCodeWithMsg("你退出房间:" + map.getRoom(oldRoomId).getName() + ",并加入了房间:" + roomId,1));
returnMsg(buildCodeWithMsg(getMembersInRoom(), 21));
}
break;
case "esc":
// delete from room list
// code = 2, 弹窗提示
// code = 12, 对所有该房间的其他用户发送该用户退出房间的信息,从list中删除
if(roomId!=-1){
int flag=map.esc(user, roomId);
sendRoomMsgExceptSelf(buildCodeWithMsg(""+user.getId(), 12));
long oldRoomId=roomId;
roomId = -1;
returnMsg(buildCodeWithMsg("你已经成功退出房间,不会收到消息", 2));
if(flag==0){
sendMsg(buildCodeWithMsg(""+oldRoomId, 13));
}
}else{
returnMsg(buildCodeWithMsg("你尚未加入任何房间", 2));
}
break;
case "list":
// list all the rooms
// code = 3, 在客户端解析rooms,并填充roomlist
returnMsg(buildCodeWithMsg(getRoomsList(), 3));
break;
case "message":
// send message
// code = 4, 自己收到的话,打印的是‘你说:....‘否则打印user id对应的name
sendRoomMsg(buildCodeWithMsg("<from>"+user.getId()+"</from><smsg>"+message+"</smsg>", 4));
break;
case "create":
// create a room
// code=5,提示用户进入了房间
// code=15,需要在其他所有用户的room列表中更新
roomId = map.createRoom(message);
map.join(user, roomId);
sendMsg(buildCodeWithMsg("<rid>"+roomId+"</rid><rname>"+message+"</rname>", 15));
returnMsg(buildCodeWithMsg("你进入了创建的房间:"+map.getRoom(roomId).getName(), 5));
returnMsg(buildCodeWithMsg(getMembersInRoom(), 21));
break;
case "setname":
// set name for user
// code=16,告诉房间里的其他人,你改了昵称
user.setName(message);
sendRoomMsg(buildCodeWithMsg("<id>"+user.getId()+"</id><name>"+message+"</name>", 16));
break;
default:
// returnMsg("something unknown");
System.out.println("not valid message from user"+user.getId());
break;
}
}
}
获得该用户房间中的所有用户列表,并构造成一定格式的消息返回
private String getMembersInRoom(){
/*先从room列表获得该用户的room*/
Room room = map.getRoom(roomId);
StringBuffer stringBuffer = new StringBuffer();
if(room != null){
/*获得房间中所有的用户的列表,然后构造成一定的格式发送回去*/
ArrayList<User> users = room.getUsers();
for(User each: users){
stringBuffer.append("<member><name>"+each.getName()+
"</name><id>"+each.getId()+"</id></member>");
}
}
return stringBuffer.toString();
}
获得所有房间的列表,并构造成一定的格式。
private String getRoomsList(){
String[][] strings = map.listRooms();
StringBuffer sb = new StringBuffer();
for(int i=0; i<strings.length; i++){
sb.append("<room><rname>"+strings[i][1]+
"</rname><rid>"+strings[i][0]+"</rid></room>");
}
return sb.toString();
}
构造成一个统一的消息格式,
private String buildCodeWithMsg(String msg, int code){
return "<code>"+code+"</code><msg>"+msg+"</msg>\n";
}
群发消息:全体用户,code>10、
private void sendMsg(String msg) {
System.out.println("In sendMsg()");
/*取出用户列表中的每一个用户来发送消息*/
for(User each:userList){
try {
pw=each.getPw();
pw.println(msg);
pw.flush();
System.out.println(msg);
} catch (Exception e) {
System.out.println("exception in sendMsg()");
}
}
}
只对同一房间的用户发:code>10、
private void sendRoomMsg(String msg){
/*先获得该用户的房间号,然后往该房间发送消息*/
Room room = map.getRoom(roomId);
if(room != null){
ArrayList<User> users = room.getUsers();
for(User each: users){
pw = each.getPw();
pw.println(msg);
pw.flush();
}
}
}
向房间中除了该用户自己,发送消息、
private void sendRoomMsgExceptSelf(String msg){
Room room = map.getRoom(roomId);
if(room != null){
ArrayList<User> users = room.getUsers();
for(User each: users){
if(each.getId()!=user.getId()){
pw = each.getPw();
pw.println(msg);
pw.flush();
}
}
}
}
对于client的来信,返回一个结果,code<10,
private void returnMsg(String msg){
try{
pw = user.getPw();
pw.println(msg);
pw.flush();
}catch (Exception e) {
System.out.println("exception in returnMsg()");
}
}
移除该用户,并向房间中其他用户发送该用户已经退出的消息,如果房间中没人了,那么就更新房间列表给所有用户。
private void remove(User user){
if(roomId!=-1){
int flag=map.esc(user, roomId);
sendRoomMsgExceptSelf(buildCodeWithMsg(""+user.getId(), 12));
long oldRoomId=roomId;
roomId = -1;
if(flag==0){
sendMsg(buildCodeWithMsg(""+oldRoomId, 13));
}
}
userList.remove(user);
}
客户端:
客户端:提供登录、主窗体及聊天等界面及对应的业务逻辑,向服务器发送相应的服务请求,并接受相应的处理结果。客户端是轻量级的软件,只负责链接远程服务器,并发出相应的服务请求,并不进行核心业务逻辑的处理。具体的处理交给服务器,而客户端只接收服务器处理的结果并显示给用户。
Client类:
package Client;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
public class Client implements ActionListener{
private JFrame frame;
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
private String name;
private HashMap<String, Integer> rooms_map;
private HashMap<String, Integer> users_map;
private JTextField host_textfield;
private JTextField port_textfield;
private JTextField text_field;
private JTextField name_textfiled;
private JLabel rooms_label;
private JLabel users_label;
private JList<String> roomlist;
private JList<String> userlist;
private JTextPane msgArea;
private JScrollPane textScrollPane;
private JScrollBar vertical;
DefaultListModel<String> rooms_model;
DefaultListModel<String> users_model;
/*
* 构造函数
* 该客户端对象维护两个map,房间的hashmap和房间中用户的hashmap
* 作为列表组件的数据模型
*/
public Client(){
rooms_map = new HashMap<>();
users_map = new HashMap<>();
initialize();
}
/**
* 连接服务端,指定host和port
* @param host
* @param port
* @return
*/
public boolean connect(String host, int port){
try {
socket = new Socket(host, port);
System.out.println("Connected to server!"+socket.getRemoteSocketAddress());
br=new BufferedReader(new InputStreamReader(System.in));
pw=new PrintWriter(socket.getOutputStream());
/*
* 创建一个接受和解析服务器消息的线程
* 传入当前客户端对象的指针,作为句柄调用相应的处理函数
*/
ClientThread thread = new ClientThread(socket, this);
thread.start();
return true;
} catch (IOException e) {
System.out.println("Server error");
JOptionPane.showMessageDialog(frame, "服务器无法连接!");
return false;
}
}
/*当前进程作为只发送消息的线程,从命令行中获取输入*/
// public void sendMsg(){
// String msg;
// try {
// while(true){
// msg = br.readLine();
// pw.println(msg);
// pw.flush();
// }
// } catch (IOException e) {
// System.out.println("error when read msg and to send.");
// }
// }
/**
* 发给服务器的消息,先经过一定的格式构造再发送
* @param msg
* @param code
*/
public void sendMsg(String msg, String code){
try {
pw.println("<code>"+code+"</code><msg>"+msg+"</msg>");
pw.flush();
} catch (Exception e) {
//一般是没有连接的问题
System.out.println("error in sendMsg()");
JOptionPane.showMessageDialog(frame, "请先连接服务器!");
}
}
/**
* 窗口初始化
*/
private void initialize() {
/*设置窗口的UI风格和字体*/
setUIStyle();
setUIFont();
JFrame frame = new JFrame("ChatOnline");
JPanel panel = new JPanel(); /*主要的panel,上层放置连接区,下层放置消息区,
中间是消息面板,左边是room列表,右边是当前room的用户列表*/
JPanel headpanel = new JPanel(); /*上层panel,用于放置连接区域相关的组件*/
JPanel footpanel = new JPanel(); /*下层panel,用于放置发送信息区域的组件*/
JPanel leftpanel = new JPanel(); /*左边panel,用于放置房间列表和加入按钮*/
JPanel rightpanel = new JPanel(); /*右边panel,用于放置房间内人的列表*/
/*最上层的布局,分中间,东南西北五个部分*/
BorderLayout layout = new BorderLayout();
/*格子布局,主要用来设置西、东、南三个部分的布局*/
GridBagLayout gridBagLayout = new GridBagLayout();
/*主要设置北部的布局*/
FlowLayout flowLayout = new FlowLayout();
/*设置初始窗口的一些性质*/
frame.setBounds(100, 100, 800, 600);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(panel);
frame.setLayout(layout);
/*设置各个部分的panel的布局和大小*/
headpanel.setLayout(flowLayout);
footpanel.setLayout(gridBagLayout);
leftpanel.setLayout(gridBagLayout);
rightpanel.setLayout(gridBagLayout);
leftpanel.setPreferredSize(new Dimension(130, 0));
rightpanel.setPreferredSize(new Dimension(130, 0));
/*以下均是headpanel中的组件*/
host_textfield = new JTextField("127.0.0.1");
port_textfield = new JTextField("9999");
name_textfiled = new JTextField("匿名");
host_textfield.setPreferredSize(new Dimension(100, 25));
port_textfield.setPreferredSize(new Dimension(70, 25));
name_textfiled.setPreferredSize(new Dimension(150, 25));
JLabel host_label = new JLabel("服务器IP");
JLabel port_label = new JLabel("端口");
JLabel name_label = new JLabel("昵称");
JButton head_connect = new JButton("连接");
// JButton head_change = new JButton("确认更改");
JButton head_create = new JButton("创建房间");
headpanel.add(host_label);
headpanel.add(host_textfield);
headpanel.add(port_label);
headpanel.add(port_textfield);
headpanel.add(head_connect);
headpanel.add(name_label);
headpanel.add(name_textfiled);
// headpanel.add(head_change);
headpanel.add(head_create);
/*以下均是footpanel中的组件*/
JButton foot_emoji = new JButton("表情");
JButton foot_send = new JButton("发送");
text_field = new JTextField();
footpanel.add(text_field, new GridBagConstraints(0, 0, 1, 1, 100, 100,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
footpanel.add(foot_emoji, new GridBagConstraints(1, 0, 1, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
footpanel.add(foot_send, new GridBagConstraints(2, 0, 1, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
/*两边的格子中的组件*/
rooms_label = new JLabel("当前房间数:0");
users_label = new JLabel("房间内人数:0");
JButton join_button = new JButton("加入房间");
JButton esc_button = new JButton("退出房间");
rooms_model = new DefaultListModel<>();
users_model = new DefaultListModel<>();
// rooms_model.addElement("房间1");
// rooms_model.addElement("房间2");
// rooms_model.addElement("房间3");
// String fangjian = "房间1";
// rooms_map.put(fangjian, 1);
roomlist = new JList<>(rooms_model);
userlist = new JList<>(users_model);
JScrollPane roomListPane = new JScrollPane(roomlist);
JScrollPane userListPane = new JScrollPane(userlist);
leftpanel.add(rooms_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
leftpanel.add(join_button, new GridBagConstraints(0, 1, 1, 1, 1, 1,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
leftpanel.add(esc_button, new GridBagConstraints(0, 2, 1, 1, 1, 1,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
leftpanel.add(roomListPane, new GridBagConstraints(0, 3, 1, 1, 100, 100,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
rightpanel.add(users_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
rightpanel.add(userListPane,new GridBagConstraints(0, 1, 1, 1, 100, 100,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
/*中间的文本区组件*/
msgArea = new JTextPane();
msgArea.setEditable(false);
textScrollPane = new JScrollPane();
textScrollPane.setViewportView(msgArea);
vertical = new JScrollBar(JScrollBar.VERTICAL);
vertical.setAutoscrolls(true);
textScrollPane.setVerticalScrollBar(vertical);
/*设置顶层布局*/
panel.add(headpanel, "North");
panel.add(footpanel, "South");
panel.add(leftpanel, "West");
panel.add(rightpanel, "East");
panel.add(textScrollPane, "Center");
/*注册各种事件*/
/*连接服务器*/
head_connect.addActionListener(this);
/*发送消息,如果没有连接则会弹窗提示*/
foot_send.addActionListener(this);
/*改名字*/
// head_change.addActionListener(this);
/*创建房间*/
head_create.addActionListener(this);
/*发送表情*/
foot_emoji.addActionListener(this);
/*加入room*/
join_button.addActionListener(this);
/*退出房间*/
esc_button.addActionListener(this);
/*最终显示*/
frame.setVisible(true);
}
/**
* 事件监听处理
*/
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
switch (cmd) {
case "连接": /*点击连接按钮*/
String strhost = host_textfield.getText();
String strport = port_textfield.getText();
connect(strhost, Integer.parseInt(strport));
String nameSeted = JOptionPane.showInputDialog("请输入你的昵称:"); /*提示输入昵称*/
name_textfiled.setText(nameSeted);
name_textfiled.setEditable(false);
port_textfield.setEditable(false);
host_textfield.setEditable(false);
/*发送设置姓名的消息和列出用户列表的消息*/
sendMsg(nameSeted, "setname");
sendMsg("", "list");
break;
// case "确认更改":
// String strname = name_textfiled.getText();
// name = strname;
// sendMsg(strname, "setname");
// break;
case "加入房间": /*选择房间后,点击加入房间按钮*/
String selected = roomlist.getSelectedValue();
if(rooms_map.containsKey(selected)){
sendMsg(""+rooms_map.get(selected), "join");
}
break;
case "退出房间": /*点击退出房间的按钮*/
sendMsg("", "esc");
break;
case "发送": /*点击发送消息的按钮*/
String text = text_field.getText();
text_field.setText("");
sendMsg(text, "message");
break;
case "表情": /*发送表情,新建一个表情窗口,并直接在表情窗口中处理消息发送*/
IconDialog dialog = new IconDialog(frame, this);
break;
case "创建房间": /*点击创建房间的按钮,弹出提示框数据房间名称*/
String string = JOptionPane.showInputDialog("请输入你的房间名称");
if(string==null || string.equals("")){
string = name+(int)(Math.random()*10000)+"的房间";
}
sendMsg(string, "create");
break;
default:
break;
}
}
/*很多辅助和clientThread互动的*/
/**
* 加入用户,通过正则表达式,匹配消息内容中的用户信息
* @param content
*/
public void addUser(String content){
if(content.length()>0){
Pattern pattern = Pattern.compile("<name>(.*)</name><id>(.*)</id>");
Matcher matcher = pattern.matcher(content);
if(matcher.find()){
/*
* 获得用户的name和id
* 加入用户列表
* 在消息区显示系统提示
*/
String name = matcher.group(1);
String id = matcher.group(2);
insertUser(Integer.parseInt(id), name);
insertMessage(textScrollPane, msgArea, null, "系统:", name+"加入了聊天室");
}
}
users_label.setText("房间内人数:"+users_map.size()); /*更新房间内的人数*/
}
/**
* 删除用户
* @param content
*/
public void delUser(String content){
if(content.length()>0){
int id = Integer.parseInt(content);
/*
* 从维护的用户map中取得所有的用户名字,然后去遍历匹配的用户
* 匹配到的用户名字从相应的数据模型中移除
* 并从map中移除,并在消息框中提示系统消息
*/
Set<String> set = users_map.keySet();
Iterator<String> iter = set.iterator();
String name=null;
while(iter.hasNext()){
name = iter.next();
if(users_map.get(name)==id){
users_model.removeElement(name);
break;
}
}
users_map.remove(name);
insertMessage(textScrollPane, msgArea, null, "系统:", name+"退出了聊天室");
}
users_label.setText("房间内人数:"+users_map.size());
}
/**
* 更新用户信息
* @param content
*/
public void updateUser(String content){
if(content.length()>0){
Pattern pattern = Pattern.compile("<id>(.*)</id><name>(.*)</name>");
Matcher matcher = pattern.matcher(content);
if(matcher.find()){
String id = matcher.group(1);
String name = matcher.group(2);
insertUser(Integer.parseInt(id), name);
}
}
}
/**
* 列出所有用户
* @param content
*/
public void listUsers(String content){
String name = null;
String id=null;
Pattern rough_pattern=null;
Matcher rough_matcher=null;
Pattern detail_pattern=null;
/*
* 先用正则表达式匹配用户信息
* 然后插入数据模型中
* 并更新用户数据模型中的条目
*/
if(content.length()>0){
rough_pattern = Pattern.compile("<member>(.*?)</member>");
rough_matcher = rough_pattern.matcher(content);
while(rough_matcher.find()){
String detail = rough_matcher.group(1);
detail_pattern = Pattern.compile("<name>(.*)</name><id>(.*)</id>");
Matcher detail_matcher = detail_pattern.matcher(detail);
if(detail_matcher.find()){
name = detail_matcher.group(1);
id = detail_matcher.group(2);
insertUser(Integer.parseInt(id), name);
}
}
}
users_label.setText("房间内人数:"+users_map.size());
}
/**
* 直接在textarea中显示消息
* @param content
*/
public void updateTextArea(String content){
insertMessage(textScrollPane, msgArea, null, "系统:", content);
}
/**
* 在textarea中显示其他用户的消息
* 先用正则匹配,再显示消息
* 其中还需要匹配emoji表情的编号
* @param content
*/
public void updateTextAreaFromUser(String content){
if(content.length()>0){
Pattern pattern = Pattern.compile("<from>(.*)</from><smsg>(.*)</smsg>");
Matcher matcher = pattern.matcher(content);
if(matcher.find()){
String from = matcher.group(1);
String smsg = matcher.group(2);
String fromName = getUserName(from);
if(fromName.equals(name))
fromName = "你";
if(smsg.startsWith("<emoji>")){
String emojiCode = smsg.substring(7, smsg.length()-8);
// System.out.println(emojiCode);
insertMessage(textScrollPane, msgArea, emojiCode, fromName+"说:", null);
return ;
}
insertMessage(textScrollPane, msgArea, null, fromName+"说:", smsg);
}
}
}
/**
* 显示退出的结果
* @param content
*/
public void showEscDialog(String content){
JOptionPane.showMessageDialog(frame, content);
/*清除消息区内容,清除用户数据模型内容和用户map内容,更新房间内人数*/
msgArea.setText("");
users_model.clear();
users_map.clear();
users_label.setText("房间内人数:0");
}
/**
* 新增一个room
* @param content
*/
public void addRoom(String content){
if(content.length()>0){
Pattern pattern = Pattern.compile("<rid>(.*)</rid><rname>(.*)</rname>");
Matcher matcher = pattern.matcher(content);
if(matcher.find()){
String rid = matcher.group(1);
String rname = matcher.group(2);
insertRoom(Integer.parseInt(rid), rname);
}
}
rooms_label.setText("当前房间数:"+rooms_map.size());
}
/**
* 删除一个room
* @param content
*/
public void delRoom(String content){
if(content.length()>0){
int delRoomId = Integer.parseInt(content);
Set<String> set = rooms_map.keySet();
Iterator<String> iter = set.iterator();
String rname=null;
while(iter.hasNext()){
rname = iter.next();
if(rooms_map.get(rname)==delRoomId){
rooms_model.removeElement(rname);
break;
}
}
rooms_map.remove(rname);
}
rooms_label.setText("当前房间数:"+rooms_map.size());
}
/**
* 列出目前所有的rooms
* @param content
*/
public void listRooms(String content){
String rname = null;
String rid=null;
Pattern rough_pattern=null;
Matcher rough_matcher=null;
Pattern detail_pattern=null;
if(content.length()>0){
rough_pattern = Pattern.compile("<room>(.*?)</room>");
rough_matcher = rough_pattern.matcher(content);
while(rough_matcher.find()){
String detail = rough_matcher.group(1);
detail_pattern = Pattern.compile("<rname>(.*)</rname><rid>(.*)</rid>");
Matcher detail_matcher = detail_pattern.matcher(detail);
if(detail_matcher.find()){
rname = detail_matcher.group(1);
rid = detail_matcher.group(2);
insertRoom(Integer.parseInt(rid), rname);
}
}
}
rooms_label.setText("当前房间数:"+rooms_map.size());
}
/**
* 插入一个room
* @param rid
* @param rname
*/
private void insertRoom(Integer rid, String rname){
if(!rooms_map.containsKey(rname)){
rooms_map.put(rname, rid);
rooms_model.addElement(rname);
}else{
rooms_map.remove(rname);
rooms_model.removeElement(rname);
rooms_map.put(rname, rid);
rooms_model.addElement(rname);
}
rooms_label.setText("当前房间数:"+rooms_map.size());
}
/**
* 插入一个user
* @param id
* @param name
*/
private void insertUser(Integer id, String name){
if(!users_map.containsKey(name)){
users_map.put(name, id);
users_model.addElement(name);
}else{
users_map.remove(name);
users_model.removeElement(name);
users_map.put(name, id);
users_model.addElement(name);
}
users_label.setText("房间内人数:"+users_map.size());
}
/**
* 获得用户的姓名
* @param strId
* @return
*/
private String getUserName(String strId){
int uid = Integer.parseInt(strId);
Set<String> set = users_map.keySet();
Iterator<String> iterator = set.iterator();
String cur=null;
while(iterator.hasNext()){
cur = iterator.next();
if(users_map.get(cur)==uid){
return cur;
}
}
return "";
}
/**
* 获得用户所在房间的名称
* @param strId
* @return
*/
private String getRoomName(String strId){
int rid = Integer.parseInt(strId);
Set<String> set = rooms_map.keySet();
Iterator<String> iterator = set.iterator();
String cur = null;
while(iterator.hasNext()){
cur = iterator.next();
if(rooms_map.get(cur)==rid){
return cur;
}
}
return "";
}
/**
* 打印一条消息,如果有图片就打印图片,否则打印content
* @param scrollPane
* @param textPane
* @param icon_code
* @param title
* @param content
*/
private void insertMessage(JScrollPane scrollPane, JTextPane textPane,
String icon_code, String title, String content){
StyledDocument document = textPane.getStyledDocument(); /*获取textpane中的文本*/
/*设置标题的属性*/
SimpleAttributeSet title_attr = new SimpleAttributeSet();
StyleConstants.setBold(title_attr, true);
StyleConstants.setForeground(title_attr, Color.BLUE);
/*设置正文的属性*/
SimpleAttributeSet content_attr = new SimpleAttributeSet();
StyleConstants.setBold(content_attr, false);
StyleConstants.setForeground(content_attr, Color.BLACK);
Style style = null;
if(icon_code!=null){
Icon icon = new ImageIcon("icon/"+icon_code+".png");
style = document.addStyle("icon", null);
StyleConstants.setIcon(style, icon);
}
try {
document.insertString(document.getLength(), title+"\n", title_attr);
if(style!=null)
document.insertString(document.getLength(), "\n", style);
else
document.insertString(document.getLength(), " "+content+"\n", content_attr);
} catch (BadLocationException ex) {
System.out.println("Bad location exception");
}
/*设置滑动条到最后*/
vertical.setValue(vertical.getMaximum());
}
/**
* 设置需要美化字体的组件
*/
public static void setUIFont()
{
Font f = new Font("微软雅黑", Font.PLAIN, 14);
String names[]={ "Label", "CheckBox", "PopupMenu","MenuItem", "CheckBoxMenuItem",
"JRadioButtonMenuItem","ComboBox", "Button", "Tree", "ScrollPane",
"TabbedPane", "EditorPane", "TitledBorder", "Menu", "TextArea","TextPane",
"OptionPane", "MenuBar", "ToolBar", "ToggleButton", "ToolTip",
"ProgressBar", "TableHeader", "Panel", "List", "ColorChooser",
"PasswordField","TextField", "Table", "Label", "Viewport",
"RadioButtonMenuItem","RadioButton", "DesktopPane", "InternalFrame"
};
for (String item : names) {
UIManager.put(item+ ".font",f);
}
}
/**
* 设置UI风格为当前系统的风格
*/
public static void setUIStyle(){
String lookAndFeel =UIManager.getSystemLookAndFeelClassName();
try {
UIManager.setLookAndFeel(lookAndFeel);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedLookAndFeelException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 测试用的main函数
* @param args
*/
public static void main(String[] args) {
Client client = new Client();
}
}
IconDialog类:
package Client;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
public class IconDialog implements ActionListener {
private JDialog dialog;
private Client client;
/**
* 通过frame和客户端对象来构造
* @param frame
* @param client
*/
public IconDialog(JFrame frame, Client client) {
this.client = client;
dialog = new JDialog(frame, "请选择表情", true);
/*16个表情*/
JButton[] icon_button = new JButton[16];
ImageIcon[] icons = new ImageIcon[16];
/*获得弹出窗口的容器,设置布局*/
Container dialogPane = dialog.getContentPane();
dialogPane.setLayout(new GridLayout(0, 4));
/*加入表情*/
for(int i=1; i<=15; i++){
icons[i] = new ImageIcon("icon/"+i+".png");
icons[i].setImage(icons[i].getImage().getScaledInstance(50, 50, Image.SCALE_DEFAULT));
icon_button[i] = new JButton(""+i, icons[i]);
icon_button[i].addActionListener(this);
dialogPane.add(icon_button[i]);
}
dialog.setBounds(200,266,266,280);
dialog.show();
}
@Override
public void actionPerformed(ActionEvent e) {
/*构造emoji结构的消息发送*/
String cmd = e.getActionCommand();
System.out.println(cmd);
dialog.dispose();
client.sendMsg("<emoji>"+cmd+"</emoji>", "message");
}
}
Room类
Room.java:
import java.util.ArrayList;
import java.util.List;
import User.User;
public class Room {
private String name;
private long roomId;
private ArrayList<User> list;
private int totalUsers;
/**
* 获得房间的名字
* @return name
*/
public String getName() {
return name;
}
/**
* 设置房间的新名字
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获得房间的id号
* @return
*/
public long getRoomId() {
return roomId;
}
/**
* 设置房间的id
* @param roomId
*/
public void setRoomId(long roomId) {
this.roomId = roomId;
}
/**
* 向房间中加入一个新用户
* @param user
*/
public void addUser(User user) {
if(!list.contains(user)){
list.add(user);
totalUsers++;
}else{
System.out.println("User is already in Room<"+name+">:"+user);
}
}
/**
* 从房间中删除一个用户
* @param user
* @return 目前该房间中的总用户数目
*/
public int delUser(User user){
if(list.contains(user)){
list.remove(user);
return --totalUsers;
}else{
System.out.println("User is not in Room<"+name+">:"+user);
return totalUsers;
}
}
/**
* 获得当前房间的用户列表
* @return
*/
public ArrayList<User> getUsers(){
return list;
}
/**
* 获得当前房间的用户昵称的列表
* @return
*/
public String[] getUserNames(){
String[] userList = new String[list.size()];
int i=0;
for(User each: list){
userList[i++]=each.getName();
}
return userList;
}
/**
* 使用房间的名称和id来new一个房间
* @param name
* @param roomId
*/
public Room(String name, long roomId) {
this.name=name;
this.roomId=roomId;
this.totalUsers=0;
list = new ArrayList<>();
}
}
RoomList.java:
import java.awt.image.DirectColorModel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import User.User;
public class RoomList {
private HashMap<Long, Room> map;
private long unusedRoomId;
public static long MAX_ROOMS = 9999;
private int totalRooms;
/**
* 未使用的roomid从1算起,起始的房间总数为0
*/
public RoomList(){
map = new HashMap<>();
unusedRoomId = 1;
totalRooms = 0;
}
/**
* 创建一个新的房间,使用未使用的房间号进行创建,如果没有可以使用的则就创建失败
* @param name: 房间的名字
* @return 创建的房间的id
*/
public long createRoom(String name){
if(totalRooms<MAX_ROOMS){
if(name.length()==0){
name = ""+unusedRoomId;
}
Room room = new Room(name, unusedRoomId);
map.put(unusedRoomId, room);
totalRooms++;
return unusedRoomId++;
}else{
return -1;
}
}
/**
* 用户加入一个房间
* @param user
* @param roomID
* @return
*/
public boolean join(User user, long roomID){
if(map.containsKey(roomID)){
map.get(roomID).addUser(user);
return true;
}else{
return false;
}
}
/**
* 用户退出他的房间
* @param user
* @param roomID
* @return
*/
public int esc(User user, long roomID){
if(map.containsKey(roomID)){
int number = map.get(roomID).delUser(user);
/*如果这个房间剩下的人数为0,那么删除该房间*/
if(number==0){
map.remove(roomID);
totalRooms--;
return 0;
}
return 1;
}else{
return -1;
}
}
/**
* 列出所有房间的列表,返回一个二维数组,strings[i][0]放房间的id,string[i][1]放房间的name
* @return
*/
public String[][] listRooms(){
String[][] strings = new String[totalRooms][2];
int i=0;
/*将map转化为set并使用迭代器来遍历*/
Set<Entry<Long, Room>> set = map.entrySet();
Iterator<Entry<Long, Room>> iterator = set.iterator();
while(iterator.hasNext()){
Map.Entry<Long, Room> entry = iterator.next();
long key = entry.getKey();
Room value = entry.getValue();
strings[i][0]=""+key;
strings[i][1]=value.getName();
}
return strings;
}
/**
* 通过roomID来获得房间
* @param roomID
* @return
*/
public Room getRoom(long roomID){
if(map.containsKey(roomID)){
return map.get(roomID);
}
else
return null;
}
}
User类:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class User {
private String name;
private long id;
private long roomId;
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
/**
*
* @param name: 设置user的姓名
* @param id:设置user的id
* @param socket:保存用户连接的socket
* @throws IOException
*/
public User(String name, long id, final Socket socket) throws IOException {
this.name=name;
this.id=id;
this.socket=socket;
this.br=new BufferedReader(new InputStreamReader(
socket.getInputStream()));
this.pw=new PrintWriter(socket.getOutputStream());
}
/**
* 获得该用户的id
* @return id
*/
public long getId() {
return id;
}
/**
* 设置该用户的id
* @param id 新的id
*/
public void setId(long id) {
this.id = id;
}
/**
* 获得用户当前所在的房间号
* @return roomId
*/
public long getRoomId() {
return roomId;
}
/**
* 设置当前用户的所在的房间号
* @param roomId
*/
public void setRoomId(long roomId) {
this.roomId = roomId;
}
/**
* 设置当前用户在聊天室中的昵称
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 返回当前用户在房间中的昵称
* @return
*/
public String getName() {
return name;
}
/**
* 返回当前用户连接的socket实例
* @return
*/
public Socket getSocket() {
return socket;
}
/**
* 设置当前用户连接的socket
* @param socket
*/
public void setSocket(Socket socket) {
this.socket = socket;
}
/**
* 获得该用户的消息读取辅助类BufferedReader实例
* @return
*/
public BufferedReader getBr() {
return br;
}
/**
* 设置 用户的消息读取辅助类
* @param br
*/
public void setBr(BufferedReader br) {
this.br = br;
}
/**
* 获得消息写入类实例
* @return
*/
public PrintWriter getPw() {
return pw;
}
/**
* 设置消息写入类实例
* @param pw
*/
public void setPw(PrintWriter pw) {
this.pw = pw;
}
/**
* 重写了用户类打印的函数
*/
@Override
public String toString() {
return "#User"+id+"#"+name+"[#Room"+roomId+"#]<socket:"+socket+">";
}
}
5 系统实现
在服务器未连接的情况下会有提示:
即使输入昵称,也无法使用聊天功能:
会出现提示,先连接服务器:
服务器连接成功会有提示:
此时输入昵称就可以创建房间:
为了效果明显,设置三个客户端:
左侧显示房间数,右侧显示房间内人数,中间显示聊天内容,发送系统消息
发送表情即为发送表情序号:
退出房间会有提示并且有系统消息:
6结论和心得
本组的聊天室项目设计,具有一些基础的功能,比较简单易懂,实现了基础的交流功能。通过不断地修改代码,直到可以实现,我们内心都是激动雀跃的,这离不开大家的细心和耐心,以及其他组的同学的分析,最重要的还是老师的前期指导和给我们打的预防针,让我们可以提前考虑好,谨慎前行。但是还有很多功能并未加入本组项目,还是有点小遗憾的,技术有限,以后多多学习,不能骄傲。
学生1姓名:温艳珍
心得:通过本项目的学习与制作,我知道了自己还是井底之蛙,要学的还有很多,但同时,这个项目也让我成长了不少,java基础也掌握了一些,通过查资料,小组互动,有了一定的团队协作能力,也知道了团队的重要性。我们都对彼此有了信心,会相互鼓励,越来越好。
学生2姓名:张倩
心得::对Eclipse开发环境更加熟悉,积累了一点Java开发的经验,同时发现自己还存在很多问题,学习不踏实,经过我们组内讨论解决了一些问题,完成这次实验,有点满足感。
学生3姓名:李佳路
心得:通过实训的阶段二,我对前面的知识点理解的更加透彻了,对前面的知识也进行了一定的复习。
总体,第二阶段,对我java的整体理解提高了不少。尤其是流的运用,这个非常重要!希望我可以再接再厉!