Novices can write a real-time chat system by themselves after reading it. There is no advanced theory, just use real scenes to push it from beginning to end.


1. What problem does this thing solve?

What I usually do when making a website is "Request-Response" model:

Browser: "Server, give me this page"  →  server: "for you"  →  Finish

The server is always passive and cannot actively talk to the browser.

But in reality, there are a lot of scenarios that requireCross-client notifications——A does something, and B needs to know immediately:

sceneWho operatesWho receives the notificationCan HTTP be done?
chatA send messageB receives the message❌ B has not sent a request, and the server cannot actively find B
Class big screenBackend review passedClassroom large screen refreshes in real time❌ The large screen is just a display page, I don’t know when it will be updated.
Background push notificationAdministrator makes announcementPop-ups for all online users❌ If the user does not request it, the server cannot push it.

The judgment criteria are simple:Operate by yourself, wait for the results by yourself(For example, "Submission successful" will pop up after submitting the registration) → HTTP request-response is enough.Others operate and you receive notifications(For example, the large screen automatically refreshes after the background review is passed) → WebSocket is required.

This is what GatewayWorker does: Establish a long WebSocket connection between the browser and the server. The connection will not be disconnected and the server can push it to whomever it wants.

Tradition HTTP:    Browser ──ask──→ server ──answer──→ Browser (end, go their separate ways))
WebSocket:    Browser ←─────long connection─────→ Server (Always connected, push at any time)

The client can be a web page, a small program, or an App, as long as it supports the WebSocket protocol.

Take "class registration + large screen real-time refresh" as an example throughout the text:

Rolewhat to do
Registration page (for parents, via HTTP)Fill out the form and submit → HTTP request-response, just like ordinary web pages
Classroom large screen (requires WebSocket)Connect to GatewayWorker and maintain a long connection, waiting for new registration data to be pushed in the background
ThinkPHP backendAfter parents submit, the registration is processed, stored in the database, and then pushed to the big screen.
GatewayWorkerMaintain the WebSocket connection of the big screen, and tell it in the background to push it to the big screen
GatewayClient (a PHP library)Let ThinkPHP control GatewayWorker

In short:Parents use HTTP to submit registration → background processing is completed → push to the classroom big screen through GatewayWorker. Parents and the big screen are two groups of people. The push solution is "one character operates and the other character receives the notification."


2. What exactly runs after startup - three processes

After GatewayWorker is started, three processes are actually started, each working without fighting:

                Register(Registration center, port 1238)
                 Tell Gateway and BusinessWorker where are each other
                 Once the matching is done, it will be fine.
                                        
       Gateway(doorman)              BusinessWorker(Worker)
       monitor 8282 port               Not exposed to the outside world, purely internal work
       Receiving client connections                run Events.php
       Maintain a long connection                    After processing, tell the guard who to push it to.
          
      The client connects to this port
processsimileportforeign?Responsibilities
RegisterRoster1238❌ This unit onlyRegister the addresses of all Gateways and Workers, and rely on it to discover each other between processes.
Gatewaydoorman8282✅ The browser connects to thisAccepting customers, maintaining long connections, and forwarding messages
BusinessWorkerWorkernoneRun Events.php to handle all business logic

⚠️ Register must listen 127.0.0.1, must not be exposed to the external network. It uses a plain text protocol without any authentication - anyone who connects can pretend to be a Gateway or Worker and issue commands, thereby hijacking the entire push system.Gateway must listen 0.0.0.0, otherwise external clients cannot connect.

Startup sequence: Register → Gateway → BusinessWorker

Register must be started first, otherwise the next two startups will report errors directly - they must report to Register as soon as they are started.

Configuration file correspondence

Applications/YourApp/ Next four files:

documentWhy?Key configuration
start_register.phpRegistertext://127.0.0.1:1238
start_gateway.phpGatewaytcp://0.0.0.0:8282, 2 processes
start_businessworker.phpBusinessWorker4 processes, pointing to Events.php
Events.phpThe only thing to writeBusiness logic is all here

start_gateway.php Core code:

$gateway = new Gateway("tcp://0.0.0.0:8282");  // external port
$gateway->count = 2;                            // open 2 processes share connections
$gateway->lanIp = '127.0.0.1';                  // Intranet communication IP
$gateway->startPort = 2900;                     // Internal communication start port
$gateway->registerAddress = '127.0.0.1:1238';   // where to find Register

start_businessworker.php:

$worker = new BusinessWorker();
$worker->count = 4;                             // 4 Processes process business in parallel
$worker->registerAddress = '127.0.0.1:1238';    // Find the same one Register
$worker->eventHandler = 'Events';               // Point to business class

So after starting, the total 1 + 2 + 4 = 7 processes.

How to adjust count

process typesuggestionreason
Registerforever 1Stateless, there is no point in opening it more
GatewayBy number of connections, not by CPUEach process can handle 5,000~10,000 connections; opening too many connections will increase broadcast overhead.
BusinessWorkerCPUNumber of cores ~ 2×Number of coresIt eats up the CPU, and too many context switches will make it slower.

Gateway count More is not better. As the number of Gateway processes increases, the cost of Register broadcasting addressing to all Gateways will increase (refer to Chapter 5 sendToUid). Official recommendation:2 openings for less than 10,000 connections, and 1 additional opening for each additional 10,000 connections. Because it is mainly I/O intensive (maintaining connections, forwarding data), not CPU intensive.

Actual: 4-core machine, 5000 concurrency → gateway.count=2, businessworker.count=8. If you don't worry about it, the default value will work.

After getting the code, actually do three things:

  1. Change port—— start_gateway.php inner handle 8282 Change to the corresponding port and the firewall will allow it.
  2. Adjust count - change according to the above table, use the default value if you don’t hesitate.
  3. Write Events.php —— The only place where you need to write code, will be expanded later

lanIp, startPort, registerAddress No changes are needed for stand-alone deployment.start_register.php and start_businessworker.php There is almost no need to move.


3. The whole process of a connection

Divide the process from opening the page to receiving the push into four stages.

Phase 1: Connection establishment → server returns client_id

new WebSocket("ws://192.168.1.6:8282")
  │
  ├── TCP three handshakes → Connected
  │
  ├── Gateway Assign a globally unique number: client_id
  │   for example "7f0000010a5400000001"
  │   Note: This number will change when disconnected and reconnected.
  │
  ├── Automatic trigger Events::onConnect($client_id)
  │   The connection is established. At this time, you can actively send messages to the client.(sendToClient)
  │   But the client has not sent any data yet(onMessage Not triggered yet)
  │
  └── onConnect Take the initiative to push client_id go back: 
      Gateway::sendToClient($client_id, '{"type":"connected","client_id":"7f0000..."}')
      This news goes WebSocket to the client, trigger ws.onmessage ✅
      Only then does the client know its own client_id

Phase 2: Identity Binding

Client receives onConnect Pushed over { type:'connected', client_id:'7f0000...' }
  │                     ↑ This message comes from the previous step onConnect inside sendToClient
  │
  ├── hair HTTP to the background /api/bind
  │   Bring two things: client_id + business identity (e.g. classroom_id=1)
  │
  ├── Background tune Gateway::bindUid("7f0000...", "classroom_1")
  │
  └── GatewayWorker Note the mapping internally: 
      _uidConnections["classroom_1"] = ["7f0000010a5400000001"]
      Talk about it later"Give classroom_1 hair", You know where to push it

Stage 3: Message push

Parents submit registration (go HTTP, Ordinary request-response)
  │                           ↑
  ├── Backend database              | Note: Parents and the big screen behind are two groups of people!
  │                            | For parents HTTP, For large screen WebSocket
  ├── Background tune: Gateway::sendToUid("classroom_1", jsondata)  → Push to the big screen
  │
  └── How to find the connection to the large screen internally?
      → Construct instruction package
      → broadcast to all Gateway Process (don’t know classroom_1 Which one is connected to Gateway superior)
      → each Gateway Check your own _uidConnections
      → Whoever is in charge of this connection is responsible for pushing it out. Those who are not in charge of it will be ignored.

Why broadcast? The background only knows the uid and does not know which Gateway process this user is connected to, so it tells everyone to claim it.

Stage 4: Disconnection

Close page / Network disconnected
  │
  └── Automatic trigger Events::onClose($client_id)
      GatewayWorker Automatically clear binding relationships
      Can keep logs and notify others "xx Offline"

4. The secret of client_id - core principles

The previous section appears repeatedly client_id, now take it apart and look at it——This is the key to understanding the entire GatewayWorker.

client_id Instead of a random string,Encoded house number:

// Whenever a client connects Gateway, Gateway Automatically generated: 
client_id = bin2hex(pack('NnN', $local_ip, $local_port, $connection_id))
//                            ↑          ↑             ↑
//                     Gateway own IP  Gateway Internal port self-increasing counter
//                      (like 127.0.0.1)   (like 2900, from   (Every new connection +1)
//                                       startPort Configuration)

Important: The IP and port here belong to the Gateway process itself and have nothing to do with the client's IP and port. The client's address does not participate in the encoding.

Decode it back:

$local_ip      → Gateway process intranet IP(like 127.0.0.1)
$local_port    → Gateway internal communication port (e.g. 2900, from start_gateway.php of startPort)
$connection_id → Should Gateway The number of connections on the process (incrementing number)

A real example:

client_id = "7f0000010b5400000001"
decoding → Gateway IP: 127.0.0.1   internal port: 2900   connection number: 1

Why does client_id change after the same client reconnects?

because connection_id is aself-increasing counter, not the client's identifier:

Same person, same computer, reconnect immediately after disconnection: 

No. 1 connections: Gateway IP=127.0.0.1, port=2900, conn_id=1"7f0000010b5400000001"
No. 2 connections: Gateway IP=127.0.0.1, port=2900, conn_id=2"7f0000010b5400000002"
                                                               ↑ Only here has changed

The IP and port of the Gateway have not changed (it is still the same process), but the counter has been +1 and the client_id is different. This is why after disconnecting and reconnecting, you mustagain bindUid——The old client_id has been invalidated, and the new connection has received a new number.

Since client_id will change, how to locate the target Gateway?

Although connection_id different but The IP and port of the Gateway remain unchanged——These two fields are enough to allow any process to directly TCP connect to the target Gateway.

This means:Getting the client_id is equivalent to getting the address of the target Gateway.. The later bindUid uses this to achieve "direct access without going through Register".

This is the basis for all subsequent understandings - since the addresses are written in it, there is no need to go to the Register to check some operations.

The analogy of courier tracking number and person's name will be discussed later.


5. bindUid and sendToUid - the core difference

This is in GatewayWorkerMost asked and most easily confusedplace.

bindUid: direct (without going through Register)

Gateway::bindUid($client_id, 'user_123');
bindUid(client_id, uid)
  → decoding client_id → get Gateway of IP + port → direct TCP Pass through ✅
  → in target Gateway write it down in memory: uid → client_id
  → Register I didn't know about this the whole time

sendToUid: broadcast person search (required to use Register)

Gateway::sendToUid('user_123', $message);
sendToUid(uid, msg)
  → only uid, Don't know where he is Gateway superior
  → go Register Q: All Gateway Give me the address list 📋
  → Send it to everyone one by one Gateway
  → each Gateway Check your own memory: uid Are you here with me??
  → The one who is there is responsible for pushing, the one who is not there is ignored.

One sentence judgment standard

There are parametersCommunication methodGo to Register?Why
client_idGet to your destination Gateway❌ Not leavingclient_id comes with IP+port
uidBroadcast Ask All Gateway✅ Must gouid is just the identity, not the address

It has nothing to do with whether it is front-end or back-end, HTTP or command line. It only depends on whether the address is included in the parameters.

Courier tracking number metaphor (distinguish three concepts at once)

  • client_id = Express tracking number, with the delivery address written on it, the courier will deliver it directly
  • uid = name, I don’t know where I live, so I can only check my address book and ask door to door.
  • bindUid = Register "This express delivery is from Zhang San", and you will know where to send it to Zhang San in the future.

A classic misjudgment

You may encounter:sendToUid No response, but bindUid normal.

Don’t be quick to doubt the code——If the Register port is misconfigured (for example, 1236 is written in the code, but 1238 is actually started), bindUid will be client_id It has its own address so it can be accessed. It is easy to mistakenly think that everything is normal. But in fact, sendToUid needs to go through Register. If the port is wrong, it will not find the person.

Troubleshooting tips: Just because bindUid works, it doesn’t mean Register is okay. Verify it with sendToUid first.


6. Two development models - how to choose

Mode 1: Pure GatewayWorker (practice/IoT)

Browser ←→ WebSocket ←→ GatewayWorker
                          └─ Events.php write all logic

Events.php onMessage Take care of business yourself:

public static function onMessage($client_id, $message)
{
    $data = json_decode($message, true);
    switch ($data['type']) {
        case 'login':
            Gateway::bindUid($client_id, $data['uid']);
            Gateway::sendToClient($client_id, json_encode(['type' => 'login_ok']));
            break;
        case 'chat':
            Gateway::sendToUid($data['to_uid'], json_encode([
                'type' => 'msg', 'from' => $data['from_uid'], 'content' => $data['content']
            ]));
            break;
    }
}

Advantages: Simple and direct, both client and server use WebSocket, one less hop.

This does not mean that mode 1 cannot connect to the database or perform authentication. BusinessWorker is essentially a PHP process that can use ThinkPHP's Model can query the database and perform session verification - the authentication example of mode 1 below is the best proof. soTechnically both modes can do everything, the difference is not in whether it can be done, but inHow to organize code.

Mode 2: GatewayWorker + background framework

client ─── HTTP ───→ ThinkPHP Backstage ─── GatewayClient ───→ GatewayWorker ──Push──→ client

Events.php is extremely simplified,onMessage You can even leave it blank. All business logic is placed in the ThinkPHP controller:

// Events.php - Only responsible for returning client_id
public static function onConnect($client_id)
{
    Gateway::sendToClient($client_id, json_encode([
        'type' => 'connected', 'client_id' => $client_id
    ]));
}

// ThinkPHP Controller - Binding
function bind() {
    Gateway::bindUid($_POST['client_id'], 'classroom_' . $_POST['classroom_id']);
    return json(['code' => 1]);
}

// ThinkPHP Controller - Push
function register() {
    // ... Save database ...
    Gateway::sendToUid('classroom_1', json_encode(['type' => 'notify', 'msg' => 'Registration successful']));
}

The real difference between the two models

To reiterate:Technically, there is no distinction between superior and inferior. Both can connect to the database and perform authentication. The difference:

Mode 1 (Pure WebSocket)Mode 2 (HTTP push)
Where is the business code?Events.php (resident process)ThinkPHP controller (request-response)
After changing the business codeNeed to restart WorkerNo need to restart, effective immediately
Authentication methodDo the Session/Token verification yourself in onMessageReuse framework middleware and cookies
suitable for whatChat, instant messaging——Sending and receiving messages is high-frequency two-wayPush notifications, large screen refresh - the client is mainlyPassive reception
Debugging difficultySlightly higher, the log depends on Worker outputLike normal web development, requests are visible

Judgment in one sentence:If the client mainly sends messages (chat), use mode one; if the client mainly receives messages (notification push), use mode two. In actual projects, the two are often mixed.

The real way to write authentication in mode one

For example, in ThinkPHP, put GatewayWorker in the addons directory, and Events.php directly use ThinkPHP classes:

public static function onMessage($client_id, $message)
{
    $message_data = json_decode($message, true);
    if (!$message_data) return;

    // Session Authentication - follow HTTP It’s the same as what’s written in the interface
    if (!isset($_SESSION['user_id']) || !$_SESSION['user_id']) {
        return; // Not logged in, ignore
    }

    // tune ThinkPHP of Chat Model processing business
    $user_id = $_SESSION['user_id'];
    $chat = new \addons\xilujob\chatprograms\library\Chat();
    $ret = $chat->{$message_data['type']}($user_id, $message_data);

    // Reply to sender
    Gateway::sendToClient($client_id, json_encode($ret));

    // If the recipient is online, forward
    if ($ret['code'] == 1 && in_array($message_data['type'], ['say'])) {
        $receive_uid = 'user_' . $ret['data']['to_uid'];
        if (Gateway::isUidOnline($receive_uid)) {
            $ret['type'] = 'new_msg';
            Gateway::sendToUid($receive_uid, json_encode($ret));
        }
    }
}

This is the correct way to open mode one——It is not that Events.php can only be written switch-case Forward, it can do anything ThinkPHP can do.

Mixed mode (most commonly used in actual projects)

Two modes coexist: WebSocket (mode 1) is used for instant chat between clients, and HTTP (mode 2) is used for proactive push notifications from the server. GatewayWorker supports it naturally without any additional configuration.


7. Complete code practice - building a chat system from scratch

This section is a code that can be copied and run through, covering everything from front-end to back-end.

Files that need to be created/modified

documentoperateillustrate
start_register.phpNot movingThe frame template is right
start_gateway.phpNot movingChange at most one port
start_businessworker.php🆕 NewStart Worker
Events.php🆕 write code hereBusiness logic entry
Front-end HTML🆕 Newchat page

1. start_businessworker.php

<?php
use Workerman\Worker;
use GatewayWorker\BusinessWorker;

require_once __DIR__ . '/vendor/autoload.php';

$worker = new BusinessWorker();
$worker->name = 'BusinessWorker';
$worker->count = 4;
$worker->registerAddress = '127.0.0.1:1238';
$worker->eventHandler = 'Events';    // Point to business processing class

Worker::runAll();

2. Events.php

<?php
use GatewayWorker\Lib\Gateway;

class Events
{
    // Connection established → return client_id to client
    public static function onConnect($client_id)
    {
        Gateway::sendToClient($client_id, json_encode([
            'type'      => 'connected',
            'client_id' => $client_id
        ]));
    }

    // Receive client message → according to type distribution
    public static function onMessage($client_id, $message)
    {
        $data = json_decode($message, true);
        
        switch ($data['type']) {
            case 'login':
                Gateway::bindUid($client_id, $data['uid']);
                Gateway::sendToClient($client_id, json_encode(['type' => 'login_ok']));
                break;
                
            case 'chat':
                Gateway::sendToUid($data['to_uid'], json_encode([
                    'type'    => 'msg',
                    'from'    => $data['from_uid'],
                    'content' => $data['content']
                ]));
                break;
        }
    }
    
    // Lost connection
    public static function onClose($client_id)
    {
        // Keep a journal and notify others
    }
}

3. Quick review of five callbacks in Events.php

callbackTrigger timefrequencyuse
onConnect($client_id)Client connected⭐⭐Return client_id.Can be pushed but not collected
onMessage($client_id, $data)Receive client message⭐⭐⭐Processing business: login, chat, binding
onClose($client_id)Client disconnected⭐⭐Clean up resources and notify "xx offline"
onWorkerStart($worker)process startInitialize timer, connection pool (Don’t put it before Worker::runAll())
onWorkerStop($worker)Process exitsSave state, clean up connections

4. Front-end HTML (chat page)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>chat Demo</title>
</head>
<body>
    <p id="status">Connecting...</p>
    <div id="messages"></div>
    <input type="text" id="input" placeholder="Enter message">
    <button onclick="sendMsg()">send</button>

    <script>
        var ws = new WebSocket('ws://127.0.0.1:8282');
        var myUid = 0;

        ws.onopen = function() {
            document.getElementById('status').innerHTML = 'Connected';
        };

        ws.onmessage = function(e) {
            var data = JSON.parse(e.data);
            switch (data.type) {
                case 'connected':
                    // get client_id → Send login message to bind identity
                    myUid = Date.now(); // Use timestamp as temporary uid
                    ws.send(JSON.stringify({ type: 'login', uid: myUid }));
                    break;
                case 'login_ok':
                    document.getElementById('status').innerHTML = 'Logged in (uid=' + myUid + ')';
                    break;
                case 'msg':
                    document.getElementById('messages').innerHTML += 
                        '<p>[' + data.from + '] ' + data.content + '</p>';
                    break;
            }
        };

        ws.onclose = function() {
            document.getElementById('status').innerHTML = 'Disconnected';
        };

        function sendMsg() {
            var text = document.getElementById('input').value;
            ws.send(JSON.stringify({
                type: 'chat',
                from_uid: myUid,
                to_uid: prompt('To whom? Enter the other party's uid'),
                content: text
            }));
            document.getElementById('input').value = '';
        }
    </script>
</body>
</html>

5. Disconnect and reconnect

Replace these three parts in the above HTML with the following:var ws = new WebSocket(...), ws.onopen, ws.onclose.var myUid, ws.onmessage and sendMsg() remain unchanged.

var ws = null;
var reconnectTimer = null;

function connect() {
    if (ws) {
        ws.onclose = null;  // Unbind the callback first to prevent closing the old connection from triggering reconnection
        ws.close();
    }
    
    ws = new WebSocket('ws://127.0.0.1:8282');
    
    ws.onopen = function() {
        console.log('Connected');
        if (reconnectTimer) {
            clearInterval(reconnectTimer);
            reconnectTimer = null;
        }
        // Put the above number4Festival onopen Move the logic here (display status, send login wait)
    };
    
    ws.onclose = function() {
        console.log('Broken, 5 Reconnect after seconds');
        if (!reconnectTimer) {
            reconnectTimer = setInterval(connect, 5000);
        }
    };
}

connect();

6. ThinkPHP backend (production mode)

// binding interface
function bind() {
    $client_id = $_POST['client_id'];
    $classroom_id = $_POST['classroom_id'];
    Gateway::bindUid($client_id, 'classroom_' . $classroom_id);
    return json(['code' => 1, 'msg' => 'Binding successful']);
}

// Registration interface (processing business + push notification)
function register() {
    $classroom_id = input('post.classroom_id');
    $student_name = input('post.student_name');
    // ... Save database, verify ...
    Gateway::sendToUid('classroom_' . $classroom_id, json_encode([
        'type' => 'notify',
        'message' => 'New students are signing up: ' . $student_name
    ]));
}

7. Message protocol specification

All messages are unified in JSON, using type Field distinction:

// Server → client
{"type":"connected", "client_id":"7f0000010a5400000001"}   // Connection confirmation
{"type":"login_ok"}                                         // Login successful
{"type":"msg", "from":123, "content":"Hello"}                 // Chat messages
{"type":"notify", "message":"Registration successful"}                       // Server notification

// client → Server
{"type":"login", "uid":123}                                 // Identity registration
{"type":"chat", "from_uid":123, "to_uid":456, "content":"Hello"}  // chat

The benefits of unified format: one in Events.php switch($data['type']) That’s it. Don’t guess the content of the message.


8. Front-end WebSocket API quick check

This isBrowser native API, not GatewayWorker. For documentation, search for "WebSocket" on MDN.

var ws = new WebSocket('ws://IP:8282');    // Note: No http://

// 4 events
ws.onopen    = function(e) { /* Connection successful */ };
ws.onmessage = function(e) { var data = JSON.parse(e.data); };
ws.onclose   = function(e) { /* disconnect */ };
ws.onerror   = function(e) { /* Error */ };

// 2 method
ws.send(JSON.stringify({ type: 'chat', content: 'Hello' }));
ws.close();

// 1 attributes
ws.readyState  // 0=Connecting  1=Connected  2=Closed  3=Closed

Notice: JS is case sensitive,WebSocket cannot be written as websocket.


9. GatewayWorker API quick check

// ===== Configuration (must be set before calling any method) =====
Gateway::$registerAddress = '127.0.0.1:1238';

// ===== Connection level (parameters with client_id, Not leaving Register, direct) =====
Gateway::sendToClient($client_id, $data);      // Send to specified connection
Gateway::closeClient($client_id);              // Kick people offline

// ===== User level (parameters only uid, Walk Register addressing) =====
Gateway::bindUid($client_id, $uid);            // binding client_id ↔ uid
Gateway::unbindUid($client_id, $uid);          // unbundle
Gateway::sendToUid($uid, $data);               // Send to designated user
Gateway::isUidOnline($uid);                    // Is the user online?
Gateway::getClientIdListByUid($uid);           // check uid Corresponds to all connections

// ===== broadcast =====
Gateway::sendToAll($data);                     // Sent to all connected clients

10. Port description

portWho evenListening IPreason
8282Browser/Client0.0.0.0Must accept external connections
1238Gateway/Worker process127.0.0.1Safe, not exposed to the outside world
8282 = Door, anyone can come
1238 = Internal roster, only your own people can see it

11. Pitfall records (12 real pitfalls)

#Phenomenonreasonsolve
1stream_socket_client Report an errorThe Register port is incorrect or the process is not started.examine registerAddress Is the port consistent with the actual port in the Register?
2?? Syntax errorWeb server PHP < 7.1phpStudy switches the website PHP version to 7.1+
3CLI PHP 8.3 but the web page reports a syntax errorPHP versions of CLI and Web are inconsistentCLI and website must use the same version
4Composer is installed with gateway-worker 4.x, and the website reports an error when running PHP 5.6The CLI is 8.3, and composer automatically selects the latest version.composer.json change ^3.0, composer update
5sendToUid No response but bindUid normalRegister port mismatchexamine registerAddress The configuration is consistent with the actual listening port of Register (bindUid has its own address and can be connected, covering up the problem)
6The client cannot connectFirewall blocked/IP configuration errorGateway required 0.0.0.0;First telnet IP 8282 try
7No response when calling GatewayClientregisterAddress and Register are inconsistentCheck both sides to confirm that the Register process is running
8The client cannot receive the message after it is sent.The bindUid is not adjusted or the client is disconnected.First isUidOnline() Confirm online
9return 1 Frontend res.json() failsThinkPHP returned non-JSONmust return json([...])
10Print Promise{pending}fetch is not written .then()post(...).then(data => console.log(data))
11{e.client_id} JS syntax errorThe dot is not a legal object keyWrite {'client_id': e.client_id}
12Windows deployment: count is invalid,start.php start Report an errorNo pcntl extensionuse start_for_win.bat, only single process

12. Complete data flow - the whole process of chatting between two users

String together all the previous knowledge points and trace the path of a message from beginning to end:

BrowserA (uid=123)                               BrowserB (uid=456)
      │                                               │
      │ ws://127.0.0.1:8282                           │ ws://127.0.0.1:8282
      ↓                                               ↓
      Gateway ───→ Register check in                     Gateway ───→ Register check in
      │ assigned to client_id_A                            │ assigned to client_id_B
      ↓                                               ↓
      onConnect → return {type:'connected', ...}        onConnect → return {type:'connected', ...}
      ↓                                               ↓
    BrowserA receive connected                             BrowserB receive connected
      │                                               │
      │ ws.send({type:'login', uid:123})              │ ws.send({type:'login', uid:456})
      ↓                                               ↓
     BusinessWorker.onMessage                         BusinessWorker.onMessage
      └→ bindUid(client_id_A, 123)                     └→ bindUid(client_id_B, 456)
      └→ return login_ok                                 └→ return login_ok
      ↓                                               ↓


  ╔══════════════ A Give B send message ═══════════════╗

    BrowserA: ws.send({type:'chat', to_uid:456, content:'Hello'})
      ↓
     BusinessWorker.onMessage
      └→ sendToUid(456, '{"type":"msg","from":123,"content":"Hello"}')
           │
           ├→ go Register(:1238) Q: All Gateway address list
           ├→ Iterate through each Gateway: uid=456 Are you here??
           ├→ Gateway-1 Check memory: none → jump over
           ├→ Gateway-2 Check memory: Yes!→ Push
           └→ BrowserB message received ✅

Key points to watch:

  • The first two steps (connect, log in)Don’t go Register, client_id comes with its own address
  • The last step (sendToUid)Must go to Register, only uid does not know where it is
  • The judgment criterion in the process is always "whether there is an address in the parameter"

13. Don’t miss a word - remember everything in seven sentences

  1. Gateway manages connections, Register manages address books, and BusinessWorker writes business ——Three processes each perform their own duties
  2. client_id is the encoded house number(IP + port + connection number), can be located directly;uid is just a name, no address
  3. bindUid is direct (does not need to go through Register), sendToUid broadcasts to find people (must go through Register) ——It doesn’t matter who adjusts it, it depends on whether the parameter has an address.
  4. Both Mode 1 and Mode 2 can connect to the database and perform authentication. ——The difference is not in technical capabilities, but in where the business code is maintained and whether it is necessary to restart the Worker
  5. Events.php is the entire business entrance ——Database, authentication, and push are all here
  6. WebSocket API is a browser standard, check it out on MDN ——Don’t confuse it with GatewayWorker’s methods
  7. CLI PHP and website PHP versions must be aligned —— Otherwise, the package installed by composer can run, but the web page cannot run.