HTTPリクエストするためのクラス書いてみた
Posted: Updated:
HTTPリクエスト再び
Twitter API用のライブラリがどうとか言いながら、全然公開してませんな。
スピンアウト品による、HTTPリクエストについてメモしとくです。この後にOAuthかな。そしてTwitter, Flickr, Google, YahooあたりのAPIと遊ぶ方法とか。一通り試した後なので、暇を見つけながら、内容をまとめて自分メモ的にブログ書くつもり。a-blog cmsと連携したTwitter BOTぐらいはちゃんと作って公開すっか。
APIを叩く用に
<?php
$http = new SimpleHttp();
$http->get('http://havelog.ayumusato.com');
$res = $http->header;
$body = $http->body;
if ( !($http->error) ) {
return true;
} else {
return false;
}
?>
GET!POST!で適当にリクエスト投げれて、レスポンスヘッダーもレスポンスボディも、一通り取ってこれるようにしたかったのです。API相手しか考えてないので、POSTメソッドもクエリーのやり取りしか考えられてません。
file_get_contentsとかだとレスポンスコード200以外のときのレスポンスボディを端折りやがるので、ソケット接続から始めてリクエストを書き込んで受け取って、と地味な作業を繰り返してます。地味ですけど、HTTPの仕様の勉強になります。裏でこういうやり取りされてたんですね。
ハードウェアやソフトウェアの深淵を見るには、文系の自分には叶いません。でも、たまーに少し深いところを垣間見ると、なんとなく、してやったりという気分になれます。ああ現金。
覚え書きリスト
- リクエスト時の改行コードはCR+LFで鉄板くさい
- 末尾の改行コードが足りないとシカトされることすらある
- ステータスコード204と304はレスポンスボディがない(仕様的に)
- HTTP1.1のchunkedデータは、送ってきたり送ってこなかったりする
- chunked受け取りたくなかったら1.0としてリクエスト
- phpのparse_url関数はステキ(regex書き終わった頃に教えてもらった)
- httpsはssl://に変換して、portも合わせて変える
おまけに普通なソースコード
<?php
class SimpleHttp
{
private $_connection;
private $_header;
private $version = '1.1';
private $maxlen = 2048;
private $blocking = true;
private $timeout = 60;
private $eol = "\r\n";
public $header;
public $body;
public function __construct()
{
$this->_initialize();
}
public function __destruct()
{
$this->disconnect();
}
public function __toString()
{
return $this->host;
}
/**
* Set Default Configuration
*/
private function _initialize()
{
$this->scheme = 'http';
$this->host = 'localhost';
$this->port = 80;
$this->sslport = 443;
$this->user = '';
$this->pass = '';
$this->path = '';
$this->query = '';
$this->fragment = '';
$this->_header = array(
'Accept' => '',
'Accept-Charset' => '',
'Accept-Language' => '',
'Accept-Encoding' => 'gzip',
'Allow' => '',
'Authorization' => '',
'Cache-Control' => '',
'Connection' => 'close',
'Content-Language' => '',
'Content-Length' => '',
'Content-Type' => '',
'Expect' => '',
'Host' => '',
'If-Modified-Since' => '',
'Max-Forwards' => '',
'Range' => '',
'Referer' => '',
'User-Agent' => 'PHP/'.phpversion(),
'WWW-Authenticate' => '',
);
$this->header = null;
$this->body = null;
$this->error = true;
}
private function _parseUrl($url)
{
$parsed = parse_url($url);
foreach ( $parsed as $key => $val ) {
$this->$key = $val;
}
}
private function _initConnection($url)
{
if ( !is_resource($this->_connection) )
{
$this->_initialize();
$this->connect($url);
}
elseif ( $this->host != parse_url($url, PHP_URL_HOST) )
{
$this->_initialize();
$this->connect($url);
}
}
/**
* HTTP Protocol Methods
*/
public function get($url)
{
$this->method = 'GET';
$this->_request($url);
}
public function post($url)
{
$this->method = 'POST';
$this->_request($url);
}
private function _request($url)
{
$this->_initConnection($url);
fwrite($this->_connection, $this->buildRequest());
$this->getResponse();
if ( 200 == $this->header['Status-Code']['code'] )
$this->error = false;
else
$this->error = true;
}
/**
* Response Resources Methods
*/
public function getResponse()
{
$eol = array("\r", "\n", '\r\n');
$regex = '/^\s?HTTP\/([0-9].[0-9x])\s+([0-9]{3})\s+([0-9a-zA-z\s]*)$/';
while ( '' !== ($line = str_replace($eol, '', fgets($this->_connection))) ) {
if ( strpos($line, ':') === false && preg_match($regex, $line, $match) )
{
$this->header['Status-Code'] = array(
'version' => $match[1],
'code' => $match[2],
'status' => $match[3],
);
}
else
{
list($key, $val) = explode(':', $line, 2);
$this->header[$key] = ltrim($val);
}
}
$code = $this->header['Status-Code']['code'];
if ( $code >= 200 && $code != 204 && $code != 304 )
{
$this->body = stream_get_contents($this->_connection);
if ( @$this->header['Transfer-Encoding'] == 'chunked' )
{
$this->body = $this->_chunkdecode($this->body);
}
if ( @$this->header['Content-Encoding'] == 'gzip' )
{
$this->body = $this->_gzdecode($this->body);
}
}
}
public function getStatusCode()
{
return $this->header['Status-Code'];
}
private function _gzdecode($data)
{
$data = "data:application/x-gzip;base64,".base64_encode($data);
$fp = gzopen($data, "r");
return gzread($fp, 524288);
}
private function _chunkdecode ($str, $eol = "\r\n")
{
$tmp = $str;
$add = strlen($eol);
$str = '';
do {
$tmp = ltrim($tmp);
$pos = strpos($tmp, $eol);
$len = hexdec(substr($tmp, 0, $pos));
$str .= substr($tmp, ($pos + $add), $len);
$tmp = substr($tmp, ($len + $pos + $add));
$check = trim($tmp);
} while ( !empty($check) );
return $str;
}
/**
* Scoket Connection Methods
*/
public function connect($url)
{
$this->_parseUrl($url);
$scheme = ($this->scheme == 'https') ? 'ssl://' : '';
$port = ($this->scheme == 'https') ? $this->sslport : $this->port;
$this->_connection = fsockopen($scheme.$this->host, $port, $errno, $errstr, $this->timeout);
}
public function disconnect()
{
if ( is_resource($this->_connection) )
@fclose($this->_connection);
else
$this->_connection = null;
}
/**
* Header and Context Manipulating Methods
*/
public function setHeader($key, $val)
{
$this->_header[$key] = $val;
}
public function buildRequest()
{
$eol = $this->eol;
$header = array_merge(array_diff($this->_header, array('')));
// Host
$header['Host'] = "{$this->host}";
// Auth
if ( !empty($this->user) && !empty($this->pass) )
{
$header['Authorization'] = 'Basic '.base64_encode("{$this->user}:{$this->pass}");
}
// Build
switch ( $this->method )
{
case 'POST' :
$request = "{$this->method} {$this->path} HTTP/{$this->version}{$eol}";
$header['Content-Type'] = 'application/x-www-form-urlencoded';
$header['Content-Length'] = strlen($this->query);
break;
case 'GET' :
$request = "{$this->method} {$this->path}?{$this->query} HTTP/{$this->version}{$eol}";
break;
}
/* header */
foreach ( $header as $key => $val ) {
$request .= "{$key}: {$val}{$eol}";
}
/* body? */
if ( $this->method == 'POST' && !empty($this->query) )
{
$data = $this->query;
$request .= "{$eol}{$data}";
}
/* important CRLF */
$request .= $eol;
return $request;
}
}