本来这个报告是要算作平时分的,但最后 Java 公选课老师觉得讲的太慢了就取消了这个大作业;暑假里没事干,还是把它做出来了。
Java 多人聊天室大作业报告
Author:天泽龟
一。简介
本来这个报告是要算作平时分的,但最后 Java 公选课老师觉得讲的太慢了就取消了这个大作业。但暑假里没事干,还是把它做出来了。
虽说是做出来,但代码还是抄的 B 站一个教程视频,讲的课感觉很拉,敲的代码也有弹幕佬说很多细节没做好,但暂且不管,反正先把这玩意的框架抄了再说。
涉及的知识要点有:IO 编程,Socket 编程,多线程编程,异常处理,GUI 编程。 笔者会对每个模块进行具体分析。
二。GUI 编程
引入 swing
和 awt
包,搭建服务器端和客户端的 GUI。想法是对于服务器端整一个 文本显示区域 输出客户端信息,并引入开启/关闭功能的按钮;对于客户端引入文本输入区域和文本显示区域。
1. 服务器 GUI 设计:
声明一个继承了 JFrame
类的 ServerChat
类用来实例化一个服务器端对象。
先 new 几个组件:
TaSer
表示文本显示区域StartSerBtn
和EndSerBtn
表示两个按钮用来实现开闭功能- 为了让两个按钮能并列的放在窗口下端,还得设置一个 Panel 对象
BtnTool
将二者绑定。
在它的 init
方法中,我们对 GUI 窗口进行初始化。代码参考如下:
public class ServerChat extends JFrame {
// 声明部分:
JTextArea TaSer = new JTextArea(10,20);
JPanel btnTool = new JPanel();
// 设置一个 Panel 对象,将多个组件整在一起
JButton StartSerBtn = new JButton("服务器启动");
JButton EndSerBtn = new JButton("服务器停止");
// 初始化部分:
public void init() throws Exception {
this.setTitle("服务器");
this.add(TaSer, BorderLayout.CENTER);
this.add(btnTool, BorderLayout.SOUTH);
btnTool.add(EndSerBtn); btnTool.add(StartSerBtn);
// 将两个 Button 组在一起放在 South,且默认是 *流式* 的
this.setBounds(300,300,300,400);
// .... 实现 EndSerBtn 的监听
this.setVisible(true); // 显示屏幕
TaSer.setEditable(false); // TaSer 组件仅用作显示,不可编辑
this.setDefaultCloseOperation(EXIT_ON_CLOSE); // 窗口关闭同时,停止服务器。
}
// ...
}
这里的 EndSerBtn
对象需要实现一个监听器的接口,在 GUI 设计部分不做过多阐释,详细可参考 第三部分:Socket 编程。
2. 客户端 GUI 设计:
同理设计客户端的 GUI,我们声明一个继承了 JFrame
类的 ClientChat
类用来实例化一个客户端的 GUI 对象。该类包含以下组件:ta
表示文本显示区域;tf
表示文本输入区域;
我们应该实现的功能是,在 tf
组件中输入非空文本后,可以在 ta
里显示出来。因此,ta
需要实现对于 tf
组件的监听。 从 tf
拿到文本串时候顺手还往服务器端传一份。
代码参考如下:
public ClientChat() {
// 可以直接写在构造函数里
this.setTitle("客户端");
this.add(ta,BorderLayout.CENTER);
this.add(tf,BorderLayout.SOUTH);
this.setBounds(300,300,300,400);
tf.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String text = tf.getText();
// 从 tf 组件拿到 text 字符串
if ( text != null && text.length() != 0) {
send(text + '\n');
} // 若 text 非空,则向服务器端发送数据
tf.setText("");
// 在 ta 上设置 text,并将 tf 清空
}
}); // 对 tf *监听*,写个匿名类
this.setVisible(true);
ta.setEditable(false);
tf.requestFocus(true); // 使光标初始时在 tf 组件
this.setDefaultCloseOperation(EXIT_ON_CLOSE); // 使用 exit 退出进程。
// ...
}
三。Socket 编程 / IO 编程
网络编程的目标,是实现 客户端向服务器发送数据,服务器再向所有客户端返回数据。我们为客户端添加一个 Socket
类变量,为服务器端添加 ServerSocket
类变量。
(背书)当客户端的 Socket 试图与服务器指定端口建立连接时,服务器被激活并通过 SeverSocket
对象与客户端的 Socket
对象建立两个主机之间的固定连接。 一旦客户端与服务器建立了连接,则两者之间就可以传送数据。由于不涉及多线程部分,这里我们只考虑单客户端的情况。
1. 服务器端 网络编程
声明如下变量:
serversocket
:通过调用accept
方法在指定的端口监听到来的连接。socket
:用来存客户端传来的套接字。dis
:服务器输入流,从 Socket 中读取数据;
① 我们先写一个 StartServer
方法用来启动服务器端,具体流程如下:
- 基于指定端口创建一个新的
ServerSocket
对象。 ServerSocket
对象调用accept
方法在指定的端口监听到来的连接。accept
一直处于阻塞状态 直到有客户端试图建立连接。- 服务器与客户端根据一定的协议交互,直到关闭连接。
由于每次接到信息时服务器不可中断,因此应该将接收数据写入死循环中!
详细代码如下:
public void StartServer() throws Exception {
try {
try {
serverSocket = new ServerSocket(8888);
IsStart = true;
TaSer.append("服务器已启动!\n");
} catch (Exception e) {
e.printStackTrace();
}
// 接每一個信息时,服务器不可以终断,所以将其写入 while() 中,判断符为服务器开关的判断符
System.out.println("等待客户端上线...");
while (IsStart) {
socket = serverSocket.accept();
System.out.println("\n" + "一个客户端连接服务器" + socket.getInetAddress() + ": " + socket.getPort());
TaSer.append("\n" + "一个客户端连接服务器" + socket.getInetAddress() + ": " + socket.getPort() + "\n");
// 输出客户端的信息
ReceiveSer();
// 服务器接受客户端一句话
}
} catch (SocketException e) {
System.out.println("服务器终断连接。");
} catch (Exception e) {
e.printStackTrace();
}
}
② 我们还需要 ReceiveSer
方法从客户端接收数据。
具体来说,我们可以通过调用 Socket
对象的 getInputStream
方法 或者 getOutputStream
方法 建立与客户端交互的输入流和输出流。
同理,read
方法也是一个阻塞函数,也需要通过死循环的方式读取数据,代码如下:
public void ReceiveSer()
{
try {
dis = new DataInputStream(socket.getInputStream());
// 创建一个输入流
while (IsStart) {
System.out.println("等待客户端输入内容...");
String Text = dis.readUTF();
// 从流中读取数据
if (Text != "") {
TaSer.append(socket.getPort()+" say: "+Text);
System.out.println(socket.getPort()+" say: "+Text);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
2. 客户端 网络编程
同理,客户端我们先得 new 一个套接字实例,用来建立与服务端的联系。
其次,我们要写一个 send
方法把文本传过去。方法里面 new 一个输出流,并将文本写进去就可以了。代码如下:
public void send(String Text) {
try {
dos = new DataOutputStream(s.getOutputStream());
dos.writeUTF(Text);
} catch (IOException e) {
e.printStackTrace();
}
}
四。多线程编程
最后,为了实现多人聊天室的功能,我们需要引入多线程编程!
对于每一个客户端与服务器端的请求,**我们都给它开一个新的线程,让服务器并发地处理数据。**具体地说,我们在服务器包中定义一个内部类 ClientConn
让他实现 Runnable
接口,并通过重写 run
方法从各个客户端读取数据。每有一个客户端链接上去,就 new 一个 ClientConn
的实例即可。
同时,为了实现 “聊天室” 这一功能,我们需要同步每个客户端的信息——即实现群聊的功能。我们可以将每个客户端塞进一个数组中,每当某一客户端向服务器上传数据,服务器就将该数据发放给每一个客户端即可。
run
方法代码如下:
public void run() {
DataInputStream dis = null;
// 服务器输入流,从 Socket 中读取数据
try {
dis = new DataInputStream(socket.getInputStream());
while (IsStart) {
System.out.println("等待客户端输入内容...");
String Text = dis.readUTF();
String str = socket.getPort()+" say: "+Text;
TaSer.append(str); System.out.println(str);
for (ClientConn cli: ccList) cli.send(str);
// 向每个客户端发送信息,实现聊天室
}
} catch (SocketException e) {
TaSer.append("一个客户端已下线:" + socket.getPort() + '\n');
// 异常处理。
}
catch (IOException e) {
e.printStackTrace();
}
}
同时,客户端也应该实现一个线程类 Receive
来接收服务器发来的数据。这里不能光写一个普通的方法! 因为主线程会一直占用 cpu
资源,你就等着接收数据了、那咋实现传数据的任务。
Receive
类实现如下:
class Receive implements Runnable{
@Override
public void run() {
while (isConn) {
DataInputStream dis = null;
try {
dis = new DataInputStream(s.getInputStream());
String str = dis.readUTF();
ta.append(str);
} catch (SocketException e) {
isConn = false;
ta.append("服务器中断,失去连接\n");
// 异常处理。
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
五。异常处理
主要有以下两种异常:
-
当服务器关闭后,客户端因无法接受数据弹出
SocketException
异常。 -
当客户端关闭后,服务器因无法接收数据弹出
SocketException
异常。
**两种情况用 try/catch
语句就可解决,**代码如下:
catch (SocketException e) {
isConn = false;
ta.append("服务器中断,失去连接\n");
// 异常处理。
}
最后我们的多人聊天室就可以跑起来啦!运行结果如下:
写在最后:待改进的部分和感悟
-
实现服务器开启/关闭功能,实现客户端的重新连接;
-
整一个数据库,支持用户注册账号(还得学 MySQL);
-
GUI 太丑,想写个漂亮点的,那还要去学点 Js,Vue 啥的应该。。
虽然跟着写完了,但对于 java 的高级编程还是没弄通透,之后可能再看看书或者多抄几个项目。没准抄着抄着就抄明白了)