├── 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 |
49 |
50 |
51 |
52 |
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 | ![Sample](https://i.ibb.co/PGgH8vy/screenshot-ibb-co-2020-05-14-19-17-38.png) 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 |
6 |
7 |
8 |
9 |
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 | } --------------------------------------------------------------------------------