protected function connect()
{
// Init state
$connectFailures = 0;
$tcpRetry = $this->tcpBackoff / 2;
$httpRetry = $this->httpBackoff / 2;
// Keep trying until connected (or max connect failures exceeded)
do {
// Check filter predicates for every connect (for filter method)
if ($this->method == self::METHOD_FILTER) {
$this->checkFilterPredicates();
}
// Construct URL/HTTP bits
$url = $this->URL_BASE . $this->method . '.' . $this->format;
$urlParts = parse_url($url);
// Setup params appropriately
$requestParams = array();
//$requestParams['delimited'] = 'length'; //No, we don't want this any more
// Setup the language of the stream
if ($this->lang) {
$requestParams['language'] = $this->lang;
}
// Filter takes additional parameters
if (($this->method == self::METHOD_FILTER || $this->method == self::METHOD_USER) && count($this->trackWords) > 0) {
$requestParams['track'] = implode(',', $this->trackWords);
}
if (($this->method == self::METHOD_FILTER || $this->method == self::METHOD_SITE) && count($this->followIds) > 0) {
$requestParams['follow'] = implode(',', $this->followIds);
}
if ($this->method == self::METHOD_FILTER && count($this->locationBoxes) > 0) {
$requestParams['locations'] = implode(',', $this->locationBoxes);
}
if ($this->count != 0) {
$requestParams['count'] = $this->count;
}
// Debugging is useful
$this->log('Connecting to twitter stream: ' . $url . ' with params: ' . str_replace("\n", '', var_export($requestParams, TRUE)));
/**
* Open socket connection to make POST request. It'd be nice to use stream_context_create with the native
* HTTP transport but it hides/abstracts too many required bits (like HTTP error responses).
*/
$errNo = $errStr = NULL;
$scheme = $urlParts['scheme'] == 'https' ? 'ssl://' : 'tcp://';
$port = $urlParts['scheme'] == 'https' ? $this->secureHostPort : $this->hostPort;
$this->log("Connecting to {$scheme}{$urlParts['host']}, port={$port}, connectTimeout={$this->connectTimeout}");
@($this->conn = fsockopen($scheme . $urlParts['host'], $port, $errNo, $errStr, $this->connectTimeout));
// No go - handle errors/backoff
if (!$this->conn || !is_resource($this->conn)) {
$this->lastErrorMsg = $errStr;
$this->lastErrorNo = $errNo;
$connectFailures++;
if ($connectFailures > $this->connectFailuresMax) {
$msg = 'TCP failure limit exceeded with ' . $connectFailures . ' failures. Last error: ' . $errStr;
$this->log($msg, 'error');
throw new PhirehoseConnectLimitExceeded($msg, $errNo);
// Throw an exception for other code to handle
}
// Increase retry/backoff up to max
$tcpRetry = $tcpRetry < $this->tcpBackoffMax ? $tcpRetry * 2 : $this->tcpBackoffMax;
$this->log('TCP failure ' . $connectFailures . ' of ' . $this->connectFailuresMax . ' connecting to stream: ' . $errStr . ' (' . $errNo . '). Sleeping for ' . $tcpRetry . ' seconds.', 'info');
sleep($tcpRetry);
continue;
}
// TCP connect OK, clear last error (if present)
$this->log('Connection established to ' . $urlParts['host']);
$this->lastErrorMsg = NULL;
$this->lastErrorNo = NULL;
// If we have a socket connection, we can attempt a HTTP request - Ensure blocking read for the moment
stream_set_blocking($this->conn, 1);
// Encode request data
$postData = http_build_query($requestParams, NULL, '&');
$postData = str_replace('+', '%20', $postData);
//Change it from RFC1738 to RFC3986 (see
//enc_type parameter in http://php.net/http_build_query and note that enc_type is
//not available as of php 5.3)
$authCredentials = $this->getAuthorizationHeader($url, $requestParams);
// Do it
$s = "POST " . $urlParts['path'] . " HTTP/1.1\r\n";
$s .= "Host: " . $urlParts['host'] . ':' . $port . "\r\n";
$s .= "Connection: Close\r\n";
$s .= "Content-type: application/x-www-form-urlencoded\r\n";
$s .= "Content-length: " . strlen($postData) . "\r\n";
$s .= "Accept: */*\r\n";
$s .= 'Authorization: ' . $authCredentials . "\r\n";
$s .= 'User-Agent: ' . $this->userAgent . "\r\n";
$s .= "\r\n";
$s .= $postData . "\r\n";
$s .= "\r\n";
fwrite($this->conn, $s);
$this->log($s);
// First line is response
list($httpVer, $httpCode, $httpMessage) = preg_split('/\\s+/', trim(fgets($this->conn, 1024)), 3);
// Response buffers
$respHeaders = $respBody = '';
$isChunking = false;
// Consume each header response line until we get to body
while ($hLine = trim(fgets($this->conn, 4096))) {
$respHeaders .= $hLine . "\n";
if (strtolower($hLine) == 'transfer-encoding: chunked') {
$isChunking = true;
}
}
// If we got a non-200 response, we need to backoff and retry
if ($httpCode != 200) {
$connectFailures++;
// Twitter will disconnect on error, but we want to consume the rest of the response body (which is useful)
//TODO: this might be chunked too? In which case this contains some bad characters??
while ($bLine = trim(fgets($this->conn, 4096))) {
$respBody .= $bLine;
}
// Construct error
$errStr = 'HTTP ERROR ' . $httpCode . ': ' . $httpMessage . ' (' . $respBody . ')';
// Set last error state
$this->lastErrorMsg = $errStr;
$this->lastErrorNo = $httpCode;
// Have we exceeded maximum failures?
if ($connectFailures > $this->connectFailuresMax) {
$msg = 'Connection failure limit exceeded with ' . $connectFailures . ' failures. Last error: ' . $errStr;
$this->log($msg, 'error');
throw new PhirehoseConnectLimitExceeded($msg, $httpCode);
// We eventually throw an exception for other code to handle
}
// Increase retry/backoff up to max
$httpRetry = $httpRetry < $this->httpBackoffMax ? $httpRetry * 2 : $this->httpBackoffMax;
$this->log('HTTP failure ' . $connectFailures . ' of ' . $this->connectFailuresMax . ' connecting to stream: ' . $errStr . '. Sleeping for ' . $httpRetry . ' seconds.', 'info');
sleep($httpRetry);
continue;
} else {
if (!$isChunking) {
throw new Exception("Twitter did not send a chunking header. Is this really HTTP/1.1? Here are headers:\n{$respHeaders}");
}
//TODO: rather crude!
}
// Loop until connected OK
} while (!is_resource($this->conn) || $httpCode != 200);
// Connected OK, reset connect failures
$connectFailures = 0;
$this->lastErrorMsg = NULL;
$this->lastErrorNo = NULL;
// Switch to non-blocking to consume the stream (important)
stream_set_blocking($this->conn, 0);
// Connect always causes the filterChanged status to be cleared
$this->filterChanged = FALSE;
// Flush stream buffer & (re)assign fdrPool (for reconnect)
$this->fdrPool = array($this->conn);
$this->buff = '';
}