Skip to content

Instantly share code, notes, and snippets.

@fabacab
Last active January 31, 2021 11:55
Show Gist options
  • Save fabacab/65b474832b5ad48f24a1808d3b989bd3 to your computer and use it in GitHub Desktop.
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.
<?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;
}
}
}
<?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'
)
)
);
}
}
<?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 ();
}
<?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