├── README.md
├── bin
└── server
├── composer.json
├── example
└── index.html
└── src
└── Websocket.php
/README.md:
--------------------------------------------------------------------------------
1 | # PHPWebsocket
2 |
3 | Simple PHP Websocket Library / Server for Fun. Just making a wrapper from PHP Socket API. Modifying source code from
4 | reference and making simple OOP for a clean code.
5 |
6 | ## Dependency
7 |
8 | PHP Socket Library
9 | https://www.php.net/manual/en/book.sockets.php
10 |
11 | ## Install
12 |
13 | Create a exercise folder. Open it and run composer require.
14 |
15 | ```
16 | composer require gemblue/php-websocket
17 | ```
18 |
19 | ## Run Server
20 |
21 | - Make server file executable
22 |
23 | ```
24 | sudo chmod +x ./vendor/gemblue/php-websocket/bin/server
25 | ```
26 |
27 | - Run websocket server with port option
28 |
29 | ```
30 | ./vendor/gemblue/php-websocket/bin/server port:3000
31 | ```
32 |
33 | Then it will show success output like
34 |
35 | ```
36 | Listening incoming request on port 3000 ..
37 | ```
38 |
39 | ## Prepare client.
40 |
41 | Create a HTML file for websocket client, for example `index.html` :
42 |
43 | ```html
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
103 | ```
104 |
105 | Open `index.html` with your browser. Use 2 tab/browser for simulation. Output will be like this :
106 |
107 | 
108 |
109 | ## Reference
110 |
111 | - https://stackoverflow.com/questions/42955033/php-client-web-socket-to-send-messages/43121475
112 | - https://phppot.com/php/simple-php-chat-using-websocket
113 | - https://medium.com/@cn007b/super-simple-php-websocket-example-ea2cd5893575
114 |
--------------------------------------------------------------------------------
/bin/server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 |
3 | run();
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gemblue/php-websocket",
3 | "description": "Simple websocket library for learning purpose",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Ahmad Oriza Sahputra",
9 | "email": "ahmadoriza@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": "^7.0"
14 | },
15 | "autoload": {
16 | "psr-4": {
17 | "Gemblue\\Websocket\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Gemblue\\Websocket\\Test\\": "test/"
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Websocket.php:
--------------------------------------------------------------------------------
1 | server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
32 | $this->address = $address;
33 | $this->port = $port;
34 |
35 | socket_set_option($this->server, SOL_SOCKET, SO_REUSEADDR, 1);
36 | socket_bind($this->server, 0, $port);
37 | socket_listen($this->server);
38 |
39 | }
40 |
41 | /**
42 | * Send
43 | *
44 | * Kirim pesan ke semua client, sebelumnya di encode json dulu.
45 | *
46 | * @return bool
47 | */
48 | function send($message) {
49 |
50 | // Build json dengan seal.
51 | $raw = $this->seal(json_encode([
52 | 'message'=> $message
53 | ]));
54 |
55 | foreach($this->clients as $client)
56 | {
57 | @socket_write($client, $raw, strlen($raw));
58 | }
59 |
60 | return true;
61 | }
62 |
63 | /**
64 | * Unseal
65 | *
66 | * Karena socket receive masih mentah, kita harus unseal dulu.
67 | *
68 | * @return string
69 | */
70 | public function unseal($socketData) {
71 |
72 | $length = ord($socketData[1]) & 127;
73 |
74 | if ($length == 126) {
75 | $masks = substr($socketData, 4, 4);
76 | $data = substr($socketData, 8);
77 | } elseif ($length == 127) {
78 | $masks = substr($socketData, 10, 4);
79 | $data = substr($socketData, 14);
80 | } else {
81 | $masks = substr($socketData, 2, 4);
82 | $data = substr($socketData, 6);
83 | }
84 |
85 | $socketData = "";
86 |
87 | for ($i = 0; $i < strlen($data); ++$i) {
88 | $socketData .= $data[$i] ^ $masks[$i%4];
89 | }
90 |
91 | return $socketData;
92 | }
93 |
94 | /**
95 | * Seal
96 | *
97 | * Untuk mengirimkan data seal.
98 | *
99 | * @return string
100 | */
101 | function seal($socketData) {
102 |
103 | $b1 = 0x80 | (0x1 & 0x0f);
104 | $length = strlen($socketData);
105 |
106 | if ($length <= 125)
107 | $header = pack('CC', $b1, $length);
108 | elseif ($length > 125 && $length < 65536)
109 | $header = pack('CCn', $b1, 126, $length);
110 | elseif ($length >= 65536)
111 | $header = pack('CCNN', $b1, 127, $length);
112 |
113 | return $header.$socketData;
114 | }
115 |
116 | /**
117 | * Handshake
118 | *
119 | * Mengirimkan handshake headers ke client yang connect.
120 | *
121 | * @return void
122 | */
123 | function handshake($header, $socket, $address, $port) {
124 |
125 | $headers = array();
126 | $lines = preg_split("/\r\n/", $header);
127 | foreach($lines as $line)
128 | {
129 | $line = chop($line);
130 | if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
131 | {
132 | $headers[$matches[1]] = $matches[2];
133 | }
134 | }
135 |
136 | $secKey = $headers['Sec-WebSocket-Key'];
137 | $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
138 |
139 | $buffer = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n";
140 | $buffer .= "Upgrade: websocket\r\n";
141 | $buffer .= "Connection: Upgrade\r\n";
142 | $buffer .= "WebSocket-Origin: $address\r\n";
143 | $buffer .= "WebSocket-Location: ws://$address:$port/Server.php\r\n";
144 | $buffer .= "Sec-WebSocket-Accept:$secAccept\r\n\r\n";
145 |
146 | socket_write($socket,$buffer,strlen($buffer));
147 |
148 | }
149 |
150 | /**
151 | * Run
152 | *
153 | * Running websocket server.
154 | *
155 | * @return void
156 | */
157 | public function run() {
158 |
159 | // Masukan koneksi server kedalam clients.
160 | $this->clients = [
161 | $this->server
162 | ];
163 |
164 | // Set address and port.
165 | $address = $this->address;
166 | $port = $this->port;
167 |
168 | // Log message
169 | echo "Listening incoming request on port {$this->port} ..\n";
170 |
171 | // Unlimited loop.
172 | while (true)
173 | {
174 | $newClients = $this->clients;
175 |
176 | socket_select($newClients, $null, $null, 0, 10);
177 |
178 | // Jika koneksi socket sekarang ada dalam clients.
179 | if (in_array($this->server, $newClients))
180 | {
181 | // Terima koneksi baru ..
182 | $newSocket = socket_accept($this->server);
183 |
184 | // Masukan dalam client/container socket.
185 | $this->clients[] = $newSocket;
186 |
187 | // Baca data masuk dari tunnel socket yang masuk tadi, browser biasanya mengirim header.
188 | $header = socket_read($newSocket, 1024);
189 |
190 | // Handshake, kirim balik data balasan.
191 | $this->handshake($header, $newSocket, $address, $port);
192 |
193 | // Beri pesan, ada client baru bergabung, ke semua connected client.
194 | socket_getpeername($newSocket, $ip);
195 | $this->send("Client dengan ip {$ip} baru saja bergabung");
196 |
197 | echo "Client dengan ip {$ip} baru saja bergabung \n";
198 |
199 | $index = array_search($this->server, $newClients);
200 | unset($newClients[$index]);
201 | }
202 |
203 | foreach ($newClients as $newClientsResource)
204 | {
205 | // Selama unlimited loop, terima data kiriman dari client, dari method "websocket.send" pada browser.
206 | while(socket_recv($newClientsResource, $socketData, 1024, 0) >= 1)
207 | {
208 | // Jika ada data diterima, baru proses
209 | if ($socketData) {
210 |
211 | // Terima data dari client, kemudian unseal dan decode json.
212 | $socketMessage = $this->unseal($socketData);
213 | $messageObj = json_decode($socketMessage);
214 |
215 | if (isset($messageObj->name) && isset($messageObj->message)) {
216 | // Kirim kembali, broadcast ke semua connected client.
217 | $this->send("{$messageObj->name} : {$messageObj->message}");
218 | }
219 |
220 | break 2;
221 | }
222 | }
223 |
224 | // Dalam looping juga selalu cek, client ada yang keluar apa engga.
225 | // Caranya baca dari socket read berdasarkan connected client, kalau keluar kasih pesan out.
226 | $socketData = @socket_read($newClientsResource, 1024, PHP_NORMAL_READ);
227 |
228 | // False berarti keluar tunnel.
229 | if ($socketData === false)
230 | {
231 | // Beri pesan keluar.
232 | socket_getpeername($newClientsResource, $ip);
233 | $this->send("Client dengan ip {$ip} baru saja keluar");
234 |
235 | echo "Client dengan ip {$ip} baru saja keluar \n";
236 |
237 | // Hapus current index dari connected client.
238 | $index = array_search($newClientsResource, $this->clients);
239 | unset($this->clients[$index]);
240 | }
241 | }
242 | }
243 |
244 | socket_close($this->server);
245 | }
246 | }
--------------------------------------------------------------------------------