基于TCP协议的通讯录课设:从协议设计到稳定演示
发布时间:2026/10/1 13:15:35来源:尧图网络
简介《基于TCP协议的通讯录网络应用课程设计报告》是一份面向计算机专业本科生的课程设计参考文档。它围绕Socket编程、TCP客户端与服务端通信、通讯录增删查改等核心功能展开完整覆盖了课程目的与意义、系统需求分析、功能分析、详细设计及总结等报告撰写模块能够帮助读者快速掌握小型网络应用的设计思路并作为同类课程设计报告的写作样板。资源包内只有1个docx文件大小约270KB格式清晰、便于直接参考或局部复用。内容具体包含套接字作为应用层与传输层接口的说明、TCP客户端与服务端流程图、服务端与客户端的核心函数模块如创建套接字、收发数据、添加/删除/浏览联系人附录还提供了服务端代码片段。报告结合两周课设实践给出了常见低级错误、调试方法及心得总结对提升计算机网络实践能力有直接的借鉴价值。该资源已有537人学习下载适合正在完成基于TCP协议通讯录课题或需要规范课程设计文档的学生。1. 基于 TCP 协议的通讯录网络应用课设的目标不是高并发是稳定演示「基于 TCP 协议的通讯录网络应用」这个课程设计题目很常见可答辩现场翻车率一点不低服务端多开两个客户端就乱、消息粘包导致命令解析错、界面代码和 Socket 代码揉在一起一断连就崩。这个题目想拿高分核心不是把功能堆得多全而是把「协议怎么定、状态怎么管、连接怎么收尾」这三件事想清楚。这篇笔记按我做过的一版课设方案展开适合用 Java 完成、需要同时交付源码和课程设计报告的同学。方案控制在 1000 行以内单人能写完也能在答辩时稳定跑完整个演示流程。2. 方案设计为什么 TCP 文本协议比 HTTP 更适合课设以及消息帧怎么定2.1 选型理由TCP、Java Socket 与控制台交互先回答最容易被问的问题为什么用 TCP 而不是 UDP。TCP 提供可靠传输数据不丢、不乱序上层不需要处理重传和校验。通讯录这种应用对延迟不敏感但对完整性要求高——增删改查的结果必须和操作一一对应。UDP 还要自己在应用层做确认和超时重传课设阶段引入这套逻辑既难讲清楚又容易被答辩老师追问细节。TCP/IP 协议栈里TCP 在传输层之上就是应用层的 Socket 编程接口。用 Java 的ServerSocket和Socket就能实现一个完整的 C/S 应用连接管理、半关闭、异常断开这些机制都是语言标准库现成的老师在代码里能看到「基于 TCP 协议」的直接证据。比起用 HTTPTCP 长连接让登录状态可以保存在服务端内存里不需要引入 Session、Cookie 这套概念状态机画起来也干净。通信数据也没必要用 JSON——课设演示时老师经常凑过来看交互内容一条ADD|张三|13800138000比一段 JSON 直观太多。自定义文本协议在这个规模下是性价比最高的选择。2.2 消息协议与状态机一条竖线分隔的文本帧协议是整个课设的骨架。我用的版本是一行一条命令字段用|分隔行尾\n作为帧边界。命令统一为ACTION|参数1|参数2的形式。方向命令说明客户端 → 服务端LOGIN|name登录name 不能为空客户端 → 服务端ADD|name|phone新增联系人客户端 → 服务端DEL|id按 id 删除联系人客户端 → 服务端UPDATE|id|name|phone更新联系人客户端 → 服务端LIST返回全部联系人客户端 → 服务端EXIT退出登录服务端 → 客户端OK|...操作成功后面带数据服务端 → 客户端ERR|原因操作失败原因可读加一个\n当帧边界乍看简单其实躲开了 TCP 粘包的大坑。TCP 是字节流协议不保证一次read()恰好读到一条完整消息。如果客户端用read(byte[])一次读一批就可能一次读到两条命令的一半。用println()写一行、readLine()读一行应用层就强制按行消费数据粘包在协议层面被规避了。代价是字段内容里不能出现|和换行通讯录场景下姓名和电话不会包含这两个字符约束可接受。服务端还要有一个最小状态机未登录时只接受LOGIN和EXIT登录后接受全部命令。这个状态机在报告里画一张图就是很实在的章节素材。返回码也统一成功OK失败ERR参数错了直接返回ERR|参数错误。不要用裸的数字返回码演示时老师在终端里看到ERR|not login比看到2容易理解得多。2.3 数据存储HashMap 加自增 id不上数据库课设报告评估的重点在协议设计和并发模型数据存储不是加分项。常见做法是内存里放一个ConcurrentHashMapInteger, Contact用一个AtomicInteger生成自增 id。重启丢数据在课设场景下完全可以接受答辩时只需要在新起的进程里重新添加几条数据就行。不建议在这个题目里引入 MySQL。原因是三个环境依赖答辩机器不一定装了数据库、JDBC 样板代码占了篇幅、演示时数据库服务没启动就是现场事故。如果非要持久化可以加一个 JSON 文件序列化服务端启动时加载、每次写操作后落盘代码量不超过 30 行已经足够。真实的生产系统当然不会这么做但课设的目标是把 TCP 通信的链路讲清楚存储越简单越好。3. 服务端实现线程模型、命令分发与通讯录数据维护3.1 线程模型一个客户端一个线程规模越小越好用服务端我选「每连接一线程」模型accept()拿到一个Socket就new Thread(() - handle(client)).start()。这是教科书里最经典的模型五十个并发以内完全够用。不要在这个课设里引入 NIO 或者 Netty——代码量翻倍答辩时还得解释 Selector、Channel、Reactor老师稍微追问就容易卡壳。每连接一线程模型配合阻塞 I/O代码路径是线性的读一行、处理、写一行出错的位置非常直观。这个模型有两个必须处理干净的细节。一是共享数据要加锁sessions和contacts用ConcurrentHashMap已经够少量复合操作再用synchronized包住。二是线程收尾客户端非正常断开时finally里必须移除sessions里的记录并关闭Socket否则服务端会留着一个半开的连接和挂起的线程积累起来就是内存泄漏。3.2 接收循环与帧解析readLine 就是天然的拆包器服务端主循环的核心代码import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; public class ContactServer { private static final int PORT 8899; private static final MapSocket, String sessions new ConcurrentHashMap(); private static final MapInteger, Contact contacts new ConcurrentHashMap(); private static final AtomicInteger idGen new AtomicInteger(1); static class Contact { int id; String name; String phone; Contact(int id, String name, String phone) { this.id id; this.name name; this.phone phone; } } public static void main(String[] args) throws IOException { ServerSocket server new ServerSocket(PORT); System.out.println([server] listening on PORT); while (true) { Socket client server.accept(); // 阻塞等待新连接 sessions.put(client, null); // 初始状态未登录 new Thread(() - handle(client)).start(); } } private static void handle(Socket client) { String clientId client.getRemoteSocketAddress().toString(); System.out.println([enter] clientId); try (BufferedReader in new BufferedReader( new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); PrintWriter out new PrintWriter( new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true)) { String line; while ((line in.readLine()) ! null) { // 按行读天然拆包 String response dispatch(line, client); out.println(response); // 写一行帧边界是 \n System.out.println([ clientId ] line response); } } catch (Exception e) { System.out.println([ clientId ] error: e.getMessage()); } finally { sessions.remove(client); try { client.close(); } catch (IOException ignored) {} System.out.println([exit] clientId); } } }这段代码里几个参数要留意。端口选了 8899避开 8080、3306 这类容易和本机其他服务冲突的端口BufferedReader包InputStreamReader时显式指定StandardCharsets.UTF_8保证不同系统间中文不乱码PrintWriter的构造参数true表示自动 flush每次println后数据立即发出不需要手动调flush()。finally块里移除 session 再做一层保险即使某个命令中途抛异常连接和 session 也会被回收。3.3 命令分发switch 分发加登录状态检查dispatch()是服务端的逻辑核心。我的写法是先判断登录态再用 switch 分发避免一串 if-else 往下堆private static String dispatch(String raw, Socket client) { String norm raw.trim(); if (norm.isEmpty()) return ERR|empty; String[] parts norm.split(\\|, -1); // 注意是 \\|不是 | String action parts[0]; boolean loggedIn sessions.get(client) ! null; if (LOGIN.equals(action)) { if (loggedIn) return ERR|already login; if (parts.length 2 || parts[1].trim().isEmpty()) return ERR|login name required; String name parts[1].trim(); sessions.put(client, name); return OK|welcome name; } if (!loggedIn) return ERR|not login; // 未登录只放行 LOGIN 和 EXIT switch (action) { case LIST: return listContacts(); case ADD: if (parts.length 3) return ERR|add name phone; Contact c new Contact(idGen.getAndIncrement(), parts[1], parts[2]); contacts.put(c.id, c); return OK|id c.id; case DEL: return delContact(parts); case UPDATE: return updateContact(parts); case EXIT: return OK|bye; default: return ERR|unknown action action; } } private static String listContacts() { if (contacts.isEmpty()) return OK|0; StringBuilder sb new StringBuilder(OK|).append(contacts.size()); for (Contact c : contacts.values()) { sb.append(;).append(c.id).append(:).append(c.name).append(:).append(c.phone); } return sb.toString(); // 形如 OK|2;1:张三:13800138000,2:李四:13900139000 } private static String delContact(String[] parts) { try { int id Integer.parseInt(parts[1]); if (contacts.remove(id) ! null) return OK|deleted id; return ERR|not found id; } catch (NumberFormatException e) { return ERR|bad id; } } private static String updateContact(String[] parts) { try { int id Integer.parseInt(parts[1]); Contact old contacts.get(id); if (old null) return ERR|not found id; old.name parts[2]; old.phone parts[3]; return OK|updated id; } catch (Exception e) { return ERR|bad args; // 参数缺失或格式错都走这里 } } }一段容易被忽略的参数说明split(\\|, -1)里-1保留尾部空字符串这样ADD||123会被解析成三个字段其中姓名为空字符串后续能拿到错误而不是数组越界。LIST的返回格式是OK|总数;id:name:phone,id:name:phone一条消息带出整个通讯录客户端解析逻辑也简单。这个设计牺牲了超长列表的可读性但课设几十条数据完全够用。提示日志里把收到的原始命令和返回结果成对打印答辩时能直接展示「客户端发了什么、服务端回了什么」的完整链路。这也是报告里「系统验证」章节的素材来源。4. 客户端实现登录、收发循环和一套演示台本4.1 最小客户端骨架连接、控制台输入、逐行收发客户端不需要界面也能把流程走通。用控制台交互每行输入一条命令逐行发出去再把服务端返回打印出来。这样一个客户端代码只有六十行左右答辩现场重连、出错、恢复的流程都可控。如果课设要求必须带图形界面可以在控制台版跑通后再套一层 Swing不建议一开始就把 Socket 和 JFrame 写在一起。import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; public class ContactClient { public static void main(String[] args) throws Exception { if (args.length 2) { System.out.println(usage: java ContactClient host port); return; } String host args[0]; int port Integer.parseInt(args[1]); try (Socket sock new Socket(host, port); BufferedReader in new BufferedReader( new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8)); PrintWriter out new PrintWriter( new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8), true); BufferedReader cmd new BufferedReader( new InputStreamReader(System.in, StandardCharsets.UTF_8))) { System.out.println(connected to host : port); while (true) { System.out.print(cmd ); String line cmd.readLine(); if (line null) break; if (QUIT.equalsIgnoreCase(line.trim())) break; out.println(line.trim()); // 发一条命令 String resp in.readLine(); // 等服务端一行响应 if (resp null) { System.out.println([server closed]); break; } prettyPrint(resp); } } catch (ConnectException e) { System.out.println(connection refused: server not running?); } } private static void prettyPrint(String resp) { // LIST 响应形如 OK|2;1:张三:13800138000,2:李四:13900139000 if (resp.startsWith(OK|) resp.indexOf(;) 0) { String body resp.substring(3); int sep body.indexOf(;); int total Integer.parseInt(body.substring(0, sep)); System.out.println(total contacts: total); if (total 0) return; for (String item : body.substring(sep 1).split(,)) { String[] f item.split(:); System.out.printf( id%s name%s phone%s%n, f[0], f[1], f[2]); } return; } System.out.println(resp); } }try-with-resources把 Socket、输入输出流、控制台流放在同一作用域任何一端关闭所有资源跟着释放。prettyPrint只处理LIST一种特殊格式其余响应原样打印。这样客户端逻辑被拆成了两块收发循环负责网络格式化负责展示互不干扰。运行方式先起服务端java ContactServer再开两个终端窗口各跑java ContactClient 127.0.0.1 8899。两个客户端可以同时登录、各自增删联系人服务端的日志会显示每个连接的操作记录。这个过程就是「多客户端并发访问」的演示证据。4.2 演示台本先 LIST 再 ADD 再 UPDATE 再 DEL答辩演示时最容易出问题的不是代码逻辑而是操作顺序乱。我习惯把演示编排成一条不会回退的路径服务端启动打印listening on 8899。客户端 A 登录LOGIN|alice看到OK|welcome alice。先执行LIST服务端返回OK|0。连续新增三条联系人每次关注返回的id自增。再LIST能看到三条记录完整展示。UPDATE|1|张三|13900139000再LIST确认修改。DEL|2再LIST确认删除。客户端 B 登录执行LIST能看到 A 添加的数据——这一步直接证明数据是服务端共享的。这套顺序每一环都在验证上一环的结果。如果某一步返回ERR|终端上的报错信息本身就是演示的一部分不要慌照着报错内容解释「服务端做了参数校验」即可。4.3 断线与重连策略不要在板书上做文章客户端关闭窗口、直接结束进程时TCP 连接会发 FIN服务端readLine()返回null线程正常退出。课设里不需要实现断线重连但要处理一个常见误操作客户端先退出再重新登录时如果立刻提示connection refused那是服务端没起来或者端口被上次残留的进程占用。演示之前先确认服务端进程还在、端口没有冲突这比任何重连代码都实在。5. 避坑清单粘包、乱码、端口占用与线程泄漏的五个现场5.1 粘包与拆包为什么别人一调就崩我这套不崩现象用Socket.getInputStream().read(byte[])一次读数据读到一条半的命令解析直接报错。原因TCP 是字节流底层可能把多条write的数据合并成一个包也可能把一条数据拆成多个包读端不按帧边界消费就会粘包。解决应用层强制按行读写写端println()读端readLine()这就是最简单的拆包方案。如果换了语言或者改用字节流协议就得自己定义长度前缀帧比如前 4 字节表示后续数据长度这是课后值得延伸研究的方向。5.2split(|)翻车竖线分隔符的正则陷阱现象ADD|张三|123.split(|)得到的是每个字符单独成数组字段全部错位。原因String.split()接收的是正则表达式|在正则里是「或」的意思不是普通字符。解决用split(\\|)做转义或者用split(Pattern.quote(|))。这两个写法写在注释里后续维护的人就不会再踩。这个坑在答辩时经常被老师拿来当追问点能主动讲出「正则需要转义」反而比蒙混过关加印象分。5.3 中文乱码全是默认字符集惹的祸现象本机 Windows 上测试正常换到 Linux 或者 macOS 上运行联系人姓名变成乱码。原因InputStreamReader和OutputStreamWriter没有指定字符集用了操作系统默认编码Windows 默认 GBKLinux 默认 UTF-8两端不一致就乱。解决所有涉及 Socket 流的地方显式传StandardCharsets.UTF_8服务端和客户端统一。注意用StandardCharsets.UTF_8常量而不是字符串UTF-8编译期就能发现拼写错误。5.4 端口占用服务端重启报 Address already in use现象CtrlC 杀掉服务端立刻重启报java.net.BindException: Address already in use。原因TCP 连接关闭后端口进入TIME_WAIT状态默认持续约两分钟在这期间端口不能被重新绑定。解决ServerSocket创建后、bind()之前调用serverSocket.setReuseAddress(true)允许端口在TIME_WAIT期间被重新绑定。这个设置要在代码里写清楚注释报告里也可以作为「协议细节处理」的一个亮点。5.5 半开连接与线程泄漏拔网线比关窗口更隐蔽现象客户端在任务管理器里被强杀服务端并没有立刻感知这个连接还挂在sessions里对应的服务端线程一直在readLine()阻塞。原因进程强杀可能不触发 TCP FIN服务端认为连接还活着。解决给Socket设置setSoTimeout(60000)读超时后抛出SocketTimeoutExceptionhandle()的 catch 捕获后走finally清理现场。这个超时时间不能设太短否则演示时稍微停顿就被服务端断开60 秒比较合适。这个坑在真实生产系统里要靠心跳机制解决课设里讲清「超时是兜底心跳是优选」这个思路就够了。6. 最后一步验证矩阵和一篇能立住的课设报告6.1 五分钟演示的节奏控制演示顺序比讲解内容更影响评分。我习惯的动作是先让服务端日志清晰可见再让两个终端窗口并排展示客户端交互演示路径严格按「登录 → 空列表 → 新增 → 查询 → 更新 → 删除 → 多客户端共享数据」推进。每做一个动作指一下服务端日志里对应的 命令 返回让老师和同学看到完整请求响应链路。五个小时核心逻辑讲完剩下的时间留给老师提问。6.2 报告里最容易被翻的三页写法课程设计报告不需要写满五十页但三处必须写得扎实。第一处是协议设计直接把 2.2 节的命令表放成表格标注每条命令的字段含义和返回格式这张表是全文的骨架。第二处是消息时序画一张登录、增删改查的往返时序图标注客户端哪一行发、服务端哪一行处理、返回哪一行。第三处是测试验证把演示台本里每步命令的实际输出贴上去不要只写「测试通过」把OK|id3这种真实交互记录截图放进去。踩过的那几个坑挑两个写进「遇到的问题与解决」这段被老师追问的概率很低——因为是你亲手排掉的生产过程本身就是可信的。6.3 如果重做一次我会改什么这套方案所有代码加起来不到两百行跑通整个演示只需要两个终端窗口。若要说它有什么短板一是文本协议在数据量大时解析效率低二是内存存储不可持久化三是每连接一线程模型撑不住高并发。如果时间充裕把文本行协议换成长度前缀帧、把存储换成 SQLite、把固定线程换成线程池就是一份可以写进简历的项目演进路线。但课设的目标是把 TCP 的可靠传输、连接管理和 C/S 通信模型讲透这套方案已经覆盖了这些点。我做完这版之后印象最深的一条教训是协议先行代码随后的顺序千万别反过来。先把消息格式和状态机写在纸上再动手写类过程会顺畅很多。希望这篇笔记帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网