├── .gitignore ├── .gitmodules ├── 1-blocking-io └── server.php ├── 2-echo-server ├── .gitignore ├── composer.json └── server.php ├── 3-broadcasting ├── .gitignore ├── composer.json └── server.php ├── 4-parsing ├── .gitignore ├── composer.json └── server.php ├── 5-multiple-instances ├── .gitignore ├── composer.json └── server.php └── docs ├── .gitignore ├── Gemfile ├── _config.yml ├── asset ├── index.html └── tcp-chat ├── basic-echo-server.md ├── broadcasting.md ├── index.md ├── multiple-instances.md └── parsing.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/.shared"] 2 | path = docs/.shared 3 | url = https://github.com/amphp/amphp.github.io 4 | -------------------------------------------------------------------------------- /1-blocking-io/server.php: -------------------------------------------------------------------------------- 1 | read()) { 20 | // Yielding a write() waits until the data is fully written to the OS's internal buffer, it might not have 21 | // been received by the client when the promise returned from write() returns. 22 | yield $socket->write($chunk); 23 | } 24 | }; 25 | 26 | // listen() is a small wrapper around stream_socket_server() returning a Server object 27 | $server = Amp\Socket\Server::listen($uri); 28 | 29 | // Like in the previous example, we accept each client as soon as we can 30 | // Server::accept() returns a promise. The coroutine will be interrupted and continued once the promise resolves. 31 | while ($socket = yield $server->accept()) { 32 | // Call $clientHandler without returning a promise. If an error happens in the callback, it will be passed to 33 | // the global error handler, we don't have to care about it here. 34 | asyncCall($clientHandler, $socket); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /3-broadcasting/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /3-broadcasting/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amphp/getting-started-echo-server", 3 | "description": "A TCP echo server based on amphp/socket.", 4 | "type": "project", 5 | "require": { 6 | "amphp/socket": "^1" 7 | }, 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Niklas Keller", 12 | "email": "me@kelunik.com" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /3-broadcasting/server.php: -------------------------------------------------------------------------------- 1 | $client 16 | private $clients = []; 17 | 18 | public function listen() { 19 | asyncCall(function () { 20 | $server = Amp\Socket\Server::listen($this->uri); 21 | 22 | print "Listening on " . $server->getAddress() . " ..." . PHP_EOL; 23 | 24 | while ($socket = yield $server->accept()) { 25 | $this->handleClient($socket); 26 | } 27 | }); 28 | } 29 | 30 | private function handleClient(Socket $socket) { 31 | asyncCall(function () use ($socket) { 32 | $remoteAddr = $socket->getRemoteAddress(); 33 | 34 | // We print a message on the server and send a message to each client 35 | print "Accepted new client: {$remoteAddr}". PHP_EOL; 36 | $this->broadcast($remoteAddr . " joined the chat." . PHP_EOL); 37 | 38 | // We only insert the client afterwards, so it doesn't get its own join message 39 | $this->clients[(string) $remoteAddr] = $socket; 40 | 41 | while (null !== $chunk = yield $socket->read()) { 42 | $this->broadcast($remoteAddr . " says: " . trim($chunk) . PHP_EOL); 43 | } 44 | 45 | // We remove the client again once it disconnected. 46 | // It's important, otherwise we'll leak memory. 47 | unset($this->clients[(string) $remoteAddr]); 48 | 49 | // Inform other clients that that client disconnected and also print it in the server. 50 | print "Client disconnected: {$remoteAddr}" . PHP_EOL; 51 | $this->broadcast($remoteAddr . " left the chat." . PHP_EOL); 52 | }); 53 | } 54 | 55 | private function broadcast(string $message) { 56 | foreach ($this->clients as $client) { 57 | // We don't yield the promise returned from $client->write() here as we don't care about 58 | // other clients disconnecting and thus the write failing. 59 | $client->write($message); 60 | } 61 | } 62 | }; 63 | 64 | $server->listen(); 65 | }); 66 | -------------------------------------------------------------------------------- /4-parsing/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /4-parsing/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amphp/getting-started-echo-server", 3 | "description": "A TCP echo server based on amphp/socket.", 4 | "type": "project", 5 | "require": { 6 | "amphp/socket": "^1" 7 | }, 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Niklas Keller", 12 | "email": "me@kelunik.com" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /4-parsing/server.php: -------------------------------------------------------------------------------- 1 | $client 16 | private $clients = []; 17 | 18 | // Store a $clientAddr => $username map 19 | private $usernames = []; 20 | 21 | public function listen() { 22 | asyncCall(function () { 23 | $server = Amp\Socket\Server::listen($this->uri); 24 | 25 | print "Listening on " . $server->getAddress() . " ..." . PHP_EOL; 26 | 27 | while ($socket = yield $server->accept()) { 28 | $this->handleClient($socket); 29 | } 30 | }); 31 | } 32 | 33 | private function handleClient(Socket $socket) { 34 | asyncCall(function () use ($socket) { 35 | $remoteAddr = $socket->getRemoteAddress(); 36 | 37 | // We print a message on the server and send a message to each client 38 | print "Accepted new client: {$remoteAddr}". PHP_EOL; 39 | $this->broadcast($remoteAddr . " joined the chat." . PHP_EOL); 40 | 41 | // We only insert the client afterwards, so it doesn't get its own join message 42 | $this->clients[(string) $remoteAddr] = $socket; 43 | 44 | $buffer = ""; 45 | 46 | while (null !== $chunk = yield $socket->read()) { 47 | $buffer .= $chunk; 48 | 49 | while (($pos = strpos($buffer, "\n")) !== false) { 50 | $this->handleMessage($socket, substr($buffer, 0, $pos)); 51 | $buffer = substr($buffer, $pos + 1); 52 | } 53 | } 54 | 55 | // We remove the client again once it disconnected. 56 | // It's important, otherwise we'll leak memory. 57 | // We also have to unset our new usernames. 58 | unset($this->clients[(string) $remoteAddr], $this->usernames[(string) $remoteAddr]); 59 | 60 | // Inform other clients that that client disconnected and also print it in the server. 61 | print "Client disconnected: {$remoteAddr}" . PHP_EOL; 62 | $this->broadcast(($this->usernames[(string) $remoteAddr] ?? $remoteAddr) . " left the chat." . PHP_EOL); 63 | }); 64 | } 65 | 66 | private function handleMessage(Socket $socket, string $message) { 67 | if ($message === "") { 68 | // ignore all empty messages 69 | return; 70 | } 71 | 72 | if ($message[0] === "/") { 73 | // message is a command 74 | $message = substr($message, 1); // remove slash 75 | $args = explode(" ", $message); // parse message into parts separated by space 76 | $name = strtolower(array_shift($args)); // the first arg is our command name 77 | 78 | switch ($name) { 79 | case "time": 80 | $socket->write(date("l jS \of F Y h:i:s A") . PHP_EOL); 81 | break; 82 | 83 | case "up": 84 | $socket->write(strtoupper(implode(" ", $args)) . PHP_EOL); 85 | break; 86 | 87 | case "down": 88 | $socket->write(strtolower(implode(" ", $args)) . PHP_EOL); 89 | break; 90 | 91 | case "exit": 92 | $socket->end("Bye." . PHP_EOL); 93 | break; 94 | 95 | case "nick": 96 | $nick = implode(" ", $args); 97 | 98 | if (!preg_match("(^[a-z0-9-.]{3,15}$)i", $nick)) { 99 | $error = "Username must only contain letters, digits and " . 100 | "its length must be between 3 and 15 characters."; 101 | $socket->write($error . PHP_EOL); 102 | return; 103 | } 104 | 105 | $remoteAddr = $socket->getRemoteAddress(); 106 | $oldnick = $this->usernames[(string) $remoteAddr] ?? $remoteAddr; 107 | $this->usernames[(string) $remoteAddr] = $nick; 108 | 109 | $this->broadcast($oldnick . " is now " . $nick . PHP_EOL); 110 | break; 111 | 112 | default: 113 | $socket->write("Unknown command: {$name}" . PHP_EOL); 114 | break; 115 | } 116 | 117 | return; 118 | } 119 | 120 | $remoteAddr = $socket->getRemoteAddress(); 121 | $user = $this->usernames[(string) $remoteAddr] ?? $remoteAddr; 122 | $this->broadcast($user . " says: " . $message . PHP_EOL); 123 | } 124 | 125 | private function broadcast(string $message) { 126 | foreach ($this->clients as $client) { 127 | // We don't yield the promise returned from $client->write() here as we don't care about 128 | // other clients disconnecting and thus the write failing. 129 | $client->write($message); 130 | } 131 | } 132 | }; 133 | 134 | $server->listen(); 135 | }); 136 | -------------------------------------------------------------------------------- /5-multiple-instances/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /5-multiple-instances/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amphp/getting-started-echo-server", 3 | "description": "A TCP echo server based on amphp/socket.", 4 | "type": "project", 5 | "require": { 6 | "amphp/socket": "^1", 7 | "amphp/redis": "^1" 8 | }, 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Niklas Keller", 13 | "email": "me@kelunik.com" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /5-multiple-instances/server.php: -------------------------------------------------------------------------------- 1 | $client 25 | private $clients = []; 26 | 27 | // Store a $clientAddr => $username map 28 | private $usernames = []; 29 | 30 | /** @var Redis */ 31 | private $redisClient; 32 | 33 | public function listen() { 34 | asyncCall(function () { 35 | $remoteExecutor = new RemoteExecutor(Config::fromUri($this->redisHost)); 36 | $this->redisClient = new Redis($remoteExecutor); 37 | 38 | $server = Amp\Socket\Server::listen($this->uri); 39 | $this->listenToRedis(); 40 | 41 | print "Listening on " . $server->getAddress() . " ..." . PHP_EOL; 42 | 43 | while ($socket = yield $server->accept()) { 44 | $this->handleClient($socket); 45 | } 46 | }); 47 | } 48 | 49 | private function handleClient(Socket $socket) { 50 | asyncCall(function () use ($socket) { 51 | $remoteAddr = $socket->getRemoteAddress(); 52 | 53 | // We print a message on the server and send a message to each client 54 | print "Accepted new client: {$remoteAddr}". PHP_EOL; 55 | yield $this->redisClient->publish("chat", $remoteAddr . " joined the chat." . PHP_EOL); 56 | 57 | // We only insert the client afterwards, so it doesn't get its own join message 58 | $this->clients[(string) $remoteAddr] = $socket; 59 | 60 | $buffer = ""; 61 | 62 | while (null !== $chunk = yield $socket->read()) { 63 | $buffer .= $chunk; 64 | 65 | while (($pos = strpos($buffer, "\n")) !== false) { 66 | $this->handleMessage($socket, substr($buffer, 0, $pos)); 67 | $buffer = substr($buffer, $pos + 1); 68 | } 69 | } 70 | 71 | // We remove the client again once it disconnected. 72 | // It's important, otherwise we'll leak memory. 73 | // We also have to unset our new usernames. 74 | unset($this->clients[(string) $remoteAddr], $this->usernames[(string) $remoteAddr]); 75 | 76 | // Inform other clients that that client disconnected and also print it in the server. 77 | print "Client disconnected: {$remoteAddr}" . PHP_EOL; 78 | $message = ($this->usernames[(string) $remoteAddr] ?? $remoteAddr) . " left the chat." . PHP_EOL; 79 | yield $this->redisClient->publish("chat", $message); 80 | }); 81 | } 82 | 83 | private function handleMessage(Socket $socket, string $message) { 84 | if ($message === "") { 85 | // ignore all empty messages 86 | return; 87 | } 88 | 89 | if ($message[0] === "/") { 90 | // message is a command 91 | $message = substr($message, 1); // remove slash 92 | $args = explode(" ", $message); // parse message into parts separated by space 93 | $name = strtolower(array_shift($args)); // the first arg is our command name 94 | 95 | switch ($name) { 96 | case "time": 97 | $socket->write(date("l jS \of F Y h:i:s A") . PHP_EOL); 98 | break; 99 | 100 | case "up": 101 | $socket->write(strtoupper(implode(" ", $args)) . PHP_EOL); 102 | break; 103 | 104 | case "down": 105 | $socket->write(strtolower(implode(" ", $args)) . PHP_EOL); 106 | break; 107 | 108 | case "exit": 109 | $socket->end("Bye." . PHP_EOL); 110 | break; 111 | 112 | case "nick": 113 | $nick = implode(" ", $args); 114 | 115 | if (!preg_match("(^[a-z0-9-.]{3,15}$)i", $nick)) { 116 | $error = "Username must only contain letters, digits and " . 117 | "its length must be between 3 and 15 characters."; 118 | $socket->write($error . PHP_EOL); 119 | return; 120 | } 121 | 122 | $remoteAddr = $socket->getRemoteAddress(); 123 | $oldnick = $this->usernames[(string) $remoteAddr] ?? $remoteAddr; 124 | $this->usernames[(string) $remoteAddr] = $nick; 125 | 126 | $this->redisClient->publish("chat", $oldnick . " is now " . $nick . PHP_EOL); 127 | break; 128 | 129 | default: 130 | $socket->write("Unknown command: {$name}" . PHP_EOL); 131 | break; 132 | } 133 | 134 | return; 135 | } 136 | 137 | $remoteAddr = $socket->getRemoteAddress(); 138 | $user = $this->usernames[(string) $remoteAddr] ?? $remoteAddr; 139 | $this->redisClient->publish("chat", $user . " says: " . $message . PHP_EOL); 140 | } 141 | 142 | private function broadcast(string $message) { 143 | foreach ($this->clients as $client) { 144 | // We don't yield the promise returned from $client->write() here as we don't care about 145 | // other clients disconnecting and thus the write failing. 146 | $client->write($message); 147 | } 148 | } 149 | 150 | private function listenToRedis() { 151 | asyncCall(function () { 152 | $redisClient = new Subscriber(Config::fromUri($this->redisHost)); 153 | 154 | do { 155 | try { 156 | /** @var Subscription $subscription */ 157 | $subscription = yield $redisClient->subscribe("chat"); 158 | 159 | while (yield $subscription->advance()) { 160 | $message = $subscription->getCurrent(); 161 | $this->broadcast($message); 162 | } 163 | } catch (RedisException $e) { 164 | // reconnect in case the connection breaks, wait a second before doing so 165 | yield new Delayed(1000); 166 | } 167 | } while (true); 168 | }); 169 | } 170 | }; 171 | 172 | $server->listen(); 173 | }); 174 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | _site 3 | vendor 4 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "github-pages" 3 | gem "kramdown" 4 | gem "jekyll-github-metadata" 5 | gem "jekyll-relative-links" 6 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | kramdown: 2 | input: GFM 3 | toc_levels: 2..3 4 | 5 | baseurl: "/getting-started" 6 | layouts_dir: ".shared/layout" 7 | includes_dir: ".shared/includes" 8 | 9 | exclude: ["Gemfile", "Gemfile.lock", "README.md", "vendor"] 10 | safe: true 11 | 12 | repository: amphp/getting-started 13 | gems: 14 | - "jekyll-github-metadata" 15 | - "jekyll-relative-links" 16 | 17 | defaults: 18 | - scope: 19 | path: "" 20 | type: "pages" 21 | values: 22 | layout: "docs" 23 | 24 | shared_asset_path: "/getting-started/asset" 25 | -------------------------------------------------------------------------------- /docs/asset: -------------------------------------------------------------------------------- 1 | .shared/asset -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | permalink: / 4 | layout: base 5 | --- 6 |
13 | Want to build something to get started? Go through a series of posts to learn building a small TCP chat server without prior knowledge. 14 |
15 |20 | Want to know some details before writing code? Then start by digging into our documentation. 21 |
22 |29 | Chat with us! It's best to just ask the questions you have. Our support page mentions multiple support channels. 30 |
31 |