Skip to content

Instantly share code, notes, and snippets.

@tsprates
Last active December 25, 2023 17:29
Show Gist options
  • Save tsprates/06569f870c566ae118641b69ec5f81cd to your computer and use it in GitHub Desktop.
Save tsprates/06569f870c566ae118641b69ec5f81cd to your computer and use it in GitHub Desktop.
Simple TCP Chat in plain PHP
<?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