The TCP Three-Way Handshake
Client — Server
Purpose of the three-way handshake: the client’s send and receive are both fine, and the server’s send and receive are both fine.
First handshake: the client sends SYN (synchronize request)
- Client: I know nothing, so I’ll send out a SYN message and see if anyone receives it.
- If no one replies, I send it a few more times (timeout retransmission).
- At this moment, what the client knows is:
- I sent it, but I don’t know whether anyone received it ❌
- Can I receive messages? Unknown ❌
- Does the other side exist? Unknown ❌
Second handshake: the server replies SYN+ACK (synchronize + acknowledge)
-
The server received the client’s SYN.
-
The server replies SYN+ACK: “I got your message; I’ll send one too and see if you can receive it.”
-
At this moment, what the server knows is:
- The client’s sending is fine ✅ (because I received it)
- The server’s receiving is fine ✅ (because I received it)
- The server doesn’t know whether the server’s sending is fine ❌ (I sent it but don’t know if the other side got it)
- The server doesn’t know whether the client’s receiving is fine ❌ (I sent it but don’t know if the other side got it)
- The server needs to send a SYN+ACK and wait for the other side’s ACK to confirm “I can send, the other side can receive”
-
When the client receives the SYN+ACK, the client can confirm:
- My sending is fine ✅ (my SYN was received)
- My receiving is fine too ✅ (I received the other side’s reply)
- The server’s receiving is fine ✅ (the other side received my SYN)
- The server’s sending is fine too ✅ (I received the other side’s SYN+ACK)
- ✅ On the client’s side, full-duplex communication is confirmed!
- But the server still has concerns (doesn’t know if it can send, doesn’t know if I can receive), so let me send it an ACK
Third handshake: the client sends ACK (acknowledge)
-
After receiving the SYN+ACK, the client replies with an ACK: “I got your SYN+ACK; my send and receive are both fine, so you can rest easy too.”
-
When the server receives this ACK, the server can confirm:
- The client’s sending is fine ✅ (verified in the first handshake)
- The server’s receiving is fine ✅ (verified in the first handshake)
- The server’s sending is fine too ✅ (my SYN+ACK was received by the client)
- The client’s receiving is fine too ✅ (my SYN+ACK was received by the client, and receiving it proves it can receive)
- ✅ On the server’s side, full-duplex communication is confirmed too!
-
Both sides then enter the
ESTABLISHEDstate and can begin transmitting real data.
⚠️ What if the third handshake’s ACK is lost?
- The client’s view: I sent the ACK and even sent real data directly (because the client thinks the connection is established).
- The server’s view: I didn’t receive the ACK, I’m still in the
SYN-RCVDstate, and I’m unsure whether I can send and whether the client can receive. - The server’s behavior: after a timeout, it retransmits SYN+ACK (a limited number of times).
- After the client receives the retransmitted SYN+ACK:
- The client realizes, “Oh, my last ACK may have been lost”
- The client sends the ACK again and piggybacks the previously sent real data (if any)
- If the real data reaches the server first:
- The server’s retransmission timer hasn’t expired yet, and it receives the real data packet from the client
- This packet’s TCP header carries the ACK flag, which happens to acknowledge the second handshake
- The server immediately moves the connection state to
ESTABLISHEDand accepts the data
- If both sides time out:
- The server retransmits SYN+ACK a limited number of times with no response → gives up the connection, releases resources
- The client retransmits the real data a limited number of times with no response → gives up the connection, notifies the upper-layer app
- Both sides return to the
CLOSEDstate
🤔 Why can’t it be just two handshakes?
If there were only two handshakes (the client sends SYN, the server replies SYN+ACK, and it ends):
| Role | Facts known | Facts unknown |
|---|---|---|
| Client | My send is fine ✅ My receive is fine ✅ Server send is fine ✅ Server receive is fine ✅ |
All confirmed ✅ |
| Server | Client send is fine ✅ Server receive is fine ✅ |
Server send is fine ❌ Client receive is fine ❌ |
Consequence: the server starts sending real data without being sure whether it can send and whether the client can receive. If the client actually didn’t receive the server’s SYN+ACK, the server keeps foolishly sending data that the client can’t receive at all, causing a deadlock.
The essence of the three-way handshake: the third ACK is a reassurance pill for the server — it lets the server confirm “I can send, the other side can receive,” and only then does it dare to send data with peace of mind.
🤔 Can the three-way handshake carry data?
-
First handshake (SYN): cannot carry data.
- Because the connection isn’t established yet, and the client doesn’t even know whether the server exists
- If it carried data and the SYN were lost, the data would be sent in vain, and it easily invites SYN attacks
-
Second handshake (SYN+ACK): cannot carry data.
- The server hasn’t received the client’s final confirmation, and is unsure whether it can send and whether the client can receive
- If it carried data and the ACK were lost, the data would be sent in vain
-
Third handshake (ACK): can carry data (allowed by RFC 793).
- The client has confirmed both sides’ send/receive ability and can piggyback real data with peace of mind
- If the ACK is lost, the server retransmits SYN+ACK, and after receiving it the client retransmits the ACK plus the piggybacked retransmitted data
- If the ACK isn’t lost, the server accepts the data upon receiving the ACK — two birds with one stone
This is TCP’s “Piggybacking” mechanism: use the third handshake to transmit data along the way, improving efficiency.
📊 Three-way handshake state transition diagram
| Step | Operation | Client state | Server state |
|---|---|---|---|
| Init | none | CLOSED |
LISTEN |
| ① | Client sends SYN | SYN_SENT |
LISTEN |
| ② | Server receives SYN, replies SYN+ACK | SYN_SENT |
SYN_RCVD |
| ②→③ | Client receives SYN+ACK | ESTABLISHED |
SYN_RCVD |
| ③ | Client sends ACK | ESTABLISHED |
SYN_RCVD |
| ③→ | Server receives ACK | ESTABLISHED |
ESTABLISHED |
💡 The three-way handshake in one sentence
The client scouts first (SYN) → the server responds and asks back (SYN+ACK) → the client finally confirms (ACK); only after both sides confirm “I can send, I can receive, the other side can send, the other side can receive” does formal communication begin. The third ACK is the reassurance pill for the server, ensuring it won’t send data while “unsure in its heart.”
The TCP Four-Way Wave (Connection Termination)
Client — Server
Purpose of the four-way wave: each of the client and server declares “I have nothing more to say” and confirms the other side also “has nothing more to say,” ensuring both complete their data send/receive before disconnecting, to avoid data loss.
First wave: the client sends FIN (finish request)
- Client: I’ve sent all my data, I don’t want to send more, but I can still receive data.
- I send a FIN message to the server, telling it: “I have nothing more to say here; I’m about to close my sending channel.”
- If no one replies, I send it a few more times (timeout retransmission).
- At this moment, what the client knows is:
- I sent a FIN, but I don’t know whether the server received it ❌
- Does the server still have data to send? Unknown ❌
- I can still receive data, but I don’t know for how long ❌
Second wave: the server replies ACK (acknowledge receipt)
-
The server received the client’s FIN and knows the client won’t send data anymore.
-
The server replies with an ACK: “Got your FIN; I know you have nothing more to say.”
-
At this moment, what the server knows is:
- The client won’t send new data ✅
- The server’s receiving is fine ✅ (it received the FIN)
- The server can still keep sending (because it may still have unsent data) 📦
- The client can still keep receiving ✅ (receiving the ACK means the client is still listening)
- The server hasn’t sent its FIN yet, because its data isn’t fully sent
-
When the client receives the ACK, the client can confirm:
- The server received my FIN ✅ (my sending is fine)
- The server knows I won’t send data anymore ✅
- The client is unsure whether the server still has data to send ❌
- The client is unsure when the server will close ❌
- The client keeps its receive window open, waiting for data that may arrive 👂
- It enters the half-close state: the client no longer sends data but can still receive it
The role of this ACK: it only tells the client “I know you’ve closed,” but the server won’t close its own sending channel immediately, because it may still have data to send to the client.
Third wave: the server sends FIN (finish request)
-
Server: my data is all sent now, and I have nothing more to say either.
-
The server sends a FIN to the client: “My data is all sent too; I’m about to close my sending channel.”
-
At this moment, what the server knows is:
- I sent a FIN, but I don’t know whether the client received it ❌
- I don’t know whether the client will reply with an ACK ❌
- If the client doesn’t reply with an ACK, I don’t know whether to fully close ❌
- I need to wait for the client’s ACK to confirm my FIN was delivered ✅
-
When the client receives the FIN, the client can confirm:
- The server sent a FIN, which means it truly has nothing more to say ✅
- The client’s earlier worry of “does the server still have data” is dispelled ✅
- The client needs to reply with an ACK so the server can close with peace of mind ✅
- But the client still can’t close immediately, because it must wait for the ACK to be delivered and confirmed
Fourth wave: the client replies ACK (final confirmation)
-
The client received the server’s FIN and replies with an ACK: “Got your FIN, confirming the close.”
-
When the server receives the ACK, the server can confirm:
- The client received my FIN ✅
- The client knows I’m about to close ✅
- The server can close the connection with peace of mind ✅
- The server releases resources and enters the
CLOSEDstate 🗑️
-
But after sending the ACK, the client won’t close immediately:
- After sending the ACK, the client enters the TIME_WAIT state ⏳
- The client needs to wait 2MSL (Maximum Segment Lifetime, usually 2 minutes)
- Why wait?
- If the last ACK is lost, the server will retransmit the FIN after a timeout
- If the client receives a retransmitted FIN during TIME_WAIT, it can send the ACK again
- This ensures the server can receive the final ACK, avoiding it waiting foolishly forever ❌
- It ensures all old packets in the network disappear and won’t interfere with a subsequent new connection (reusing the same port)
- Only after the wait ends does the client truly close the connection and enter the
CLOSEDstate
⚠️ What if the fourth wave’s ACK is lost?
- The client’s view: I sent the ACK, entered TIME_WAIT, and I’m waiting for the server’s possible retransmitted FIN.
- The server’s view: I sent a FIN — why haven’t I gotten the ACK? Was it lost?
- The server’s behavior: after a timeout it retransmits the FIN (a limited number of times).
- During the client’s TIME_WAIT:
- If it receives the server’s retransmitted FIN → my last ACK was lost
- The client sends the ACK again and resets the TIME_WAIT timer (restarts the 2MSL countdown)
- If the server retransmits the FIN several times and never gets an ACK:
- The server gives up waiting, forcibly closes the connection (enters
CLOSED), and releases resources
- The server gives up waiting, forcibly closes the connection (enters
- If the client’s TIME_WAIT ends without receiving a retransmitted FIN:
- The client assumes the server received the ACK, closes normally, and enters
CLOSED
- The client assumes the server received the ACK, closes normally, and enters
This is the significance of TIME_WAIT: after sending the last ACK, the client can’t close immediately and must wait a while, ensuring that if this ACK is lost, it can be resent to let the server close with peace of mind.
🤔 Why is the wave four steps but the handshake three?
| Three-way handshake | Four-way wave | |
|---|---|---|
| Can they merge? | The server’s SYN and ACK can merge into SYN+ACK | The server’s ACK and FIN cannot merge |
| Why | When the server receives the SYN, it doesn’t need to wait for anything and immediately knows “I also want to establish the connection,” so it can combine SYN and ACK into one | When the server receives the FIN, it may still have unsent data and can’t close immediately. It must first reply ACK to say “I know,” and only send FIN separately after all data is sent |
| Essential difference | Establishing a connection is a mutual synchronization, with no order | Closing a connection is independent for each side; who closes first depends on who finishes sending first |
A real-scenario analogy:
- Three-way handshake: two people reach out to shake hands at the same time (SYN), respond to each other at the same time (SYN+ACK), and finally confirm (ACK). Synchronized.
- Four-way wave: A says “I’m done talking” (FIN), B says “got it” (ACK), but B keeps talking; when B is done (FIN), A says “got it” (ACK). Independent.
📊 Four-way wave state transition diagram
| Step | Operation | Client state | Server state |
|---|---|---|---|
| Init | data transferring | ESTABLISHED |
ESTABLISHED |
| ① | Client sends FIN | FIN_WAIT_1 |
ESTABLISHED |
| ② | Server receives FIN, replies ACK | FIN_WAIT_2 |
CLOSE_WAIT |
| ②→③ | Client receives ACK | FIN_WAIT_2 |
CLOSE_WAIT |
| ③ | Server sends FIN (after data is sent) | FIN_WAIT_2 |
LAST_ACK |
| ③→④ | Client receives FIN | TIME_WAIT |
LAST_ACK |
| ④ | Client sends ACK | TIME_WAIT |
LAST_ACK |
| ④→ | Server receives ACK | TIME_WAIT |
CLOSED |
| after 2MSL | Client’s wait ends | CLOSED |
CLOSED |
💡 The four-way wave in one sentence
The client closes first (sends FIN) → the server replies ACK (agrees, but is still sending data) → the server closes later (sends FIN) → the client replies ACK (final confirmation, and waits foolishly for a while)
Core purposes:
- Ensure both sides have nothing more to say (all data delivered)
- Ensure the last ACK won’t be lost, avoiding the server “with its heart hanging” not knowing whether the other side received it
- Ensure old packets in the network disappear so they don’t affect a subsequent new connection
- The half-close mechanism: allows one side, after closing its sending channel, to keep receiving the other side’s data until the other side also closes.
The WebSocket Interaction Process
1. Basic WebSocket interaction flow (including the handshake)
sequenceDiagram
participant Client as Client
participant Server as WebSocket server
Note over Client,Server: 1. HTTP handshake upgrade
Client->>Server: HTTP GET /ws
Upgrade: websocket
Sec-WebSocket-Key: xxx
Server-->>Client: HTTP 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: yyy
Note over Client,Server: WebSocket connection established (full-duplex)
Client->>Server: send a text message
Server-->>Client: reply message
Server->>Client: proactively push a message
Client-->>Server: confirm receipt (business layer)
Note over Client,Server: 2. Close the connection
Client->>Server: Close Frame (status code: 1000)
Server-->>Client: Close Frame (confirm close)
Note over Client,Server: Connection closed
2. Detailed interaction flow (including Ping/Pong heartbeat)
sequenceDiagram
participant Client as Client
participant Server as WebSocket server
participant App as Business app layer
Note over Client,App: Phase 1: handshake establishment
Client->>Server: HTTP request (Upgrade: websocket)
Server-->>Client: 101 Switching Protocols
Server->>App: trigger the @OnOpen event
Note over App: save the Session to memory
Note over Client,App: Phase 2: bidirectional communication
Client->>Server: send a business message (e.g. query order)
Server->>App: trigger @OnMessage
App-->>Server: process business logic
Server-->>Client: push response data
Server->>Client: proactive push (e.g. system notification)
Client-->>Server: receipt confirmation (business-layer ACK)
Note over Client,App: Phase 3: heartbeat keep-alive (WebSocket protocol layer)
loop every 30 seconds
Server->>Client: Ping Frame (protocol layer)
Client-->>Server: Pong Frame (protocol layer)
Note over Server: update the last-heartbeat time
end
Note over Client,App: Phase 4: abnormal disconnect or explicit close
alt explicit close
Client->>Server: Close Frame (1000 Normal)
Server-->>Client: Close Frame (confirm)
Server->>App: trigger @OnClose
else abnormal disconnect (network timeout)
Note over Server: heartbeat timeout detected
Server->>App: trigger @OnError
Server->>App: trigger @OnClose
end
Note over App: clean up the Session reference
3. WebSocket in a distributed scenario (multi-node + Redis)
sequenceDiagram
participant Client1 as Client 1
(connected to Node-A)
participant Client2 as Client 2
(connected to Node-B)
participant NodeA as Node-A
(WebSocket server)
participant NodeB as Node-B
(WebSocket server)
participant Redis as Redis Pub/Sub
Note over Client1,NodeA: Client 1 connects to Node-A
Client1->>NodeA: WebSocket handshake
NodeA-->>Client1: connection established
Note over NodeA: save Session1 to local memory
Note over Client2,NodeB: Client 2 connects to Node-B
Client2->>NodeB: WebSocket handshake
NodeB-->>Client2: connection established
Note over NodeB: save Session2 to local memory
Note over Client1,Redis: Scenario: Client 1 sends a message to Client 2
Client1->>NodeA: send message (target: Client 2)
NodeA->>Redis: publish message to a channel
(target user: Client 2)
Redis->>NodeB: broadcast the message to all subscribed nodes
NodeB->>NodeB: look up the local Session2
NodeB-->>Client2: push the message to Client 2
Note over Client1,Client2: forwarding complete
4. Complete lifecycle state diagram
stateDiagram-v2
[*] --> Connecting: initiate HTTP request
(Upgrade: websocket)
Connecting --> Connected: received 101 response
(handshake succeeded)
Connecting --> HandshakeFailed: non-101 response
or timeout
Connected --> MessageExchange: send/receive messages
MessageExchange --> Connected: ongoing communication
Connected --> Heartbeat: periodically send Ping
Heartbeat --> Connected: received Pong response
Heartbeat --> TimeoutDisconnect: multiple Pings with no response
Connected --> ExplicitClose: send Close Frame
ExplicitClose --> Closed: received Close confirmation
TimeoutDisconnect --> Closed: trigger OnClose
HandshakeFailed --> [*]
Closed --> [*]
note right of Connected
Session available
isOpen() == true
end note
note right of Closed
Session invalid
isOpen() == false
clean up the memory reference
end note
5. WebSocket frame-structure interaction (protocol-layer details)
sequenceDiagram
participant Client as Client
participant Server as Server
Note over Client,Server: Data frame interaction (RFC 6455)
Client->>Server: Text frame (Opcode=0x1)
FIN=1, Payload="Hello"
Server-->>Client: Text frame (Opcode=0x1)
FIN=1, Payload="World"
Note over Client,Server: Fragmented transmission (large message)
Client->>Server: Text frame (Opcode=0x1, FIN=0)
fragment 1
Client->>Server: Continuation frame (Opcode=0x0, FIN=0)
fragment 2
Client->>Server: Final frame (Opcode=0x0, FIN=1)
fragment 3
Note over Client,Server: Control frames (heartbeat)
Server->>Client: Ping frame (Opcode=0x9)
Client-->>Server: Pong frame (Opcode=0xA)
Note over Client,Server: Close the connection
Client->>Server: Close frame (Opcode=0x8)
status code=1000, Reason="Bye"
Server-->>Client: Close frame (Opcode=0x8)
status code=1000
6. Interaction flow for exception handling
sequenceDiagram
participant Client as Client
participant Server as Server
participant Session as Session object
participant Cleaner as Cleaner
Note over Client,Cleaner: Handling exceptional scenarios
rect
Note over Client,Server: Scenario 1: network suddenly drops
Client--xServer: network interrupted (no Close sent)
Note over Server: heartbeat timeout detection (60s)
Server->>Session: timeout detected
Session->>Cleaner: trigger @OnError
Cleaner->>Cleaner: remove the local Session reference
Cleaner->>Cleaner: close resources (thread pools, etc.)
Note over Cleaner: Session.isOpen() == false
end
rect
Note over Client,Server: Scenario 2: business exception
Client->>Server: send an invalid message
Server->>Server: parsing exception
Server->>Session: trigger @OnError
Session-->>Client: send a close frame (status code: 1002)
Session->>Cleaner: trigger @OnClose
Cleaner->>Cleaner: clean up resources
end
7. Summary: where the WebSocket Session sits in the flow
graph TB
subgraph "Connection establishment phase"
A[HTTP handshake] --> B[create a Session instance]
B --> C[trigger OnOpen]
C --> D[save to the in-memory Map]
end
subgraph "Communication phase"
E[receive message] --> F[get the connection via the Session]
F --> G[trigger OnMessage]
G --> H[business processing]
H --> I[reply via Session.send]
end
subgraph "Connection close phase"
J[receive Close frame or timeout] --> K[trigger OnClose]
K --> L[remove the Session from the Map]
L --> M[close the underlying Channel]
M --> N[Session invalidated]
end
D --> E
I --> J
The code-writing flow in Spring
- @Configuration WebSocketConfig
- addEndpoint - which URL to accept connections from
- setApplicationDestinationPrefixes - which path to accept messages from
- enableSimpleBroker - which paths to enable broadcasting on
- JwtHandshakeInterceptor - verify the user’s token during the handshake, put it into the ws attributes
- CustomHandshakeHandler extends DefaultHandshakeHandler - during the handshake, store the user info from attributes into the Principal
- @Component WebSocketEventListener - handle the behavior on connect, subscribe, unsubscribe, and disconnect
- Note: disconnecting does not automatically unsubscribe from everything!
- @ControllerAdvice WebSocketExceptionHandleController - handle exceptions
- @MessageExceptionHandler(Exception.class) specifies which Exceptions
- @SendToUser("/queue/errors") specifies which path the error message is broadcast to; note Spring will prepend “/user”
- @Controller ChatController - how to handle a received message
- @MessageMapping("/chat.send") - no need to write the prefix; setApplicationDestinationPrefixes already sets the default prefix
MQ
The code-writing flow in Spring
- @Configuration RabbitMQConfig - define queues, exchanges, and BINDings, including dead-letter ones
- @Bean Queue, @Bean DirectExchange, @Bean Binding
- @Configuration MultiRetryConfig - define the conditions under which a message goes to the dead-letter queue
- @Bean RetryOperationsInterceptor
- @Bean SimpleRabbitListenerContainerFactory - define a Container implementing retryOperationsInterceptorChatMessage and jackson2JsonMessageConverter
- @Component ChatMessageConsumer - consume messages