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; } }