
简介这份资源是广开国开电大网络编程技术实践技能训练1的参考答案面向正在学习Web前端基础、需要完成购物车页面实训任务的电大学员与自学者。压缩包共5个文件包含html页面结构、css样式表、js交互脚本以及两张jpg图片素材整体约62KB体量轻便便于快速查阅与对照练习。资源围绕HTML布局、CSS美化与JavaScript交互三条主线展开涵盖商品列表、数量输入、加入购物车按钮、总价计算、输入合法性校验及localStorage数据持久化等典型知识点并涉及AJAX异步更新思路可帮助读者理解前端数据存储与异步通信的基本用法。目前已有253人学习适合作为实训提交前的参考模板与排错对照也可用于梳理购物车页面的完整实现逻辑。1. 网络编程技术实践技能训练1从套接字到并发服务的完整落地路径很多同学拿到“网络编程技术实践技能训练1”这个任务时第一反应是去搜答案但真正做过一轮的人都知道这个训练的核心不是背概念而是把 TCP 客户端-服务端通信、多线程并发处理、异常断开重连这三件事串起来跑通。它解决的是“看得懂 socket 函数但写不出稳定服务”的典型断层适合已经学过 C 或 Python 基础、但没独立写过网络程序的人。我当年第一次做的时候服务端 accept 之后直接阻塞在 recv客户端一断整个进程就挂血泪经验告诉我训练1真正要练的是对阻塞、并发和连接生命周期的控制感而不是抄一份能跑的代码。2. 训练1到底在练什么TCP 通信的最小闭环与三个必调参数2.1 从 socket 到 accept一次完整 TCP 连接经历了什么训练1通常要求实现一个基本的客户端-服务端通信程序服务端监听指定端口客户端连接后发送数据服务端接收并回显。这个流程看起来简单但每一步都有容易翻车的地方。先看服务端的最小骨架。以 Python 为例核心代码不超过 15 行但参数选错就会出玄学问题import socket # 创建 TCP socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置端口复用避免重启时 Address already in use server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定地址和端口0.0.0.0 表示监听所有网卡 server.bind((0.0.0.0, 8888)) # 开始监听backlog 设为 5 server.listen(5) print(服务端启动监听 8888 端口) while True: # 阻塞等待客户端连接 conn, addr server.accept() print(f新连接来自 {addr}) # 接收数据缓冲区 1024 字节 data conn.recv(1024) if data: print(f收到: {data.decode()}) # 回显数据 conn.sendall(data) # 关闭本次连接 conn.close()这段代码的逻辑说明AF_INET指定 IPv4 地址族SOCK_STREAM指定 TCP 协议。SO_REUSEADDR是关键参数不设置的话服务端程序退出后端口会处于 TIME_WAIT 状态短时间内无法重新绑定调试时反复重启就会遇到OSError: [Errno 98] Address already in use。listen(5)中的 5 是 backlog表示内核为该 socket 维护的已完成连接队列长度高并发场景下这个值需要调大。客户端对应代码import socket client socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接服务端注意这里用 127.0.0.1 而非 0.0.0.0 client.connect((127.0.0.1, 8888)) client.sendall(hello.encode()) # 接收回显同样 1024 缓冲区 resp client.recv(1024) print(f服务端回显: {resp.decode()}) client.close()这里有个新手常踩的坑客户端连接时目标地址写0.0.0.0会直接报错0.0.0.0只能用于 bind 表示监听所有网卡不能用于 connect。2.2 backlog、缓冲区大小、超时时间三个参数决定程序能不能用训练1的评分往往不只看能不能跑通还看程序在异常情况下的表现。以下三个参数直接决定程序的健壮性参数位置典型值作用调错后果backloglisten()5~128已完成连接队列长度太小导致并发连接被拒缓冲区大小recv()/send()1024~65536单次读写最大字节数太小导致大数据分片太大浪费内存超时时间settimeout()5~30 秒阻塞操作最长等待时间不设置则永久阻塞设置过短误判断开settimeout的用法值得单独说。默认情况下recv和accept都是永久阻塞的如果客户端连上不发数据服务端就卡死在recv上整个程序失去响应。加上超时后conn.settimeout(10) # 10 秒内没有数据就抛 socket.timeout try: data conn.recv(1024) except socket.timeout: print(接收超时关闭连接) conn.close()注意超时后连接不一定已经断开只是本次 recv 没有拿到数据需要根据业务决定是重试还是关闭。我一般会在训练1里把超时设为 10 秒既能演示异常处理又不会让调试等太久。2.3 多线程并发让服务端同时处理多个客户端单线程服务端的致命问题是处理第一个连接时第二个连接只能排队等 accept。训练1通常要求支持至少两个客户端同时通信这就必须引入并发。常见做法是每个连接开一个线程import socket import threading def handle_client(conn, addr): print(f处理来自 {addr} 的连接) conn.settimeout(10) try: while True: data conn.recv(1024) if not data: break # 客户端正常关闭 conn.sendall(data) except socket.timeout: print(f{addr} 接收超时) finally: conn.close() print(f{addr} 连接关闭) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(10) while True: conn, addr server.accept() # 每个连接启动一个守护线程 t threading.Thread(targethandle_client, args(conn, addr), daemonTrue) t.start()逻辑说明daemonTrue表示主线程退出时子线程自动结束避免程序无法退出。handle_client里用while True循环持续接收直到recv返回空字节串这表示客户端调用了close()或shutdown()。注意if not data判断的是空 bytes不是 None这是 TCP 连接正常关闭的标准信号。参数方面listen(10)的 backlog 调到了 10因为现在有并发需求。线程数没有硬上限但训练1的场景下超过 50 个并发连接就应该考虑线程池或 IO 多路复用否则线程切换开销会拖慢响应。3. 从能跑到能过训练1的完整实现步骤与调试方法3.1 环境准备与分步实现顺序训练1一般不限语言C、Python、Java 都可以。我建议用 Python因为 socket API 封装层次适中能看清底层行为又不用处理内存管理。环境只需要标准库不需要额外安装包。实现顺序建议按以下步骤走每步验证后再进入下一步写一个单线程服务端accept 后打印客户端地址不接收数据用telnet或客户端脚本测试连接是否建立。加入 recv 和 sendall实现回显用客户端发一条消息验证。加入 settimeout测试客户端连上不发数据时服务端是否能在超时后释放连接。加入多线程同时启动两个客户端验证两个连接互不阻塞。加入异常处理测试客户端强制断开直接杀进程时服务端是否报错。每一步的验证命令# 测试端口是否监听 netstat -an | grep 8888 # 用 telnet 手动测试部分系统需先安装 telnet 客户端 telnet 127.0.0.1 88883.2 用日志定位阻塞和异常断开训练1最常见的调试需求是程序卡住了不知道卡在哪。我的习惯是在关键路径上加日志import logging logging.basicConfig(levellogging.DEBUG, format%(asctime)s %(levelname)s %(message)s) # 在 accept 前 logging.debug(等待新连接...) conn, addr server.accept() logging.debug(f接受连接: {addr}) # 在 recv 前 logging.debug(f等待 {addr} 数据...) data conn.recv(1024) logging.debug(f收到 {len(data)} 字节)日志能直接告诉你卡在 accept 还是 recv。如果卡在 accept说明没有客户端连上来检查客户端目标 IP 和端口。如果卡在 recv说明连接已建立但客户端没发数据检查客户端是否执行了 send。异常断开的表现是ConnectionResetError或BrokenPipeError。客户端直接杀进程时服务端 recv 会收到ConnectionResetError: [Errno 104] Connection reset by peer。这不是 bug是 TCP 的正常行为需要用 try-except 捕获try: data conn.recv(1024) except ConnectionResetError: logging.warning(f{addr} 强制断开) conn.close() return3.3 验证并发是否真正生效多线程写完后怎么确认两个客户端是并发处理的而不是排队的方法很简单客户端 A 连上后不发数据客户端 B 连上后发数据。如果 B 能立刻收到回显说明并发生效如果 B 卡住直到 A 超时说明还是串行。测试脚本# client_a.py连上后 sleep 15 秒不发数据 import socket, time c socket.socket(socket.AF_INET, socket.SOCK_STREAM) c.connect((127.0.0.1, 8888)) print(A 已连接等待 15 秒) time.sleep(15) c.close()# client_b.py连上后立即发数据 import socket c socket.socket(socket.AF_INET, socket.SOCK_STREAM) c.connect((127.0.0.1, 8888)) c.sendall(from B.encode()) print(c.recv(1024).decode()) c.close()先运行 A再运行 B。如果 B 在 1 秒内打印出from B并发正确。如果 B 等到 A 的 15 秒结束才打印说明 accept 或 recv 被串行化了检查是否忘了开线程。4. 训练1避坑指南五个让程序翻车的典型问题4.1 端口被占用导致服务端启动失败现象运行服务端时报OSError: [Errno 98] Address already in use。原因上一次运行的服务端进程没有完全退出或者虽然退出了但端口处于 TIME_WAIT 状态。TCP 规定主动关闭方需要等待 2MSL通常 60 秒才能释放端口。解决设置SO_REUSEADDR为 1允许绑定处于 TIME_WAIT 的端口。如果已经设置了还报错用lsof -i :8888或netstat -tlnp | grep 8888找到残留进程并 kill 掉。4.2 recv 返回空字节串被误判为错误现象客户端正常关闭后服务端 recv 返回b程序把它当成空数据继续循环导致死循环。原因TCP 中recv返回空 bytes 表示对端已关闭连接这是正常关闭信号不是错误。解决在循环中判断if not data: break收到空字节串就跳出循环并关闭连接。注意区分b和Nonerecv永远不会返回 None。4.3 多线程共享变量导致数据错乱现象多个客户端同时连接时服务端打印的地址或数据出现串台。原因多个线程共享了同一个全局变量比如用一个全局 list 存客户端信息没有加锁。解决训练1的场景下每个线程只操作自己的 conn 和 addr不共享可变状态。如果确实需要共享用threading.Lock()保护临界区。我一般建议训练1不要引入共享状态保持每个连接独立处理。4.4 客户端 connect 被拒绝现象客户端报ConnectionRefusedError: [Errno 111] Connection refused。原因服务端没有启动或者服务端 bind 的地址和客户端 connect 的地址不匹配。常见错误是服务端 bind 了127.0.0.1客户端却连192.168.x.x。解决服务端 bind0.0.0.0表示监听所有网卡客户端连本机用127.0.0.1连局域网其他机器用服务端的实际 IP。先用ping确认网络可达再用telnet确认端口开放。4.5 发送大数据时 recv 只收到一部分现象客户端 sendall 发送 10000 字节服务端一次 recv 只收到 4096 字节。原因TCP 是字节流协议不保证 send 和 recv 的次数一一对应。内核发送缓冲区和接收缓冲区大小有限大数据会被分片。解决在应用层定义消息边界。简单做法是发送方先发 4 字节的长度头接收方先读 4 字节得到长度再循环读取直到收满。训练1如果只传短消息可以把缓冲区设为 65536 减少分片概率但正式场景必须做消息 framing。5. 进阶技巧用 select 替代多线程做 IO 多路复用训练1做完多线程版本后如果还想深入下一步就是用select或selectors模块实现单线程并发。这不是训练1的硬性要求但能让你真正理解“并发不一定靠线程”。select的核心思路是把所有需要监听的 socket 放入一个列表调用select阻塞等待只要有任何一个 socket 可读或可写就返回然后逐个处理。这样单线程就能管理多个连接没有线程切换开销。import socket import select server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(10) server.setblocking(False) # 设为非阻塞 inputs [server] # 监听列表 while True: # 阻塞等待直到有 socket 可读 readable, _, _ select.select(inputs, [], []) for sock in readable: if sock is server: # 新连接 conn, addr sock.accept() conn.setblocking(False) inputs.append(conn) print(f新连接: {addr}) else: # 已有连接有数据 try: data sock.recv(1024) if data: sock.sendall(data) else: # 客户端关闭 inputs.remove(sock) sock.close() except ConnectionResetError: inputs.remove(sock) sock.close()逻辑说明select.select(inputs, [], [])的第一个参数是读监听列表第二个是写监听列表第三个是异常监听列表。返回的 readable 列表中包含所有可读的 socket。setblocking(False)是关键非阻塞模式下 accept 和 recv 不会卡住没有数据时立刻返回异常所以必须配合 select 使用。参数方面select在 Linux 下默认最多监听 1024 个文件描述符训练1的场景完全够用。如果连接数超过这个量级应该用epollLinux或kqueuemacOSPython 的selectors模块会自动选择最优实现。我自己的习惯是训练1先用多线程跑通理解连接生命周期再用 select 重写一遍理解事件驱动。两版都跑过之后后面遇到高并发场景就不会慌。这个训练的价值不在于答案本身而在于你亲手处理过连接断开、超时、分片这些真实问题。希望帮到你。本文还有配套的精品资源点击获取