Files
test-repo/docs/.vitepress/theme/components/appendix/web-basics/TcpHandshakeDemo.vue
T
2026-02-24 00:18:09 +08:00

446 lines
8.9 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="tcp-handshake-demo">
<div class="controls">
<div class="status-indicator">
{{ t.statusLabel }}:
<span :class="connectionStatus.toLowerCase()">{{ statusText }}</span>
</div>
<div class="buttons">
<button
v-if="step === 0"
class="action-btn"
@click="startHandshake"
>
{{ t.connect }}
</button>
<button
v-else
class="reset-btn"
@click="reset"
>
{{ t.reset }}
</button>
</div>
</div>
<div class="sequence-diagram">
<!-- Client Timeline -->
<div class="timeline client">
<div class="actor">
<span class="icon">💻</span>
<span class="name">{{ t.client }}</span>
</div>
<div class="line" />
<div
class="state-marker"
:class="{ active: step >= 1 }"
>
SYN_SENT
</div>
<div
class="state-marker"
:class="{ active: step >= 3 }"
>
ESTABLISHED
</div>
</div>
<!-- Interaction Area -->
<div class="interaction-space">
<!-- SYN Packet -->
<div class="packet-track">
<transition name="slide-right">
<div
v-if="showSyn"
class="packet syn"
>
<div class="packet-body">
SYN
</div>
<div class="packet-detail">
SEQ=0
</div>
</div>
</transition>
</div>
<!-- SYN-ACK Packet -->
<div class="packet-track reverse">
<transition name="slide-left">
<div
v-if="showSynAck"
class="packet syn-ack"
>
<div class="packet-body">
SYN-ACK
</div>
<div class="packet-detail">
SEQ=0, ACK=1
</div>
</div>
</transition>
</div>
<!-- ACK Packet -->
<div class="packet-track">
<transition name="slide-right">
<div
v-if="showAck"
class="packet ack"
>
<div class="packet-body">
ACK
</div>
<div class="packet-detail">
SEQ=1, ACK=1
</div>
</div>
</transition>
</div>
</div>
<!-- Server Timeline -->
<div class="timeline server">
<div class="actor">
<span class="icon">🖥</span>
<span class="name">{{ t.server }}</span>
</div>
<div class="line" />
<div
class="state-marker"
:class="{ active: step >= 2 }"
>
SYN_RCVD
</div>
<div
class="state-marker"
:class="{ active: step >= 3 }"
>
ESTABLISHED
</div>
</div>
</div>
<div class="description-box">
<p>{{ currentDescription }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
lang: {
type: String,
default: 'zh'
}
})
// Bilingual text directly
const t = {
statusLabel: '连接状态',
connect: '建立连接',
reset: '断开重连',
client: '我 (浏览器)',
server: '对面 (B站服务器)',
status: {
closed: '未连接',
handshaking: '正在打招呼确认通道...',
established: 'TCP 通道已建立 (ESTABLISHED)'
},
steps: {
0: '点击 "建立连接" 开始三次握手(电话试音)。',
1: '第一次握手: "喂,服务器老哥在吗?我能发信息,你能收到吗?" (SYN)',
2: '第二次握手: "喂喂在的!我收到了!那你现在能听到我说话吗?" (SYN-ACK)',
3: '第三次握手: "妥了,我也能听到!通道没问题,准备看视频!" (ACK)'
}
}
const step = ref(0)
const showSyn = ref(false)
const showSynAck = ref(false)
const showAck = ref(false)
const connectionStatus = computed(() => {
if (step.value === 0) return 'closed'
if (step.value < 3) return 'handshaking'
return 'established'
})
const statusText = computed(() => {
const s = connectionStatus.value
return t.status[s] || s.toUpperCase()
})
const currentDescription = computed(() => {
return t.steps[step.value] || ''
})
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const startHandshake = async () => {
if (step.value > 0) return
// Step 1: SYN
step.value = 1
showSyn.value = true
await wait(1500)
// Step 2: SYN-ACK
step.value = 2
showSynAck.value = true
await wait(1500)
// Step 3: ACK
step.value = 3
showAck.value = true
}
const reset = () => {
step.value = 0
showSyn.value = false
showSynAck.value = false
showAck.value = false
}
</script>
<style scoped>
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 1.5rem;
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.status-indicator {
font-weight: bold;
}
.status-indicator span.closed {
color: var(--vp-c-text-3);
}
.status-indicator span.handshaking {
color: #f59e0b;
}
.status-indicator span.established {
color: #10b981;
}
.action-btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.reset-btn {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.sequence-diagram {
display: flex;
justify-content: space-between;
height: 300px;
position: relative;
}
.timeline {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.actor {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1rem;
z-index: 2;
background: var(--vp-c-bg);
}
.timeline .line {
width: 2px;
background: var(--vp-c-divider);
flex: 1;
}
.state-marker {
margin-top: 2rem;
padding: 0.3rem 0.6rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.state-marker.active {
background: #10b981;
color: white;
border-color: #10b981;
}
.interaction-space {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 2rem 0;
}
.packet-track {
height: 40px;
position: relative;
display: flex;
align-items: center;
}
.packet-track.reverse {
justify-content: flex-end;
}
.packet {
background: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.packet.syn-ack {
background: #f59e0b;
}
.packet.ack {
background: #10b981;
}
.packet-body {
font-weight: bold;
}
.packet-detail {
font-size: 0.7rem;
opacity: 0.9;
}
/* Animations */
.slide-right-enter-active {
animation: slide-right 1.5s linear;
}
.slide-left-enter-active {
animation: slide-left 1.5s linear;
}
@keyframes slide-right {
0% {
transform: translateX(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 1;
} /* Not quite right, need to stick */
}
/*
Vue transitions are tricky for "moving across".
Let's use a simpler approach: CSS transitions on left/right property or keyframes.
Actually, for a "send" animation, we want it to move from A to B and then stay or disappear.
Here I want it to appear and move.
*/
.slide-right-enter-active,
.slide-left-enter-active {
transition: all 1.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide-right-enter-from {
transform: translateX(-150px);
opacity: 0;
}
.slide-right-enter-to {
transform: translateX(0);
opacity: 1;
}
/* This is getting complicated with Vue transitions for simple movement.
Let's just use CSS keyframes on the element itself when it renders.
*/
.packet {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timing-function: ease-in-out;
}
.packet-track .packet {
animation-name: moveRight;
}
.packet-track.reverse .packet {
animation-name: moveLeft;
}
@keyframes moveRight {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes moveLeft {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.description-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
text-align: center;
min-height: 3rem;
color: var(--vp-c-text-2);
}
</style>