引言

当讨论到一个聊天软件是如何运行的时候,我们需要想到它的主要功能是消息传递。对于多台主机或者是一台主机上的多个客户端来说,他们实现消息传递都需要使用到服务器。当客户端A将消息发送给服务端的时候,服务端再将消息转发给客户端B。这个发送与转发的过程我们可以借助Socket来实现,为了确保端A和端B之间的通信不被端C影响,消息在传输的过程中需要确保发送和转发时都能够确认他的发送方与接收方。如果端A上需要同时和端B端C端D进行通信,我们可以为每一个新的通信开启一个新的线程,确保通信不会被主线程阻塞。

具体实现的技术使用到了Java、Swing、Socket,全部源码附在末尾。

界面搭建

在Swing中写界面其实就相当于调用了多个方框控件进行位置的摆放,主要思路是为界面创建一个大的方框Panel后向里面添加不同功能的方框,如Button、Label、TextArea等等的内容,更改样式的话也只是设置一下控件大小、文本颜色等等的基础样式,远不及CSS的美,所以这里不过多叙述,直接在源代码中查看即可。

登录以及登录验证

假设你对于前端有一些了解,对于表单提交来说我们常常使用一个user object包裹account与password属性,当用户点击button提交后将整个的user对象提交给后端。这里亦然,我们可以创建一个User对象,在这个对象中设置私有属性account与password。但是,由于Java在传输对象的时候需要将其序列化(这其实是I/O操作中序列化传输的内容),我们需要对这个类进行改进。

同时需要注意的是,客户端将User类生成的实例对象发送给服务端时,我们应该保证两端中User字段的一致性,因此创建一个新的common包,在这个包下存放消息传递时使用到的对象,并且在服务端也如此设置。

package common;public class User implements java.io.Serializable{private String account;private String password;public String getAccount() {return account;}public void setAccount(String account) {this.account = account;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}

首先考虑客户端部分。我们已经构建好了User类,接下来考虑如何将表单当中的数据方法绑定到具体的User对象身上。Swing中提供了一个ActionListener鼠标事件监听接口,在重写了actionPerformed方法后我们可以根据形参e来判断点击的是什么按钮,进而向服务端传输数据。

if ( 点击的部分 == 通过Swing生成的某个按钮 ) {执行操作....User u = new User();u.setAccount(jp_center_jf.getText().trim());// 密码拿到的是一个字符数组,所以需要通过创建一个字符串进行转换u.setPassword(new String(jp_center_jpf.getPassword()));
}

现在我们已经获取到了表单当中的对象,可以向服务端发送消息进行身份验证,这一请求验证的过程需要使用到Socket通信。由于在一开始设计的时候采用MVC结构,我们将视图view和操作model放在了不同的包下,因此我们可以在model包下创建一个对象LinkServer负责客户端和服务端之间的连接。

public class LinkServer {public Socket s;// 发送请求public boolean sendLoginToServer(Object o) {try {s = new Socket("127.0.0.1",8080);ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());// Object o = new User()oos.writeObject(o);}catch (Exception e) {e.printStackTrace();}finally{}}
}

通过设置了IP地址和端口号,我们可以通过ObjectOutputStream将user对象进行消息的传递。接下来考虑服务端部分。在服务端中我们可以创建一个ServerStart类进行服务器的开启以及验证操作。当我们通过ServerSocket将服务器的端口号设置的和客户端一致时,就已经可以通过ObjectInputStream来接收相应的数据。

public class ServerStart {public static void main(String[] args) {new ServerStart();}public ServerStart() {try {System.out.println("服务器已开启....");ServerSocket ss = new ServerSocket(8080);while (true) {// 等待客户端的连接Socket s = ss.accept();// 接收用户端初次连接时候传来的user状态ObjectInputStream ois = new ObjectInputStream(s.getInputStream());User u = (User)ois.readObject();}} catch (IOException e) {e.printStackTrace();} }
}

接下来在服务端进行身份验证。验证之前需要考虑,不论验证是否通过,我们都需要传递一个消息给客户端。这样看来又是一个C/S之间的消息传输,为了确保消息的一致性,此时又需要在服务端与客户端的common包下设置一个Message类,并同样进行序列化处理。

package common;public class Message implements java.io.Serializable{// 用于登录的验证 1 为登录成功 2 为登陆失败private String mesType;// 发送者private String sender;// 接收者private String getter;// 信息内容private String content;// 发送时间private String sendTime;public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getGetter() {return getter;}public void setGetter(String getter) {this.getter = getter;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSendTime() {return sendTime;}public void setSendTime(String sendTime) {this.sendTime = sendTime;}
}

此时就可以进行身份验证了,本来应该通过服务端获取到User实例对象,然后去和数据库中的用户数据进行验证对比,但这样的话会使用到 jdbc 的连接技术,这里姑且偷个懒,直接使用固定的密码1902151512来进行验证。

public ServerStart() {try {System.out.println("服务器已开启....");ServerSocket ss = new ServerSocket(8080);while (true) {// 等待客户端的连接Socket s = ss.accept();// 接收用户端初次连接时候传来的user状态ObjectInputStream ois = new ObjectInputStream(s.getInputStream());User u = (User)ois.readObject();// 返回同样序列化的MessageMessage m = new Message();ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());// 服务端返回信息 1为成功 2为失败if(u.getPassword().equals("1902151512")){m.setMesType("1");oos.writeObject(m);// 成功登录,单独开启一个线程,保持客户端与服务端之间的通信}else{m.setMesType("2");oos.writeObject(m);s.close();}}} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}

在服务端执行验证操作后会有Message发送到客户端,由于在客户端请求验证的时候开启了一条Socket线路与服务端s.accept进行连接,故返回Message的时候不用执行其他操作,在客户端中便可接收到数据,接下来再看看客户端代码。

public class LinkServer {public Socket s;// 发送请求public boolean sendLoginToServer(Object o) {// 设置一个bool值进行登录验证的判断// 当登录成功时将 b 的值更改为trueboolean b = false;try {s = new Socket("127.0.0.1",8080);ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());oos.writeObject(o);ObjectInputStream ois = new ObjectInputStream(s.getInputStream());Message ms = (Message)ois.readObject();if (ms.getMesType().equals("1")) {b = true;}else{//关闭Scokets.close();}} catch (Exception e) {e.printStackTrace();}finally{}return b;}
}

现在已经实现了验证的功能,我们可以对该功能进行一点优化。

在平时写前端的过程中,我会将具体的路由请求进行一次封装,具体页面中直接使用封装好的函数即可,不必考虑其中的具体逻辑,这样不仅可以提高代码的可读性,还可以在以后修改的时候直接修改封装后的代码,不用到处去找更改位置。

// 一个前端例子
// 对登录接口进行封装
export const loginAPI = data => {return request.request({url: Api.login,data: data,method: 'post'})
}// 在页面中直接调用该方法
const onSubmit = function(values){loginAPI(values).then(res=>{console.log(res)}
}

因此推荐你也这样操作,将具体的验证操作放在某个类中,然后新创建一个loginCheck类,在Swing的页面中直接调用loginCheck类中的方法即可。具体的页面只考虑方法的调用,而不去考虑该方法是如何实现的。如果是登录验证,对于登录的页面来说,噢,原来我直接在点击button之后,直接通过LoginCheck的实例对象调用checkUser的方法就能验证了,这太方便了!

在验证完成后跳转到好友列表页面进行之后的操作。

package client.model;import common.User;public class LoginCheck {public boolean checkUser(User u) {return new LinkServer().sendLoginToServer(u);}
}
// clientLogin 的页面类
@Overridepublic void actionPerformed(ActionEvent e) {if(e.getSource() == jp_bottom_login){LoginCheck lc = new LoginCheck();User u = new User();u.setAccount(jp_center_jf.getText().trim());// 密码拿到的是一个字符数组,所以需要通过创建一个字符串进行转换u.setPassword(new String(jp_center_jpf.getPassword()));if(lc.checkUser(u)){// 跳转到好友列表页面new clientFriend(u.getAccount());// 关闭登录页面this.dispose();}else{JOptionPane.showMessageDialog(this,"用户名或者密码错误!");}}}

一对一的聊天

在登录后成功打开了好友列表,现在先暂停一下,缕一缕思路。

我们现在所有的操作都是对于主线程来说的,如果说端口A上进行了登录操作,用户1打算同时和用户2、3进行聊天,由于线程一直由用户1、2所占用,用户1、3之间的聊天则会被阻塞。因此对于聊天来说,每次开启一个聊天的服务,客户端和服务端都需要开启一个新的线程。

对于服务端来说,需要在多个线程中准确的找到消息的发送方,以及接收方同服务器的Socket的连接。对于客户端来说,需要在每次建立聊天的时候开启一个线程,防止单个用户无法同时和多个用户进行聊天。

我们先来看看服务端。

首先你需要想想在什么时候开启新的线程,前面我们在用户登录的时候可以做到每个账号的验证,在这里便可以为每个通过了验证的账号开启一个线程,并且传递他所拥有的Socket(不然在后续的操作中无法通过Socket交互实现消息的传输)。

因此在开启服务端的实现类中找到验证账户信息的方法,在验证成功之后开启线程。

if(u.getPassword().equals("123")){m.setMesType("1");oos.writeObject(m);// 成功登录,单独开启一个线程,保持客户端与服务端之间的通信ServerConnection sc = new ServerConnection(s);ManageClientThread.addThread(u.getAccount(),sc);sc.start();
}else{m.setMesType("2");oos.writeObject(m);s.close();
}

开启线程的这一步中我添加了两个新的类 ServerConnection和ManageClientThread类,前者负责开启具体的线程,后者负责管理线程之间的对应关系。

还记得引言中讲解到的内容吗,当端A将消息发送给服务端的时候,服务端可以顺利拿到消息,之后怎么处理呢?假设现在只有一个ServerConnection类负责开启线程,但是在服务端的这条线程中仅仅只能保持服务端与某一个客户端之间的联系。我们无法在某个通信中获取另外一个通信中的数据(因为每个客户端都是单一的和服务端进行连接,所以无法直接保存每个通信的Socket),也就意味着无法通过发送方准确找到接收方的Socket进行通信。

因此创建一个Manage类,在类内设置一个HashMap,每次创建一个线程便向HashMap中存放用户的登录账号表示key值,以及它所对应的value,即开启的Socket。当具体的通信进程需要进行消息的接收与转发时,在服务端内通过Message消息类传递过来的接收方account作为key值,拿到发送方的Socket,最后便可以在一个线程中将这两条连接接通实现消息的发送。

package server;import java.util.HashMap;public class ManageClientThread {public static HashMap hm = new HashMap<String,ServerConnection>();// 添加线程的映射public static void addThread(String uid,ServerConnection sc){hm.put(uid,sc);}public static ServerConnection getServerConnection(String uid){return (ServerConnection)hm.get(uid);}
}

按照前面的思路,我们接下来就可以实现ServerConnection类,实现线程之间的socket传输。在这个类中先通过ObjectInputStream接收到客户端传来的Message数据,解析这个数据中所包含的发送方、接收方、消息内容的数据,然后通过HashMap获取到接收方的Socket进行消息转发。

package server;import common.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;public class ServerConnection extends Thread{private Socket s;// 让线程保持客户端的Socketpublic ServerConnection (Socket s) {this.s = s;}@Overridepublic void run () {while(true){try {ObjectInputStream ois = new ObjectInputStream(s.getInputStream());// 现在 单个客户端已经可以将消息发送给服务端了// 之后再处理转发给另外一个客户端的任务// 如果需要将服务器上接收到的消息转发给服务端,不仅需要sender的socket,还需要getter的Message m = (Message)ois.readObject();// 取得接收者的socket// 这样设置后 必须有两个人才能实现信息的传递ServerConnection sc = ManageClientThread.getServerConnection(m.getGetter());ObjectOutputStream oos = new ObjectOutputStream(sc.s.getOutputStream());oos.writeObject(m);} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}}
}

服务端设计完成,接下来考虑客户端。同样,服务端需要进行消息的接收和转发,客户端也需要进行对应的发送和接收,那么可以相同模式的创建ClientConnection线程类来保存Socket,和保存线程信息的ManageClientThread类,具体的实现和服务端的一致,都是在某个线程中进行消息的发送和接收,并且通过Manage类中的HashMap存储目标的Socket。这里就不过多叙述,具体的代码可以点开文末的源代码地址进行下载。

服务端的通信也需要考虑多个线程的通信问题,因此在登陆时接收到服务端的返回验证时开启通信线程,让一个账号和服务器保持通信的连接。

public class LinkServer {public Socket s;// 发送请求public boolean sendLoginToServer(Object o) {boolean b = false;try {s = new Socket("127.0.0.1",8080);ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());oos.writeObject(o);ObjectInputStream ois = new ObjectInputStream(s.getInputStream());Message ms = (Message)ois.readObject();if (ms.getMesType().equals("1")) {// 就创建一个该qq号和服务器端保持通讯连接得线程ClientConnection cc = new ClientConnection(s);//启动该通讯线程cc.start();ManageClientThread.addClientThread(((User)o).getAccount(), cc);b = true;}else{//关闭Scokets.close();}} catch (Exception e) {e.printStackTrace();}finally{}return b;}
}

现在我们已经实现了多个账号之间的通信问题,最后需要考虑的是如何将逻辑实现的通信结果,展示到聊天窗口,以及如何成功将聊天框中的消息发送出去。

回顾一下之前写的客户端内容,我们在登录界面进行了身份验证,和某个账号登录后线程的开启,登录成功后跳转到好友列表页面(传参为个人的账号:String),在好友列表页面通过双击头像打开对话框,这一过程传参为自己的账号:String和别人的账号:String。现在如果想将信息展现到对话框界面中,肯定需要获取到Socket接收服务端转发回来的消息。但是无法靠多次的传参获取到这个Socket。此时就需要考虑借助一些函数封装来实现不同类之间的调用。

现在已知的是在创建线程的时候我们不仅有特定的socket成员变量,还拥有着等待着传输的Message消息。那么如果在客户端的线程类当中调用负责聊天界面类中的方法,并将具体的Message作为参数传递过去,不免是一种不错的解决办法。我认为这和 前端React 中的那种消息发布和接收的方法类似,在父组件中定义方法,在子组件中调用该方法,并将自己的属性值作为参数传递给父组件接收。

在好友列表界面类定义一个鼠标点击事件,在用户双击头像后打开聊天框,并将发送方和接收方的数据存入一个HashMap中,确保一个用户A在打开了和用户B的聊天界面后,再次点击头像时不会再次触发一个聊天框。

@Overridepublic void mouseClicked(MouseEvent e) {JLabel j1 = (JLabel)e.getSource();j1.setForeground(Color.blue);// 限定双击才能打开聊天界面if(e.getClickCount() == 2) {// 得到该好友的编号String friendNo = ((JLabel)e.getSource()).getText();// 呼出聊天框clientChat qqChat = new clientChat(this.owner,friendNo);ManageChat.addChat(this.owner + "" +friendNo,qqChat);}}

在聊天界面类中设置一个展示消息的方法,等待线程控制类中将消息传递过来。

//写一个方法,让它显示消息
public void showMessage(Message m) {String info= m.getSender() + "对你说:" + m.getContent() + "\r\n";this.jt_area.append(info);
}

在线程类中确认是哪一个聊天框需要显示信息,并将信息发送过去。

 ObjectInputStream ois = new ObjectInputStream(s.getInputStream());Message m = (Message)ois.readObject();// 将发送的消息传递到Chat页面上client.view.clientChat cc = ManageChat.getChat(m.getGetter() + "" + m.getSender());cc.showMessage(m);

至此已经成功实现了QQ聊天系统。

如果需要源代码的话可以到下面的链接中去下载:

Java+Swing+Socket实现qq聊天系统-Java文档类资源-CSDN下载

下载后将本文当作理解的参考手册也可以,毕竟将从头至尾的设计思路介绍了一遍。

JAVA + Socket + Swing实现QQ聊天软件相关推荐

  1. Swing写qq聊天软件(想要QQ表情@我呦)

    //主要实现页面等功能的实现 package com.zou.chat; import java.awt.BorderLayout; import java.awt.Color; import jav ...

  2. 仿QQ聊天软件(登录界面、好友界面、聊天界面)-Java(Swing、Socket)

    文章目录 一.项目结构 二.项目功能 三.制作界面 (一).登录界面的制作 (二).好友列表界面 (三).聊天界面 四.制作服务器 五.设计通信协议 六.项目缺点 学习了socket通信后,就想来制作 ...

  3. Java TCP实现高仿版QQ聊天(一)

    前言 ​ 记录一下这套简陋的系统说明,把所遇到的问题和难点以及操作说明在这篇文档中说明清楚,当个回顾吧,万一以后那一天查看也能及时找到问题.这套系统是在本人大三时期完成的,从GitHub上借鉴了很多经 ...

  4. 仿QQ聊天软件(JavaFX+云端数据库)

    仿QQ聊天软件(JavaFX+云端数据库) 这个项目是这学期(大二上学期学完Java后的期末项目),寒假闲着无聊就整理下发上来供大家学习以及参考啦(因为国内关于JavaFX的各种资料感觉都太浅了,本来 ...

  5. Socket编程 ------ 模拟QQ聊天工具

    模拟QQ聊天 一.要求 1.一个服务器可以与多个用户同时通讯 2.用户可以通过服务器与用户之间通讯 3.用户可以选择和所有人发消息,也可以选择和某个用户单独发消息 4.服务器要显示当前所有在线人员 5 ...

  6. java课程设计qq,模块java课程设计报告qq聊天

    河南工业大学 课程设 计 课程设计名称: ja  a qq聊天系统 学生姓名 : x  aoy    指导教 师: 王高平 课程设计时间: 2016.7.7 计科 专业课程设计任务书 说明: ...

  7. Java TCP实现高仿版QQ聊天(二)

    前言 ​ 这是在上一篇博客基础上开展的,第一部分我们只实现了本机的聊天,无法将程序放置另外机器上和本机进行聊天.这篇博客我将介绍如何实现不同机器之间实现聊天,达到真正意义上的聊天.不过这篇博客在其他机 ...

  8. 转载:仿QQ聊天软件2.0版

    仿QQ聊天软件2.0版 这是大神的地址:牟尼的专栏 http://blog.csdn.net/u012027907 详细的过程本人没看,但是看见他的实现效果,相当诱人!     上次课设做了Java版 ...

  9. linux qq多进程客户端,基于多进程QQ聊天软件设计.doc

    基于多进程QQ聊天软件设计 基于多进程的QQ聊天程序设计功能需求描述用户名登陆聊天,人与人之间交流是必不可少的.私聊,与特定的用户聊天群聊,向所有的用户发送消息,大家一起聊欢乐多 server端 输入 ...

  10. java Socket实现简单在线聊天(二)

    出处:http://blog.csdn.net/tuzongxun 接<java Socket实现简单在线聊天(一)>,在单客户端连接的基础上,这里第二步需要实现多客户端的连接,也就需要使 ...

最新文章

  1. RMQ(Range Minimum/Maximum Query)问题:
  2. linux 下i2c读写命令,S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作
  3. php 判断美国zip code
  4. 诗与远方:无题(九十)
  5. n皇后问的三种解答方式
  6. 计算机设置重启时间表,电脑定时开关和重启方法
  7. Python零基础入门(一)——Python基础关键字和语法[学习笔记]
  8. linux脚本自动修改网卡,Linux脚本程序自动修改网卡配置文件中的MAC地址
  9. Ubuntu18.04解决sudo执行慢的问题
  10. 互联网创业的重重风险
  11. 鸿蒙os运行内存,体验亮点满满!鸿蒙OS系统6月份开启适配,不只有华为手机
  12. 解读《海纳云智慧城市白皮书》:智慧城市的风吹向何处?
  13. 短信接口——阿里云短信接口
  14. AR图书,看着很美其实有点坑
  15. 打开计算机任务栏有桌面没,电脑桌面任务栏不显示打开的窗口怎么办
  16. Written English-书面-现在完成进行时
  17. 在移动硬盘(U盘)上安装最新版Windows11+PE双系统m.2硬盘选购
  18. 开一间煎饼果子店能挣多少钱?
  19. xy苹果助手未受信任_【安全问题】关于苹果信任问题
  20. Java 9、10、11,谁才是Java程序员的本命?

热门文章

  1. 让你搞懂 administrator最高权限
  2. 元旦给计算机老师发贺词,给老师的元旦祝福语
  3. 金鳞岂是池中物,一遇风云便化龙
  4. 谷歌神经网络机器翻译NMT:人人可利用TensorFlow快速建立翻译模型
  5. 在线支付功能的设计及其实现
  6. Groovy的规则脚本引擎实战
  7. 计算机上打印机删除不了怎么办,win7系统的打印机删除不掉怎么办?完美解决方法看这里!...
  8. js验证银行卡身份证手机号中文数字金额等
  9. 基于Promethues与Grafana的Greenplum分布式数据库监控的实现
  10. [统计学笔记] 统计学中的相关关系和三大相关系数