一、前言
聊天室v4是一个基于Java Socket的多人聊天系统,包含服务端,命令行客户端,GUI客户端和用户信息类。原版本功能只支持私聊和群聊文本和表情两种信息,但不支持发送文件。本pro版新增文件信息的收发功能。
二、问题瓶颈分析
瓶颈1:长度字段只有一字节。所有消息长度通过os.write(data.length)发送,这只写了一个字节,最大只能表示255字节,文件动辄几KB到几MB,255字节根本不够用
瓶颈2:Server服务端向Client用户端发信息的时候没有类型区分,服务端send方法没有区分消息类型,客户端无法区分消息类型,会把文件二进制数据当文本解析导致乱码
瓶颈3:read函数不保证一次读完。原版本代码中in.read(data);调用一次就认为读完了。TCP是流式协议,一次read函数可能只返回部分数据,文件传输必须循环读取。
三、函数1(Server.send())改动和解释
改动原因:原来的客户端无法区分消息类型。改造后的服务端发给客户端的消息有两种,需要加一个类型前缀的字节来区分

解释
1.在方法开头加了 os.write(0),表示这条消息是文本类型
2.后续的 [长度][内容] 格式完全不变,原有文本收发逻辑不受影响
3.客户端 receive() 会先读这个类型字节,type=0 走文本逻辑,type=1 走文件逻辑
4.注意:这个类型前缀只用于 Server→Client 方向,Client→Server 方向仍然用原有的 type=1~4 体系
四、函数2(Server.sendFile())新增
新增原因
服务端收到客户端发来的文件后,需要把文件转发给目标客户端。原有的 send() 只能发文本(类型0),需要一个新方法按文件协议(类型1)发送文件。

解释
**1.类型码 1:**告诉客户端接下来是文件消息,走文件接收逻辑
**2.发送者名称:**让接收者知道文件是谁发的。长度用 1 字节(名字不会超过 255 字节)
**3.文件名:**让接收者知道文件叫什么,保存时用原名。长度也是 1 字节
4.文件长度用 4 字节 int: 这是关键改动。OutputStream.write(int) 每次只写低 8 位,一个 int 有 4 字节,需要通过位移拆成 4 次写入,最大可表示约 2GB
5.文件数据: 直接 os.write(fileData) 把整个字节数组写入输出流
五、函数3(Server.readFully())新增

解释:readFully(): 循环调用 in.read() 直到缓冲区填满。每次读取从 totalRead 位置开始,读 buffer.length - totalRead 个字节,累加 totalRead,直到读满或流关闭。
六、函数4(Server.handleClient())逻辑改动解释

七、函数5(Client.sendFile())新增
java
public void sendFile(String filePath,String friendName){
try {
java.io.File file=new java.io.File(filePath);
if(!file.exists()||!file.isFile()){
System.out.println("文件不存在: "+filePath);
return;
}
byte[] fileData=Files.readAllBytes(file.toPath());
String fileName=file.getName();
OutputStream os=socket.getOutputStream();
if(friendName!=null&&!friendName.isEmpty()){
// 私聊文件 type=5
// 协议:[5][好友名长度][好友名][文件名长度][文件名][文件长度4字节][文件数据]
os.write(5);
os.write(friendName.getBytes().length);
os.write(friendName.getBytes());
}else{
// 群聊文件 type=6
// 协议:[6][文件名长度][文件名][文件长度4字节][文件数据]
os.write(6);
}
os.write(fileName.getBytes().length);
os.write(fileName.getBytes());
int fileLen=fileData.length;
os.write((fileLen >> 24) & 0xFF);
os.write((fileLen >> 16) & 0xFF);
os.write((fileLen >> 8) & 0xFF);
os.write(fileLen & 0xFF);
os.write(fileData);
os.flush();
System.out.println("文件 "+fileName+" 已发送 ("+fileLen+"字节)");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
解释
- 读取本地文件:
Files.readAllBytes()一次性把文件读成 byte\[\],简单直接。注意大文件可能占内存,生产环境应分块发送 - **type=5 vs type=6:**私聊多写一个好友名字段(1字节长度 + 内容),群聊直接跳过。后续的文件名 + 文件长度 + 文件数据两种类型完全一致
- 文件长度写入: 和
Server.sendFile()一样,用位移拆 4 字节写入。两端写入方式必须对称
八、效果展示
