Last active
January 31, 2021 11:55
-
-
Save fabacab/65b474832b5ad48f24a1808d3b989bd3 to your computer and use it in GitHub Desktop.
Pairing interview code sample. Use `php server.php` to start the server, then access http://localhost:4000/set?somekey=someval as per pairing interview instructions.
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 | |
/** | |
* Adds HTTP protocol support to the server. | |
* | |
* @package RecurseCenter\Interivew\SocketServer | |
*/ | |
require_once dirname(__FILE__) . '/interface-rcinterview-reader.php'; | |
/** | |
* HTTP protocol reader. | |
*/ | |
class HTTP_Reader implements RCInterviewProtocolReader { | |
/** | |
* The HTTP verb. | |
* | |
* @var string | |
*/ | |
private $method = ''; | |
/** | |
* The requested URI. | |
* | |
* @var string | |
*/ | |
private $request_uri = ''; | |
/** | |
* The HTTP protocol version. | |
* | |
* @var string | |
*/ | |
private $proto_version = ''; | |
/** | |
* The HTTP request headers. | |
* | |
* @var string[] | |
*/ | |
private $request_headers = array(); | |
/** | |
* Body of the HTTP request. | |
* | |
* @var string | |
*/ | |
private $request_body = ''; | |
/** | |
* Any parameters sent in the request. | |
* | |
* @var string[] | |
*/ | |
private $request_params = array(); | |
/** | |
* Constructor. | |
* | |
* @param string $request Raw request string. | |
*/ | |
public function __construct ($request = '') { | |
$this->parse($request); | |
} | |
// Simple getters. | |
public function getMethod () { | |
return $this->method; | |
} | |
public function getRequestURI () { | |
return $this->request_uri; | |
} | |
public function getProtoVersion () { | |
return $this->proto_version; | |
} | |
public function getRequestHeaders () { | |
return $this->request_headers; | |
} | |
public function getRequestBody () { | |
return $this->request_body; | |
} | |
public function getRequestParams () { | |
return $this->request_params; | |
} | |
public function getCommand () { | |
return ltrim(parse_url($this->request_uri, PHP_URL_PATH), '/'); | |
} | |
public function getDatakeys () { | |
return $this->request_params; | |
} | |
/** | |
* Parses the HTTP request and initializes instance variables. | |
* | |
* @param string $request | |
*/ | |
private function parse ($request) { | |
$parts = explode("\r\n\r\n", $request, 2); | |
if (!empty($parts[1])) { | |
$http_body = $parts[1]; | |
} | |
$http_head = $parts[0]; | |
$this->parseHead($http_head); // sets $this->request_headers | |
$q = parse_url($this->request_uri, PHP_URL_QUERY); | |
if (!empty($q)) { | |
parse_str($q, $this->request_params); | |
} | |
// TODO Parse body more fully. | |
$this->request_body = $http_body; | |
} | |
/** | |
* Parses an HTTP request head section into headers, etc. | |
* | |
* @param string $http_head | |
* | |
* @return bool | |
*/ | |
private function parseHead ($http_head) { | |
$lines = explode("\r\n", $http_head); | |
list($this->method, $this->request_uri, $this->proto_version) = array_map('trim', explode(' ', array_shift($lines))); | |
foreach ($lines as $line) { | |
list($hdr, $val) = array_map('trim', explode(':', $line, 2)); | |
$this->request_headers[$hdr] = $val; | |
} | |
} | |
} |
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 | |
/** | |
* A simple database server that uses HTTP to set and get values. Written for | |
* my Recurse Center interview. | |
* | |
* @package RecurseCenter\Interivew\SocketServer | |
*/ | |
/** | |
* Server class implementing the socket connections. | |
*/ | |
class SocketServer { | |
/** | |
* Server configuration. | |
* | |
* @var array $config | |
* @see self::getServerDefaults() | |
*/ | |
private $config = array(); | |
/** | |
* The server's stream socket. | |
* | |
* @var resource $server | |
*/ | |
private $server; | |
/** | |
* The incoming request. | |
* | |
* @var string $request | |
*/ | |
private $request = ''; | |
/** | |
* A protocol reader object, or false if we don't have one. | |
* | |
* @var mixed | |
*/ | |
private $reader = false; | |
/** | |
* An in-memory key=value store "database." | |
* | |
* @TODO | |
* During your interview, you will pair on saving the data to a file. You can | |
* start with simply appending each write to the file, and work on making it more | |
* efficient if you have time. | |
* | |
* @var string[] | |
*/ | |
private $database = array(); | |
/** | |
* Constructor. | |
* | |
* @param string $config_file Path to an `.json` configuration file. | |
*/ | |
public function __construct ($config_file = '') { | |
if (!empty($config_file) && is_readable($config_file)) { | |
$this->config = json_decode($config_file, true); | |
} else { | |
$this->config = $this->getServerDefaults(); | |
} | |
} | |
/** | |
* Starts the server by binding to a socket. | |
* | |
* @throws UnexpectedValueException | |
*/ | |
private function start () { | |
$c = $this->config; | |
$this->server = stream_socket_server( | |
"{$c['protocol']}://{$c['contexts']['socket']['bindto']}", | |
$errno, | |
$errormsg, | |
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, | |
stream_context_create($c['contexts']) | |
); | |
if (false === $this->server) { | |
throw new UnexpectedValueException("Could not bind to socket: $errormsg"); | |
} | |
} | |
/** | |
* Runs the server. | |
*/ | |
public function run () { | |
$this->start(); | |
while (true) { | |
$stream = stream_socket_accept($this->server, $this->config['timeout']); | |
if ($stream) { | |
$client_name = stream_socket_get_name($stream, true); | |
fwrite(STDOUT, "Accepted connection from $client_name" . PHP_EOL); | |
stream_set_timeout($stream, $this->config['timeout']); | |
$this->request = stream_get_contents($stream); | |
$reader = $this->getProtocolReader( | |
$this->detectClientProtocol($this->request) | |
); | |
switch ($reader->getCommand()) { | |
case 'set': | |
foreach ($reader->getDatakeys() as $k => $v) { | |
$this->database[$k] = $v; | |
fwrite($stream, "Saved $v at $k" . PHP_EOL); | |
} | |
break; | |
case 'get': | |
default: | |
foreach ($reader->getDatakeys() as $k => $v) { | |
// TODO: Respond with the correct protocol? | |
fwrite($stream, $this->database[$k] . PHP_EOL); | |
} | |
break; | |
} | |
stream_socket_shutdown($stream, STREAM_SHUT_RDWR); | |
fclose($stream); // disconnect client on EOF, too | |
fwrite(STDOUT, "Disconnected client $client_name" . PHP_EOL); | |
} | |
} | |
} | |
/** | |
* Takes a stab at guessing what the client is speaking to us. | |
* | |
* @param string $request The full incoming request. | |
* | |
* @return string | |
*/ | |
private function detectClientProtocol ($request) { | |
$protocol = false; | |
// Detect HTTP/1 and HTTP/1.1 protocol request. | |
$lines = explode("\r\n", $request, 2); | |
if (preg_match('/HTTP\/1(?:\.1)?$/i', $lines[0])) { | |
$protocol = 'http'; | |
} | |
return $protocol; | |
} | |
/** | |
* Loads a protocol reader and returns it. | |
* | |
* @throws UnexpectedValueException | |
* | |
* @return mixed A protocol reader of the given type. | |
*/ | |
private function getProtocolReader ($proto) { | |
$allowed_protocols = array( | |
'http' | |
); | |
if (in_array($proto, $allowed_protocols)) { | |
require_once "class-$proto-reader.php"; | |
$class = strtoupper($proto) . '_Reader'; | |
} else { | |
throw new UnexpectedValueException(); | |
} | |
return new $class($this->request); | |
} | |
/** | |
* Defaults for the server. | |
*/ | |
private function getServerDefaults () { | |
return array( | |
'protocol' => 'tcp', | |
'timeout' => 2, | |
'contexts' => array( | |
'socket' => array( | |
'bindto' => 'localhost:4000' | |
) | |
) | |
); | |
} | |
} |
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 | |
/** | |
* Interface for a protocol reader. | |
* | |
* @package RecurseCenter\Interivew\SocketServer | |
*/ | |
interface RCInterviewProtocolReader { | |
/** | |
* Gets the command sent by the client. | |
* | |
* @return string | |
*/ | |
public function getCommand (); | |
/** | |
* Gets the key for the requested data. | |
* | |
* @return string[] | |
*/ | |
public function getDatakeys (); | |
} |
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 | |
/** | |
* Runs the server. | |
* | |
* Before your interview, write a program that runs a server that is accessible | |
* on http://localhost:4000/. When your server receives a request on | |
* http://localhost:4000/set?somekey=somevalue it should store the passed key and | |
* value in memory. When it receives a request on | |
* http://localhost:4000/get?key=somekey it should return the value stored at | |
* somekey. | |
* | |
* @package RecurseCenter\Interivew\SocketServer | |
*/ | |
require_once dirname(__FILE__) . '/class-socket-server.php'; | |
//require_once dirname(__FILE__) . '/class-database.php'; | |
$server = new SocketServer(); | |
$server->run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment