Java Socket实现多线程聊天室:代码解析与运行流程
本文详细介绍如何使用Java原生Socket类实现一个简单的多线程聊天室。通过服务端监听端口并利用CopyOnWriteArrayList管理多个客户端连接通道(Channel),配合独立的线程处理读写操作,实现了群聊、私聊及系统通知等功能。
整体架构设计
本聊天室基于TCP协议构建,核心依赖Java的java.net.Socket和ServerSocket类。为了实现多用户并发通信,服务端采用“一客户端一通道”的设计模式,并使用多线程技术隔离每个用户的读写逻辑。
通信机制主要依靠数据流进行封装:
- 发送端:使用DataOutputStream.writeUTF()写入字符串数据。
- 接收端:使用DataInputStream.readUTF()读取数据。
这种机制天然处理了字节流的定界问题,简化了文本传输的实现。
准备工作与工具类
在进行具体的业务逻辑开发前,我们需要定义一个资源释放的工具类,确保在程序关闭或异常发生时,Socket和输入输出流能够正确断开连接,避免内存泄漏。
/**
* @类名 CloseUtils
* @描述 TODO 工具类,统一释放资源
*/
public class CloseUtils {
public static void close(Closeable... targets ) {
for(Closeable target:targets) {
try {
if(null!=target) {
target.close();
}
}catch(Exception e) {
// 静默处理或记录日志
}
}
}
}第一步:创建客户端(Client)
客户端的主要职责是发起连接请求,并开辟两个独立的线程分别负责消息的输入(发送)和消息的输出(接收)。由于Socket通信中读和写互不阻塞,必须采用多线程模型。
第1步:建立Socket连接与初始化线程
- 入口:
main方法。 - 动作:
- 获取用户控制台输入的昵称。
- 实例化
Socket连接本地地址localhost和端口8888。 - 启动一个
Send线程用于读取控制台输入并发送给服务器。 - 启动一个
Receive线程用于从服务器流中读取数据并打印。
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("-----Client-----");
BufferedReader br =new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入用户名:");
String name =br.readLine();
// 1、建立连接: 使用Socket创建客户端 +服务的地址和端口
Socket client =new Socket("localhost",8888);
// 2、开启多线程分别处理收发
new Thread(new Send(client,name)).start();
new Thread(new Receive(client)).start();
}
}第2步:实现发送端(Send Runnable)
- 参数:包含目标
client、name及控制台缓冲对象。 - 完成状态:进入死循环等待键盘输入,一旦获得非空消息,便通过
writeUTF发送至服务端并刷新缓冲区。
public class Send implements Runnable {
private BufferedReader console ;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String name;
public Send(Socket client,String name) {
this.client =client;
console =new BufferedReader(new InputStreamReader(System.in));
this.isRunning = true;
this.name = name;
try {
dos =new DataOutputStream(client.getOutputStream());
// 连接成功后先发送名称告知服务器
send(name);
} catch (IOException e) {
release();
}
}
@Override
public void run() {
while(isRunning) {
String msg = getStrFromConsole();
if(!msg.equals("")) {
send(msg);
}
}
}
private void send(String msg) {
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
release();
}
}
private String getStrFromConsole() {
try {
return console.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
private void release() {
this.isRunning = false;
CloseUtils.close(dos,client);
}
}第3步:实现接收端(Receive Runnable)
- 动作:在
run方法中持续调用readUTF()。如果抛出异常说明连接断开,否则将收到的消息打印到控制台。
public class Receive implements Runnable {
private DataInputStream dis ;
private Socket client;
private boolean isRunning;
public Receive(Socket client) {
this.client = client;
this.isRunning = true;
try {
dis =new DataInputStream(client.getInputStream());
} catch (IOException e) {
release();
}
}
private String receive() {
String msg ="";
try {
msg =dis.readUTF();
} catch (IOException e) {
release();
}
return msg;
}
@Override
public void run() {
while(isRunning) {
String msg =receive();
if(!msg.equals("")) {
System.out.println(msg);
}
}
}
private void release() {
this.isRunning = false;
CloseUtils.close(dis,client);
}
}第二步:创建服务端(Chat Server)
服务端是整个系统的枢纽。它使用ServerSocket在指定端口监听请求。当有新的客户端接入时,服务端并不直接处理其业务逻辑,而是将其封装为一个Channel对象并加入列表管理,随后启动该Channel对应的线程。
线程安全容器:
考虑到多个线程需要同时访问“当前所有在线用户列表”,我们选用java.util.concurrent.CopyOnWriteArrayList。它是一种线程安全的可变列表,适用于读多写少或并发访问的场景。
public class Chat {
// 使用线程安全的动态数组存放所有在线通道
private static CopyOnWriteArrayList<Channel> all =new CopyOnWriteArrayList<Channel>();
public static void main(String[] args) throws IOException {
System.out.println("-----Server-----");
// 1、指定端口 使用ServerSocket创建服务器
ServerSocket server =new ServerSocket(8888);
// 2、主线程无限循环阻塞等待连接
while(true) {
Socket client =server.accept();
System.out.println("一个客户端建立了连接");
// 为每个连接创建一个专门的Channel处理
Channel c =new Channel(client);
all.add(c); // 管理所有的成员
new Thread(c).start();
}
}
}
// 内部静态类:代表一个独立的通信通道
static class Channel implements Runnable{
// ... (此处省略部分属性与构造方法见下文分析)
}
}第三步:Channel通道的逻辑实现
Channel类是实现核心通信逻辑的地方。每个Channel实例拥有自己的输入输出流。
1. 初始化与身份注册
当Channel被构造函数激活时:
- 获取客户端发来的第一帧数据(即用户名)。
- 向该客户端发送欢迎语。
- 遍历all列表,将“XXX加入了聊天室”的系统消息群发给其他人。
2. 消息分发策略(群聊与私聊)
服务端接收到某用户的消息后,通过sendOthers方法判断转发规则:
- 私聊规则:如果消息以@开头(格式如@targetName:msg),解析出目标用户名,仅在all列表中查找匹配者,并只将该消息发送给目标用户。
- 群聊规则:如果不是私聊,则遍历列表排除自己,将所有普通消息转发给其余在线用户。
private void sendOthers(String msg,boolean isSys) {
boolean isPrivate = msg.startsWith("@");
if(isPrivate) { // 私聊处理
int idx =msg.indexOf(":");
String targetName = msg.substring(1,idx);
msg = msg.substring(idx+1);
for(Channel other: all) {
if(other.name.equals(targetName)) {// 找到目标
other.send(this.name +"悄悄地对您说:"+msg);
break;
}
}
}else {
for(Channel other: all) {
if(other==this) continue; // 跳过自己
if(!isSys) {
other.send(this.name +"对所有人说:"+msg);
}else {
other.send(msg); // 系统消息
}
}
}
}
3. 退出处理
当某通道发生IO异常或手动关闭时,触发release()方法:
- 将自身从all列表中移除。
- 向剩余用户广播“XXX离开大家庭”的消息。
- 关闭自身的dis, dos及client。
效果演示与验证
按以下步骤运行即可看到完整效果:
第4步:启动服务端
编译并运行Chat.java(或主类Chat)。控制台应显示“-----Server-----”及“一个客户端建立了连接”等日志。此时服务端处于accept阻塞等待状态。
第5步:并行启动多个客户端
依次编译并运行Client.java至少两次,在每次运行时输入不同的用户名(例如:Alice, Bob)。
第6步:执行测试交互
- 查看入群通知:新加入的Bob控制台上会收到“Alice来了XX聊天室”的通知;同时Alice的控制台也会收到“Bob来了XX聊天室”的提示。
- 群发消息:Alice在控制台输入文字,Bob及其他人都能立刻在各自的屏幕上看到带有前缀的群聊消息。
- 私聊测试:Alice尝试输入
@Bob:Secret Message。只有Bob能在其屏幕上看到“Alice悄悄地对您说:Secret Message”。而其他人不会受到影响。
总结
通过这个案例,我们可以清晰看到Java Socket网络编程的基础结构。利用ServerSocket监听、Socket通信以及Thread调度,就能搭建出一个基本的即时通讯框架。虽然在实际生产环境中我们会选择Netty等高性能框架来处理复杂的并发与编解码问题,但理解这一底层原语对于掌握分布式通信原理至关重要。
以上就是Java Socket实现简单聊天室功能的详细内容,更多关于Java Socket教程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













