模拟QQ聊天小项目收尾---界面展示服务端与客户端进行信息交互(用到的知识:io,线程,Swing界面,面向对象思想...... )
程序员文章站
2022-10-03 18:35:17
大家好,我是一位在java学习圈中不愿意透露姓名并苟且偷生的小学员,如果文章有错误之处,还望海涵,欢迎多多指正如果你从本文学到有用的干货知识,那么请您尽量点赞,关注,评论,收藏这两天我一直在设计这个小项目,一边跟着老师学,一边自己试着尝试设计,优化代码,以及改bug,接下来为小伙伴们分享我的心路历程以及模拟QQ聊天小项目的实现,具体过程解释已在注释中详细说明,接下来跟我一起手动将小项目做出来吧:这里附上模拟QQ聊天小项目的效果展示图,嘿嘿,先干为敬:若图片无法显示,图片链接如下:https:....
大家好,我是一位在java学习圈中不愿意透露姓名并苟且偷生的小学员,如果文章有错误之处,还望海涵,欢迎多多指正
如果你从本文学到有用的干货知识,那么请您尽量点赞,关注,评论,收藏
这两天我一直在设计这个小项目,一边跟着老师学,一边自己试着尝试设计,优化代码,以及改bug,接下来为小伙伴们分享我的心路历程以及模拟QQ聊天小项目的实现,具体过程解释已在注释中详细说明,接下来跟我一起手动将小项目做出来吧:
这里附上模拟QQ聊天小项目的效果展示图,嘿嘿,先干为敬:
若图片无法显示,图片链接如下:
http://images5.10qianwan.com/10qianwan/20200722/b_0_202007221502157849.png
先声明,这个小项目的基本功能已经实现,若想要完善功能,感兴趣的小伙伴可以自己尝试嗷;
延续上一篇文章的思想,在此基础上,不难发现正常的QQ聊天服务器并不能自己发消息给我们这些用户(除一些特别信息以外),因为跟我们聊天的是真人呀,而非冷冰冰的服务器(*^▽),另外服务器应该是一个中转站,我们发出的消息都是经过服务器接收然后解析帮我们转发出去的,故而服务器是强大的,能知道我们每个人发的消息内容是什么,什么时间发的等等。但是上一篇文章中有个错误,细心的小伙伴应该知道了,嘿嘿,不过是保护一下我幼小的心灵,所以没有告诉小小陈对吧;哈哈废话不多说,这个错误是因为服务器只有一个吗,而我在上一篇文章中虽然通过暴力解决了java.net.BindException: Address already in use: JVM_Bind,但是现在发现这不合实际,服务器与客户端应该是一对多的关系,故而另一个解决办法就是将服务器的创建移到while循环外即可(呜呜呜,这么简单我居然没想到),好啦下面附上服务器代码:
package server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
public class Server {
//为了找寻用户信息方便,采用HashMap进行存储,静态无需创建对象,类名点即可
//权限修饰符默认不写方便同包下也能正常访问
static HashMap<String,User> userBox = new HashMap<>();
public static void main(String[] args){
try {
System.out.println("服务端启动");
//创建服务端
ServerSocket serverSocket = new ServerSocket(9999);
while(true) {
Socket socket = serverSocket.accept();
//每获取到一个用户信息就添加进集合中
//通过返回的socket获取一个字节型输入流
InputStream is = socket.getInputStream();
//将输入流包装成低级的字符型输入流,因为高级流里面不能直接放下字节型输入流
InputStreamReader isr = new InputStreamReader(is);
//构建高级字符型输入流 因为用BufferedReader流中有readLine方法可以直接读取一行
BufferedReader br = new BufferedReader(isr);
String uid = br.readLine();
//提示哪个用户已上线
System.out.println(uid + "上线了");
//接收客户端的传过来的uid 并存入集合中
User user = new User(uid, socket);
Server.userBox.put(uid, user);
ServerThread st = new ServerThread(user);
st.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
同样的用杜曼实体对象来存储用户信息 如下:
package server;
import java.net.Socket;
public class User {
private String uid;
private Socket socket;
public User(String uid, Socket socket) {
this.uid = uid;
this.socket = socket;
}
public String getUid() {
return uid;
}
public Socket getSocket() {
return socket;
}
}
上面已经提到,服务器是一个中转站,那么如何去实现呢?因为消息的传递应该与客户端是同步的,这时考虑到线程,于是通过创建一个线程类来帮服务器接收并解析消息,同时还能将消息转出,但是考虑到实际QQ中的群发消息(即@全体成员)这种效果,于是我们可以将两个人单聊和一群人群聊两种情况分开讨论,代码实现如下:
package server;
import java.io.*;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
/**
* 根据分析可知,本类的功能为将服务端读到的信息转发至另一人(单聊)或者一群人(群聊)
*/
public class ServerThread extends Thread {
private User user;
public ServerThread(User user){
this.user = user;
}
//为防止代码冗余 设计一个传递信息的方法
private void writerMessage(Socket socket , String message , String time){
OutputStream os = null;
PrintWriter writer = null;
try {
//回服务端消息
os = socket.getOutputStream();
writer = new PrintWriter(os);
writer.println(time + "####" + user.getUid() + ":" + message);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
//重写run方法 一直读写即可
public void run(){
//获取所有用户信息
HashMap<String,User> messages = Server.userBox;
try {
//接收客户端的消息
InputStream is = user.getSocket().getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
while(true) {
//根据实际情况 我们单聊假定为:发送的信息要满足 (内容@另一个人的uid)
//当然可能存在不止@一个人,比如(你在干嘛呀,小老弟@张三@李四)
//若未解析到 @ 我们假定为群聊
String message = br.readLine();
//拼接一个时间
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd kk:mm:ss");
String time = sdf.format(date);
System.out.println(time + "####" + user.getUid() + ":" + message);
//按照@拆分成String数组
String[] messageAndUid = message.split("@");
//解析读到信息
if (messageAndUid.length == 1) {
//此时是群聊 获取所有的键(即uid)通过迭代器遍历
Iterator<String> uids = messages.keySet().iterator();
while (uids.hasNext()) { //判断是否有元素
String uid = uids.next(); //将元素取出来
User user1 = messages.get(uid);
受篇幅限制剩余代码分开展示
//因为if和else中传递信息的代码一致 故另外封装一个方法负责传递信息
this.writerMessage(user1.getSocket(), message,time);
}
} else {
//先给自己发一份
this.writerMessage(user.getSocket(),messageAndUid[0],time);
//此时是单聊 可以做个严谨的判断 @的用户是否存在 但是QQ或者微信中@时弹出的页面显示的人都是存在的
//故此处我们假定@的都存在 由于未知@了几个人故此处采用循环
for (int i = 1; i < messageAndUid.length; i++) {
this.writerMessage(messages.get(messageAndUid[i]).getSocket(), messageAndUid[0],time);
}
}
}
} catch (IOException e) {
// e.printStackTrace();
// 防止客户端下线时出现异常 这里做个输出提示即可
System.out.println(user.getUid() + "下线了");
}
}
}
服务器已经准备就绪,现在开始讨论客户端应该如何实现呢?首先要跟真实的QQ接轨,所以要用Swing简单的画个奇丑无比的小窗口咯,那么细想一下,这个窗口应该有哪些组件和功能呢?起码聊天对话框得有吧(JTextArea),发消息的框得有吧(JTextArea),点击发送的按钮得有吧(JButton),点击取消时发消息的框里面的内容得清空,所以也得有吧(JButton)…那么这个奇丑无比的小窗口出现后是不是还得跟客户端绑一块吧,毕竟是一条绳上的蚂蚱,那么代码实现窗口来了:
package client;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.Socket;
public class QQFrame extends JFrame{
//添加一个属性---表示QQ窗口的名字
private String uid;
//添加一个属性---表示客户端连接的socket对象
private Socket socket;
private JPanel panel = new JPanel();//无色透明的小容器 小盒子
//2.有一些组件
// 文本域(接收信息并展示的 上面部分)
private JTextArea messArea = new JTextArea();
private JScrollPane messPane = new JScrollPane(messArea);
// 文本域(发送信息的 下面部分)
private JTextArea speakArea = new JTextArea();
private JScrollPane speakPane = new JScrollPane(speakArea);
// 按钮(发送)
private JButton sendButton = new JButton("发送");
// 按钮(取消)
private JButton cancelButton = new JButton("取消");
//构造方法(规定调用的流程)
public QQFrame(String uid){
super(uid);
//加载窗口的组件
this.setOther();
this.addElements();
this.addListener();
this.setFrameSelf();
//窗口相当于是一个客户端 产生一个客户端连接
try {
//与服务器连接
socket = new Socket("localhost",9999);
//通过连接后用socket来获取流
OutputStream os = socket.getOutputStream();
PrintWriter writer = new PrintWriter(os);
writer.println(uid);
writer.flush();
//只需要一个读取信息的来为我们做事------客户端读线程
ClientReader cr = new ClientReader(socket);
cr.start();
} catch (IOException e) {
e.printStackTrace();
}
}
//设计一个方法 设置那些乱七八糟的东西
private void setOther(){
//设置组件的位置
//将默认布局清空
panel.setLayout(null);
//将所有的组件自定义放在panel中
messPane.setBounds(10,10,320,220);
speakPane.setBounds(10,240,320,140);
sendButton.setBounds(180,390,60,30);
cancelButton.setBounds(260,390,60,30);
受篇幅限制剩余代码分开展示
//设置一下上面展示的文本域不允许修改了
messArea.setEditable(false);
messArea.setFont(new Font("宋体",Font.BOLD,18));
speakArea.setFont(new Font("宋体",Font.BOLD,18));
}
//设计一个方法 添加组件
private void addElements(){
//将这些组件放置在窗体里
panel.add(messPane);
panel.add(speakPane);
panel.add(sendButton);
panel.add(cancelButton);
this.add(panel);
}
//设计一个方法 给组件添加事件(功能)
private void addListener(){
//取消按钮绑定一个功能 这里是lanmbda表达式的写法
cancelButton.addActionListener(e -> {
speakArea.setText("");
});
//给发送按钮绑定一个功能 这里是lanmbda表达式的写法
sendButton.addActionListener(e -> {
try {
OutputStream os = socket.getOutputStream();
PrintWriter writer = new PrintWriter(os);
String message = speakArea.getText();
writer.println(message);
writer.flush();
//发送完毕之后,让发送的说话框清空
speakArea.setText("");
} catch (IOException e1) {
e1.printStackTrace();
}
});
}
//设计一个方法 设置窗体自身的一些元素
private void setFrameSelf(){
//2.设置窗体一些样式
this.setResizable(false);
//设置窗体点击右上角X 程序结束
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//设置窗体的初始位置
this.setBounds(500,200,350,480);
//让窗体可见
this.setVisible(true);
}
受篇幅限制剩余代码分开展示
//内部类写法 这个类帮客户端去接收消息 由于每个客户端之间的消息同步
//所以这个类需要实现线程
private class ClientReader extends Thread {
//通过属性来获取自己客户端的那个socket
private Socket socket;
public ClientReader(Socket socket){
this.socket = socket;
}
public void run(){
StringBuilder result = new StringBuilder();
try {
//客户端 接收发来的数据
InputStream is = socket.getInputStream();
//将字节流转化成字符流
InputStreamReader isr = new InputStreamReader(is);
//字符流基础上 可以读取一行
BufferedReader reader = new BufferedReader(isr);
while(true) {
//每次读取一行数据
String value = reader.readLine();
//一行数据进行处理 换行
value = value.replace("####","\r\n");
//追加到StringBuilder对象中 频繁拼接效果更好
result.append(value);
result.append("\n");
//展示在上面的聊天框中(文本域中)
messArea.setText(result.toString());
}
} catch (IOException e) {
//e.printStackTrace();
//避免异常 简单做个提示即可
System.out.println("服务器宕机了");
}
}
}
}
接下来看看如何创建客户端窗口吧,哈哈,简单至极啦:
package client;
public class Client {
public static void main(String[] args){
new QQFrame("张三");
new QQFrame("李四");
new QQFrame("王五");
}
}
这里附上服务端与客户端的运行结果如下:
服务端启动
张三上线了
李四上线了
王五上线了
2020-07-21 09:35:50####李四:你们在干吗
2020-07-21 09:36:00####李四:我在喝西北风
2020-07-21 09:36:12####张三:啧啧啧@李四
2020-07-21 09:36:51####王五:我笑了,你呢@张三
2020-07-21 09:37:33####张三:李四要是真的喝西北风我直播倒立洗头
细心的小伙伴应该知道为什么客户端的展示为啥什么都没有,因为展示的部分已经和窗口绑一块啦,当然这里由于只有一个电脑,所以直接在创建了三个客户端窗口,感兴趣的小伙伴可以和宿舍里的同学一起玩哦,但是要保证在同一个局域网下嗷。另外特别注意,不管是服务端还是客户端,里面用到的流通道不要放finally里面关掉,一旦关掉就会出现一直发null的消息,因为这些流通道一直都要使用,除非客户下线和服务器宕机。
全剧终
本文地址:https://blog.csdn.net/bw_cx_fd_sz/article/details/107481950
上一篇: Oracle入门学习二
下一篇: JVM使用总结