一·概述
- 本次实现的是基于Oracle数据库的网络聊天程序。为了实现较好的网络聊天效果,程序采用了“客户端”与“服务器端”分离的设计思路。
1.服务器与客户端之间数据传输的相关约定
- 服务器和客户端之间由输入输出串流连接,每次的请求和返回均以三个Ojbect为单位进行。
- 第一个Object:记为“o1”,表示请求代号,以String作为原本形态。关于用户端到服务器的请求代号说明:用户端到服务器的请求代号与服务器到用户端的请求代号是一一对应的,即用户传的是“1”返回的也是“1”
- 第二个Object:记为“o2”,承载一些后续会用到的一些数值或文字,如:用户的用户名、密码和账号等。以String表示,用“/”分隔。如:
String test =user_ID+"/"+request_user_ID;//当前用户的ID和要添加的用户的ID
- 第三个Object:记为“o3”,承载查询结果的返回(ArrayList)和图片(byte[])等,由于Object的兼容性,可以传输多样的数据。
2.服务器端
服务器端实现了:
- 建立对数据库的连接
- 监听特定端口
- 将接收到的请求由serverScoket,移交至Socket
- 创建新线程维持连接,取得输入输出串流
- 读取输入串流中的数据
- 分析请求代码,并由相应的函数执行。
3.客户端
客户端实现了:
- 新用户注册请求
- 用户登入,即账密的检查
- 首页,用户好友列表刷新请求
- 添加好友的好友信息查询
- 确认添加好友
- 好友间聊天
二·交互逻辑
1.新用户注册登入流程
为还未拥有账号的新用户准备。以登入界面的“新用户”按钮作为入口,采集用户的一些基本信息、用户希望在聊天中显示的用户名以及登入时的密码。点击“确认注册”按钮,显示由服务器返回的账号,和密码一起作为登入凭证。
2.登入、添加好友
1)登入:
- 登入界面获取账号密码,判断是否允许登入。
- 允许登入后,以弹窗显示“登入成功”字样。
2)添加好友:
- 在好友列表下设置“搜索新好友”按钮,作为添加好友的入口
- 设置“搜索好友”的搜索框和按钮,查询对应账号的用户的一些公开信息。如:账号、用户名、注册时间和性别。用以给添加方确认是否为自己想要添加的人。
- 以“添加好友”确认添加对方为好友,返回首页通过“更新”按钮即可刷新好友列表,可以看到刚刚添加的好友,好友关系成功确立。(好友列表的刷新引用了:https://blog.csdn.net/weixin_34323858/article/details/86055417的部分内容)
3)登入失败:
登入失败(账密原因)将以弹窗的形式提示用户。
3.打开聊天窗口
用户通过点击好友列表对应好友的用户,即可打开与相应用户的聊天窗口。好友列表显示用户名和账号,方便用户核对。
4.聊天窗口
聊天窗口实现用户之间聊天消息的收发(经oracle数据库读写),支持发送文字和图片。可以自定义本窗口输出文字的字体、样式、字号、颜色和文字背景颜色。(文字内容是连滚键盘,不要在意)
(本窗口主体引用自:https://blog.csdn.net/zhuiqiuzhuoyue583/article/details/79047454)
(ImageIcon与byte[]的装换引自:https://yq.aliyun.com/articles/432246)
-
聊天窗口基本功能展示
-
聊天窗口文字样式变换展示
5.错误提示
本提示框用以向用户传达客户端或服务器执行失败情况,表示由用户非法输入或非用户的程序故障。
三·实现
下面将列出部分比较重要的代码。
1.以“用户名”和“密码”连接Oracle数据库[服务器部分]
result=OpenOracleConnection("用户名", "密码");
//登入数据库
public boolean OpenOracleConnection(String account, String password) throws ClassNotFoundException {
boolean result=false;
//load jdbc driver
Class.forName("oracle.jdbc.driver.OracleDriver");
//Creating a connection between the Java program and the Oracle database.
try {
this.con=DriverManager.getConnection(
"jdbc:oracle:thin:@127.0.0.1:1521:orcl",account,password);
result=true;
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
result=false;
}
return result;
}
2.建立对窗口的连接[服务器部分]
ServerSocket serverSock = new ServerSocket(20001);//提供20001端口
while(true) {//以while(true)来使服务器一直处于接收请求的状态
Socket clientSocket = serverSock.accept();//收到请求后,以新的端口建立连接
Thread t = new Thread(new ClientHandler(clientSocket));
t.start();//连接由新的线程维持
System.out.println("got a connection");
}
以下部分为一次用户请求的完整流程:
3.客户端连接服务器[客户端部分]
用户以点击按钮或新建窗口等行为触发用户请求后,客户端会向服务器提出连接请求,并将输入输出串流保存至实例变量。(每个请求的IO串流会互相覆盖)
button_2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Socket sock = null;
try {
sock = new Socket("127.0.0.1", 20001);
} catch (IOException e1) {
// TODO 自动生成的 catch 块
e1.printStackTrace();
}
try {
out = new ObjectOutputStream(sock.getOutputStream());
} catch (IOException e1) {
// TODO 自动生成的 catch 块
e1.printStackTrace();
}
try {
in = new ObjectInputStream(sock.getInputStream());
} catch (IOException e1) {
// TODO 自动生成的 catch 块
e1.printStackTrace();
}
friend_list_start();//启动输出
startUp();//重设界面
}
});
4.客户端输出“用户请求”[客户端部分]
public void friend_list_start(){
try {
String a ="3";//请求代号
out.writeObject(a);
out.writeObject(user_ID+"/");
out.writeObject("#");//在第三位输出“#”表示占位,满足IO要求
} catch(Exception ex) {
ex.printStackTrace();
System.out.println("sorry dude. Could not send it to the server");
}
}
5.连接线程的run()[服务器部分]
- 取得连接的输入和输出串流
sock = clientSOcket;
out = new ObjectOutputStream(clientSOcket.getOutputStream());
in = new ObjectInputStream(sock.getInputStream());
- 以三个Object接收输入
Object o1;//请求代号
Object o2;//String的分割
Object o3 = null;//文件
- 读取o1和o2,并装换为对应的类型
while ((o1 = in.readObject()) != null) {
String code_String = (String) o1;
int code = Integer.parseInt(code_String);
System.out.println("code="+code);
o2 = in.readObject();
String content = (String) o2;
System.out.println(content);
//if code="7"那么就in.read,如果不是7,就in.readObject if_else
o3 = in.readObject();
if(code == 7) {
imageByte =(byte[])o3;
}
/*
这里是switch,以及连带部分(见下一点)
*/
}
- 依照o1的用户请求代号,组合调用相应的函数,并为其传入相应的参数(o2,o3)
仅以case1,3作为示例,case2~case 8的具体代码隐去
boolean yes_or_no =false;
ArrayList k ;//为了方法的多个返回值预留
//判断请求
//用户端到服务器的请求代号说明:
//①__用户端到服务器的请求代号__与__服务器到用户端的请求代号__是一一对应的,即用户传的是“1”返回的也是“1”
switch(code){
case 1:{//1代表新用户注册请求
k=new ArrayList();//分别newArrayList 以保证每个List都是新的,旧的会被GC回收哦
k=new_user_registration_in(content);
yes_or_no=(boolean)k.get(0);
if(yes_or_no==true) {//判断上一步是否执行成功,是否进行下一步操作
new_user_registration_return(yes_or_no,out,k.get(1));
}
break;//忘记break();会导致下面一起执行,出现多重输出
}
case 2:{/*2代表用户登入,即账密的检查*/}
case 3:{//3代表首页,用户好友列表刷新请求
k=new ArrayList();
k=friend_list_in(content);
yes_or_no=(boolean)k.get(0);
int columns=(int)k.get(1);
int rows=(int)k.get(2);
if(yes_or_no==true) {
friend_list_return(yes_or_no,rows,columns,out,k.get(3));
}
break;
}
case 4:{/*4代表添加好友的好友信息查询*/}
case 5:{/*确认添加好友*/}
case 6:{/*聊天信息(文字部分)*/}
case 7:{/*聊天信息(图片部分)*/}
case 8:{/*聊天信息查询*/}
}
- 以函数执行用户的查找和插入请求
public ArrayList friend_list_in(String content) {
boolean yes_or_no=false;//对于本函数操作是否成功的判断
rs=null;//用于从oracle接收返回的查询结果
Object q=null;//从oracle取出的不知道是什么类型的数据
String[] result = content.split("/");
System.out.println(result[0]);
ArrayList row=new ArrayList();
int columns=0;
int rows=0;
boolean Query_returns_results=false;//查询结果返回
String sql= "select\"用户信息\".\"用户名\",\"用户信息\".\"用户ID\"from \"用户信息\"where \"用户信息\".\"用户ID\"IN((Select \"好友关系\".\"用户_2ID\"from \"好友关系\"where \"好友关系\".\"用户_1ID\"="+result[0]+")UNION(Select \"好友关系\".\"用户_1ID\"from \"好友关系\"where \"好友关系\".\"用户_2ID\"="+result[0]+"))";
System.out.println(sql);
try {
PreparedStatement pStmt=con.prepareStatement(sql);
rs=pStmt.executeQuery();//如果写成rs=pstat.executeQuery(sql);会报ORA-01008: 并非所有变量都已绑定.。错误原因,sql这个变量并没有在pstat.executeQuery()的参数中用到。
//刚刚套了两层while(rs.next()),导致rs.next()移位了两次.
columns = rs.getMetaData().getColumnCount();
while(rs.next())
{
ArrayList col = new ArrayList();
for (int i = 1; i <= columns; i++)
{
col.add(rs.getObject(i));
System.out.println(rs.getObject(i));
}
rows++;
row.add(col);
}
yes_or_no=true;
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ArrayList k =new ArrayList();//以ArrayList承载多样化的输出
k.add(yes_or_no);//操作是否成功
k.add(columns);//返回的列数
k.add(rows);//返回的行数
k.add(row);//结果矩阵
return k;
}
6.返回switch块进行“是否成功”的判断,而后调用函数向客户端返回结果
public void friend_list_return(boolean yes_or_no,int rows,int columns,ObjectOutputStream out,Object k) {
try {
String a ="3";
String b = yes_or_no+"/"+columns+"/"+rows;
out.writeObject(a);
out.writeObject(b);
out.writeObject(k);
} catch (Exception ex) { ex.printStackTrace(); }
}
6.客户端接收结果
以与服务器相似的流程,经由switvh判断请求类型
switch(code){
case 3:{//1代表新用户注册请求
yes_or_no=friend_list_back(content,o3);
}
default :yes_or_no=true;
}
并交由对应函数处理
public boolean friend_list_back(String content, Object o3) {
boolean yes_or_no;
String[] result = content.split("/");
System.out.println(result[0]);
// yes_or_no=Boolean.getBoolean(result[0]);//以Boolean.getBoolean(string)进行转换,会将true的string转为false的boolean
// System.out.println(yes_or_no);
yes_or_no=Boolean.valueOf(result[0]).booleanValue();//使用本行可进行true(string)装换为true(boolean)
System.out.println(yes_or_no);
if(yes_or_no!=false) {
yes_or_no =false;
int columns=Integer.parseInt(result[1]) ;
rows=Integer.parseInt(result[2]) ;
ArrayList row=(ArrayList)o3;//将类型定义为Object[],而调用Object[][]会报:表达式的类型必须是数组类型,但是它却解析为 Object
list_String=new String [rows];
for(int m=0;m<rows;m++) {
String lala=null;
ArrayList current=(ArrayList)row.get(m);
for (int i = 1; i <= columns; i=i+2)
{
lala =(String)current.get(i-1)+"/"+(String)current.get(i);
}
list_String[m]=lala;
}
yes_or_no =true;
}
initialize();
frame.setVisible(true);//如果用window.frame.setVisible(true);会报空指针异常,原来可能也是这个原因.大概就是调用对象的问题前面的调用都是frame.XXXXX,可以执行.这里用window.frame.XXXX报异常,就猜可能是对象调用的问题,改成一致的就好了.但是具体逻辑不明//
return yes_or_no;
}
}
}
四.小结(感想)
总体的实现效果和运行效率和预想中还是有比较大的不同的。一部分是个人技术上的不足,一部分是在一步步实现的过程中预先规划与现实需要的偏差。
诸如:
- 在各个窗口之间如何维持用户身份(维持登入窗口的身份)
用了将用户ID以各个窗口类的构造器的参数,传入到由此窗口创建的新窗口,并作为新窗口的实例变量,保留在新窗口中。 - 在刷新聊天记录的时候出现的等待时间过久和不稳定
在上面的动图,大家应该都看到了,关于聊天窗口更新的部分我剪掉了等待的时间。大概是10秒左右,作为聊天软件,不是自动刷新,而是点击按钮刷新是因为在服务器端维持大量的“客户-服务器”间连接的输入输出串流过于复杂,在多线程和并发问题还没有解决的情况下,没有使用。而为了给图片等大文件留下足够的空间以应对可能的极端情况,对于byte[]的定义偏大,导致对数据库和服务器的压力偏大。对象引用错误(window.frame引用了frame)导致的窗口不更新。
上图是使用ArrayList来实现的,实时通信的聊天窗口,设计时的样子。
因为在聊天窗口输出部分,使用到了网络上现有的程序代码,加以改动,添加网络连接等等功能。由于对于引用的部分不够了解,或者说对于swing的绘制还不够了解。导致现在聊天窗口还有一些bug。 - 多线程在本程序实现中可以说是“居功至伟”,但是也翻了不少车。因为执行的顺序不确定导致的刷新异常,最后用按钮解决了。解决了蛮多奇奇怪怪的错误,结果还是令人满意的。
又是一次尽“毕生所学”,寻找“可能性”的尝试。