点击上方关注“追梦 Java”↑,一起追梦!
前面的 2 篇文章,我们介绍了 Java 网络编程的基础,介绍了 UDP 与 TCP 的编程实现,今天我们利用前面的知识,通过 Java 网络编程来完成一个聊天室的功能,支持文本的群聊和私聊。
1
聊天室功能与原理
1、聊天室的功能
每个客户端在连接到服务器端时,要通过控制台输入自己的名称,然后开始发送消息到服务端,服务端在接收到客户端的连接时,首先输出谁进入了聊天室,然后把客户端发来的消息转发给其他客户端,实现群聊的功能,如果客户端按照约定以@name#开头的格式输入消息,服务端需要解析到客户端要私聊的对象,把消息单独发送给要私聊的客户端。
2、聊天室的原理
聊天室需要一个服务器来支持,多个客户端连接到服务器端,服务器的作用就是接收不同客户端的数据,并转发到其他客户端。
客户端可发发送数据给服务器端,同时客户端也需要接收服务器端返回的数据。客户端的发送数据和接收数据是两个独立的通道,互不影响。即客户端的输出与输入要独立,可以使用多线程来实现。
服务端要为每一个客户端建立一个通道,服务端也使用多线程来实现。
服务端需要创建一个通道的列表,统一管理客户端的通道,为了实现自己发的消息,别人可以看到,不需要返回自己的通道。这样就实现了群聊的功能。也就是自己发一个消息,其他人都可以看到。
在客户端程序里为每一个客户端设置一个名称,约定以@name#开头的格式为私聊,就可以实现私聊的功能。
当程序中发生异常时,线程就停止执行。
2
聊天室的代码实现
聊天室的代码分为三大块,服务端代码和客户端代码以及工具类。
1、公共关闭资源方法
由于代码里会处理很多 IO 异常,当程序中发生异常时,线程就停止执行,并且关闭掉对应的资源,因此我们定义一个公告的关闭资源的类和方法。
public class Util {
public static void closeAll(Closeable... io) {
for (Closeable temp : io) {
try {
if (null != temp) {
temp.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2、服务端开发
ChatChannel 类,实现服务器端的多线程,维护一个客户端的通道列表,服务器端既能接收客户端的数据,又能把数据转发给对应的客户端。
public class ChatChannel implements Runnable {
public static List all = new ArrayList();// 通道列表
private DataInputStream dis; // 输入流
private DataOutputStream dos;// 输出流
private String name;// 客户端名称
private boolean isRunning = true;
public ChatChannel(Socket client) {
try {
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
this.name = dis.readUTF();
System.out.println(this.name + "进入了聊天室");
this.send(this.name + ",您好!欢迎您进入聊天室");
sendOthers(this.name + "进入了聊天室", true); // 系统消息
} catch (IOException e) {
e.printStackTrace();
Util.closeAll(dis, dos);
isRunning = false;
}
}
/**
* 读取数据
*/
private String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
Util.closeAll(dis);
isRunning = false;
all.remove(this); // 移除自身
}
return msg;
}
/**
* 发送数据
*/
private void send(String msg) {
if (msg != null && !"".equals(msg)) {
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
Util.closeAll(dos);
isRunning = false;
all.remove(this); // 移除自身
}
}
}
/**
* @param msg 消息内容
* @param sysMsg 是否是系统消息
*/
private void sendOthers(String msg, boolean sysMsg) {
// 加入私聊的判断,约定@name#格式为私聊
if (msg.startsWith("@") && msg.indexOf("#") > -1) { // 私聊
// 获取name
String name = msg.substring(1, msg.indexOf("#"));
String content = msg.substring(msg.indexOf("#") + 1);
for (ChatChannel other : all) {
if (name.equals(other.name)) {
other.send(this.name + "悄悄地对您说:" + content);
}
}
} else {
for (ChatChannel other : all) {
if (other == this) {
continue;
}
if (sysMsg) {
other.send("系统信息:" + msg);
} else {
// 发送其他客户端
other.send(this.name + "对所有人说:" + msg);
}
}
}
}
@Override
public void run() {
while (isRunning) {
sendOthers(receive(), false); // 用户消息
}
}
}
创建服务端类 Server,使用多线程和通道容器。
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while (true) {
Socket client = server.accept();
ChatChannel channel = new ChatChannel(client);
ChatChannel.all.add(channel);// 统一管理客户端的通道
new Thread(channel).start(); // 启动一条通道
}
}
}
3、客户端开发
客户端的消息发送线程类 Send,每个客户端要设定自己的名字,同时接收控制台输入的数据并发送给服务端。
public class Send implements Runnable {
// 控制台输入
private BufferedReader console;
// 输出流
private DataOutputStream dos;
// 客户端名称
private String name;
// 控制线程
private boolean isRunning = true;
public Send(Socket client, String name) {
try {
console = new BufferedReader(new InputStreamReader(System.in));
dos = new DataOutputStream(client.getOutputStream());
this.name = name;
send(this.name); // 把自己的名字发给服务端
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
Util.closeAll(dos, console);
}
}
/**
* 从控制台接收数据并发送数据
*/
public void send(String msg) {
try {
if (msg != null && !"".equals(msg)) {
dos.writeUTF(msg);
dos.flush(); // 强制刷新
}
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
Util.closeAll(dos, console);
}
}
// 从控制台接收数据
private String getMsgFromConsole() {
try {
return console.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
@Override
public void run() {
while (isRunning) {
send(getMsgFromConsole());
}
}
}
客户端的消息接收线程类 Receive,用于独立接收服务端返回的数据。
public class Receive implements Runnable {
// 输入流
private DataInputStream dis;
// 线程标识
private boolean isRunning = true;
public Receive(Socket client) {
try {
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
Util.closeAll(dis);
}
}
/**
* 接收数据
*/
public String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
Util.closeAll(dis);
}
return msg;
}
@Override
public void run() {
while (isRunning) {
System.out.println(receive());
}
}
}
创建客户端类 Client,发送数据和接收数据分布使用独立的多线程处理。
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("请输入您的名称:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String name = br.readLine();
if ("".equals(name)) {
return;
}
Socket client = new Socket("localhost", 8888);
new Thread(new Send(client, name)).start(); // 发送一条通道
new Thread(new Receive(client)).start(); // 接收一条通道
}
}
3
聊天室的功能测试
首先运行服务端 Server 类,然后运行客户端 Client 类,为第一个客户端起名叫 aaa,再运行客户端 Client 类,为第二个客户端起名叫 bbb,接着运行客户端 Client 类,为第三个客户端起名叫 ccc。
此时的控制台输出如下:
服务端控制台输出:
aaa进入了聊天室
bbb进入了聊天室
ccc进入了聊天室
客户端1控制台输出:
请输入您的名称:
aaa
aaa,您好!欢迎您进入聊天室
系统信息:bbb进入了聊天室
系统信息:ccc进入了聊天室
客户端2控制台输出:
请输入您的名称:
bbb
bbb,您好!欢迎您进入聊天室
系统信息:ccc进入了聊天室
客户端3控制台输出:
请输入您的名称:
ccc
ccc,您好!欢迎您进入聊天室
以上的测试结果说明,我们的聊天室已经支持了一个服务端可以支持多个客户端的连接,并且可以给多个客户端同时返回消息。
然后在控制台1里输入:“大家好,我是aaa,我喜欢Java。”,在控制台2里输入:“大家好,我是bbb,我喜欢唱歌。”,在控制台3里输入:“大家好,我是ccc,我喜欢跳舞。”,此时的客户端控制台输出如下:
客户端1控制台输出:
请输入您的名称:
aaa
aaa,您好!欢迎您进入聊天室
系统信息:bbb进入了聊天室
系统信息:ccc进入了聊天室
大家好,我是aaa,我喜欢Java。
bbb对所有人说:大家好,我是bbb,我喜欢唱歌。
ccc对所有人说:大家好,我是ccc,我喜欢跳舞。
客户端2控制台输出:
请输入您的名称:
bbb
bbb,您好!欢迎您进入聊天室
系统信息:ccc进入了聊天室
aaa对所有人说:大家好,我是aaa,我喜欢Java。
大家好,我是bbb,我喜欢唱歌。
ccc对所有人说:大家好,我是ccc,我喜欢跳舞。
客户端3控制台输出:
请输入您的名称:
ccc
ccc,您好!欢迎您进入聊天室
aaa对所有人说:大家好,我是aaa,我喜欢Java。
bbb对所有人说:大家好,我是bbb,我喜欢唱歌。
大家好,我是ccc,我喜欢跳舞。
以上的测试说明,我们的聊天室已经支持了群聊。
下面我们来测试一下私聊的功能,按照私聊的约定格式以“@name#”开头,bbb与ccc私聊,在控制台2里输入:“@ccc#你好ccc,我是bbb,我们一起唱歌跳舞好吗?”,ccc给bbb回复,在控制台3里输入:“@bbb#你好bbb,不好意思,没有时间。”,此时的客户端控制台输出如下:
客户端1控制台输出:
请输入您的名称:
aaa
aaa,您好!欢迎您进入聊天室
系统信息:bbb进入了聊天室
系统信息:ccc进入了聊天室
大家好,我是aaa
大家好,我是aaa,我喜欢Java。
bbb对所有人说:大家好,我是bbb,我喜欢唱歌。
ccc对所有人说:大家好,我是ccc,我喜欢跳舞。
客户端2控制台输出:
请输入您的名称:
bbb
bbb,您好!欢迎您进入聊天室
系统信息:ccc进入了聊天室
aaa对所有人说:大家好,我是aaa
aaa对所有人说:大家好,我是aaa,我喜欢Java。
大家好,我是bbb,我喜欢唱歌。
ccc对所有人说:大家好,我是ccc,我喜欢跳舞。
@ccc#你好ccc,我是bbb,我们一起唱歌跳舞好吗?
ccc悄悄地对您说:你好bbb,不好意思,没有时间。
客户端3控制台输出:
请输入您的名称:
ccc
ccc,您好!欢迎您进入聊天室
aaa对所有人说:大家好,我是aaa
aaa对所有人说:大家好,我是aaa,我喜欢Java。
bbb对所有人说:大家好,我是bbb,我喜欢唱歌。
大家好,我是ccc,我喜欢跳舞。
bbb悄悄地对您说:你好ccc,我是bbb,我们一起唱歌跳舞好吗?
@bbb#你好bbb,不好意思,没有时间。
以上的测试说明,我们的聊天室已经支持了私聊的功能。
本篇文章综合运用了 Java 网络编程、IO 流、多线程的知识,完成了一个聊天室的功能,希望大家可以熟练掌握。
追梦Java
知识指导行动,行动决定命运。
长按二维码关注追梦Java
1、入职新公司,如何快速凸显个人价值
2、Java 开发分享
3、Java 开发经验分享
4、Java面试之volatile和synchronized及Lock的区别
5、关于多线程创建的几个问题
6、关于多线程共享资源的几个安全性问题
7、关于多线程操作的几个方法
8、Java中的死锁问题
9、带你认识 File 类
10、IO 流,掌控一切
11、不能不懂的 IO 处理流
12、IO 操作大结局
13、聊一聊 Object 类
14、Java 反射之根基 Class 类
15、Java 反射机制的应用
16、代理设计模式与AOP
17、反射高级应用:自定义AOP框架
18、Java 网络编程基础
19、Java UDP 与 TCP 编程精华
有用的话点个在看