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)

  1. Client: I know nothing, so I’ll send out a SYN message and see if anyone receives it.
  2. If no one replies, I send it a few more times (timeout retransmission).
  3. 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)

  1. The server received the client’s SYN.

  2. The server replies SYN+ACK: “I got your message; I’ll send one too and see if you can receive it.”

  3. 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”
  4. 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)

  1. 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.”

  2. 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!
  3. Both sides then enter the ESTABLISHED state and can begin transmitting real data.


⚠️ What if the third handshake’s ACK is lost?

  1. The client’s view: I sent the ACK and even sent real data directly (because the client thinks the connection is established).
  2. The server’s view: I didn’t receive the ACK, I’m still in the SYN-RCVD state, and I’m unsure whether I can send and whether the client can receive.
  3. The server’s behavior: after a timeout, it retransmits SYN+ACK (a limited number of times).
  4. 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)
  5. 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 ESTABLISHED and accepts the data
  6. 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 CLOSED state

🤔 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)

  1. Client: I’ve sent all my data, I don’t want to send more, but I can still receive data.
  2. 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.”
  3. If no one replies, I send it a few more times (timeout retransmission).
  4. 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)

  1. The server received the client’s FIN and knows the client won’t send data anymore.

  2. The server replies with an ACK: “Got your FIN; I know you have nothing more to say.”

  3. 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
  4. 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)

  1. Server: my data is all sent now, and I have nothing more to say either.

  2. The server sends a FIN to the client: “My data is all sent too; I’m about to close my sending channel.”

  3. 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 ✅
  4. 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)

  1. The client received the server’s FIN and replies with an ACK: “Got your FIN, confirming the close.”

  2. 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 CLOSED state 🗑️
  3. 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 CLOSED state

⚠️ What if the fourth wave’s ACK is lost?

  1. The client’s view: I sent the ACK, entered TIME_WAIT, and I’m waiting for the server’s possible retransmitted FIN.
  2. The server’s view: I sent a FIN — why haven’t I gotten the ACK? Was it lost?
  3. The server’s behavior: after a timeout it retransmits the FIN (a limited number of times).
  4. 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)
  5. 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
  6. 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

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:

  1. Ensure both sides have nothing more to say (all data delivered)
  2. Ensure the last ACK won’t be lost, avoiding the server “with its heart hanging” not knowing whether the other side received it
  3. Ensure old packets in the network disappear so they don’t affect a subsequent new connection
  4. 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

  1. @Configuration WebSocketConfig
    1. addEndpoint - which URL to accept connections from
    2. setApplicationDestinationPrefixes - which path to accept messages from
    3. enableSimpleBroker - which paths to enable broadcasting on
  2. JwtHandshakeInterceptor - verify the user’s token during the handshake, put it into the ws attributes
  3. CustomHandshakeHandler extends DefaultHandshakeHandler - during the handshake, store the user info from attributes into the Principal
  4. @Component WebSocketEventListener - handle the behavior on connect, subscribe, unsubscribe, and disconnect
    1. Note: disconnecting does not automatically unsubscribe from everything!
  5. @ControllerAdvice WebSocketExceptionHandleController - handle exceptions
    1. @MessageExceptionHandler(Exception.class) specifies which Exceptions
    2. @SendToUser("/queue/errors") specifies which path the error message is broadcast to; note Spring will prepend “/user”
  6. @Controller ChatController - how to handle a received message
    1. @MessageMapping("/chat.send") - no need to write the prefix; setApplicationDestinationPrefixes already sets the default prefix

MQ

The code-writing flow in Spring

  1. @Configuration RabbitMQConfig - define queues, exchanges, and BINDings, including dead-letter ones
    1. @Bean Queue, @Bean DirectExchange, @Bean Binding
  2. @Configuration MultiRetryConfig - define the conditions under which a message goes to the dead-letter queue
    1. @Bean RetryOperationsInterceptor
    2. @Bean SimpleRabbitListenerContainerFactory - define a Container implementing retryOperationsInterceptorChatMessage and jackson2JsonMessageConverter
  3. @Component ChatMessageConsumer - consume messages