构建一个交互式网络协议教学工具,重点演示
2026-01-08
构建一个交互式网络协议教学工具,重点演示 TCP 连接建立(三次握手)和断开(四次挥手)的全过程。界面分为左右两端代表 Client 和 Server,中间为网络传输通道。用户点击“发起连接”后,动画展示数据包(SYN, ACK, FIN)在两端间的飞越,同时显示状态机变化(如 SYN_SENT, ESTABLISHED)。右侧日志栏实时解释当前数据包标志位的含义。UI 风格采用扁平化拓扑图设计。
思考过程
**Conceptualizing the Interactive Tool**
I've been contemplating the core functionality of this tool. Right now, I'm envisioning the user interface and interactions. I'm focusing on the 3-way handshake, visualizing the SYN, SYN-ACK, and ACK packets. I'm also thinking about a visual representation of the connection state.
**Refining the UI Elements**
Now, I'm delving deeper into the UI. I'm focusing on the "Start Connection" button and how it triggers the handshake. I'm considering using simple CSS transitions for packet animations. For the state machine, I'm leaning toward a clear, concise visual representation – maybe a flowchart-like display that updates dynamically. I'll need to figure out how to represent the network channel.
**Planning the Architecture**
My current focus is the technical architecture. I've broken down the project into manageable phases. First, the UI layout and design using HTML/CSS. Then, the critical state management in JavaScript, defining client and server states. Finally, animation logic and a code draft. The challenge lies in making it all work seamlessly together.
**Structuring the Project's Logic**
I'm deep in the implementation plan now. First, I'll need the HTML structure, including divs for client, server, and channel. CSS will manage flat, clean visuals. Then, the state machine in JavaScript is critical, with clear state definitions for the client and server sides. Animations using CSS transitions will be essential. The challenge will be linking events to state changes, the sequence controller is also important.
**Structuring the Code and Logic**
Now, I'm defining the states, client-side and server-side. I'm focusing on HTML structuring with client, channel, and server divs. The CSS will create the flat, clean visuals. The key is in the Javascript; I need an animation helper. The log messages need a CSS class. I'm prioritizing the sequence controller and `async/await` for time delays.
**Designing the Core Functionality**
I'm now thinking about the client/server state changes. I need to define the flow between different states for both client and server: CLOSED, SYN_SENT, ESTABLISHED, and the teardown sequence. I'm focusing on the animation logic and how to visually represent the packets. The sequence controller is the key for making the steps visible.
这是一个非常棒的教学工具构想。为了实现这个需求,我为你编写了一个单文件(Single File)解决方案,包含了 **HTML**(结构)、**CSS**(扁平化拓扑设计与动画)和 **JavaScript**(核心逻辑与状态机控制)。
你可以直接将下面的代码保存为 `tcp_demo.html`,然后在浏览器中打开即可运行。
### 功能亮点:
1. **完全交互**:支持“建立连接”和“断开连接”操作。
2. **视觉反馈**:
* **数据包动画**:SYN/ACK/FIN 包在连线中间真实移动。
* **状态机同步**:Client 和 Server 的状态标签(如 `ESTABLISHED`)会随交互实时改变颜色和文字。
3. **日志详解**:右侧日志栏会模拟 Wireshark 风格,显示 Flag 标志位(如 `[SYN] Seq=0`)并附带详细的教学解释。
4. **防抖逻辑**:动画执行期间自动禁用按钮,防止状态错乱。
### 完整代码
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TCP 协议交互式教学工具</title>
<style>
:root {
--bg-color: #f4f7f6;
--client-color: #3498db;
--server-color: #2ecc71;
--packet-color: #e74c3c;
--text-color: #2c3e50;
--line-color: #bdc3c7;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: var(--bg-color);
display: flex;
height: 100vh;
overflow: hidden;
}
/* 左侧:演示区域 */
.demo-area {
flex: 2;
display: flex;
flex-direction: column;
position: relative;
padding: 20px;
border-right: 1px solid #ddd;
}
.topology {
flex: 1;
display: flex;
justify-content: space-between; /* 左右两端 */
align-items: center;
padding: 0 50px;
position: relative;
}
/* 网络通道线 */
.network-channel {
position: absolute;
top: 50%;
left: 150px;
right: 150px;
height: 4px;
background-color: var(--line-color);
transform: translateY(-50%);
z-index: 0;
}
/* 终端节点 (Client/Server) */
.node {
width: 140px;
height: 140px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
position: relative;
transition: all 0.3s;
}
.node-icon {
font-size: 40px;
margin-bottom: 10px;
}
.node-title {
font-weight: bold;
color: var(--text-color);
}
.node-state {
margin-top: 10px;
padding: 4px 8px;
background: #eee;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
font-weight: bold;
transition: background 0.3s, color 0.3s;
}
#client .node-icon { color: var(--client-color); }
#server .node-icon { color: var(--server-color); }
/* 状态颜色 */
.state-closed { color: #7f8c8d; }
.state-listen { color: #e67e22; }
.state-sent { color: #3498db; }
.state-syn-rcvd { color: #9b59b6; }
.state-established { color: #2ecc71; background: #e8f8f5 !important; }
.state-teardown { color: #e74c3c; }
/* 数据包 */
.packet {
position: absolute;
top: 50%;
left: 150px; /* 起始位置 Client */
width: 80px;
height: 36px;
background-color: var(--packet-color);
color: white;
border-radius: 18px;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-weight: bold;
transform: translate(0, -50%) scale(0); /* 默认隐藏 */
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 20;
transition: transform 0.5s ease-in-out, left 1.5s linear;
}
/* 控制栏 */
.controls {
height: 80px;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: transform 0.1s, opacity 0.3s;
font-weight: bold;
}
button:active { transform: scale(0.98); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-connect { background-color: var(--client-color); color: white; }
.btn-disconnect { background-color: var(--packet-color); color: white; }
.btn-reset { background-color: #95a5a6; color: white; }
/* 右侧:日志区域 */
.log-area {
flex: 1;
background: #282c34;
color: #abb2bf;
padding: 20px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
display: flex;
flex-direction: column;
}
.log-title {
color: #61afef;
font-size: 16px;
margin-bottom: 15px;
border-bottom: 1px solid #3e4451;
padding-bottom: 10px;
}
.log-entry {
margin-bottom: 12px;
padding: 8px;
border-left: 3px solid transparent;
background: rgba(255,255,255,0.05);
animation: fadeIn 0.3s ease;
}
.log-entry.client-act { border-left-color: var(--client-color); }
.log-entry.server-act { border-left-color: var(--server-color); }
.log-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
color: #e06c75;
}
.log-desc { color: #98c379; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
/* 工具提示 */
.tooltip {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
color: #7f8c8d;
font-size: 12px;
}
</style>
</head>
<body>
<!-- 演示区域 -->
<div class="demo-area">
<div class="topology">
<!-- Client Node -->
<div class="node" id="client">
<div class="node-icon">💻</div>
<div class="node-title">Client</div>
<div class="node-state state-closed" id="client-state">CLOSED</div>
</div>
<!-- Network Channel -->
<div class="network-channel"></div>
<!-- Packet Element (Animated) -->
<div class="packet" id="packet">SYN</div>
<!-- Server Node -->
<div class="node" id="server">
<div class="node-icon">🖥️</div>
<div class="node-title">Server</div>
<div class="node-state state-listen" id="server-state">LISTEN</div>
</div>
</div>
<div class="tooltip">点击下方按钮开始演示</div>
<div class="controls">
<button class="btn-connect" id="btn-connect" onclick="startHandshake()">发起连接 (三次握手)</button>
<button class="btn-disconnect" id="btn-disconnect" onclick="startTeardown()" disabled>断开连接 (四次挥手)</button>
<button class="btn-reset" onclick="resetSim()">重置</button>
</div>
</div>
<!-- 日志区域 -->
<div class="log-area" id="log-container">
<div class="log-title">网络交互日志</div>
<div class="log-entry">
<div class="log-header">System Ready</div>
<div class="log-desc">等待用户发起操作... <br>Server 处于 LISTEN (监听) 状态。</div>
</div>
</div>
<script>
// 配置常量
const TRAVEL_TIME = 1500; // 包飞行时间 ms
const PAUSE_TIME = 500; // 处理暂停时间 ms
// DOM 元素
const clientStateEl = document.getElementById('client-state');
const serverStateEl = document.getElementById('server-state');
const packetEl = document.getElementById('packet');
const logContainer = document.getElementById('log-container');
const btnConnect = document.getElementById('btn-connect');
const btnDisconnect = document.getElementById('btn-disconnect');
const topologyWidth = document.querySelector('.topology').offsetWidth;
// 状态辅助函数
function setClientState(state, cssClass) {
clientStateEl.innerText = state;
clientStateEl.className = 'node-state ' + cssClass;
}
function setServerState(state, cssClass) {
serverStateEl.innerText = state;
serverStateEl.className = 'node-state ' + cssClass;
}
function log(source, flags, desc) {
const entry = document.createElement('div');
entry.className = `log-entry ${source === 'Client' ? 'client-act' : 'server-act'}`;
entry.innerHTML = `
<div class="log-header">
<span>${source}</span>
<span>${flags}</span>
</div>
<div class="log-desc">${desc}</div>
`;
logContainer.appendChild(entry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// 动画工具函数
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function sendPacket(from, to, text, flags) {
// 设置包的内容
packetEl.innerText = text;
packetEl.style.transition = 'none'; // 重置以便瞬间定位
// 计算位置
// 假设 Client 在左侧约 140px 处,Server 在右侧
// 简单处理:Left=0% (client edge) Right=100% (server edge) relative to channel
// 由于使用了 absolute 定位,我们需要动态计算 channel 的两端
const startX = from === 'client' ? '150px' : `calc(100% - 150px - 80px)`; // 80px is packet width
const endX = from === 'client' ? `calc(100% - 150px - 80px)` : '150px';
packetEl.style.left = startX;
packetEl.style.transform = 'translate(0, -50%) scale(1)'; // 显示
// 强制重绘
void packetEl.offsetWidth;
// 开始移动
packetEl.style.transition = `left ${TRAVEL_TIME}ms linear, transform 0.3s`;
packetEl.style.left = endX;
await sleep(TRAVEL_TIME);
// 到达后隐藏
packetEl.style.transform = 'translate(0, -50%) scale(0)';
await sleep(PAUSE_TIME); // 处理时间
}
// --- 核心逻辑:三次握手 ---
async function startHandshake() {
btnConnect.disabled = true;
btnDisconnect.disabled = true;
log('System', '---', '开始 TCP 三次握手过程...');
// Step 1: Client sends SYN
setClientState('SYN_SENT', 'state-sent');
log('Client', '[SYN] Seq=0', '客户端希望建立连接。设置标志位 SYN=1,随机生成序列号 Seq=0。进入 SYN_SENT 状态。');
await sendPacket('client', 'server', 'SYN', 'SYN');
// Step 2: Server receives SYN, sends SYN+ACK
setServerState('SYN_RCVD', 'state-syn-rcvd');
log('Server', '[SYN, ACK] Seq=0 Ack=1', '服务端收到 SYN。同意连接,回复 SYN+ACK。Seq=0 (新序列号), Ack=1 (期望下一个是1)。进入 SYN_RCVD 状态。');
await sendPacket('server', 'client', 'SYN+ACK', 'SYN, ACK');
// Step 3: Client receives SYN+ACK, sends ACK
setClientState('ESTABLISHED', 'state-established');
log('Client', '[ACK] Seq=1 Ack=1', '客户端收到 SYN+ACK。连接已在客户端建立。回复 ACK 确认收到。进入 ESTABLISHED 状态。');
await sendPacket('client', 'server', 'ACK', 'ACK');
// Final: Server receives ACK
setServerState('ESTABLISHED', 'state-established');
log('Server', '---', '服务端收到 ACK。连接在双端完全建立。双方可以开始传输数据。');
log('System', 'STATUS: ESTABLISHED', '通道就绪 (Green)');
// 启用断开按钮
btnDisconnect.disabled = false;
}
// --- 核心逻辑:四次挥手 ---
async function startTeardown() {
btnDisconnect.disabled = true;
log('System', '---', '开始 TCP 四次挥手过程 (断开连接)...');
// Step 1: Client sends FIN
setClientState('FIN_WAIT_1', 'state-teardown');
log('Client', '[FIN, ACK] Seq=100 Ack=1', '客户端没有数据要发送了。发送 FIN 标志位,请求断开。进入 FIN_WAIT_1。');
await sendPacket('client', 'server', 'FIN', 'FIN');
// Step 2: Server receives FIN, sends ACK
setServerState('CLOSE_WAIT', 'state-teardown');
log('Server', '[ACK] Seq=1 Ack=101', '服务端收到 FIN。发送 ACK 确认收到。但我可能还有数据要处理,进入 CLOSE_WAIT 状态。');
await sendPacket('server', 'client', 'ACK', 'ACK');
// Client receives ACK
setClientState('FIN_WAIT_2', 'state-teardown');
log('Client', '---', '客户端收到 ACK。进入 FIN_WAIT_2,等待服务端发送它的 FIN。');
await sleep(1000); // 模拟服务端处理剩余数据
// Step 3: Server sends FIN
setServerState('LAST_ACK', 'state-teardown');
log('Server', '[FIN, ACK] Seq=1 Ack=101', '服务端数据处理完毕,发送 FIN 请求关闭连接。进入 LAST_ACK。');
await sendPacket('server', 'client', 'FIN', 'FIN');
// Step 4: Client receives FIN, sends ACK
setClientState('TIME_WAIT', 'state-listen');
log('Client', '[ACK] Seq=101 Ack=2', '客户端收到 FIN。发送最后一个 ACK。进入 TIME_WAIT (等待 2MSL 以确保服务端收到 ACK)。');
await sendPacket('client', 'server', 'ACK', 'ACK');
// Server receives ACK -> CLOSED
setServerState('CLOSED', 'state-closed');
log('Server', '---', '服务端收到 ACK。连接关闭,回到 CLOSED 状态 (演示中重置为 LISTEN)。');
// Client Wait -> CLOSED
await sleep(1000);
setClientState('CLOSED', 'state-closed');
log('Client', '---', '时间到,客户端关闭连接。');
// 自动重置 Server 为 Listen 以便下次演示
setTimeout(() => {
setServerState('LISTEN', 'state-listen');
btnConnect.disabled = false;
log('System', 'RESET', '演示结束,系统重置。');
}, 1000);
}
function resetSim() {
location.reload();
}
</script>
</body>
</html>
```
### 如何使用
1. **建立连接**:点击蓝色按钮。你会看到:
* **第一次握手**:Client 发出红色的 `SYN` 包。Client 状态变为 `SYN_SENT`。
* **第二次握手**:Server 收到后回复 `SYN+ACK`。Server 状态变为 `SYN_RCVD`。
* **第三次握手**:Client 回复 `ACK` 并进入 `ESTABLISHED`(绿色)。
* Server 收到 ACK 后也变绿。
2. **断开连接**:连接建立后,点击红色按钮。
* 动画将展示标准的四次挥手流程(Client FIN -> Server ACK -> Server FIN -> Client ACK)。
* 观察日志栏,它会解释为什么中间会有 `CLOSE_WAIT` 和 `TIME_WAIT`。
### 设计细节解析
* **CSS 变量**:使用了 CSS Variable (`--client-color` 等) 方便统一调整配色主题。
* **Promise & Async/Await**:JavaScript 逻辑中使用了 `await sleep(ms)`。这使得代码看起来像是在一步步执行协议(同步写法),而实际上是异步处理动画,避免了“回调地狱”,非常适合初学者阅读代码逻辑。
* **状态类名管理**:通过切换 CSS class(如 `.state-established`)来改变 UI 样式,模拟真实的状态机变迁。