拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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 }
  • type:join上线、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_ascii=False).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, host='0.0.0.0', port=8023): self.host = host self.port = port self.clients = {} # conn -> nickname self.lock = threading.Lock() def broadcast(self, msg: dict, exclude=None): 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_ascii=False).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} 加入聊天室'}, exclude=conn) elif msg['type'] == 'chat': self.broadcast({'type': 'chat', 'from': nickname, 'content': msg['content']}, exclude=conn) 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(target=self.handle_client, args=(conn, addr), daemon=True).start() if __name__ == '__main__': ChatServer().start()

这里有几个参数值得说明。host='0.0.0.0'表示监听本机所有网卡,这样局域网内其他机器才能通过你的 IP 连进来;如果绑成127.0.0.1,外部机器永远连不上。port=8023是自定义端口,避开常见的 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_ascii=False).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(target=self.receive_loop, daemon=True).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时抛了异常,daemon=True保证这个线程不会拖住进程,主线程还能继续让用户输入命令退出。另外,input()和print()混用会出现提示符和消息交错输出现象,这是控制台程序的正常行为,不影响功能。

3.4 局域网内跑通的最小步骤:从本机回环到两台电脑

先做单机验证,再上真机,不要一上来就在两台电脑之间调。步骤是:

# 终端 1:启动服务端 python server.py # 终端 2:启动第一个客户端,IP 填 127.0.0.1 python client.py # 终端 3:启动第二个客户端,验证两个客户端能互相收发 python client.py

本机测试通过后,把服务端换到一台真机(或者同一台电脑),客户端连服务端的局域网 IP。查看 IP 的命令:Windows 用ipconfig,Linux 用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-port=8023/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 8023,Linux 执行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 现象:一个客户端退出,其他客户端全部卡死

其中一个用户按了 Ctrl+C 关窗口,过一会儿整个服务端没反应,其他客户端也发不出消息。原因是服务端在向已经关闭的连接执行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: 'file',content字段里包含文件名和 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 行代码加上一句说明,比抄教材有价值得多。

我后来做任何网络程序,都是先写一页协议文档再动代码,消息边界、断线清理、字段命名都定清楚才动手。这个习惯帮我少踩了很多坑,希望帮到你。

本文还有配套的精品资源,点击获取

返回列表