简介:在计算机网络课程设计中,理解TCP/IP分层模型是构建可靠应用的基础。HTTP协议作为应用层最常用的协议,定义了客户端与服务器之间的请求-响应语义;而TCP则在传输层通过三次握手、确认重传等机制保障数据可靠到达。实际开发电子购物网站时,Flask框架的请求处理、Session机制、数据库连接池等都对应着具体的网络知识点。通过一个可运行的网站项目,可以清晰观察到浏览器与后端之间的连接建立、Cookie/Session的状态维持,以及并发下单时的数据库事务与行锁。围绕课程设计场景,从架构选型到代码实现,再到Wireshark抓包验证,这些网络原理在工程中的落地方式便一目了然,能帮助读者把课本知识转化为可解释的实践能力。
1. 这题目不是让你写个网页:先弄懂课程设计在考什么
“计算机网络课程设计电子购物网站设计”这个题目,十个人有九个会理解成“写个购物网页”。我见过太多交上来的作业,前端页面挺漂亮,一问数据库为什么用连接池、TCP连接是在用户点击的哪一刻建立的,就卡壳了。这门课设真正要检验的,是你能不能把课本里的分层模型、HTTP协议、Socket通信,串进一个能跑的业务系统里。购物网站只是载体,网络才是考点。我这里按自己做课程设计的路线,从架构、代码实现到抓包验证,把每个网络知识点和代码对应的位置讲清楚,也把最容易翻车的五个坑提前点掉。适合正在选题的学生,也适合想复习网络编程的从业者。
2. 选型与架构:B/S结构是主流,但网络层设计才是评分重点
做课程设计的第一步不是打开IDE,而是先在纸上把网络拓扑画出来。电子购物网站最常见的形态是B/S结构:浏览器当客户端,后端服务器处理业务,数据库单独跑在一台机器上。老师不会只看你的页面能不能点,他会从网络分层的角度追问你:用户登录时到底发生了什么?所以架构选型上,你越能讲清楚每一层的作用,得分越高。
2.1 三层架构里的网络分层:自顶向下设计
购物网站逻辑上可以拆成三层:表现层(浏览器里的页面)、业务层(处理登录、下单)、数据层(存储商品和订单)。对应到计算机网络,就是应用层的HTTP、传输层的TCP、网络层的IP。最常见的教材,比如谢希仁《计算机网络》,讲的是自顶向下的思路:先规定应用层的数据格式,再决定传输层怎么传。
开发时也按这个顺序来:先把接口定好,比如商品列表接口返回什么JSON、登录接口传什么字段,然后再去写数据库表。这样到了答辩环节,你可以直接画一张分层图,说清楚一次“查看商品列表”的请求从浏览器到服务器,每一层做了什么。HTTP报文在应用层生成,TCP给数据分段并做可靠性保障,网络层加上源IP和目标IP,最后链路层转成帧。服务器收到后逐层剥掉头部,最后把请求交给Flask路由处理。
2.2 前端、后端、数据库之间的网络连接方式:一张表理清端口与协议
购物网站里同时存在两段网络连接:浏览器到后端,后端到数据库。这两段的协议和端口完全不同,很多同学把它们混在一起,导致出了问题也不知道查哪一段。
| 连接路径 | 协议 | 默认端口 | 典型地址 |
|---|---|---|---|
| 浏览器 → Flask后端 | HTTP | 5000(或80) | http://127.0.0.1:5000 |
| Flask后端 → MySQL | MySQL协议(基于TCP) | 3306 | 127.0.0.1:3306 |
本机调试时都用127.0.0.1,但到了答辩演示,如果想让教室里的另一台电脑也访问你的网站,浏览器里输入的就应该是后端的局域网IP,比如http://192.168.1.101:5000。这要求后端必须监听0.0.0.0,而不是默认的127.0.0.1。数据库连接也是一样的道理,如果数据库不在本机,host就要改成那台机器的IP。这里能自然引出DNS的作用:如果不用IP而是配置一个域名,就需要经过DNS解析,这也是计算机网络课里的常考知识点。
2.3 技术栈选择:用Flask做最小闭环,Java/Servlet也是备选
我不太推荐课设直接用Spring Boot,倒不是学不会,而是框架封装太多,你很难向老师解释清楚“Tomcat到底在哪一步监听了TCP端口”。Flask自带一个基于Werkzeug的开发服务器,代码量小,适合把网络通信原原本本露出来。
当然,学校如果指定Java,用Servlet也完全没问题。Servlet本身不监听端口,真正监听8080端口的是Tomcat。你写的doGet/doPost方法只是在处理已经解析好的HTTP请求。原理是通用的,只是描述的时候要说清楚“Servlet容器负责TCP连接与HTTP解析”。
我的习惯是用Flask,依赖只需要flask和pymysql。安装命令是:
pip install flask pymysql dbutils这里dbutils是连接池工具,后面会用到。如果老师只允许用标准库,也可以只用socket手写一个HTTP服务器,但工作量大很多,Flask能让你把精力放在购物流程本身,同时又保留了“监听端口、解析请求”的可解释性。
3. 把购物网站拆成四个网络模块:从商品浏览到订单提交
购物网站看起来功能多,但课程设计只要覆盖四个核心模块就能讲清楚全部网络知识点:商品展示、用户登录、购物车、提交订单。这四个模块分别对应无状态HTTP请求、Cookie与Session、会话状态维持、TCP可靠传输与事务的一致性。
3.1 商品展示:一个GET请求背后的HTTP报文
商品展示是唯一不用登录就能访问的页面,它最能说明HTTP的基础知识。前端访问后端接口,后端从数据库查商品,再以JSON格式返回。Flask里的写法非常简单:
from flask import Flask, jsonify app = Flask(__name__) @app.route("/api/goods", methods=["GET"]) def goods_list(): goods = [ {"id": 1, "name": "计算机网络第八版教材", "price": 59.0, "stock": 100}, {"id": 2, "name": "水晶头网线套装", "price": 19.9, "stock": 200}, ] return jsonify(code=0, data=goods)这段代码里的methods=["GET"]限定了HTTP方法。浏览器访问这个地址时,发出的请求行是GET /api/goods HTTP/1.1,请求头里有Host、User-Agent、Accept等字段。服务器返回的响应状态码是200,Content-Type是application/json。如果把路径写错,Flask会返回404;如果浏览器用POST请求访问这个接口,会得到405 Method Not Allowed。这三个状态码是计算机网络期末复习里最常刷的基础题,做课设时顺手就能验证。
接口返回JSON,前端拿到后渲染成商品卡片。一定要理解,后端不需要返回整个HTML页面,返回数据即可,这样浏览器和后端的交互就变成了纯粹的数据交换,更贴近现在主流的“前后端分离”。
3.2 用户登录:Cookie与Session的第一次握手
登录模块是理解HTTP无状态特性的最佳例子。HTTP本身不记得你上一次干了什么,所以需要一套会话机制。Flask里用session来保存登录状态:
from flask import Flask, request, session app.secret_key = "coursedesign-secret-key" @app.route("/login", methods=["POST"]) def login(): username = request.form.get("username") password = request.form.get("password") if username == "admin" and password == "123456": session["user_id"] = 1 return {"code": 0, "msg": "登录成功"} return {"code": 1, "msg": "用户名或密码错误"}, 401登录成功时,服务器会在响应头里加一个Set-Cookie字段。浏览器收到后把这个Cookie保存下来,后续每一次请求都会在请求头里带上它。服务器通过Cookie里的session id找到对应的用户信息。这就是常说的“HTTP无状态,Cookie和Session给它加了个记忆”。
需要特别说明的是,Flask默认的session是把数据用签名算法压缩后直接存在Cookie里,不是传统意义上的服务端session。如果你在答辩时说“session存在服务器内存”,严格来说在Flask默认实现下不对。想严格走服务端session,可以引入Flask-Session扩展,把session数据放到Redis中。这一点是区分懂不懂原理的关键,也是避免被问住的一个技巧。
3.3 购物车:无状态HTTP下的状态维持策略
购物车可以做成前端存储,也可以做成后端存储。课程设计里我更推荐放在session里,因为这样可以顺带讲明白“为什么购物车不丢”:
@app.route("/api/cart/add", methods=["POST"]) def add_cart(): user_id = session.get("user_id") if not user_id: return {"code": 1, "msg": "请先登录"}, 401 goods_id = request.json.get("goods_id") quantity = request.json.get("quantity", 1) cart = session.get("cart", {}) cart[str(goods_id)] = cart.get(str(goods_id), 0) + quantity session["cart"] = cart return {"code": 0, "msg": "已加入购物车"}购物车数据结构是字典,商品ID为key,数量为value。因为session跟着浏览器走,只要session没过期,刷新页面、关闭浏览器再打开,购物车里的内容都还在。但Flask的session存在Cookie里,有大小限制,大约4KB,所以不能塞太多商品。真实项目里一般用Redis存购物车,Redis过期策略也值得在答辩时讲两句。
另一种做法是把购物车存到localStorage,不占用服务器内存,但换一台电脑购物车就没了。写课程设计时,我更侧重后一种方案,因为它能自然引出“状态到底该放在客户端还是服务端”这个讨论。你把两种方案都准备好,老师问哪边你都有话说。
3.4 提交订单:TCP可靠传输与数据库事务的配合
下单是整个系统里网络和业务结合得最紧的一步。从浏览器发出订单请求,到服务器返回订单号,数据要经过TCP连接可靠传输。TCP负责分段、确认、重传,保证用户提交的订单报文不会丢失。但这只保证了数据“传到了”,没保证业务“算对了”。扣库存的正确性要靠数据库事务:
from mysql.connector import get_db_conn # 自定义连接池函数 @app.route("/api/order/create", methods=["POST"]) def create_order(): user_id = session.get("user_id") if not user_id: return {"code": 1, "msg": "未登录"}, 401 cart = session.get("cart", {}) if not cart: return {"code": 1, "msg": "购物车为空"}, 400 conn = get_db_conn() try: conn.begin() total_price = 0 for goods_id, quantity in cart.items(): cur = conn.cursor() cur.execute( "SELECT price, stock FROM goods WHERE id=%s FOR UPDATE", (goods_id,) ) row = cur.fetchone() if row["stock"] < quantity: raise Exception("商品 %s 库存不足" % goods_id) cur.execute( "UPDATE goods SET stock=stock-%s WHERE id=%s", (quantity, goods_id) ) total_price += row["price"] * quantity cur.execute( "INSERT INTO orders(user_id, total_price) VALUES(%s, %s)", (user_id, total_price) ) conn.commit() session["cart"] = {} return {"code": 0, "order_id": cur.lastrowid, "total_price": total_price} except Exception as e: conn.rollback() return {"code": 1, "msg": str(e)}, 500 finally: conn.close()这里有一个关键参数:SELECT ... FOR UPDATE。它会给匹配到的商品行加上行级锁,两个并发请求同时操作同一件商品时,后一个必须等前一个提交或回滚才能执行。没有这个锁,超卖几乎必然发生。TCP保证你的请求不丢,数据库事务保证你的库存不错,两层一起构成了“可靠”的含义。
4. 动手实现:基于Flask的购物网站核心代码与网络监听
前面把网络模块讲清楚了,这一章直接给出一套能跑起来的工程结构。代码不多,但每一段都对应一个网络知识点。完整的项目文件大致是:app.py、db.py、init_db.sql、templates/index.html。这里我重点讲后端三个文件,前端用一个fetch示例带过。
4.1 数据库表设计:用户、商品、订单与字符集陷阱
先用SQL初始化三张表:
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE shop; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字符集统一用utf8mb4,这是很多项目中文乱码的根源。如果你把表和字段的字符集设成latin1或utf8,遇到商品名里的特殊符号就会变成问号。另外,订单表外键指向用户表,便于讲解关系型数据库的约束。值得一提的是InnoDB引擎才支持事务和行锁,所以建表时必须指定ENGINE=InnoDB。若用MyISAM,上一章的库存扣减代码在并发下会失效。
数据库连接不能直接写在每个请求里。如果用PyMySQL直连,每个请求都要走一次TCP三次握手加MySQL认证,成本太高。课程设计里最好用连接池:
from dbutils.pooled_db import PooledDB import pymysql pool = PooledDB( creator=pymysql, maxconnections=10, mincached=2, maxcached=5, blocking=True, host="127.0.0.1", port=3306, user="root", password="yourpassword", database="shop", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor ) def get_db_conn(): return pool.connection()maxconnections=10表示并发最多10个MySQL连接,超过后因为blocking=True会排队等待。连接池预创建2条连接,避免第一秒请求刚到的时去现建连接。你说出“这是为了复用与数据库之间的TCP连接”,老师就知道你懂网络编程和连接管理的价值。
4.2 后端路由与Socket监听:app.run那行代码拆开看
组装一个最简单的app入口:
from flask import Flask, request, session, jsonify import db app = Flask(__name__) app.secret_key = "coursedesign-secret-key" if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)host=“0.0.0.0”表示监听本机所有网卡IP,这样局域网内的其他设备可以用http://你的IP:5000访问。如果写成127.0.0.1,就只有本机能进,这也是很多人演示时翻车的原因。port=5000是TCP监听端口,可以换成80,但Linux下1024以下端口需要root权限,课设环境建议用5000或8080。debug=False是必须的,调试模式的浏览器交互会额外带来安全隐患,也容易让人误以为服务只跑了一个进程。
从Socket编程的角度看,app.run的本质是创建了一个TCP服务端socket:先bind(("0.0.0.0", 5000)),再listen(5),然后进入while循环等待客户端连接。浏览器连接成功后,Werkzeug读取socket里的字节流,按HTTP协议解析出方法、路径和请求头,再根据路由表调用你的视图函数。如果你手写过socket HTTP服务器,会更容易理解这一行代码背后发生的事情。
4.3 登录、加购、下单:一条业务链路的网络通信实现
把前面模块整合成实际接口,重点在“响应状态码”和“会话保持”。登录接口已写过,这里再补一个通用的鉴权写法。比如查看购物车接口:
@app.route("/api/cart", methods=["GET"]) def get_cart(): user_id = session.get("user_id") if not user_id: return jsonify(code=1, msg="未登录"), 401 cart = session.get("cart", {}) return jsonify(code=0, data=cart)商品列表接口从数据库读取,而不是写死在代码里:
@app.route("/api/goods", methods=["GET"]) def goods_list(): conn = db.get_db_conn() cur = conn.cursor() cur.execute("SELECT id, name, price, stock FROM goods") rows = cur.fetchall() conn.close() return jsonify(code=0, data=rows)前端用fetch调用时,很多人会遇到“登录成功,但购物车接口还是401”。原因大多是fetch默认不带Cookie。需要在请求里加credentials:
fetch("/api/cart/add", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ goods_id: 1, quantity: 1 }) });credentials: "include"告诉浏览器,即使是同源请求,也把当前域名下的Cookie带上。少了这一行,session永远为空,这是一个很容易被忽略的网络细节。
5. 避坑:计算机网络课程设计最常见的五个翻车现场
我把这些年批课设和帮人调试的踩坑记录整理一下,全是从真实代码和网络抓包里看到的。每一条都对应一个网络层原因,整理成“现象 → 原因 → 解决”给读者直接抄。
5.1 页面Pending到超时:监听地址和防火墙的锅
现象:浏览器访问127.0.0.1:5000没问题,但换到另一台机器上用http://192.168.x.x:5000访问,页面一直转圈,最后显示连接超时。
原因:一种可能是Flask启动时host参数写的还是127.0.0.1,只监听了回环接口,外部数据包根本进不来。另一种可能是系统防火墙拦截了5000端口,Windows和Linux默认规则不同,教室里的公用电脑还经常锁着端口。
解决:先把app.run(host="0.0.0.0")改好,再到防火墙加一条入站规则放行TCP 5000。Linux下用sudo ufw allow 5000/tcp,Windows在“高级安全Windows Defender防火墙”里新建入站规则。改完后可以在浏览器所在机器上先ping一下后端IP,再telnet IP 5000,能通说明网络层通,不通就去查防火墙。
5.2 中文乱码:响应头字符集和数据库编码都没对齐
现象:商品名称在数据库里是中文,查询出来交给前端显示变成“???”,或者MySQL命令行里看到的是中文,但HTTP返回乱码。
原因:数据库连接参数charset没设置,PyMySQL默认可能是latin1,导致从表里取出的utf8mb4字节被错误解码。另一种是后端返回响应时没有在Content-Type里指定UTF-8。Flask的jsonify默认会用UTF-8,但如果你直接返回字符串,浏览器就可能用GBK解析。
解决:连接池参数统一加上charset="utf8mb4",创建数据库时也用utf8mb4。写一个测试接口,用curl查看响应头里有没有Content-Type: application/json; charset=utf-8。没有的话,给Flask配置app.json.ensure_ascii = False,并统一在响应头设置字符集。这一条是数据链路层“透明传输”思想的反面教材。
5.3 购物车刷新就丢:Session的存储位置和过期时间
现象:登录成功后往购物车加了两件商品,刷新页面购物车变成空的;但session的user_id还在,登录状态没丢。
原因:Flask默认session存在浏览器Cookie里,Cookie有大小上限,大约4KB。如果你往session里放的购物车字典太大,或者商品ID太多,超过了4KB,浏览器会拒绝接收或截断。另一种可能是session过期时间设置太短,默认浏览器关闭就过期,你刷新页面的方式触发了新的会话。
解决:把购物车从session搬到数据库表,或者用Redis。课程设计为了省事,可以把购物车存入一个cart表,用user_id做外键,这样既不占Cookie空间,也能演示“服务端状态”。如果坚持用session,至少把session的过期时间调长:app.permanent_session_lifetime = timedelta(hours=2),登录后设置session.permanent = True。到答辩时你要说清楚“Flask默认session在Cookie里,不是内存”。
5.4 库存变负数:并发请求下的数据库行锁
现象:商品库存显示100件,但两个用户同时下单买100件,都提示成功,查数据库发现库存变成了-100。
原因:扣库存的代码先查stock,再算新库存,最后UPDATE。两个并发请求同时读到stock=100,各自计算出0,然后互相覆盖,最后写回一个错误结果。TCP只保证请求完整到达,不保证业务计算顺序,这属于并发控制问题。
解决:在查询库存时加FOR UPDATE,让事务持有行锁,后一个事务等待前一个事务提交后再读。这是最有效也最容易在课程设计里讲清楚的办法。还有一个隐藏问题是PyMySQL默认autocommit=True,如果你不显式conn.begin(),前面的SELECT FOR UPDATE根本没生效,所以要牢牢把关在事务里。
5.5 局域网其他机器访问不了:调试模式只绑定了本机回环
现象:代码里明明写了host=“0.0.0.0”,别人访问还是不行,但你自己访问正常。
原因:一种可能是Flash自带的Werkzeug调试模式在debug=True时,会启用调试器进程,有些版本还会要求输入PIN码,外部访问被卡在PIN验证上。另一种可能是Windows防火墙对Python程序弹出了拦截窗口,你没点“允许访问”,导致外部连接被静默丢弃。
解决:真机演示时一定用debug=False启动,避免调试器纠结。然后确认监听地址:
netstat -ano | findstr :5000看看监听地址是不是0.0.0.0:5000,如果是127.0.0.1:5000,说明代码没生效或缓存了旧进程。用完netstat查到PID后,用任务管理器结束残留进程再重启。这个问题我在课设验收时见了不下十次,多半是改完代码后没重启Python进程。
6. 把课程设计答进优秀:抓包验证网络交互与答辩自检
如果你想让课设从“能跑”变成“能讲”,建议花半小时做一轮网络级验证。
6.1 用Wireshark抓三次握手:眼见为实的TCP连接
启动Wireshark,选择回环接口或局域网网卡,访问一次商品列表接口,然后停止抓包。在过滤栏输入tcp.port == 5000,你能看到三次握手的三个包:SYN、SYN+ACK、ACK。再往后能看到带HTTP协议的包,右键“追踪流→HTTP流”能看到完整的请求行和响应头。答辩时直接把这些截图放进PPT,比空口讲“TCP是可靠的”有说服力得多。
6.2 用curl和开发者工具验证HTTP状态码
不用打开浏览器,直接用curl就能模拟各种请求:
curl -i http://127.0.0.1:5000/api/goods返回的状态码、Content-Type、响应体都在终端里。再用curl -X POST http://127.0.0.1:5000/login -d "username=admin&password=123456" -c cookies.txt模拟登录并保存Cookie,之后加购物车都带上这个Cookie文件,就能验证Session是否真的被维持。浏览器开发者工具里的Network面板也能看到每个请求的状态码,把状态码和原因对应起来,本身就是计算机网络的复习。
6.3 一份答辩自检清单
我给自己学生出的自检问题就这五个:三次握手为什么需要第三次?HTTP为什么是有状态还是无状态的?Cookie和Session有什么区别?购物车数据为什么刷新不丢?两个用户同时下单库存会超卖吗?如果你能一边指着自己的代码,一边回答出“HTTP无状态,所以我们用Session记录用户;购物车存在服务端,所以刷新不丢;扣库存用事务加行锁,所以不会超卖”,这个课设离优秀就不远了。我当年吃亏在只讲功能不讲数据,后来习惯做任何项目都先画一遍数据流,这个习惯帮我省了很多次翻车。希望帮到你。
本文还有配套的精品资源,点击获取