一般来说,聊天工具大多数由客户端程序和服务器程序,外加服务器端用于存放客户数据的数据库组成,本系统采用客户机/服务器架构模式,通过Java提供的Socket类来连接客户机和服务器并使客户机和服务器之间相互通信,由于聊天是多点对多点的,而Java提供的多线程功能,用多线程可完成多点对多点的聊天。数据库管理系统用SQL Server2000,完成并通过JDBC-ODBC桥访问数据库。聊天系统完成后将可进行多人对多人的聊天,对好友进行添加、删除,对新用户的注册,发送消息、接受消息,传输文件等功能。界面设计细分化,方便使用者操作和理解。服务器实现了查询和修改等功能,程序逻辑联系较紧密。
3.1 总体构架
基于Java的网络聊天系统设计与实现主要考虑三个个方面的设计,即服务器模块设计,服务器客与客户端通信规则设计和客户端模块设计,本系统整体构架如图3.1所示。
3.2 系统整体功能
基于JAVA的网络聊天系统分为服务器端和客服端,其中:
服务器端包括:在线用户管理模块,用户管理模块,部门管理模块,日志管理模块;
客服端包括:用户认证模块,主界面模块,聊天模块,文件传输模块。
系统整体功能设计如下图2.2所示
3.3 系统模块功能设计
整个系统分为3个模块,其中主要功能如下:
(一)服务器端模块
服务器端模块首先设计多线程来处理客户端的连接,当受到客户端请求的时候,建立一个新的线程来处理客户端的连接。并且在一个注册中心中登记该线程,并存储客户端用户的一些信息,方便服务器统计在线用户,以便与这些在线用户进行通信。除此之外,为了方便用户更好的操作和掌握服务器端,系统还设计四个管理服务器的模块,分别是在线用户管理模块,用户管理模块块,部门管理管理模块和日志管理模块。各个模块功能设计如下:
- 在线用户管理模块
该模块主要实现在线用户管理,查看当前登陆账号的所有用户信息(在线用户的)。并且管理员有权限设置用户的在线状态,可以强制用户下线。同时添加一些辅助的功能,比如服务器发送一些系统公告信息等,这样有利于系统消息的及时发送。
- 用户管理模块
该模块主要实现新用户的添加、注册用户的删除、修改和查看的功能,管理数据库中存储的用户信息。该模块可以对注册用户的信息(注册用户的编号,姓名,性别,头像,年龄,家庭住址,联系电话,注册时间爱你)进行修改以及查看,并且保证客户端使用这些用户信息登陆服务器。
- 部门管理模块
该模块主要实现部门的添加、注册部门的删除、修改和查看的功能,管理数据库中存储的部门信息。该模块可以对注册部门的信息(部门名称,部门描述)进行修改以及查看。
- 日志管理模块
日志管理模块主要实现服务器运行状态信息,以及注册用户登陆的一些信息进行记录,管理员可以在需要的时候查看日志,监控服务器的一些状态,以及客户端一些用户的状态。
(二)协议规则模块
该模块主要实现服务器与客户端之间通信规则的制定。其具体思路是:在服务器使用Socket通信的时候,把服务器与客户端通信的信息封装为一个类对象,然后通过自己手动将这些类对象转化为输入流,并在另一端输出流,根据自己制定的规则逆序解析流,把流转化为类对象。实现服务器与客户端之间的通信。其中具体的表示信息要自己制定,比如登陆成功标示,失败标示,修改密码标示,聊天标示等,这些都需要自己通过一些数字标记。
(三)客户端模块
客户端模块主要实现,用户账号到服务器的认证,以及登陆之后与其他用户通信或者部门通信,除了简单文本聊天之外,也考虑用户之间文件传输的功能。其中客户端之间的通信,是在客户端之间搭建服务器,也即客户端自己建立SocketServer,并用多线程来处理对不同用户的聊天。用户之间文件传输也是通过搭建客户端之间的服务器来实现文件出传的,不需要通过服务器来实现信息的传输,可以减少延迟,也减少了服务器端的流量损失。但是在客户之间搭建连接的时候仍然需要通过服务器来实现一些简单的通知操作,实现客户端与客户端之间搭建其连接。因此客户端大体分四个模块,分别是用户认证模块,主界面模块,聊天模块和文件传输模块。各个功能模块设计如下:
- 用户认证模块
可以模仿QQ登陆界面,输入用户名和密码,以及服务器IP和端口,实现账号和密码到服务器的认证,判断用户是否有权限登录主界面进行信息沟通。
- 主界面模块
该模块主要实现类似QQ友好的界面,查看自己的信息,修改密码,显示好友列表组列表信息。使用Swing现有组建扩展制作合适的界面设计,并搭配后台功能。
- 聊天模块
该模块主要实现不同用户之间信息的发送,如何实现点对点用户通信,多点用户通信,要考虑多线程的设计才可以实现。同时信息发送除了文本之外还要考虑其他信息格式比如图片之类。
- 文件传输模块
实现不同用户之间多线程的文件传输。
4.1 系统E-R图
(1) E-R模型
基于网络聊天系统数据库E-R模型,如图 所示:
4.2 数据字典
(1) 用户表 (用户ID(主键),用户名,密码,用户密码,性别,头像ID,部门ID,年龄,电话号码,地址,注册时间,状态)。
4.3 各个数据表的创建
基于网络聊天系统数据库包括部门信息以及用户信息,部门信息表格和用户信息表格的设计结果如表4.1,表4.2所示:每个表格表示在数据库中的一个表。
表4.1部门信息表(T_DEPARTMENT)
列名 | 数据类型 | 可否为空 | 字段名称 | 字段大小 |
D_ID | VARCHAR | No null | 分组ID | 3 |
D_NAME | VARCHAR | √ | 分组名字 | 50 |
D_REMARK | VARCHAR | √ | 标记 | 100 |
表4.2 用户表(T_USER)
列名 | 数据类型 | 可否为空 | 字段名称 | 字段大小 |
U_ID | VARCHAR | Not null | 用户ID | 6 |
U_NAME | VARCHAR | √ | 用户名 | 50 |
U_PASSWORD | VARCHAR | √ | 用户密码 | 20 |
U_SEX | VARCHAR | √ | 性别 | 3 |
U_ICONID | INT | √ | 头像ID | 4 |
U_DEPTID | VARCHAR | √ | 部门ID | 3 |
U_AGE | INT | √ | 年龄 | 4 |
U_TEL | VARCHAR | √ | 电话号码 | 20 |
U_ADDRESS | VARCHAR | √ | 地址 | 100 |
U_REGTIME | DATETIME | √ | 注册时间 | 8 |
U_ISONLINE | INT | √ | 状态 | 4 |
4.4 数据库的连接
打开控制面板,找到管理工具,在选中数据源配置,进行ODBC数据源的配置其中主要的配置流程如下面图所示:
5.1 服务器功能模块的设计
5.1.1 Socket和ServerSocket介绍
Socket,简称套接字,用于实现网络上客户和服务器之间的连接。也就是说网络上两个或两个以上双工方式通信的进程之间总有一个连接,这个连接的端点成为套接字,套接字是在比较低的层次上通信的。
具体的说:一个服务器应用程序一般侦听一个特定的端口等待客户端的连接请求,当一个连接请求到达时,客户端和服武器端建立一个通信连接,在连接过程中,客户端被分配一个本地端口与一个socket建立连接,客户端通过写socket来通知服务器,以读socket中的信息,类似的服务器也获得一个本地端口,它需要一个新的端口号来侦听原始端口上的其他连接请求。服务器也通过它的本地端口连接一个socket,通过读写和客户端通信。
Socket程序的工作过程:
1、建立Socket连接:在通信开始之前由通信双方确认身份,建立一条专用的虚拟连接通道。
2、数据通信:利用虚拟连接通道传送数据信息进行通道。
3、关闭:通信结束时,再将所建的虚拟连接拆除。
实现套接字的服务端,需要使用ServerSocket 类。ServerSocket类是服务器程序的运行基础,它允许程序绑定一个端口号来监听客户端的请求,一旦产生客户端请求,它将接受这一请求,同时产生一个完整的Socket 连接对象。服务器绑定的端口必须公开,以便让客户端程序知道如何连接这个服务器。同时,作为服务器,它必须能够接收多个客户的请求,这就需要为服务器设置一个请求队列,如果服务器不能马上响应客户端的请求,要将这个请求放进请求队列中,等服务器将当前的请求处理完,会自动到请求队列中按照先后顺序取出请求进行处理。服务器的资源是有限的,这就导致它的最大连接数是有限的,通过ServerSocket 的构造函数可以指定这个最大连接数。如果不明
确指定这个连接数,默认最大连接数为50,也就是说,客户端的请求队列最大可以容纳50 个请求,当超过这个最大连接数时,用户的请求将不再会被响应。 利用SocketServer 也提供了一些方法,它们主要有:
accept() 返回一个“已连接”的Socket 对象
getInetAddress() 得到该服务器的IP 地址
getLocalPort() 得到服务器所侦听的端口号
setSoTimeout() 设置服务器超时时间
getSoTimeout() 得到服务器超时时间
服务器和客户端通过Socket简单通信框架下图所示。
5.2 服务器功能模块的实现
5.2.1 多线程服务器模块实现
服务器模块核心功能是使用Socket Server实现多线程的服务器,针对每一个客户建立一个单独的线程处理客户端的请求。其具体思路是,首先建立Socket Server,并绑定服务器IP和某个未使用的端口。然后监听该端口,如果有客户端的Socket连接则建立一个客户端线程ClientThread类对象,有该对象处理该客户的一些请求,并在ClientThread中保存User Bean对象,该对象保存了用户的一些基本信息,比如账号、密码、年龄、是否在线等。如果服务器没有关闭Socket Server则服务器则一直处理客户端连接。其核心代码如下。
public class MessageServer extends Thread {
//服务器端SocketServer
private ServerSocket server;
private boolean isStop = false;
public MessageServer(int port) throws IOException {
server = new ServerSocket(port);
}
/**
* 消息服务
*/
public void run() {
Socket client;
try {
while (!isStop) {
//建立处理客户端连接的线程
client = server.accept();
(new ClientThread(client)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
try {
server.close();
} catch (IOException e1) {
// e1.printStackTrace();
}
}
isStop = true;
}
}
}
5.2.2 在线用户管理模块实现
该模块主要实现在线用户统计、多线程服务器的启动关闭、以及服务器发送系统公告等功能。其中在线用户统计是在客户端每次连接的时候服务器建立了ClientThread来单独处理某个客户请求,如果该客户登陆成功的话则在工具类PubValue中一个HashMap添加该用户信息,查看在线用户信息的时候,可以通过工具类PubValue来的HashMap读取在线用户列表。系统公告则是通过服务器根据在线用户列表,广播系统公告到每个客户端,实现信息的公布。其实核心代码如下:
private void login(Message message) throws IOException {
if (message.getType().equals(PackOper.LOGIN)) {
User user = (User) message;
List list = UserDaoFactory.getUserDao().selectId(user.getId());
if (list.size() > 0) {
User tempUser = (User) list.get(0);
if (tempUser.getPassword().equals(user.getPassword())) {
if (tempUser.getIsOnline() == Parameter.ONLINE) { message.setType(PackOper.LOGIN_ONLINED);
myObjectOut.writeMessage(message);
this.setStop();
return;
}
ServerUI.getInstance().getLogUI().addLog(
LogOper.getInstance().insertOnLineLog(tempUser));
//登录成功后
//先通知所有人
//再通知此用户
//再把自己加到线程组中
//接着更新数据库
//下载树给此用户
tempUser.setType(PackOper.LOGIN_SUCCEED);
tempUser.setIsOnline(pub.Parameter.ONLINE);
this.chatCompany(tempUser);
this.user = tempUser;
this.myObjectOut.writeMessage(tempUser);
PubValue.addUserThread(this.user.getId(), this);
UserDaoFactory.getUserDao().setOnline(tempUser.getId(),
Parameter.ONLINE);
ServerUI.getInstance().getOnLineUI().updateOnLine();
try {
Thread.sleep(10);
} catch (InterruptedException e1) {
// e1.printStackTrace();
}
// 登录成功后发送树
PubValue.company.setType(PackOper.ADD_COMPANY);
myObjectOut.writeMessage(PubValue.company);
list = DepartmentDaoFactory.
getDepartmentDao().select(null);
Iterator it = list.iterator();
while (it.hasNext()) {
message = (Message) it.next();
message.setType(PackOper.ADD_DEPARTMENT);
myObjectOut.writeMessage(message);
}
list = UserDaoFactory.getUserDao().select(null);
it = list.iterator();
while (it.hasNext()) {
message = (Message) it.next();
message.setType(PackOper.ADD_USER);
myObjectOut.writeMessage(message);
}
return;
}
}
message.setType(PackOper.LOGIN_DEFEATED);
myObjectOut.writeMessage(message);
this.setStop();
}
}
其实现效果如图所示:
5.2.3 部门管理模块实现
该模块主要实现对部门进行管理,根据情况添加,删除或者修改部门等。其设计主要是通过SQL语句来操作数据库数据的。跟普通的管理系统设计思路一样。没有太多的算法,主要是SQL语句的构造,以及数据库操作的API函数的使用。其实现效果如图所示.
5.2.4 用户管理模块实现
该模块类似于部门管理模块的设计,主要是针对用户的一些信息进行管理,根据需要添加、删除或者修改用户信息。其实现主要也是SQL语句的构造以及JDBC驱动提供函数的使用。比较复杂的部分设计主要体现界面的设计,如何使用Swing构造友好并且易于交互的界面需要花费一些时间。以及实现组合条件的用户查询,可以根据用户账号查询,或者所在部门查询,或者编号查询,或者根据这三个条件组合查询,这一功能比较复杂,如何根据不同的情况构造合适的SQL语句进行数据查询,显得很重要,否则粗略的设计会增加很多冗余代码。其整体实现效果如下图5.4所示。
5.2.5日志管理模块实现
日志管理模块主要是监控服务器的一些状态,以及客户端用户登陆的情况。这些日志信息记录是通过工具类LogOper实现日志的记录。该类使用了设计模式中单例模式,保证服务器端在进行日志操作的时候只有一个实例在内存中进行日志的记录。其日志的存放路径主要是在系统目录下,其文件名为server.log。通过服务器UI界面查询日志信息也是通过读取该文件来查询日志记录的。其实现效果如下图5.5:
5.3协议设计与实现
5.3.1 协议规则
协议规则里主要用一些整数来表示服务器与客户端通信的时候包的类型。服务器可以按照这个规则来解析包,并根据包中的信息做出相应的操作。其标示的具体含义和定义规则下表5.1所示。
表5.1 协议规则表
标示名字 | 标示值 | 标示意义 |
LOGIN | 10 | 登录包标识 |
LOGIN_SUCCEED | 11 | 登录成功标识 |
LOGIN_DEFEATED | 12 | 登录失败标识 |
LOGIN_ONLINED | 13 | 已经在线标识 |
ADD_COMPANY | 14 | 添加公司标识 |
ADD_DEPARTMENT | 15 | 添加部门标识 |
ADD_USER | 16 | 添加用户包标识 |
UPDATE_COMPANY | 17 | 更新公司信息标识 |
UPDATE_DEPARTMENT | 18 | 更新部门信息 |
UPDATE_USER | 19 | 更新用户信息 |
DELETE_DEPARTMENT | 20 | 删除部门 |
DELETE_USER | 21 | 删除用户 |
CHAT_USER | 22 | 私聊 |
CHAT_DEPARTMENT | 23 | 部门聊天 |
CHAT_ALL | 24 | 公司聊天 |
MESSAGE | 25 | 公司通知 |
UPFILE | 26 | 要求传送文件 |
UPFILE_FIAT | 27 | 许可传送文件 |
SEND_DEFUSE | 28 | 发送方取消 |
UPFILE_DEFUSE | 29 | 不许可传送文件 |
DOWN_LINE | 30 | 下线包 |
FORCEDOWN_LINE | 31 | 强制下线包 |
UPPASSWORD | 32 | 修改密码 |
UPPASSWORD_DEFEATED | 33 | 原密码不对 |
UPPASSWORD_NEW_NULL | 34 | 新密码为空 |
UPPASSWORD_SUCCEED | 35 | 修改密码成功 |
SERVERCLOSE | 36 | 服务器关闭 |
5.3.2 协议实现
协议的指定主要是考虑在进行通信的过程中,保证服务器和客户端的通信消息能够被双方正确的理解,并作出相应的处理,比如登陆失败消息,登陆成功消息,文件传输消息,聊天消息等,服务器和客户端必须要按照指定的规则去打包和解析消息。其中协议的设计是通过对Socket流进行自定义的封装,把已经定义好的对象的每一个属性按照unicode编码中的一些特殊的编码\u0000(空格)进行分隔封装,并发送输入和输出流,另一端安装封装的逆序把输出流封装为对象,并识别该对象随对应是什么样的消息,客户端或者服务器作出相应的处理。而通过继承输入输出流实现了服务器器信息的发送,但是具体的包类型是什么样的消息是通过标示来标记。通过标示来解析这些包是什么样类型的包,实现服务器和客户端之间可理解的通信。其核心类PackOper类主要代码如下:
/**
* 解包类
* @author Administrator
*/
public class PackOper implements PackInterface {/**
* 创建一个包,根据object类型
* @param object
* @return
*/
public byte[] createPackage(Object object) {
if (object == null) {
return null;
}
if (object instanceof MessagePack) {
return createMessagePack((MessagePack) object);
}
if (object instanceof User) {
return createUser((User) object);
}
if (object instanceof Department) {
return createDepartment((Department) object);
}
if (object instanceof Company) {
return createCompany((Company) object);
}
return null;
}
/**
* 创建消息包
* @param pack
* @return
*/
private byte[] createMessagePack(MessagePack pack) {
return createByte(STARTSEPARATOR + pack.getType() + SEAS
+ pack.getFrom() + SEAS + pack.getFromIP() + SEAS
+ pack.getFromPort() + SEAS + pack.getTo() + SEAS
+ pack.getMessage() + ENDSEPARATOR);
}
/**
* 解一般消息包
* @param strs
* @return
*/
private Message unbindMessagePack(String[] strs) {
MessagePack messagePack = null;
if (strs != null) {
try {
messagePack = new MessagePack();
messagePack.setType(strs[0]);
messagePack.setFrom(strs[1]);
messagePack.setFromIP(strs[2]);
messagePack.setFromPort(Integer.parseInt(strs[3]));
messagePack.setTo(strs[4]);
messagePack.setMessage(strs[5]);
} catch (Exception e) {
return null;
}
}
return
}
}
MyObjectInputStream.java,该文件继承输出流,并自定义解析输出流的规则。Unit包里封装了所有消息类封装Bean。MyObjectInputStream.java,该文件继承输入流,并自定义解析输入流的规则。PackOper.java该文件主要实现了,Socket发送字节流的解包规则,并转化字节流为对象。也即服务器实现客户端不同消息识别,服务器端并作出相应处理。
5.4客户端功能模块的设计与实现
5.4.1 登陆认证模块
该模块主要实现用户的登陆认证,以及服务器代理IP和端口的设置,并检查数据的合法性。主要是参考现在通用即时聊天工具QQ界面进行设计,没有采用具体的算法实现漂亮的算法,只是截取QQ界面的图片作为自己设计界面的背景。同时用户登录身份认证使用简单模拟的QQ协议通过发送登陆验证包消息给服务器,服务器解包之后查询数据库检查用户身份的合法性,并发送给客户端认证成功或者失败包,客户端解包之后如果认证成功则登陆成功,显示主界面,否则提示用户登陆失败,提示用户重新登陆。并且可以根据服务器IP和端口的变化进行动态设置服务器地址,具有很好的灵活性。具体实现效果如下图5.6所示。
未完待续。。。