这个基于TCP协议的网上考试系统采用 C/S结构,是对学习j2se的一个检验,基本涉及到j2se的所有方面:多线程,IO,GUI,网络编程,JDBC等等。
开发平台:eclipse3.1
数据库:Oracle 9i
一、该系统主要解决的问题
该系统主要处理了三个方面的问题:
1、客户端和服务器的间连接
2、服务器和数据库间的连接
3、界面的设计和控制
下面分别做简要说明:
1、客户端和服务器的间连接:
客户端和服务器的间连接,发送对象可以说是该系统的核心内容
具体步骤:
(1)服务器端建立一个ServerSocket,然后调用accept()方法等待客户连接。
(2)客户端建立一个Socket并发送自己的登陆信息(用户名,密码),请求与服务器连接。
(3)服务器接收到用户登陆请求,如果通过验证则为该用户建立一个新的线程与客户端进行专线连接
(4)刚才建立的两个Socket在服务器建立的新的线程上对话,服务器根据用户发送对象的
flag解析出要求服务的类型,并进行相应的处理,直到用户退出,释放该连接。
(5)服务器等待新的连接请求。
将客户端和服务器之间要发送的所有消息封装成类,以对象的方式传递:
登陆时客户端向服务器发送消息封装成的类 LoginMsg
登陆后服务器向客户端发送反馈消息封装成的类 LoginEcho
获取试题时客户端向服务器发送消息封装成的类 GetTextMsg
退出时客户端向服务器发送消息封装成的类 ExitMsg
修改密码时客户端向服务器发送消息封装成的类 PasswordChangeMsg
修改密码后服务器向客户端发送反馈消息封装成的类 ChangeEcho
为了使该类的对象能在网络上传输,以上所有的类均实现Serializable接口,同时客户端向服务器发送消息封装成的类都继承了Msg类,都包含成员变量flag,这样在服务器端可以根据接收到的对象的flag值判断消息的类型,进行相应的处理,这里用到了面向对象的多态性。
2、服务器和数据库间的连接
用户、试题等信息都存在数据库中,客户端发送请求后服务器要连接数据库,为了实现代码复用、提高效率(因为服务器所有对数据库的操作只需一个连接),把建立连接的代码写入ConnectionManager的静态代码段,并用静态方法getConnection()返回获得的连接。在该类中还用私有的构造方法实现了单例模式,不允许其它类创建该类的实例对象。
核心代码:
Class.forName("oracle.jdbc.driver.OracleDriver");
String url="jdbc:oracle:oci8:@neuqsoft";
String user="scott";
String password="tiger";
conn= DriverManager.getConnection(url,user,password);
3、界面的设计和控制
服务器界面:
客户端界面:
客户端和服务器的主界面用swing中新增加的BoxLayout布局管理器,该布局管理器允许多个组件全部垂直摆放或全部水平摆放,通过嵌套组合使用多个BoxLayout布局管理器,可以实现类似GridBagLayout的功能,使界面布局合理、美观实用。另外通过使用DefaultFont类的静态方法initGlobalFontSetting统一设置字体风格。通过控制按钮是否可用严格控制流程,比如没有启动服务器前不能使用服务器的其它功能,当答到最后一题时,下一题按钮不可用,提交按钮变为交卷等等。
二、功能
服务器端实现的功能:
1:管理员登录,启动服务器,并设置考试时间,考试人数;
2:查询考试动态信息,包括服务器信息,用户信息,网络信息;
3:接收用户呼叫;
4:验证用户登陆;
5:用户开始考试,读取考试时间,读取试题;
6:接收用户答案进行判断,将答案存到数据库,将成绩返回到客户端;
7:对数据库信息进行维护(查询数据库中表的信息);
8:对学生信息进行管理(包含查询某考生考试成绩);
9: 对试题进行维护(增加删除试题);
10:考试结束关闭服务器。
客户端实现的功能:
1:用户登录,不能重复登陆,达到最大连接数不能登陆,密码错误三次不能登陆;
2:根据身份证号和旧密码修改密码;
3:选择试题科目试题类型进行考试;
4:发送前预览所做答案信息,并能进行修改;
5:考试完成可以查看成绩并退出考场;
6:考试过程显示考生信息,考试信息,答题信息,时间信息。
三、开发中遇到的问题及解决方案
1、启动服务器后原来界面不正常
问题描述:在ExamClient(负责界面)中点击“启动服务器”后新建了ClientNet类的对象,在这个类中新建了serversocket和socket,这时候原来的界面不正常了。
问题原因:在界面上点启动服务器后new了一个类,在这个类中新建了serversocket和socket,当前这个线程用于监听用户请求,而这个线程正是刚才处理界面的线程,所以界面无法刷新了,所以界面就不正常了。更深入的探讨:为什么点击后出现一个界面的情况就没有出现这个问题?因为:程序在产生一个Frame时自动产生一个awt线程,所以不存在上述问题。
解决方案:启动服务器后启动一个线程new一个类,这样原来那个线程就可以负责刷新界面了。
2、服务器和客户端间传对象时出现:java.io.EOFException ,java.io.StreamCorruptedException: invalid stream header
问题原因:服务器和客户端的
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
ObjectOutputStream ois = new ObjectInputStream(s.getInputStream());
必须一一对应!
一个ObjectOutputStream的构造和一个ObjectInputStream的构造必须一一对应.ObjectOutputStream的构造函数会向输出流中写入一个标识头,而ObjectInputStream会首先读入这个标识头.因此,多次以追加方式向一个文件中写入object时,该文件将会包含多个标识头.所以用ObjectInputStream来deserialize这个ObjectOutputStream时,将产生StreamCorruptedException.
解决方案:
Server:
while(start)
{
ois = new ObjectInputStream(s.getInputStream());
oos = new ObjectOutputStream(s.getOutputStream());
}
//登陆成功后启动的线程
class ExamThread extends Thread
{
public void run()
{
while(flag)
{
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Msg msg = (Msg)ois.readObject();
int read = msg.flag;
if(read==Msg.GETTEST)
{}
if(read==Msg.EXIT)
{}
if(read==Msg.CHANGEPASSWORD)
{}
}
}
Client:
//发送,接收登陆的消息
public void send(LoginMsg login)
{
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
}
//发送,接收获取试题的消息
public void send(GetTextMsg getTextMsg)
{
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
}
//发送修改密码请求向服务器
public void send(PasswordChangeMsg passwordChangeMsg)
{
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
}
//发送退出消息封装的对象
public void send(ExitMsg exitMsg)
{
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
}
另外:
DataInputStream ois = new DataInputStream(s.getInputStream());
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
不能同时出现!
3、关于网络多线程问题:服务器通过用户请求后,建立一个新的线程与客户端进行专线连接,怎样使这个线程得到socket和用户的信息。
解决方案:在线程构造函数中传递socket,解决网络多线程问题,同时传递给线程该用户的信息,方便线程对用户的控制。
新建线程的构造函数:
public ExamThread(Socket s,User user)
{
this.socket = s;
this.user = user;
}
4、关于用socket传对象的问题:
被传的对象必须implements JAVA.io.Serializable接口,然后用ObjectInputStream 和ObjectOutputStream就可以传递对象了。
实现 java.io.Serializable 接口的类是可序列化的。没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。序列化类的所有子类本身都是可序列化的。这个序列化接口没有任何方法和域,仅用于标识序列化的语意。
5、关闭确认时按否依然关闭
没有写jFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
默认是dispose,所以看不见了,其实还存在
6、用JOptionPane的showInputDialog方法输入考试时间和人数时选撤消出现异常
问题原因:JOptionPane.showInputDialog方法出来的对话框如果按取消的话, 返回值是null,不输入而确定的话,返回值是一个空字符串.
这句按取消会异常,
String stu_number = JOptionPane.showInputDialog(ExamServer.this,"请输入参加考试的人数:").trim();
解决方案:先通过返回值看看点击的是不是“取消”,然后再决定是否进行下一步动作。
String stu_number = JOptionPane.showInputDialog(ExamServer.this,"请输入参加考试的人数:");
if(null == stu_number)
{
//用户取消了。
}
else
{
stu_number = stu_number.trim();
//进行下一步动作。
}
7、如果一个用户登陆二次后另外一个用户登陆会提示三次错误
解决方案:把验证放在本地即可互不影响,注意count要为static!因为一个用户每按一次呼叫服务器就new一个clientnet,count清零,永远都到不了三!
8、用户登陆只可以是admin,其它用户名和表都不可以
问题原因:JDBC默认normal用户登陆,而我用pl/sql登陆时选的是sysdba,二种登陆方式看到的表和记录完全不同,admin可用是因为以前在normal下插入过该记录。
9、关于多次连接数据库的问题
解决方案:把建立连接的代码写入ConnectionManager的静态代码段,并用静态方法getConnection()返回获得的连接。在该类中还用私有的构造方法实现了单例模式,不允许其它类创建该类的实例对象。
10、if(loginMsg.equals("登陆成功"))写成了:if(loginMsg= ="登陆成功")
终于体会到equal和==的区别了!
11、关于test_array[][]存放数据的约定:
test_array[0][0]存放考试题目个数,test_array[0][1]存放考试时间,
test_array[i][0]存放第i题题目,test_array[i][1]存放第i题答案
12、关于使用swing 界面默认字体为粗体的问题
解决方案:通过使用DefaultFont类的静态方法initGlobalFontSetting统一设置字体风格。
13、提交和交卷用一个done的问题
具体代码和解决方案见ExamClient393行
类似问题还出现在ServerManager116行:
exist = false; //将用户已经存在信息置为false,否则一次为true后用户将永远不能登陆
14、BorderLayout布局中怎样让中间的空隙不显示
答案:用FlowLayout()可以解决这个问题,不过界面太窄,依然不太好看
15、Server_EditExam下面if开始出现异常 if(L_NOField.getText().trim().equals("")||answerField.getText().trim().equals("")||text.getText().trim().equals(""))
问题原因:
//TextArea text ;
//因为没有注释掉这句,在界面上定义和增加都是指这个text,不过取text.getText()的时候却//是指46行定义的text,因为46行只有一句TextArea text ;没有赋值,所有出现异常!
详细问题见代码处
16、服务器判断是否达到最大连接数的时候:
if((userCount++)>es.stu_count)
{
loginMsg = new String("已达最大连接数,请稍后");
userCount--;
//没有登陆成功人数却加一,所以要减掉,一个简单的数学逻辑导致用户退出后其他用户//依然无法登陆
}
其实更好的方法或者说问题的根源是不应该在这里给userCount++,毕竟还没有登陆成功,
所以如果在登陆成功后加一更符合逻辑。
四、版本
该系统分二个版本,2.0版对1.0版本的改进:
1、版本1.0中用DataInputStream和DataOutputStream传递int和String进行了改进,2.0中改为使用ObjectInputStream 和ObjectOutputStream 直接传递对象,这样增强了程序的可读性,提高了运行的效率,并且更符合面向对象的特征。
2、版本1.0中用户获取试题时服务器用DataOutputStream的writeUTF方法传给客户端一个试题,按下一题时传给下一个,2.0中改为直接传给客户端一个二维数组,并且在数组中包含试题个数和考试时间的信息,使考试过程的处理更加简洁。
3、版本2.0中解决了1.0中多次连接数据库、设置考试时间和人数如果按撤消会异常、允许重复登陆等问题。
五、总结
用了十几天的时间做完了这个共3000行系统,算是对我学习java三个月的一个总结, 过程中遇到很多问题,也学到很多东西。这个系统可以算是学java以来从模仿到创作的一个转折点。
最后感谢所有帮助过我的人,是他们带给我每一点进步。
2007-4-12
苏强