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:
| scene | Who operates | Who receives the notification | Can HTTP be done? |
|---|---|---|---|
| chat | A send message | B receives the message | ❌ B has not sent a request, and the server cannot actively find B |
| Class big screen | Backend review passed | Classroom 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 notification | Administrator makes announcement | Pop-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:
| Role | what 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 backend | After parents submit, the registration is processed, stored in the database, and then pushed to the big screen. |
| GatewayWorker | Maintain 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
| process | simile | port | foreign? | Responsibilities |
|---|---|---|---|---|
| Register | Roster | 1238 | ❌ This unit only | Register the addresses of all Gateways and Workers, and rely on it to discover each other between processes. |
| Gateway | doorman | 8282 | ✅ The browser connects to this | Accepting customers, maintaining long connections, and forwarding messages |
| BusinessWorker | Worker | none | ❌ | Run 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 listen0.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:
| document | Why? | Key configuration |
|---|---|---|
start_register.php | Register | text://127.0.0.1:1238 |
start_gateway.php | Gateway | tcp://0.0.0.0:8282, 2 processes |
start_businessworker.php | BusinessWorker | 4 processes, pointing to Events.php |
Events.php | The only thing to write | Business 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 type | suggestion | reason |
|---|---|---|
| Register | forever 1 | Stateless, there is no point in opening it more |
| Gateway | By number of connections, not by CPU | Each process can handle 5,000~10,000 connections; opening too many connections will increase broadcast overhead. |
| BusinessWorker | CPUNumber of cores ~ 2×Number of cores | It 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:
- Change port——
start_gateway.phpinner handle8282Change to the corresponding port and the firewall will allow it. - Adjust count - change according to the above table, use the default value if you don’t hesitate.
- 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 parameters | Communication method | Go to Register? | Why |
|---|---|---|---|
client_id | Get to your destination Gateway | ❌ Not leaving | client_id comes with IP+port |
uid | Broadcast Ask All Gateway | ✅ Must go | uid 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 directlyuid= 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
useThinkPHP'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 code | Need to restart Worker | No need to restart, effective immediately |
| Authentication method | Do the Session/Token verification yourself in onMessage | Reuse framework middleware and cookies |
| suitable for what | Chat, instant messaging——Sending and receiving messages is high-frequency two-way | Push notifications, large screen refresh - the client is mainlyPassive reception |
| Debugging difficulty | Slightly higher, the log depends on Worker output | Like 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
| document | operate | illustrate |
|---|---|---|
start_register.php | Not moving | The frame template is right |
start_gateway.php | Not moving | Change at most one port |
start_businessworker.php | 🆕 New | Start Worker |
Events.php | 🆕 write code here | Business logic entry |
| Front-end HTML | 🆕 New | chat 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
| callback | Trigger time | frequency | use |
|---|---|---|---|
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 start | ⭐ | Initialize timer, connection pool (Don’t put it before Worker::runAll()) |
onWorkerStop($worker) | Process exits | ⚪ | Save 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.onmessageandsendMsg()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
| port | Who even | Listening IP | reason |
|---|---|---|---|
| 8282 | Browser/Client | 0.0.0.0 | Must accept external connections |
| 1238 | Gateway/Worker process | 127.0.0.1 | Safe, 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)
| # | Phenomenon | reason | solve |
|---|---|---|---|
| 1 | stream_socket_client Report an error | The 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 error | Web server PHP < 7.1 | phpStudy switches the website PHP version to 7.1+ |
| 3 | CLI PHP 8.3 but the web page reports a syntax error | PHP versions of CLI and Web are inconsistent | CLI and website must use the same version |
| 4 | Composer is installed with gateway-worker 4.x, and the website reports an error when running PHP 5.6 | The CLI is 8.3, and composer automatically selects the latest version. | composer.json change ^3.0, composer update |
| 5 | sendToUid No response but bindUid normal | Register port mismatch | examine 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) |
| 6 | The client cannot connect | Firewall blocked/IP configuration error | Gateway required 0.0.0.0;First telnet IP 8282 try |
| 7 | No response when calling GatewayClient | registerAddress and Register are inconsistent | Check both sides to confirm that the Register process is running |
| 8 | The client cannot receive the message after it is sent. | The bindUid is not adjusted or the client is disconnected. | First isUidOnline() Confirm online |
| 9 | return 1 Frontend res.json() fails | ThinkPHP returned non-JSON | must return json([...]) |
| 10 | Print Promise{pending} | fetch is not written .then() | post(...).then(data => console.log(data)) |
| 11 | {e.client_id} JS syntax error | The dot is not a legal object key | Write {'client_id': e.client_id} |
| 12 | Windows deployment: count is invalid,start.php start Report an error | No pcntl extension | use 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
- Gateway manages connections, Register manages address books, and BusinessWorker writes business ——Three processes each perform their own duties
- client_id is the encoded house number(IP + port + connection number), can be located directly;uid is just a name, no address
- 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.
- 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
- Events.php is the entire business entrance ——Database, authentication, and push are all here
- WebSocket API is a browser standard, check it out on MDN ——Don’t confuse it with GatewayWorker’s methods
- CLI PHP and website PHP versions must be aligned —— Otherwise, the package installed by composer can run, but the web page cannot run.