Last active
December 25, 2023 17:29
-
-
Save tsprates/06569f870c566ae118641b69ec5f81cd to your computer and use it in GitHub Desktop.
Simple TCP Chat in plain PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
/** | |
* Simple TCP Chat. | |
* | |
* @author Thiago Prates <tsprates@gmail.com> | |
*/ | |
final class SimpleTcpChat | |
{ | |
private const MICROSECONDS_REFRESH = 50000; | |
private const EXIT_COMMAND = '/exit'; | |
private $host; | |
private $port; | |
private $server; | |
private $connections; | |
private $cache; | |
public function __construct(string $host, int $port) | |
{ | |
$this->host = $host; | |
$this->port = $port; | |
$this->connections = []; | |
$this->cache = []; | |
$this->create(); | |
} | |
public function __destruct() | |
{ | |
if (is_resource($this->server)) { | |
fclose($this->server); | |
} | |
} | |
public function run(): void | |
{ | |
$this->addConnection($this->server); | |
for (;;) { | |
$reads = $this->connections; | |
if (stream_select($reads, $writes, $excepts, 0, self::MICROSECONDS_REFRESH) !== false) { | |
foreach ($reads as $index => $connection) { | |
if ($this->isServer($index)) { | |
$this->handleNewConnection(); | |
continue; | |
} | |
if ($this->isLeaving($connection, $index)) { | |
$this->broadcastLeaveChat($connection); | |
$this->removeConnectionByIndex($index); | |
continue; | |
} | |
$data = fgets($connection); | |
$this->putCache($index, $data); | |
if ($this->canSendMessage($data, $index)) { | |
$message = sprintf("%s said: %s", $this->getName($connection), $this->cache($index)); | |
$this->broadcast($message); | |
$this->removeCache($index); | |
} | |
} | |
} | |
} | |
} | |
private function create(): void | |
{ | |
print "Running server on port $this->port" . PHP_EOL; | |
$address = sprintf("tcp://%s:%d", $this->host, $this->port); | |
$this->server = @stream_socket_server($address, $errorCode, $errorMessage); | |
if (!is_resource($this->server)) { | |
throw new InvalidArgumentException($errorMessage); | |
} | |
stream_set_blocking($this->server, false); | |
} | |
private function handleNewConnection(): void | |
{ | |
if (($newConnection = @stream_socket_accept($this->server, 0, $peerName)) !== false) { | |
stream_set_blocking($newConnection, false); | |
$this->addConnection($newConnection); | |
$this->broadcastWelcome($peerName); | |
} | |
} | |
private function isLeaving($connection, int $index): bool | |
{ | |
return $this->isClosed($connection) || $this->hasRequestToExit($index); | |
} | |
private function canSendMessage(string $data, int $index): bool | |
{ | |
return $this->isEnterCode($data) && !$this->isEmptyCache($index); | |
} | |
private function addConnection($connection): void | |
{ | |
$this->connections[(int)$connection] = $connection; | |
} | |
private function removeConnectionByIndex(int $index): void | |
{ | |
if (isset($this->connections[$index])) { | |
fclose($this->connections[$index]); | |
unset($this->connections[$index]); | |
} | |
} | |
private function isServer(int $index): bool | |
{ | |
return $index === (int)$this->server; | |
} | |
private function broadcastWelcome(string $peerName): void | |
{ | |
$message = sprintf('Welcome: %s', $peerName); | |
$this->broadcast($message); | |
} | |
private function broadcast(string $message): void | |
{ | |
print $message . PHP_EOL; | |
foreach ($this->connections as $index => $connection) { | |
if (!$this->isServer($index)) { | |
fwrite($connection, trim($message) . PHP_EOL); | |
} | |
} | |
} | |
private function isClosed($connection): bool | |
{ | |
return feof($connection); | |
} | |
private function hasRequestToExit(int $index): bool | |
{ | |
return !empty($this->cache[$index]) && strtolower(trim($this->cache[$index])) === self::EXIT_COMMAND; | |
} | |
private function broadcastLeaveChat($connection): void | |
{ | |
$message = sprintf("%s has left.", $this->getName($connection)); | |
$this->broadcast($message); | |
} | |
private function getName($connection) | |
{ | |
return stream_socket_get_name($connection, true); | |
} | |
private function putCache(int $index, string $data): void | |
{ | |
$this->cache[$index] = $this->cache($index) . $data; | |
} | |
private function isEnterCode(string $str): bool | |
{ | |
return $str === PHP_EOL; | |
} | |
private function isEmptyCache(int $index): bool | |
{ | |
return empty(trim($this->cache($index))); | |
} | |
private function cache(int $index): string | |
{ | |
return $this->cache[$index] ?? ''; | |
} | |
private function removeCache(int $index): void | |
{ | |
unset($this->cache[$index]); | |
} | |
} | |
// Example: How to use | |
$chat = new SimpleTcpChat('0.0.0.0', 9000); | |
$chat->run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment