ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python局域网聊天程序开发:socket编程与TCP三次握手实战指南

Python局域网聊天程序开发:socket编程与TCP三次握手实战指南 简介这份计算机网络课设资料以P2P点对点技术为核心完整呈现局域网聊天程序的设计与实现过程面向计算机及相关专业的学生可用于课程设计、毕业设计或Socket编程入门参考。文档围绕需求分析、总体设计、详细设计展开覆盖用户注册登录、聊天、文件传输、好友管理四大模块并给出了基于Windows Visual Studio 2010 C#的客户端/服务器架构与数据流程适合需要快速搭建同类项目并撰写设计说明书的读者。包内仅含1个doc文件大小161KB内容为完整的课程设计说明书论文包含摘要、目录、需求分析、总体设计、详细设计、系统实现编码及运行结果等章节文字与图表可直接用于论文排版与代码思路参考。已有559人学习下载是P2P局域网通信类课设中一份清晰、实用的参考资料。1. 局域网聊天程序课设里性价比最高的那个题目这学期的计算机网络课设如果你还在网页管理系统和路由器配置里纠结我建议直接做局域网聊天程序。它把谢希仁教材里最抽象的 TCP 三次握手、socket 套接字、粘包问题全部变成肉眼可见的东西你在客户端打一行字另一台电脑立刻收到。这个题目所有重点都落在 socket 编程上不依赖外部框架一台电脑加一根网线就能起步后期又能自然延伸到并发、文件传输和抓包验证是性价比很高的课设方向。适合计算机网络刚结课、手里有 Python 基础、想拿一个“能演示又能讲清楚”的项目的同学。2. 动手前的选型TCP 还是 UDP决定你后面三天的姿态2.1 聊天场景为什么优先选 TCP而不是 UDP 广播很多同学拿到“局域网聊天”第一个想法是 UDP 广播一条消息发到255.255.255.255整个局域网都能收到实现看起来最短。但广播有三个现实问题。第一路由器一般不转发广播包跨 VLAN、跨网段直接失效你只能在一个广播域里玩。第二Windows 防火墙对入站广播报文经常直接丢弃宿舍里两台电脑开了防火墙就收不到。第三UDP 不保证到达、不保证顺序消息丢了程序不会报错你的聊天记录会随机缺一条课设报告里根本没法解释。反过来看 TCP它帮你解决三个问题消息不丢、消息有序、连接状态可探测。更重要的是TCP 是计算机网络课的知识核心服务端accept出连接、客户端connect发起握手这些动作都能在 Wireshark 里看到 SYN、SYN-ACK、ACK 三个包答辩时这就是一个现成的演示点。我一般建议主链路全走 TCP如果你觉得不用 UDP 可惜可以把客户端心跳探测做成 UDP——心跳丢几次无所谓不影响聊天。2.2 Python socket 是捷径但你要能说清原理课设实现语言我推荐 Python 3.x 标准库的socket模块零第三方依赖。原因很实际C 语言写聊天程序大量时间耗在字符串处理和缓冲区管理上Java 又绕不开复杂的线程模型。Python 的socket是对操作系统 socket 接口的直接封装你写的bind、listen、accept、sendall这些方法底层就是教材上讲的那套 POSIX socket 函数能很好地对应谢希仁教材里的知识点。这里有一个答辩老师必问的问题“别人用 Java 写你为什么用 Python”你不能只说“Python 简单”。正确回答是socket 是操作系统提供的网络编程接口Python 只是把socket()系统调用封装成了模块课设的重点是通信协议的设计和并发模型而不是语言本身的语法特性。只要把这个逻辑说清楚用什么语言反而成了你主动的技术选型。2.3 通信模型一对一、群聊、文件功能先画到纸上写代码之前先确定用服务端中转模型还是 P2P 点对点模型。局域网聊天程序最简单的成熟方案是前者所有客户端连到一台中心服务器消息发给服务器服务器再转发给目标客户端。功能模型是重点一对一私聊A 发到服务器服务器转给 B消息中带to字段服务器按目标路由群聊A 发到服务器服务器遍历在线列表广播服务器维护当前连接列表文件传输A 先发文件元信息再由服务器转发数据块块大小、粘包和超时控制上下线通知连接建立和关闭时广播系统消息try/finally保证清理为什么要保留服务器而不是纯 P2P因为客户端上线身份管理、在线列表维护、离线处理都需要一个中心节点。课设里如果两台电脑直接互联你得先解决 NAT 穿透那已经是另一个课题了。所以老老实实做服务端中转老师也更容易看懂你的架构。2.4 局域网聊天程序的接口约定消息怎么定义才不乱聊天程序本质是“消息协议”的设计这是很多课设翻车的重灾区。常见做法是定义一套 JSON 格式的消息体每个消息都包含这几个字段{ type: chat, from: alice, to: bob, content: 你好, time: 1711000000 }typejoin上线、chat聊天、system系统通知、file文件传输。from发送方昵称。服务端不要信任客户端发来的昵称而应该以连接登记为准。to目标用户缺省表示群聊。content消息正文type 为file时里面放文件名和 base64 数据。time时间戳用于展示和排序。有了这个结构功能扩展就很自然加私聊就是在to字段里填对方昵称加文件传输就是在type里加一个枚举值。协议是聊天程序的地基先定协议再写代码后面所有收发逻辑都围着这个结构转。3. 用 Python 把局域网聊天程序跑通服务端到客户端的完整代码3.1 先写消息协议用 JSON 和长度头解决“消息边界”问题TCP 是字节流协议recv(1024)读回来的数据可能是半条消息也可能包含两条完整消息这就是课上讲的粘包和半包。解决办法是在每条消息前拼一个 4 字节的长度头接收方先读满 4 字节得到长度再读满对应长度的字节才算拿到一条完整消息。这里给出一个公共的收发函数服务端和客户端共用import json import struct def send_msg(sock, msg: dict): data json.dumps(msg, ensure_asciiFalse).encode(utf-8) packet struct.pack(!I, len(data)) data sock.sendall(packet) def recv_exact(sock, n: int) - bytes: buf b while len(buf) n: chunk sock.recv(n - len(buf)) if not chunk: raise ConnectionError(连接已关闭) buf chunk return buf def recv_msg(sock) - dict: header recv_exact(sock, 4) length struct.unpack(!I, header)[0] body recv_exact(sock, length) return json.loads(body.decode(utf-8))struct.pack(!I, ...)里的!I表示大端序的无符号 4 字节整数网络字节序就是大端序这是行业惯例。recv_exact的作用是循环接收因为一次recv不一定能收满指定字节数必须循环直到拿够。这段代码解决了课设里最核心的粘包问题建议直接写进报告。3.2 服务端实现维护在线列表转发聊天消息服务端要干三件事监听端口、接收新连接、为每个连接开一个线程处理消息。下面是一个能直接跑通的最小实现import socket import threading import json import struct class ChatServer: def __init__(self, host0.0.0.0, port8023): self.host host self.port port self.clients {} # conn - nickname self.lock threading.Lock() def broadcast(self, msg: dict, excludeNone): for conn in list(self.clients): if conn ! exclude: self._send(conn, msg) def _send(self, conn, msg: dict): try: data json.dumps(msg, ensure_asciiFalse).encode(utf-8) conn.sendall(struct.pack(!I, len(data)) data) except Exception: pass def _recv(self, conn) - dict: # 先用 4 字节长度头拿到消息长度再读正文 header self._recv_exact(conn, 4) length struct.unpack(!I, header)[0] body self._recv_exact(conn, length) return json.loads(body.decode(utf-8)) def _recv_exact(self, conn, n: int) - bytes: buf b while len(buf) n: chunk conn.recv(n - len(buf)) if not chunk: raise ConnectionError(客户端断开) buf chunk return buf def handle_client(self, conn, addr): nickname None try: while True: msg self._recv(conn) if msg[type] join: nickname msg[content] with self.lock: self.clients[conn] nickname self.broadcast({type: system, content: f{nickname} 加入聊天室}, excludeconn) elif msg[type] chat: self.broadcast({type: chat, from: nickname, content: msg[content]}, excludeconn) except Exception: pass finally: # 连接断开时的清理移除客户端并广播下线 with self.lock: if conn in self.clients: nickname self.clients.pop(conn) conn.close() if nickname: self.broadcast({type: system, content: f{nickname} 离开聊天室}) def start(self): srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((self.host, self.port)) srv.listen(5) print(f服务端已启动监听 {self.host}:{self.port}) while True: conn, addr srv.accept() threading.Thread(targetself.handle_client, args(conn, addr), daemonTrue).start() if __name__ __main__: ChatServer().start()这里有几个参数值得说明。host0.0.0.0表示监听本机所有网卡这样局域网内其他机器才能通过你的 IP 连进来如果绑成127.0.0.1外部机器永远连不上。port8023是自定义端口避开常见的 8000、8080 能少很多冲突。listen(5)表示内核维护的连接队列上限是 5课设规模完全够用。SO_REUSEADDR允许服务端重启时复用端口防止出现端口占用报错。这段代码的逻辑主线是accept每拿到一个新连接就开一个handle_client线程每个线程循环recv消息收到join时把连接登记到clients字典并广播上线收到chat时把消息广播给除发送方外的所有在线客户端。finally块里的清理逻辑很关键客户端断开后必须删掉字典里的记录并广播下线否则在线列表会越攒越假。3.3 客户端实现发送线程与接收线程各司其职客户端比服务端简单但要明确一个原则接收消息必须放在独立线程里主线程负责读用户输入并发送。如果直接在input()等待用户输入的循环里调用recv消息到达时程序正卡在输入上界面不会刷新。import socket import threading import json import struct class ChatClient: def __init__(self, host, port, nickname): self.host host self.port port self.nickname nickname self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) def send_msg(self, msg: dict): data json.dumps(msg, ensure_asciiFalse).encode(utf-8) packet struct.pack(!I, len(data)) data self.sock.sendall(packet) def recv_msg(self) - dict: header self._recv_exact(4) length struct.unpack(!I, header)[0] body self._recv_exact(length) return json.loads(body.decode(utf-8)) def _recv_exact(self, n: int) - bytes: buf b while len(buf) n: chunk self.sock.recv(n - len(buf)) if not chunk: raise ConnectionError(服务端连接断开) buf chunk return buf def receive_loop(self): try: while True: msg self.recv_msg() if msg[type] chat: print(f\n[{msg[from]}] {msg[content]}) elif msg[type] system: print(f\n[系统] {msg[content]}) except Exception: print(\n连接已断开按回车退出) self.sock.close() def start(self): self.sock.connect((self.host, self.port)) self.send_msg({type: join, content: self.nickname}) threading.Thread(targetself.receive_loop, daemonTrue).start() print(输入消息回车发送输入 /quit 退出) while True: text input() if text /quit: self.sock.close() break if text.strip(): self.send_msg({type: chat, content: text}) if __name__ __main__: host input(请输入服务器 IP).strip() or 127.0.0.1 nickname input(请输入昵称).strip() or user ChatClient(host, 8023, nickname).start()客户端代码有一个容易忽略的细节如果你的程序在后台线程recv_msg时抛了异常daemonTrue保证这个线程不会拖住进程主线程还能继续让用户输入命令退出。另外input()和print()混用会出现提示符和消息交错输出现象这是控制台程序的正常行为不影响功能。3.4 局域网内跑通的最小步骤从本机回环到两台电脑先做单机验证再上真机不要一上来就在两台电脑之间调。步骤是# 终端 1启动服务端 python server.py # 终端 2启动第一个客户端IP 填 127.0.0.1 python client.py # 终端 3启动第二个客户端验证两个客户端能互相收发 python client.py本机测试通过后把服务端换到一台真机或者同一台电脑客户端连服务端的局域网 IP。查看 IP 的命令Windows 用ipconfigLinux 用ip addr或者ifconfig。两台机器必须处于同一个网段最稳妥的方式是都连同一个路由器或交换机。虚拟机里有坑虚拟机网络模式如果是 NAT客户机不能通过宿主机 IP 访问虚拟机里的服务端要改成桥接模式让虚拟机拿到和宿主机同网段的 IP。环境服务端配置客户端配置单机回环python server.py监听0.0.0.0连接127.0.0.1:8023同一局域网两台真机服务端 IP 设为0.0.0.0防火墙放行 8023连接服务端 IP例如192.168.1.10:8023虚拟机互联虚拟机网卡改为桥接模式连接虚拟机 IP不是宿主机 IP防火墙这一步几乎是必坑项。Windows 在第一次运行 Python 时会弹“允许访问网络”如果点了取消后面连不上就把防火墙入站规则打开新建一条允许 TCP 端口 8023 的规则。Linux 下如果开了 firewalld执行firewall-cmd --add-port8023/tcp放行。4. 局域网聊天程序常见问题粘包、端口和防火墙的四个硬坑4.1 现象发送两条消息收到时粘在一起你连续发“你好”和“世界”对方收到的是“你好世界”或者收到一条断成两截的消息。原因是 TCP 是字节流协议根本没有“消息边界”多个send的数据可能在内核缓冲区里合并成一个包发给对方也可能一个包被拆成多次recv拿到。这就是计算机网络课上讲的粘包和半包。解决方法是自定义消息边界。我上面的代码用的是“4 字节长度头 JSON 正文”接收方先读满 4 字节算长度再按长度读正文一次循环拿一条完整消息。要注意长度头自己也要用recv_exact循环读因为 4 个字节也可能半路被拆开。不要用send之后sleep来缓解那是在赌网络时序课设评委一问就露馅。4.2 现象重启服务端提示端口被占用服务端报OSError: [Errno 98] Address already in use或者 Windows 下的WinError 10048通常是因为上一个程序实例没退出干净。连接断开后 TCP 会进入 TIME_WAIT 状态默认等 1 到 4 分钟才释放端口这个状态是协议保证数据完整性的正常运行机制不是 bug。解决分两层。第一层代码里在bind之前加srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)告诉系统这个端口可以复用。第二层如果还是提示占用用命令找到占用进程Windows 执行netstat -ano | findstr 8023Linux 执行lsof -i:8023看输出的 PID然后用任务管理器或kill结束掉旧进程。注意排查到 8023 端口没被其他课程设计程序抢占换个不常用的高位端口能省掉这类麻烦。4.3 现象同一台电脑能连换成局域网就超时本机测试一切正常把服务端 IP 填给室友客户端一直Connection refused或者超时。这个问题的排查顺序有三条。第一服务端bind的是不是0.0.0.0如果绑的是127.0.0.1操作系统只接受本机回环连接局域网请求直接被拒。第二两台机器是不是真的同网段建议在客户端机器上ping服务端 IP能通再讨论代码问题。第三防火墙拦截入站连接入站规则里要放行 TCP 8023。还有一个容易踩的坑如果服务端跑在虚拟机的 Ubuntu 里虚拟机默认 NAT 模式下外部机器访问虚拟机端口需要做端口映射不是只改代码就能解决的换成桥接模式最省事。4.4 现象一个客户端退出其他客户端全部卡死其中一个用户按了 CtrlC 关窗口过一会儿整个服务端没反应其他客户端也发不出消息。原因是服务端在向已经关闭的连接执行sendall时抛了BrokenPipeError/ConnectionResetError而这个异常发生在广播代码里没被捕获导致那个客户端对应的 handler 线程崩掉如果在线列表没清干净后续广播还在尝试发给死连接最终服务端线程池全部被异常阻塞。解决思路是“异常的归异常资源的归资源”。所有send操作都包一层try/except发送失败时移除该连接连接关闭时在finally里删字典对clients字典的遍历要用list(self.clients)先拷贝一份否则边遍历边删除会抛RuntimeError。服务端的remove_client方法要保证幂等重复删同一连接不会出错。5. 从“能跑”到“能答辩”验证方法和三个加分项5.1 用 Wireshark 给老师展示你的 TCP 连接过程答辩时最加分的一步是现场抓包。打开 Wireshark选择回环网卡本机验证或实际网卡显示过滤器输入tcp.port 8023然后重新启动服务端和客户端。你会看到连接建立时依次出现 SYN、SYN-ACK、ACK 三个报文段这就是三次握手。客户端发送聊天消息时能看到一个 PSH 标志的数据包里面包含你定义的消息内容关闭客户端时能看到 FIN、ACK 的四次挥手过程。钩子点报告里怎么写TCP 三次握手抓包截图标注 SYN、SYN-ACK、ACK 序列号粘包缓解抓包展示多个 send 合并成一个段说明你的长度头设计连接关闭展示 FIN 包说明服务端清理流程端口状态配合netstat展示 TIME_WAIT解释 SO_REUSEADDR 的意义不用抓太多包三次握手的三个包加上一条聊天消息的 PSH 包就够了这比写三百行代码描述更有说服力。5.2 加一个文件传输功能扩展消息类型而不是改协议聊天程序加分项里最实惠的是文件传输。我建议走复用现有协议的方案新增type: filecontent字段里包含文件名和 base64 编码的文件内容。客户端发送方读文件、base64 编码、放进消息体接收方收到后解码写盘。小文件几 MB 以内这样处理完全没问题代码改动也很小。import base64 def send_file(client, filepath): with open(filepath, rb) as f: raw base64.b64encode(f.read()).decode(utf-8) client.send_msg({ type: file, filename: filepath.split(/)[-1], content: raw })注意这个方案只适合课设演示因为整个文件塞进一条 JSON 消息会占大量内存。真正大文件应该拆成多个块、每块单独发送并且加上校验和。这块内容可以作为报告里的“未来改进”答辩老师问起来你能说出这个边界说明你真的想过而不是只会贴代码。5.3 把异常处理代码写进报告这些细节最值钱我批过不少同学的课设代码功能都正常但报告里看不到异常处理一问“客户端断了会怎样”就答不上来。建议在报告里专门起一节写异常处理策略发送失败时移除连接、接收循环里捕获连接断开、finally清理资源。这三件事每件配 5 行代码加上一句说明比抄教材有价值得多。我后来做任何网络程序都是先写一页协议文档再动代码消息边界、断线清理、字段命名都定清楚才动手。这个习惯帮我少踩了很多坑希望帮到你。本文还有配套的精品资源点击获取
返回列表