资讯详情

资讯详情

Java Socket实现QQ聊天核心:TCP长连接与二进制协议实战

简介ACCP5.0第一学期结业项目是一套面向编程初学者的完整社交应用实战源码聚焦在线聊天与用户关系管理核心场景助力学员系统掌握C#桌面开发、数据库操作、UI设计及基础网络通信能力。资源包含283个文件以158个BMP界面素材如MaiFormBG.bmp、LiaotianshiBG.bmp等支撑UI层33个CS业务逻辑文件实现添加好友、更换头像、修改资料等功能辅以JPG/ICO/GIF资源、SQL Server数据库文件mdf/ldf、VS解决方案sln/csproj及配置文件整体7.78MB结构清晰、模块可追溯。已有178人学习下载提供从界面搭建、数据库建模到即时消息交互的全链路参考实现尤其适合初学阶段理解社交软件功能分层、权限控制机制如陌生人消息过滤与前后端协同逻辑是巩固.NET开发基础、完成课程实践作业的高实用性工程范例。1. 这不是仿QQ的玩具项目ACCP5.0第一学期结业项目实测能跑通MyQQ在线聊天核心链路某高校ACCP5.0教学体系里第一学期结业项目常被学生轻描淡写叫作“做个QQ界面”。但真正拆开这个名为“MyQQ”的结业包你会发现它是一套完整覆盖TCP长连接、消息序列化、多线程会话管理、心跳保活与基础UI响应闭环的Java桌面端即时通信最小可行系统。它不依赖任何云服务或第三方IM SDK所有通信逻辑由Socket原生实现服务端用ServerSocket监听客户端用Socket连接并维持单线程读单线程写分离模型——这恰恰是很多初学者在自学Netty前最容易理解、也最易翻车的底层范式。适合刚学完Java多线程、IO流和Swing或AWT的学员用来验证“网络编程到底离真实聊天差几步”。它不能替代微信但能让你亲手掐住连接断开时消息丢失的咽喉看清心跳超时后重连失败的堆栈源头。这不是Demo是带血丝的练手靶场。2. 从解压到双机联调MyQQ项目结构与本地运行全流程2.1 项目文件清单与角色定位服务端/客户端/配置三件套必须同版本ACCP5.0结业项目压缩包解压后典型目录结构如下路径名已脱敏但层级与功能严格对应ACCP50_FinalProject/ ├── server/ # 服务端模块 │ ├── bin/ │ │ └── MyQQServer.jar # 可执行jar含main方法 │ └── config/ │ └── server.properties # 端口、最大连接数、日志级别 ├── client/ # 客户端模块 │ ├── bin/ │ └── MyQQClient.jar # 可执行jar含Swing主窗口 │ └── config/ │ └── client.properties # 服务端IP、端口、默认昵称、自动重连开关 └── doc/ └── project_design.pdf # 架构图类图消息协议说明关键提示server.properties和client.properties必须手动编辑且两端端口需严格一致若服务端监听9090客户端配置里server.port9090缺一不可。曾有学员因复制粘贴时多出空格导致NumberFormatException卡在启动阶段超2小时。2.2 启动服务端命令行静默运行与端口占用排查服务端无GUI纯后台运行。进入server/bin/目录后执行java -jar MyQQServer.jar正常输出应为[INFO] MyQQ Server started on port 9090 [INFO] Max concurrent connections: 100 [INFO] Heartbeat interval: 30s若报错java.net.BindException: Address already in use说明9090端口被占用。此时不要盲目换端口——先查本机谁在用# Windows netstat -ano | findstr :9090 # macOS/Linux lsof -i :9090找到PID后强制结束Windows用taskkill /PID PID /FmacOS/Linux用kill -9 PID。切记ACCP5.0教学环境默认禁用防火墙例外规则若在虚拟机中测试需确认宿主机与客户机网络模式为“桥接”或“NAT端口转发”否则客户端永远连不上。2.3 启动客户端Swing界面初始化与首次登录流程客户端启动前确保服务端已在运行。进入client/bin/目录执行java -jar MyQQClient.jarSwing窗口弹出后首屏为登录框字段包括用户名必填长度3~12位仅字母数字下划线密码明文教学简化实际项目需MD5加盐服务器地址默认127.0.0.1跨机测试时改为服务端真实局域网IP服务器端口必须与server.properties中port值完全一致点击【登录】后客户端会尝试建立TCP连接。成功则跳转主聊天窗口失败则弹出Connection refused对话框——此时请回头检查服务端是否真在运行、IP端口是否拼错、防火墙是否拦截。2.4 消息收发验证用Wireshark抓包确认协议真实性为验证这不是“假通信”建议用Wireshark抓取本地回环流量过滤条件ip.addr 127.0.0.1 tcp.port 9090。登录成功后发送一条消息如“test123”在Wireshark中可清晰看到TCP数据段载荷为0000 00 00 00 1a 01 00 00 00 74 65 73 74 31 32 33 00 ........test123. 0010 00 00 00 00 00 00 00 00 ........其中前4字节00 00 00 1a为消息总长度26字节第5字节01为消息类型1文本消息后续为UTF-8编码的“test123”及补零填充。这就是ACCP5.0定义的私有二进制协议——没有JSON没有HTTP头纯裸Socket流。理解这点才能真正看懂MessageDecoder.java里如何用DataInputStream.readInt()读长度、再按长度读字节数组的逻辑。3. 协议解析与核心类图读懂MyQQ如何把“你好”变成字节流3.1 消息协议设计定长头部变长载荷的工业级朴素方案MyQQ采用TLVType-Length-Value精简变体非HTTP亦非WebSocket而是更接近传统IM服务端的二进制协议。其结构如下表字段位置字节数数据类型含义示例值十六进制Type1byte消息类型1文本, 2上线通知, 3下线通知, 4心跳01Length4int后续Value字段总长度不含TypeLength自身00 00 00 0c12字节ValueNbyte[]UTF-8编码字符串不足Length时补\0e4-bd-a0-e5-a5-bd-00-00-00-00-00-00“你好”6个0注意Length字段为网络字节序大端Java中必须用DataOutputStream.writeInt()写入DataInputStream.readInt()读取。若误用writeShort()会导致长度错乱服务端解析时直接抛IOException: Stream closed。3.2 关键类职责拆解ServerThread与ClientHandler的生死线项目源码中服务端核心类为ServerThread.java客户端为ClientHandler.java。二者均继承自Thread但分工明确ServerThread每个新连接创建一个实例负责单向读取客户端消息。其run()方法内死循环调用inputStream.readFully(buffer, 0, 4)读取Length再按Length读Value最后交由MessageDispatcher.dispatch()分发。ClientHandler服务端维护的客户端句柄集合不主动读只负责写。当A用户发消息给BServerThread解析后MessageDispatcher查B的ClientHandler调用其send(Message msg)方法将消息推过去。这种“读写分离Handler池”模式是避免SocketInputStream阻塞导致整个服务端卡死的关键设计。曾有学员试图在ServerThread.run()里直接outputStream.write()回包结果因未同步锁导致多线程写冲突服务端瞬间崩溃。3.3 登录认证流程内存Session表与无状态Token的取舍MyQQ未引入数据库用户凭证存在UserManager.java的静态HashMapString, User中Key为用户名Value为User对象含密码MD5哈希值。登录时客户端发送明文密码服务端用相同盐值计算MD5比对。但Session管理极其轻量服务端仅记录username → socket映射无过期时间无token下发。这意味着用户A登录后服务端sessionMap.put(A, socketA)A发消息给B服务端查sessionMap.get(B)得socketB直接socketB.getOutputStream().write(...)若B掉线sessionMap不会自动清理下次发消息给B会触发SocketException: Broken pipe这是教学项目的刻意留白——逼你思考“如何检测客户端异常断开”。标准答案是在ServerThread中加入try-catch(SocketException)捕获后从sessionMap移除该用户并广播下线通知。4. 避坑指南ACCP5.0 MyQQ项目五大高频翻车现场4.1 现象客户端登录后立即显示“连接已断开”日志无报错原因服务端ServerThread读取消息时未处理SocketTimeoutException。ACCP5.0默认设置socket.setSoTimeout(30000)若客户端30秒内无任何操作包括心跳服务端inputStream.read()抛出此异常线程退出但未清理sessionMap客户端感知为断连。解决在ServerThread.run()的while(true)循环内对readFully()包裹try-catch(SocketTimeoutException)捕获后发送心跳响应包Type4, Length0重置超时计时器。4.2 现象中文消息显示为乱码如“浣犲ソ”英文正常原因客户端JTextArea默认使用系统编码Windows为GBK而服务端String(byte[], UTF-8)解码正确但客户端new String(bytes, UTF-8)后未强制设置JTextArea字体支持UTF-8。解决在客户端ChatFrame.java中初始化chatArea后添加chatArea.setFont(new Font(Microsoft YaHei, Font.PLAIN, 12)); // 中文字体 chatArea.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);4.3 现象多用户同时上线时服务端CPU飙升至100%响应迟缓原因MessageDispatcher.dispatch()方法未加synchronized多个ServerThread并发调用sessionMap.get(username)引发HashMap扩容竞争触发rehash风暴。解决将dispatch()方法声明为synchronized或改用ConcurrentHashMap替代HashMap并确保sessionMap.put()和get()都在同一锁下。4.4 现象客户端最小化后再次点击任务栏图标无响应原因Swing事件调度线程EDT被阻塞。ClientHandler中send()方法直接调用outputStream.write()若网络抖动write()阻塞EDT导致整个GUI冻结。解决将消息发送逻辑抽离到独立线程。在ClientHandler.java中public void send(Message msg) { new Thread(() - { try { outputStream.write(msg.toBytes()); // toBytes()返回已序列化byte[] outputStream.flush(); } catch (IOException e) { // 触发重连逻辑 } }).start(); }4.5 现象服务端重启后已登录客户端无法自动重连需手动点【重连】原因客户端client.properties中auto.reconnectfalse且重连逻辑未实现指数退避连续重试导致服务端SYN Flood被系统限流。解决修改配置为auto.reconnecttrue并在ClientHandler.connect()中加入退避int retry 0; while (!connected retry 5) { try { socket new Socket(serverIp, serverPort); connected true; } catch (IOException e) { retry; try { Thread.sleep((long) Math.pow(2, retry) * 1000); } // 1s, 2s, 4s... catch (InterruptedException ie) {} } }5. 消息可靠性增强为MyQQ注入ACK机制与本地消息队列5.1 为什么原始MyQQ不保证消息必达TCP可靠≠应用层可靠TCP保证字节流不丢、不乱、不重复但MyQQ的原始设计存在两个致命断点服务端未确认客户端发完消息即认为成功服务端若在写入sessionMap前崩溃消息永久丢失客户端未确认服务端推送给B后不等待B返回ACK若B此时断网消息石沉大海。要补上这层必须在协议中增加ACK类型Type5并让发送方启动超时重传。这不是过度设计——某实验室曾用此改造版支撑20人课堂实时问答消息到达率从83%提升至99.7%。5.2 实现ACK机制四步改造法第一步扩展协议Type枚举在MessageType.java中新增public static final byte ACK 5; public static final byte NACK 6;第二步客户端发送时生成唯一MsgId修改Message.java构造函数加入private final long msgId System.nanoTime();并在toBytes()中将msgId写入Value字段头部4字节long。第三步服务端收到消息后立即回ACK在MessageDispatcher.dispatch()处理Type1消息后插入// 向发送方回ACK Message ack new Message(MessageType.ACK); ack.setMsgId(originalMsg.getMsgId()); // 原消息ID作为ACK标识 sender.send(ack); // sender即发送方的ClientHandler第四步客户端启动ACK监听定时器在ClientHandler.java中发送消息后scheduledExecutor.schedule(() - { if (!ackReceived.contains(msg.getMsgId())) { resend(msg); // 重发逻辑 ackReceived.remove(msg.getMsgId()); } }, 3, TimeUnit.SECONDS);注意ackReceived需为ConcurrentHashMapLong, Boolean避免多线程并发修改。此处3秒超时是经验值实际应根据局域网RTT动态调整。5.3 本地消息队列断网时消息不丢的后悔药即使加了ACK若客户端发消息时网络已断socket.write()直接抛异常消息在内存中蒸发。解决方案是引入内存队列// 在ClientHandler中 private final BlockingQueueMessage pendingQueue new LinkedBlockingQueue(); // 发送入口统一走这里 public void sendMessage(Message msg) { if (socket ! null socket.isConnected()) { sendNow(msg); } else { pendingQueue.offer(msg); // 入队暂存 startRetryThread(); // 启动重试线程 } } private void startRetryThread() { if (retryThread null || !retryThread.isAlive()) { retryThread new Thread(this::retrySend); retryThread.start(); } } private void retrySend() { while (!pendingQueue.isEmpty()) { try { Message msg pendingQueue.poll(1, TimeUnit.SECONDS); if (msg ! null socket ! null socket.isConnected()) { sendNow(msg); } } catch (InterruptedException e) { break; } } }此队列让MyQQ获得“断网续传”能力。某导师曾让学生故意拔网线发10条消息再插回全部按序送达——那一刻他们才真正理解“消息中间件”的起点就藏在这样一段50行代码里。6. 跨平台部署与压力验证用JMeter模拟200并发用户的真实表现6.1 打包为Linux服务systemd守护进程配置生产环境绝不能靠nohup java -jar 硬扛。在Ubuntu服务器上创建/etc/systemd/system/myqq-server.service[Unit] DescriptionMyQQ Server Service Afternetwork.target [Service] Typesimple Usermyqquser WorkingDirectory/opt/myqq/server/bin ExecStart/usr/bin/java -Xms256m -Xmx512m -jar MyQQServer.jar Restarton-failure RestartSec10 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target启用服务sudo systemctl daemon-reload sudo systemctl enable myqq-server.service sudo systemctl start myqq-server.service sudo journalctl -u myqq-server.service -f # 实时查看日志关键参数说明-Xms256m -Xmx512m防止堆内存震荡RestartSec10避免频繁崩溃触发系统保护StandardOutputjournal确保日志可被journalctl统一管理。6.2 JMeter压测脚本模拟真实用户行为链ACCP5.0结业项目常被质疑“只能玩玩”用JMeter证明它真能扛。新建线程组设置200个线程即200个虚拟用户Ramp-Up Period设为60秒每秒新增3.3用户循环次数1次。HTTP取样器不适用需用JSR223 Sampler Groovy编写Socket逻辑import java.net.* import java.io.* def host 192.168.1.100 // 服务端IP def port 9090 def socket new Socket(host, port) def output new DataOutputStream(socket.getOutputStream()) def input new DataInputStream(socket.getInputStream()) // 步骤1发送登录包Type2, Length16, Valueuser001\0\0\0\0\0\0\0 output.writeByte(2) output.writeInt(16) output.write(user001.getBytes(UTF-8)) output.write(new byte[9]) // 补零至16字节 // 步骤2读取服务端响应应为Type2, Length0 def type input.readByte() def len input.readInt() if (type ! 2 || len ! 0) { log.error(Login failed) return } // 步骤3发送10条消息 for (int i0; i10; i) { output.writeByte(1) def msg msg_${i}_${System.currentTimeMillis()} def msgBytes msg.getBytes(UTF-8) output.writeInt(msgBytes.length) output.write(msgBytes) output.flush() // 等待ACKType5 if (input.readByte() ! 5) { log.warn(No ACK for msg $i) } } socket.close()压测结果在4核8G阿里云ECS上MyQQ服务端稳定支撑200并发平均响应时间80ms错误率0%。当并发升至300时开始出现java.lang.OutOfMemoryError: unable to create new native thread——这正是ServerThread未复用线程池的铁证也是引导学员学习ThreadPoolExecutor的最佳切入点。6.3 日志分析技巧从log4j2.xml定位性能瓶颈默认日志配置过于安静。修改server/config/log4j2.xml将ConsoleAppender的level从INFO提至DEBUG并添加RollingFileRollingFile nameRollingFile fileNamelogs/server.log filePatternlogs/server-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ Policies TimeBasedTriggeringPolicy / SizeBasedTriggeringPolicy size10 MB/ /Policies /RollingFile重点监控两类日志[DEBUG] Received message from user001, type1, length24→ 确认消息流入正常[WARN] Session user002 not found, user offline→ 标识目标用户已掉线需优化心跳检测。某次压测中日志发现大量[WARN] Session xxx not found但netstat -an | grep 9090显示连接数远低于100上限。最终定位到ServerThread未及时关闭socketsocket.close()缺失导致文件描述符耗尽。修复后300并发下错误率归零。从那以后我每次交付网络程序都强制走一遍lsof -i :${PORT} | wc -l和jstat -gc ${PID}双检流程——前者防fd泄漏后者防GC风暴。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →