基于JAVA的聊天工具开发
转眼大四,在紧张的考研备战间隙,我开始整理大学三年来的学习资料与感悟,希望与大家分享一些我的总结与感悟。
以下的报告是大二下学期参加计算机学院工程训练后所写。这个小项目很简单,主要涉及三大方面:网络编程、数据库访问、图形界面。但我认为它是“麻雀虽小五脏俱全”,一是包含的内容比较丰富,便于新手练习,二是设计较为巧妙,包含了“信封模式”,“策略模式”,“模拟对象与桩”等设计模式,对于初学者与高年级学生来说都很有借鉴意义。
因为当时还是大二学生,文笔难免青涩,一些表述还不够专业,我希望大家能指出我的不足与错误,大家共同进步!
因为排版的一些原因,文章图片丢失,如果有兴趣,可以免费下载word文档。
文档连接:
http://download.csdn.net/detail/solomon1558/7947025
项目源代码连接:
http://download.csdn.net/detail/solomon1558/8106463
/**********************************************************************************************************************************/
前言:
写作目的:
本届工程训练历时两周,行将结束。本报告将以项目的进展顺序架构,梳理项目层次,整理项目思路,从而加深对项目的理解,提出自己的感悟与思考。
行文思路:
本次工程训练主要涉及3大方面:网络编程、数据库访问、图形界面。我将以这3方面综合项目进度组织报告的层次、内容。在正文部分力求抓住主要问题,阐明项目的思路与具体实现,做到层次清晰,简洁准确。同时我还会在附录部分针对项目中提到的知识、模式加以补充,达到拓展思维、深刻认识的效果。
第一章. 网络编程
1.1 C/S结构的编写
作为一个IM软件,最基本的工作就是编写client与server。本项目采用基于TCP的Socket进行两个程序之间的双向通信。其基本工作原理如下:
•在服务器端通过指定一个用来等待的连接的端口号创建一个ServerSocket实例。
•在客户端通过规定一个主机和端口号创建一个socket实例,连到服务器上。
•ServerSocket类的accept方法使服务器处于阻塞状态,等待用户请求。
# 我们需要考虑的问题有:
两个或多个程序之间的通信,涉及到请求和回复,并且通信所传输的内容根据请求的不同而不同。因此在实际设计、编程时要考虑的复用问题与多线程。
为了解决以上问题,我们在客户端与服务器端建立一个通道,用来传输可串行化(Serializable)的对象,这个对象有两个参量:类型与对象的内容。
因而我们需要定义两个继承了Serializable接口的实体类:RequsetObject和 ResponseObject。这两个实体类具有两个私有成员:int型的xType(表示请求/回复类型),Object型的xBody(表示内容)。这两个实体类都有set(),get(),toString()方法和构造函数。
# 编程实现:
client端的网络编程初步编写非常简单:只需new一个Socket 类的对象server,规定一个主机和端口号,连到服务器上。
实际上,我们对client端的要求很简单:就是信息的发送与接收,并不涉及复杂的处理过程。一种较好的方法是在类中写一个返回类型为ResponseObject的静态方法:sendRequest(),并将输入输出封装为ObjectInputStream 、
ObjectInputStream 对象,将对象串行化[1]。在这个静态方法中创建Socket实例与服务器连接,并在对象流中write 请求,read 回复,即send 与receive功能。
这种设计称作“信封模式”,或“门面模式”(Façade Pattern)。[2]其优点是:建立连接、发送、接收对象等操作被封装到门面方法sendRequest()中,使用时直接调用门面的方法就可以与服务器通信,不用了解具体的实现方法以及相关的业务顺序。
以下是NetAccessHelper类的主体框架。
public static ResponseObject sendRequest(RequestObject reqObject) {
ResponseObject resObject = null;
try {
// 1.connect to the server
// 2.get oos,ois
// 3.send
// 4.receive
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return resObject;
}
现在考虑服务器端:通过指定一个用来等待连接的端口号创建一个ServerSocket实例,ServerSocket类的accept方法使服务器处于阻塞状态,等待用户请求。ChatServer 类的代码如下:
public class ChatServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(10000);
System.out.println("Server is running....");
Socket clientSocket;
WorkerThread worker;
for (;;) {
clientSocket = serverSocket.accept();
worker = new WorkerThread(clientSocket);
worker.start();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1.2多线程创建
在for循环中,每有一个客户端的连接,就实例化一个WorkerThread对象。WorkerThread类有两个私有的成员变量 clientSocket , handler,构造函数和run()函数。
此处采用了“策略模式”(Strategy Pattern)[3]:
*需要定义一个策略接口RequestHandler,其后的实现类都继承这个接口。
*从对象流中读取请求信息RequestObject requestObject =(RequestObject) ois.readObject();
*因为RequestObject/ResponseObject 实体都含有 X-Type,X-Body的私有成员变量。使用switch case对ReqType进行判断,选相应的实现类X-RequestHandler的构造函数初始化RequestHandler 类型的对象handler。
*最后调用实现方法,将返回对象赋予responseObject,将其write进对象流。
采用策略模式的好处在于:具有高内聚低耦合的特点,还有很好扩展性,也就是OCP 原则,策略类可以继续增加下去,这对支持后续的多种请求-响应非常重要。
1.3实体类
请求/回复
为了支持策略模式,实现不同请求的响应回复,创建实体类RequestObject,
ResponseObject类是有必要的。
这两个类为了使其对象可串行化,需要实现Serialization接口。它们都有静态成员变量表示状态,int型 reqType、Object 型 reqBody 的私有成员变量,以及get(),set(),toString() 和构造函数。
package entity;
import java.io.Serializable;
public class RequestObject implements Serializable {
public static final int XXXX = 999999;
. . . . . .
private int reqType;
private Object reqBody;
public RequestObject(int reqType, Object reqBody) {
super();
this.reqType = reqType;
this.reqBody = reqBody;
}
getXXX(){} …
setXXX(){}…
toString(){}
}
第二章.数据库的建立与访问
2.1创建数据库
使用Navicat for MySQL,建立数据库chatdb,新建contacts表,chatstore表。
contaccts表存储用户信息。属性分别uid , uname , email , age , pass ,online,peerip , peerport ,其中uid是主键。如下图所示:
chatstore表存储会话信息,属性:chatid , senderid ,receiverid , content , sendtime, 主键chatid。
2.2数据库的访问
为了将数据库的访问、查询封装起来,我们需要写一个DBAccessHelper类。这个类成员有:
*私有类成员 dao,只能实例化一次。
*私有构造函数 DBAccessHelper(),加载驱动。
*公共类方法 getDao(),检查类对象是否实例化,返回类DBAccessHelper对象dao。
*私有成员方法 getConnection() , 返回Connection类型对象。将连接连接到MySQL的url以及pass、name作为参数传入DriverManager类getConnection方法中,建立与数据库连接。
*成员方法excute(),无结果返回的查询。
*成员方法excuteQuary(),有查询值返回。需要ResultSet rs作为结果集,返回结果集对象rs。
public class DBAccessHelper {
private static DBAccessHelper dao;
// 加载驱动
private DBAccessHelper() { // private构造函数
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 希望只执行一次
public static DBAccessHelper getDAO() {
if (dao == null)
dao = new DBAccessHelper();
return dao;
}
private Connection getConnection() {
Connection conn=null;
String url = "jdbc:mysql://localhost:3306/chatdb";
String pass = "1992919";
String name = "root";
try {
conn = DriverManager.getConnection(url, name, pass);
System.out.println(conn);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return conn;
}
//无结果返回的查询execute()
public void execute(String sqlString) {
try {
Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sqlString);// 变量在try外初始化
stmt.execute();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 查询语句
// excuteQuary有返回值,execute返回值!
public ResultSet executeQuery(String sqlString) {
ResultSet rs = null;
try {
Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sqlString);// 变量在tyr外初始化
rs=stmt.executeQuery(); //出错了,打印成了.execute
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return rs;
}
}
现在写一个测试类DAOTest,测试数据库的连接。
将sql插入语句以String类型传入DBAccessHelper类的excute()方法中,该方法由私有类方法getDAO()返回的对象dao调用。
String sqlString = " Insert into contacts (uid,uname,age,pass,email)"
+"values("+uid+",'"+tempString+"',21,'1234','[email protected]')";
DBAccessHelper.getDAO().execute(sqlString);
将contacts数据库中用户信息全部显示,使用excuteQuery()方法。
sqlString = "select uid,uname,age,email,online from contacts";
System.out.println(sqlString);
ResultSet rs = DBAccessHelper.getDAO().executeQuery(sqlString);
第三章.图形化界面
在网络编程,数据库建立、访问工作完成后,现在进行图形化界面的编写。
图形化界面主要包括:注册界面、登录界面、主界面、对话框。
这里所涉及的功能较多,比如访问、操作数据库,从数据库插入、提取信息,信息刷新,端与端用户间的离线消息、即时通讯等等,每一个问题又有很多具体的子问题需要逐步去完善。根据增量开发的原则,作者将不在开头部分将所有的设计细节一一罗列,而是以项目的进程分别阐述。
3.1 注册界面
3.1.1 简述
注册界面工作流程:
内部处理流程:
由以上流程图可以十分清晰的看到注册流程在程序中的对应关系,这一部分新加入的类有:
public class RegForm extends javax.swing.JFrame {}
public class RegRequestHandler implements RequestHandler {}
public class RegInfo implements Serializable {}
3.1.2 程序实现
实体类 RegInfo中含有用户注册信息:私有成员变量uid,uname,email,age,password 及成员方法set(),get() ,toString()。RegInfo继承了Serializable 接口,是一个可串行化的类。
RegForm类是注册功能的主要部分,它继承了javax.swing.JFrame。
如下图,登录界面有3个TxtField,4个Lable,2个 button,1个passwordfield。
RegInfo类中主要的成员方法是:
btnRegisterMouseClicked(java.awt.event.MouseEvent evt){}
*当单击事件发生后,首先实例化一个RegInfo类的对象regInfo,将文本框中的信息set进来:
RegInfo regInfo = new RegInfo();
regInfo.setUname(this.nametxt.getText());
regInfo.setEmail(this.emailtxt.getText());
int age = Integer.parseInt(this.agetxt.getText());
regInfo.setAge(age);
String passString = new String(this.password.getPassword());
regInfo.setPassword(passString);
*将RegInfo实体对象regInfo与请求信息封装为RequesetObject类型对象。
RequestObject requestObject = new RequestObject(RequestObject.REG_REQ,regInfo);
*客户端将requestObject发送到server端,经ReqRequetHandler处理并返回注册结果:失败或成功。
ResponseObject responseObject = NetAccessHelper.sendRequest(requestObject);
*提示框显示注册结果。
JOptionPane.showMessageDialog(this,(String)responseObject.getResBody());
接下来分析服务器端的处理过程:
通过WorkerThread 的switch-case,请求状态RequestObject.REG_REQ被识别。根据策略模式,这里实例化RequestHandler接口中的RegRequestObject类,并调用被覆盖的方法handleRequset(),最后将返回的responseObject写入对象流。
case RequestObject.REG_REQ:
handler=new RegRequestHandler();
break;
responseObject=handler.handleRequest(requestObject);
oos.writeObject(responseObject);
这里的处理工作由ReqReuqestHandler类完成。
ublic class RegRequestHandler implements RequestHandler {
public ResponseObject handleRequest(RequestObject requestObject) {
ResponseObject responseObject=null;
//0. get RegInfo
//1.validate
//2.assignid
//3.insert
//4.responseObjec
}
}
其中,判断email重复,设定uid,插入信息都有相应的成员方法:
DBAccessHelper私有类方法getDAO()返回的对象dao调用executeQuery(),将返保存入结果集对象rs中,统计相同email号的个数。
//1.validate 判断email是否重复
private boolean validated(String email) {
// TODO Auto-generated method stub
boolean isValid=false;
try {
String sqlString="select count(0) from contacts "
+" where email='"+email+"'";
System.out.println(sqlString);
ResultSet rs=
DBAccessHelper.getDAO().executeQuery(sqlString);
int count=0;
if(rs.next())
count=rs.getInt(1);
if(count==0)
isValid=true;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return isValid;
}
调用无结果返回查询execute(),向数据库中插入uid及regInfo
private void insertNew(int uid,RegInfo regInfo) {
// TODO Auto-generated method stub
String sqlString= "insert into contacts "+
"(uid,uname,age,email,pass) "+
"values("+uid+",'"+regInfo.getUname()+
"',"+regInfo.getAge()+
",'"+regInfo.getEmail()+
"','"+regInfo.getPassword()+"')";
System.out.println(sqlString);
DBAccessHelper.getDAO().execute(sqlString);
}
设置uid,如果数据库中的uid小于10000,新uid加1设为10001,否则以数据库中最大数为准,加1设为新uid。
private int assignUID() {
// TODO Auto-generated method stub
int uid=10000;
try {
String sqlString="select max(uid) from contacts";
ResultSet rs=DBAccessHelper.getDAO().executeQuery(sqlString);
if(rs.next()){
int maxid=rs.getInt(1);
if(maxid>uid)uid=maxid;
}
uid++;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return uid;
}
}
3.2 登录界面
3.2.1 LoginForm内部工作流程
3.2.2 程序实现
登录部分新加入的类:
publicclassLogForm extends javax.swing.JFrame {}
publicclassLoginRequestHandler implements RequestHandler {}
publicclassLoginfo implements Serializable {}
# 实体类Loginfo拥有私有成员变量 uid,password 以及get(),set()和toString()函数。
# LoginForm类是登录的主要部分:
& 其中,有一个构造函数public LogForm() {}用以初始化;
public LogForm() {
initComponents();
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
frame.setDefaultCloseOperation()是设置用户在此窗体上发起 "close" 时默认执行的操作。DISPOSE_ON_CLOSE(在 WindowConstants 中定义):调用任意已注册 WindowListener的对象后自动隐藏并释放该窗体。
& 私有成员方法btnLoginMouseClicked(java.awt.event.MouseEvent evt){},当button Login被单击事件发生,发送带有LogInfo请求,接收responseObject,并进行相关处理。
*实例化LogInfo对象logInfo,调用set()函数将登陆uid,pass存入实体对象logInfo中。
*将RegInfo实体对象regInfo与请求信息封装为RequesetObject类型对象。
RequestObject requestObject = newRequestObject(RequestObject.LOGIN_REQ, loginfo);
*客户端将requestObject发送到server端,经LoginRequetHandler处理并返回responseObject。
ResponseObject responseObject = NetAccessHelper.sendRequest(requestObject);
*如果responseObject.getResType() == ResponseObject.LOGIN_FAILED
提示框:"Log Failed!Check your uid/pass!"
*否则,实例化MainForm对象,调用setVisible(true)。
public LogForm() {
initComponents();
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
}
& 私有成员方法btnRegisterMouseClicked(java.awt.event.MouseEvent evt){}
当被单击事件发生,实例化RegForm的对象,并调用setVisible
RegForm regForm = new RegForm();
regForm.setVisible(true);
因为在Server端采用策略模式,每一种请求在服务器端的处理过程相似,只是策略方法不同:
# 服务器端策略方法LoginRequestHandler()
*从client端来的请求实体中得到登陆的uid,password
Loginfologinfo=(Loginfo)requestObject.getReqBody();
int uid=loginfo.getUid();
String pass=loginfo.getPassword();
*使用有结果返回查询,统计uid与对应password在数据库中的个数,如果个数为1,isOK为true。
String sqlString="select count(0) from contacts where uid="+uid+" and pass='"+pass+"'";
ResultSet rs=DBAccessHelper.getDAO().executeQuery(sqlString);
boolean isOK=false;
if(rs.next()){if(1==rs.getInt(1))isOK=true; }
*如果isOK为true,更新该用户online信息为1,返回回复实体,实体信息为ResponseObject.LOGIN_SUCCESS。
if(isOK){
sqlString="update contacts set online=1 where uid="+uid+" and pass='"+pass+"'";
System.out.println(sqlString);
DBAccessHelper.getDAO().execute(sqlString);
responseObject =
newResponseObject(ResponseObject.LOGIN_SUCCESS,null); }
*如果isOk为false,返回
responseObject=newResponseObject(ResponseObject.LOGIN_FAILED,
"Log Failed!Check your uid/pass!")
3.3主界面MainForm
聊天工具主要完成两项功能:离线消息的发送、接受;在线聊天。
主界面的编写也按照实现离线消息,在线聊天的次序进行。
3.3.1 离线消息功能分析
实现离线消息的发送、接收,主要流程:
3.3.2 具体实现
其中每一部分又有细节部分需要完善,现分类讨论:
# 刷新部分 refreshContact(BooleanisInit)
&此方法首先将经服务器端InitRequestHandler返回的responseObject
放入vector容器allContacts中。
RequestObject requestObject =newRequestObject(RequestObject.INIT_REQ,null);
ResponseObject responseObject =
NetAccessHelper.sendRequest(requestObject);
VectorallContacts=(Vector)responseObject.getResBody();
&随后将容器allContacts中数据拷贝到列表contactsModel中。这个过程中有一个排序:将自己置于联系人首行,并将在线好友排在前面。
*设置intonidx=1,offidx=size-1;
两个DefaultListModel类型对象contactsModel,oldModel。
*用for循环遍历容器:
if(!isInit) {oldContact= getContactByuid(tmpUid,oldModel);
contact.setSender(oldContact.isSender());}
其中,getContactByuid(intuid,DefaultListModel Model)方法的功能是通过参数uid找到列表中索引从而找到相应的实体对象contact。实际上就是当没有初始化时,刷新后的列表的信息与之前旧的列表是一样的。
if(tmpUid==currUID)
//将自己置于首行0号位置;
if(contact.getOnline()==1)
{contactsModel.setElementAt(contact, onidx);onidx++}
//好友在线,将实体与索引存入contactsModel中,onidx索引递增。
else{contactsModel.setElementAt(contact,offidx);offidx--;}
//最后,将排好序的contactsModel放入ListContacts中。
# 离线消息初始化 public voidinitOfflineMsgs()
& 此方法首先将经服务器端InitMsgsRequestHandler返回的responseObject放入vector容器allChats中。
& 实例化3个Contact类型的实体对象:tmpContact , gfContact , bfContact .
其中,将currUid 设置为tmpContact的uid。curruid从Loginfo中得到(即登陆者自己)。再通过“模拟对象与桩“[4]的方法,通过tmpContact在联系人列表中的索引找到登陆者自己:实体对象bfContact:
tmpContact.setUid(currUID);
int idx=contactsModel.indexOf(tmpContact);
ContactbfContact,gfContact;
bfContact=(Contact)contactsModel.elementAt(idx);
& 遍历allChats容器,从容器中得到每一个对话实体chatInfo,senderId,
将临时实体tmpContact的uid设为senderId,根据如上的方法间接地得到发送方实体gfContact,并将gfContact的isSender设为true。
chatInfo=(ChatInfo)allChats.elementAt(i);
senderId=chatInfo.getSenderId();
tmpContact.setUid(senderId);
idx=contactsModel.indexOf(tmpContact);
gfContact=(Contact)contactsModel.elementAt(idx);
gfContact.setSender(true);
& 调用getChatBox方法,给实体chatBox赋公共存储区boxRegistry中gfContacts实体的信息。如果chatBox为null,使用chatBox的构造函数实例化chatBox,并将实体bfContact、gfContact放入公共存储区哈希表boxRegistry,并chatBox调用appendMsg(chatInfo.getContent),显示离线消息。
gfContact.setSender(true);
chatBox=getChatBox(bfContact,gfContact);
chatBox.appendMsg(chatInfo.getContent());
MainForm类中其他的成员方法:
privatevoidListContactsMouseClicked(java.awt.event.MouseEvent evt){ //排序后,双击自己将屏蔽
if(evt.getClickCount()==2){
if(ListContacts.getSelectedIndex()==0)return;
ContactbfContact,gfContact;
//获取gfcontact实体的信息
gfContact=(Contact)ListContacts.getSelectedValue();
Contact tmpContact=new Contact();
tmpContact.setUid(this.currUID);
int idx=this.contactsModel.indexOf(tmpContact);
bfContact=(Contact)contactsModel.elementAt(idx);
ChatBox chatBox=getChatBox(bfContact,gfContact);
//修改isSender状态
gfContact.setSender(false); //将发送者实体gfContact设为false
chatBox.setVisible(true);
}
}
当退出主界面时,修改online状态
privatevoidformWindowClosing(java.awt.event.WindowEvent evt) {
// TODO add your handling code here:
//传uid
RequestObjectrequestObject =newRequestObject(
RequestObject.LOGOFF_REQ,this.currUID +"");
//int 专为Integer//new Integer(value);
NetAccessHelper.sendRequest(requestObject);}
# 使用到的策略类
& public class InitRequestHandlerimplements RequestHandler {}
*查找contact信息,
StringsqlString="select uid, uname,age,email,online,peerip,peerportfrom contacts";
ResultSetrs =DBAccessHelper.getDAO().executeQuery(sqlString);
*实例化vector对象allContacts,调用set()函数赋值,并将实体contact添加进容器。
VectorallContacts=newVector();
while(rs.next()){
Contact contact= new Contact();
contact.set. . .
allContacts.add(contact);
}
*返回包含类型信息ResponseObject.INIT_RES和容器allContacts
responseObject=new ResponseObject(ResponseObject.INIT_RES,allContacts);
& public class InitMsgsRequestHandler implements RequestHandler {}
*调用查询语句,查找所有receiverid是登陆者uid(currUID)的记录。
StringsqlString="selectsenderid,sendtime,content,receiverid from chatstore where receiverid="+uid;
System.out.println(sqlString);
ResultSetrs=DBAccessHelper.getDAO().executeQuery(sqlString);
*实例化一个vector容器allChatInfos,将结果集rs中的chatInfo装入容器。
VectorallChatInfos=newVector();
while(rs.next()){
chatInfo=new ChatInfos();
chatInfo.set...();
...
allChatInfos.add(chatInfo);
}
*将已使用过的离线消息删除,并返回实体responseObject。
sqlString="delete from chatstore wherereceiverid="+uid;
DBAccessHelper.getDAO().execute(sqlString);
DBAccessHelper.getDAO().execute(sqlString);
responseObject=
newResponseObject(ResponseObject.ININ_MSGS_RES,allChatInfos);
publicclassLogoffRequestHandlerimplements RequestHandler {}
在MainForm类formWindowClosing(java.awt.event.WindowEventevt)方法中被使用,主要作用是当用户退出后在contact数据库将online改为0。
*从请求体中得到登陆者uid,使用无结果查询execute更改该用户的online为0,返回responseObject。
Stringuid=(String)requestObject.getReqBody();
StringsqlString="update contacts set online=0 whereuid="+uid;
DBAccessHelper.getDAO().execute(sqlString);
responseObject =
newResponseObject(ResponseObject.LOGOFF_RES,null);
# publicclassContactsListCellRendererimplements ListCellRenderer {}
主界面渲染器,对背景、文字、图片的处理。
*设置图标
mageIconoffIcon=newImageIcon("images/offline.jpg");
ImageIcononIcon=newImageIcon("images/online.jpg");
*设置标题栏,前景色,背景色
if(valueinstanceof Contact){
Contactcontact = (Contact)value;
int uid = contact.getUid();
Stringuname = contact.getUname();
StringlableText = "<"+uid+">"+uname;
if(isSelected){
cellComp.setIcon(onIcon);
cellComp.setForeground(list.getSelectionForeground());
cellComp.setBackground(list.getSelectionBackground());
}
if(contact.getOnline()==1){ cellComp.setIcon(onIcon);
cellComp.setForeground(Color.GREEN);
cellComp.setBackground(list.getBackground());
}else{
cellComp.setIcon(offIcon);
cellComp.setForeground(Color.RED);
cellComp.setBackground(list.getBackground());
}
if (contact.isSender()) {
lableText=lableText+" ";
cellComp.setForeground(Color.BLUE);}
if(isSelected){
cellComp.setForeground(list.getSelectionForeground());
cellComp.setBackground(list.getSelectionBackground());}
3.4 对话框ChatBox
3.4.1 分析
ChatBox类用来发送对话框中的信息,显示对话消息。在实现离线功能与在线聊天功能时的区别只是依据online信息在消息发送方法有所不同,添加一个一个if判断语句即可。
故首先介绍实现离线消息的发送、信息显示需要做的工作:
*一个公有构造函数用以初始化。
public ChatBox(ContactbfContact, Contact gfContact)
*私有成员方法btnSendMouseClicked()
privatevoidbtnSendMouseClicked(java.awt.event.MouseEvent evt)
当单击Send button事件发生时,文本框中消息依据online状态选择发送的方式。
3.4.2具体实现
# 消息的发送方法btnSendMouseClicked()
*实例化ChatInfo实体对象,将登陆者uid设为SenderId,消息接受者对象
(gfContact)的uid设为receiverid,然后和发送时间,发送内容content一起set进chatInfo。之后显示框txtHist调用append()方法,显示发送的content。
ChatInfo chatInfo =new ChatInfo();
chatInfo.setSenderId(bfContact.getUid());
chatInfo.setReceiverid(gfContact.getUid());
Datenow = newDate(System.currentTimeMillis());
StringsentTime = now.toString();
chatInfo.setSendTime(sentTime);
System.out.println(sentTime);
Stringcontent = sentTime + "\n" + bfContact.getUname()+ " Said to"
+gfContact.getUid()+":\n"+txtChat.getText()+"\n";
chatInfo.setContent(content);
txtChat.setText("");
txtHist.append(content);
*if语句判断:if(gfContact.getOnline()!=1)发送离线消息。将请求类型RequestObject.OFF_CHAT与对话实体chatInfo封装到requestObject中,传给Server端。
RequestObjectrequestObject =newRequestObject(RequestObject.OFF_CHAT,chatInfo);
NetAccessHelper.sendRequest(requestObject);
# 策略类OffChatRequestHandler,用来将放发送方发送的离线消息插入到数据库chatstore中。
publicclass OffChatRequestHandlerimplements RequestHandler{}
实例化chatInfo对象,将请求实体中的对象赋给新对象,get()到实体对象chatInfo的senderid、receiverid、sendtime、content。
ChatInfo chatInfo = (ChatInfo) requestObject.getReqBody();
int senderId =chatInfo.getSenderId();
int receiverId =chatInfo.getReceiverid();
StringsendTime = chatInfo.getSendTime();
Stringcontent = chatInfo.getContent();
调用无结果查询execute()将
String sqlString =
"insert into chatstore (senderid,receiverid,sendtime,content)"+" values('"+ senderId+"',"+ receiverId+ ",'"+ sendTime + "','" + content + "')";
DBAccessHelper.getDAO().execute(sqlString);
至此,聊天工具的离线消息功能已经实现,只需在登录类LogForm中添加刷新方法,初始化离线消息方法
mainForm.refreshContacts(true);
mainForm.initOfflineMsgs();
mainForm.startTimer();
dispose();
语句,即可成功发送、接收离线消息
3.5小结:
在此处作者将离线功能一些实现细节加以总结,以便深刻认识:
1. 模拟对象与桩
使用一个临时Contact实体对象tmpContact,通过信息发送者uid,信息接收者uid间接找到实体在contactsModel中的索引,根据索引找到这两个实体,从而也就得到了实体中的所有信息 。
这在MainForm类的initOfflineMsgs()方法中有典型应用:通过登陆者(currUID)找到自己(bfContact);通过实体chatInfo的senderid(实际上就是发送者的uid)找到发送者(gfContact)。
注意:chatInfo实体中只有senderid、reveiverid、sendtime、content 4种信息,并没有contact所有的其他信息。而通过tmpContact,就可以通过senderid找到gfcontact。我们可以发现,tmpContact就想一个桥梁,可以用有限的信息找到相关实体的其他信息。
2. 窗口唯一性
当单击一个联系人时,就要用chatBox调用setVisible()打开一个窗口,
但同时又要求每一个用户至多只需打开一个ChatBox窗口,即窗口唯一性。
解决这个问题的方法是写一个getChatBox()方法,以gfContact分类,只将ChatBox实例化一次,然后put进公共存储区boxRegistry。
第四章.在线聊天
4.1 概要分析
在线聊天是客户端间点与点间连接,不需server中转消息。
如上图,A,B,C 3个用户与服务器相连,若A 想与C通信,则A通过服务器向C发送一个请求A 请求 C,在C端要注册一个侦听器并创建侦听线程。在客户端NetAccessHelper中添加notify(NotifyObjectnotifyObject) ,单向发送,只需写信息到对象流。然后交给EventHandler处理。
4.2具体实现
# 在线消息发送实体类NotifyObject
实现了序列化Serializable接口,类比于之前的requestObject实体。
成员变量:
privateintnotifyType;
private ObjectnotifyBody;
private StringsourceIp; //源IP
privateintsourcePort; //源端口号
private StringdestIp; //发送信息的ip、port
privateintdestPort; //目的端口号
以及相应的get(),set(),toString()函数。
# 修改NetAccessHelper类,增加notify()方法。在线发送只是单向发送,
只需向对象流中write信息,删除所有与input有关内容,并且无需readObject。
这里在线消息的接受者注册了一个侦听器,作为服务器,所以Socket连接的ip与端口号应该是notifyObject中的DestIp,DestPort。
publicstaticvoidnotify(NotifyObject notifyObject) {
try {
StringipString= notifyObject.getDestIp();
int idx=ipString.indexOf('/');
ipString=ipString.substring(idx+1);
System.out.println(ipString);
Socketserver = new Socket(ipString,notifyObject.getDestPort());
OutputStreamos = server.getOutputStream();
ObjectOutputStreamoos = newObjectOutputStream(os);
oos.writeObject(notifyObject);
System.out.print(resObject);
}
# 修改ChatBox类,在btnSendMouseClicked()方法中
增加gfContact.getOnline()!=1情况下的处理:实例化NotifyObject的对象,设置NotifyType,NotifBody,SourceIp,DestIp,SourcePort,DestPort,最后调用NetAccessHelper的类方法notify(),发送实体notifyObject
else{
NotifyObjectnotifyObject=newNotifyObject();
notifyObject.setNotifyType(NotifyObject.TEXT_MSG);
notifyObject.setNotifyBody(chatInfo);
notifyObject.setSourceIp(bfContact.getPeerIp());
notifyObject.setDestIp(gfContact.getPeerIp());
notifyObject.setSourcePort(bfContact.getPeerPort());
notifyObject.setDestPort(gfContact.getPeerPort());
NetAccessHelper.notify(notifyObject);
}
# 事件侦听线程类publicclass EventListenerThreadextends Thread {}
拥有3个私有成员变量peerId,peerPort,eventListener,相应的get()函数,以及主要的run()方法。run()方法主要作用是实例化ServerSocket对象eventListener,获得分配好的端口号,调用InetAddress.getLocalHost()得到
本地主机名/ip(hostname/ip)。之后实例化Socket对象peerSocket和PeerWorkSocket对象worker。最后开始侦听,eventListener调用accept()方法使服务器(用户自己)处于阻塞状态,等待用户请求。
publicvoid run() {
eventListener = new ServerSocket(0);
this.peerPort =eventListener.getLocalPort();//获得分配的端口号
this.peerIp = InetAddress.getLocalHost();
SocketpeerSocket=null;
PeerWorkerThreadworker=null;
while (true) {
peerSocket= eventListener.accept();
worker= newPeerWorkerThread(peerSocket);
worker.start();
}
# 修改LogForm类中if (responseObject.getResType()
==ResponseObject.LOGIN_FAILED)的else语句中增加注册线程peerListenerThread,
开启线程。之后创建新的requestObject,set由EventListListenerThread类获得的目的方peerIp,peerPort。最后调用类方法send(),将请求体交给服务器。
else{
EventListenerThread peerListenerThread=new EventListenerThread();
peerListenerThread.start();
Thread.sleep(2000);
InetAddress peerIp=peerListenerThread.getPeerIp();
intpeerPort=peerListenerThread.getPeerPort();
//建立新的requestObject
PeerInfopeerInfo=newPeerInfo();
peerInfo.setPeerIp(peerIp);
peerInfo.setPeerPort(peerPort);
peerInfo.setUid(uid);
requestObject=newRequestObject(RequestObject.REG_PEER_REQ, peerInfo);
NetAccessHelper.sendRequest(requestObject);
}
# 服务器端策略类
publicclassRegPeerRequestHandlerimplements RequestHandler{}
用以处理客户端发过来的带peerInfo的请求实体,将这些信息插入到cantacts数据库中。
String sqlString=
"update contacts set "+
"peerip='"+peerIp.toString()+
"',peerport="+peerPort+
" where uid="+uid;
System.out.println(sqlString);
DBAccessHelper.getDAO().execute(sqlString);
responseObject=newResponseObject(ResponseObject.REG_PEER_RES,null);
# publicclassPeerWorkerThreadextendsThread{}此类采用策略模式,与信息处理相关,类比于离线消息功能中的WorkerThread类。该类拥有一个构造函数,一个run()方法。run()方法的主要功能是:
从socket中read信息
ObjectInputStream ois=
new ObjectInputStream(peerSocket.getInputStream());
NotifyObjectnotifyObject=(NotifyObject)ois.readObject();
根据notifyObject的类型信息实例化相应的策略类对象,最后该对象调用handleEvent()方法。
switch(notifyObject.getNotifyType()){
case NotifyObject.TEXT_MSG:
handler=new TesxMsgHandler();
break;
default:
break;
handler.handleEvent(notifyObject);
# publicclass SysRegistry{}
单子的概念,返回唯一的HashTable容器的sysReg对象,用来将ContactsModel和boxRegistry放入。sysReg实际上是一个公共存储区(全局)。只是将索引放入HashTable容器中,而非值。
publicclass SysRegistry {
privatestaticHashtablesysReg;
publicstatic HashtablegetSysReg(){
if(sysReg==null){
sysReg=new Hashtable();
}returnsysReg;
}
}
# 定义EventHandler接口
publicinterfaceEventHandler{
publicvoid handleEvent(NotifyObject notifyObject);
}
# 策略类publicclass TesxMsgHandlerimplements EventHandler {}
重写方法handleEvent(NotifyObjectnotifyObject)覆盖EventHandler接口的方法。
* 实例化ChatInfo对象chatInfo,赋予notifyObject对象的内容信息,实例化一个HashTable容器对象boxRegistry,模版类型是Hashtable
publicvoid handleEvent(NotifyObject notifyObject) {
ChatInfochatInfo=(ChatInfo)notifyObject.getNotifyBody();
HashtableboxRegistry=(Hashtable)SysRegistry.getSysReg().get("boxRegistry");
DefaultListModel contactsModel=
(DefaultListModel)SysRegistry.getSysReg().get("contactsModel");
* 从chatInfo中get()到senderId,receiverId,通过“模拟对象与桩”,用uid找到contactsModel中的联系人实体gfContact,bfContact。将gfContact的isSender设为true。new一个chatBox对象,将boxRegistry中的发送者gfContact对象赋予它,并保证一个发送消息者只new一个chatBox。最后将对话信息实体chatInfo的content显示在窗口。
int senderId=chatInfo.getSenderId();
int receiverId=chatInfo.getReceiverid();
ContactgfContact=getContactByUid(senderId,contactsModel);
gfContact.setSender(true);
ContactbfContact=getContactByUid(receiverId,contactsModel);
ChatBoxchatBox=boxRegistry.get(gfContact);
if(chatBox==null){
chatBox=newChatBox(bfContact, gfContact);
boxRegistry.put(gfContact,chatBox);
}
chatBox.appendMsg(chatInfo.getContent());
System.out.println(chatInfo);
[1]对象的串行化(Serializable)
对象的寿命通常随着生成该对象的程序的终止而终止。有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。我们把对象的这种能记录自己的状态以便将来再生的能力。叫作对象的持续性(persistence)。对象通过写出描述自己状态的数值来记录自己,这个过程叫对象的串行化(Serialization) 。串行化的主要任务是写出对象实例变量的数值。如果交量是另一对象的引用,则引用的对象也要串行化。这个过程是递归的,串行化可能要涉及一个复杂树结构的单行化,包括原有对象、对象的对象、对象的对象的对象等等。
Serializable接口中没有任何的方法。当一个类声明要实现Serializable接口时,只是表明该类参加串行化协议,而不需要实现任何特殊的方法。
1. 定义一个可串行化类 :一个类,如果要使其对象可以被串行化,必
须实现Serializable接口。
2. 要串行化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。
java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象 。在ObjectInputStream中用readObject()方法可以直接读取一个对象,ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。
[2]门面(Facade)模式
外部与一个子系统的通信必须通过一个统一的门面(Facade)对象进行,这就是门面模式。
门面模式要求一个子系统的外部与其内部的通信必须通过一个统一的门面(Facade)对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
就如同医院的接待员一样,门面模式的门面类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与门面对象打交道,而不需要与子系统内部的很多对象打交道。
门面模式是对象的结构模式。门面模式没有一个一般化的类图描述,下图演示了一个门面模式的示意性对象图:
在这个对象图中,出现了两个角色:
门面(Facade)角色:客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
子系统(subsystem)角色:可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。
一个系统可以有几个门面类
【GOF】的书中指出:在门面模式中,通常只需要一个门面类,并且此门面类只有一个实例,换言之它是一个单例类。当然这并不意味着在整个系统里只能有一个门面类,而仅仅是说对每一个子系统只有一个门面类。或者说,如果一个系统有好几个子系统的话,每一个子系统有一个门面类,整个系统可以有数个门面类。
在本次项目中,NetAccessHelper类使用了该模式,其优点是:建立连接、发送、接收对象等操作被封装到门面方法sendRequest()中,使用时直接调用门面的方法就可以与服务器通信,不用了解具体的实现方法以及相关的业务顺序。
[3] “策略模式”(Strategy Pattern)
策略模式(StrategyPattern)中体现了两个非常基本的面向对象设计的基本原则:封装变化的概念;编程中使用接口,而不是对接口实现。策略模式的定义如下:
定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化。
策略模式使开发人员能够开发出由许多可替换的部分组成的软件,并且各个部分之间是弱连接的关系。弱连接的特性使软件具有更强的可扩展性,易于维护;更重要的是,它大大提高了软件的可重用性。
[4]模拟对象与桩
空对象的逻辑变体是模拟对象与桩。与空对象一样,它们都表示在最终的程序中所使用的“实际”对象。但是,模拟对象与桩都只是假扮可以传递实际信息的存活对象,而不是像空对象一样可以成为null的一种更智能化 的替代物。
模拟对象与桩之间的差异在于成都不同。模拟对象往往是轻量级、测试级的,通常很多模拟对象被创建出来是为了处理各种不同的测试情况。桩只是返回数据,它往往是重量级的,并且经常在测试之间被复用。桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一种复杂对象,他要做很多好事。