分类目录归档:HTTP

Stunnel构建加密隧道

最新版本可到:http://www.stunnel.org/downloads.html下载,Stunnel工作过程就是在客户端和服务端之间建立一条加密通道,所有Stunnel有两个工作模式,默认工作模式是服务端,如果需要作为客户端,那么就是需要使用client=yes来指定。客户端加密数据需要一个证书(可以自签发),服务端需要私钥,以解密数据,这个工作过程就是HTTPS的工作流程。

一般使用yum或者apt-get安装即可。

##############################################
###
apt-get update
apt-get install openssl

###
apt-get install stunnel

#开机启动,ENABLED改为1
vi /etc/default/stunnel4
ENABLED=1

#
cd /etc/stunnel
vi stunnel.conf

[squid]
accept = 4041 
connect = 127.0.0.1:4040
cert = /etc/stunnel/stunnel.pem

#产生证书 私钥 - 实际就是产生一对RSA秘钥,公钥在证书中
openssl genrsa -out key.pem 2048
openssl req -new -x509 -key key.pem -out cert.pem -days 3650
cat key.pem cert.pem >> /etc/stunnel/stunnel.pem

#启动
/etc/init.d/stunnel4 restart


##############################################
## 客户端
client = yes
[squid]
accept = 127.0.0.1:4444
connect = [服务端IP]:4041
cert = D:/stunnel.pem

添加监控脚本:

cat stunnel.sh 
#!/bin/bash

live=`ps -efH | grep '/usr/bin/stunnel' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/stunnel 2>&1 >> /dev/null
fi

Windows下下载一个GUI工具:
stunnel

默认Stunnel以服务器为工作模式,所以在作为客户端时务必添加client = yes。

对于客户端,可以开启多个端口,分别指向多个代理:

client = yes
cert= E:/var/stunnel.pem

[fq1]
accept = 4441
connect = 47.90.x.43:4041

[fq2]
accept = 4442
connect = 47.90.x.152:4041

[fq3]
accept = 4443
connect = 47.90.x.140:4041

[fq4]
accept = 4444
connect = 47.90.x.215:4041

HTTP代理 之 Squid与Node.js

实现HTTP代理有两种方式:
一种是普通代理,代理服务器充当的是一个中间的组件。这种方式简单来说就是接受来自客户端的HTTP报文(原本不应该发送给代理服务器的,强制发送给代理服务器),然后在服务器端做一些必要修改再转发出去(可能涉及via头,请求uri,来源ip等)。这种方式一般认为不能代理HTTPS流量。Nginx、Squid支持这种代理转发。

另一种是TCP隧道代理。这种代理可以以HTTP协议的方式来实现理论上任意TCP之上的应用层协议代理(可以代理HTTPS)。Squid支持这种方式,而Nginx不支持这种方式。

在Node.js中,实现这两种代理方式,只需数行代码。


普通代理(正向代理)

###Nginx HTTP正向代理示例
server {
    resolver 8.8.8.8;
    resolver_timeout 5s;
 
    listen x.x.x.x:8080;
 
    location / {
        proxy_pass $scheme://$host$request_uri;
        proxy_set_header Host $http_host;
    }
}

###Squid HTTP正向代理示例
acl localnet src 0.0.0.0/0

acl SSL_ports port 443
acl Safe_ports port 80

acl CONNECT method CONNECT

http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

http_access allow localnet
http_access allow localhost
http_access deny all

http_port 8080

#举例
# 1 原始HTTP报文
GET /action?b=123 HTTP/1.1
Host: blog.ifeeline.com
注:通过浏览器的代理后,GET /action?b=123 HTTP/1.1会被变成GET http://blog.ifeeline.com/action?b=123 HTTP/1.1

# 2 这个报文原样转发给x.x.x.x:8080,原本应该是直接发给blog.ifeeline.com的(Host字段)

# 3 根据HOST DNS到IP,把这个Http报文转发出去(如必要可做必要修改,比如添加请求头)

在Node.js中,这种方式的代理转发,实现非常简单:

var http = require('http');
var url = require('url');

function request(req, res) {
    var u = url.parse(req.url);
	
    //var options = {
	//  hostname : u.hostname, 
	//  port     : u.port || 80,
	//  path     : u.path,       
	//  method   : req.method,
	//  headers  : req.headers
    //};

    // 继续发给其它代理(比如Squid Nginx)  
    var options = {
        hostname : "120.xx.xx.192", 
        port     : 3333,
        path     : 'http://'+u.host+u.path,       
        method   : req.method,
        headers  : req.headers
    };

    var ireq = http.request(options, function(ires) {
        res.writeHead(ires.statusCode, ires.headers);
        ires.pipe(res);
    }).on('error', function(e) {
        res.end();
    });

    req.pipe(ireq);
}

http.createServer().on('request', request).listen(8080, '0.0.0.0');

另一种代理方式是隧道代理,目前应用非常的广泛,实际上原来非常的简单:HTTP客户端(浏览器)通过CONNECT方法请求代理服务器创建一条到达目的服务器和端口的TCP连接,然后把HTTP客户端(浏览器)传输过来的数据通过这个TCP连接直接转发。

很明显,相比之下,多了一次来回(CONNECT请求)。而后续的操作就是把数据从一个链接转到另一个链接,仅仅是转发TCP数据,实际就是把前面的输出作为后面的输入,如此而已。

Nginx不支持CONNECT请求,自然就无法支持隧道代理了。Squid支持隧道代理。在Node.js中用数行代码就可以实现隧道代理:

var http = require('http');
var net = require('net');
var url = require('url');

function connect(req, sock) {
    var u = url.parse('http://' + req.url);

    var insock = net.connect(u.port, u.hostname, function() {
        sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
	// 回传客户端
        insock.pipe(sock);
    }).on('error', function(e) {
        sock.end();
    });
    // 客户端传送数据
    sock.pipe(insock);
}

http.createServer().on('connect', connect).listen(8080, '0.0.0.0');

代理接收到connect方法请求后,建立一条到目标服务器的TCP链接,然后向客户端回送确认,客户端得到确认后继续使用sock把数据发送过来,代理服务器把从sock过来的数据推送到目标服务器。

一个基本事实:
浏览器设置HTTP代理时,如果是HTTP链接,则把HTTP报文直接发送到代理服务器。如果是HTTPS链接,则采用以上描述的隧道方式,先发送一个connet请求,然后发送数据。大体意思是能用普通代理的就不用隧道代理,毕竟隧道代理多了一个来回。(并不是HTTP链接不能走隧道代理)

把以上两段Node.js代码合并一起就可以实现全功能的HTTP代理:

var http = require('http');
var net = require('net');
var url = require('url');

function request(req, res) {
    var u = url.parse(req.url);
	var options = {
	    hostname : u.hostname, 
	    port     : u.port || 80,
	    path     : u.path,       
	    method   : req.method,
	    headers  : req.headers
	};
    };

    var ireq = http.request(options, function(ires) {
        res.writeHead(ires.statusCode, ires.headers);
        ires.pipe(res);
    }).on('error', function(e) {
        res.end();
    });

    req.pipe(ireq);
}

function connect(req, sock) {
    var u = url.parse('http://' + req.url);

    var insock = net.connect(u.port, u.hostname, function() {
        sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
	// 回传客户端
        insock.pipe(sock);
    }).on('error', function(e) {
        sock.end();
    });
    // 客户端传送数据
    sock.pipe(insock);
}

http.createServer().on('request', request).on('connect', connect).listen(8080, '0.0.0.0');

对于HTTPS链接的代理,从客户端到代理服务器,如果走的是HTTP,实际也不会增加安全问题,除了connect链接暴露的目标服务器和端口号外,后续的数据是加密的,中间人不可能解密。

如果确实担心这个问题,可以签发自签名的证书。 让客户端到代理之间也走HTTPS:

openssl genrsa -out root.pem 2048
openssl req -new -x509 -key root.pem -out root.crt -days 99999

注意:Common Name务必填写代理服务器IP。

把root.crt下载下来添加到系统受信任根证书列表中,完成这个步骤后从浏览器和代理,代理和目标服务器之间就可以建立安全链接了(虽然过程有伪造)。

对于以上Node.js的代码,只需要修改一点点:

var options = {
    key: fs.readFileSync('./root.pem'),
    cert: fs.readFileSync('./root.crt')
};

https.createServer(options).on();

添加根证书,http改为https而已。

—————————————————————-
以下是一个Squid代理配置实例:

via off
forwarded_for transparent
dns_nameservers 8.8.4.4 8.8.8.8

## IP白名单
acl localnet src 120.x.x.x
## IP白名单_公司内网
acl localnet src x.x.0.0/16
acl localnet src x.x.0.0/16
## IP白名单_本地网络
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
acl localnet src 192.168.0.0/16

## SSL端口
acl SSL_ports port 443

## 安全端口
acl Safe_ports port 80		# http
acl Safe_ports port 21		# ftp
acl Safe_ports port 443		# https
acl Safe_ports port 70		# gopher
acl Safe_ports port 210		# wais
acl Safe_ports port 1025-65535	# unregistered ports
acl Safe_ports port 280		# http-mgmt
acl Safe_ports port 488		# gss-http
acl Safe_ports port 591		# filemaker
acl Safe_ports port 777		# multiling http

## 启用HTTP隧道代理 
acl CONNECT method CONNECT

## 非安全端口
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

## 运行IP白名单
http_access allow localnet
http_access allow localhost
http_access deny all

## 端口监听
http_port 4040

coredump_dir /var/spool/squid3

refresh_pattern ^ftp:		1440	20%	10080
refresh_pattern ^gopher:	1440	0%	1440
refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
refresh_pattern .		0	20%	4320

参数via off和forwarded_for transparent是建立透明代理的基础。目标机器就无法完全感知是被代理了。

由于内容直接使用HTTP转发,不管原始链接是HTTP还是HTTPS,目标IP与域都是暴露的,所以一旦被过滤,就会出现连接被重置的提示,所以,为了避免过滤,需要做加密。默认安装的Squid并不支持开启https端口,所以需要重新编译。可以使用Stunnel来建立加密隧道,然后由Stunnel解密后转发给Squid,这个做法很常见。

Node.js Http实例

配置文件

module.exports = {
	server: '127.0.0.1',
	port: 9999,
	uas: [
		'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0',
		'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
		'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) Gecko/20100101 Firefox/43.0',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
		'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
		'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) DumpRenderTree/0.0.0.0 Safari/536.11',
		'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8) Presto/2.10.289 Version/12.00',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
		'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36'
	]
}

以下是主文件:

var config = require('./config');

var http = require('http');
var https = require('https');
var querystring = require('querystring');
var url = require('url');
var zlib = require('zlib');
var cheerio = require('cheerio');

// 如果没有输出则list为空数组
var parseHtml = function(html, itemid) {
	var $ = cheerio.load(html);

	var trs = $("table:contains('Date of Purchase')").last().find("tr");

	var tr = trs.first();
	var hasVar = false;
	if(trs.first().find("th:contains('Variation')").length > 0) {
		hasVar = true;
	}

	var data = [];
	trs.each(function(){
		var row = $(this);
		var colmns = row.find("td");

		if(hasVar) {
			var variation = colmns.eq(2).text().replace(/^\s+|\s+$/,'');
			var price = colmns.eq(3).text().replace(/^\s+|\s+$/,'');
			var qty = colmns.eq(4).text().replace(/^\s+|\s+$/,'');
			var datetime = colmns.eq(5).text().replace(/^\s+|\s+$/,'');
		} else {
			var variation = '-';
			var price = colmns.eq(2).text().replace(/^\s+|\s+$/,'');
			var qty = colmns.eq(3).text().replace(/^\s+|\s+$/,'');
			var datetime = colmns.eq(4).text().replace(/^\s+|\s+$/,'');
		}
        if(!price || (price === 'Price')) {
            return true;
        }
		//console.log(variation + "|" + price.replace(/\s+/, ' ')+"|"+qty+"|"+datetime);
		data.push({'variation': variation, 'price': price, 'qty': qty, 'date_purchase': datetime});
	});

	return {'itemid': itemid, 'list': data};
};

var showMem = function () {
        var mem = process.memoryUsage();
        var format = function (bytes) {
            return (bytes / 1024 / 1024).toFixed(2) + ' MB';
        };

        console.log('Process: heapTotal ' + format(mem.heapTotal) + ' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
        console.log('-----------------------------------------------------------');
};

var server = http.createServer(function (req, res) {
	//showMem();

	/*
	这里的req是一个IncomingMessage对象,实现了Readable Stream接口(https://nodejs.org/dist/latest-v4.x/docs/api/stream.html#stream_class_stream_readable)

	对于客户端请求服务端,客户端的请求进来的数据是IncomingMessage,在服务端对外发起请求(组装数据),远程的响应类似于远程对服务端发起请求,那么这些响应信息就是一个IncomingMessage。

	从客户端读请求数据,还是从服务端读响应数据,都封装为对Readable Stream的读取。

	console.log(req.headers);
	{ 
		host: '127.0.0.1:9999',
		connection: 'keep-alive',
		'cache-control': 'max-age=0',
		accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*\/*;q=0.8',
		'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/
		'accept-encoding': 'gzip,deflate,sdch',
		'accept-language': 'zh-CN,zh;q=0.8,en;q=0.6' 
	}
	console.log(req.httpVersion);		// http版本
	console.log(req.method);			// 请求方法,GET POST PUT
	console.log(req.rawHeaders);		// 原始头,headers中是去掉了重复头,这里是原始头
	console.log(req.rawTrailers);
	console.log(req.statusCode);		// http.ClientRequest.
	console.log(req.statusMessage);		// http.ClientRequest.
	console.log(req.url);				// http.Server.
	*/

	// 第二参数解析查询字符串
	var ur = url.parse(req.url, true);
	if(typeof ur.query.u !== 'undefined') {
		ur.query.u = req.url.replace(/[^=]+u\=/i, '');
	}
	/*
	{
	    protocol: null,
	    slashes: null,
	    auth: null,
	    host: null,
	    port: null,
	    hostname: null,
	    hash: null,
	    search: '?u=xxxx',
	    query: { u: 'xxxx' },
	    pathname: '/craw',
	    path: '/craw?u=xxxx',
	    href: '/craw?u=xxxx'
	}
	*/
	// 是否在线
    if(ur.pathname === '/ping') {
		res.write("online");
		res.end();
		return;
    }   

	// 只处理/craw
    if(ur.pathname !== '/craw') {
		res.statusCode = 404;
		res.statusMessage = 'Not found';
		res.end();
		return;
    }

	var ghost = '';
	var gpath = '/';
	var protocol = 'http';
	var itemid = '';
	// 传递了u参数,u=http://offer.xxx.com/ws/g.php?a=xxx
	if(typeof ur.query.u !== 'undefined') {
		var m = ur.query.u.match(/item=([0-9]+)/);
		if(typeof m[1] !== 'undefined') {
			itemid = m[1];
		} else {
			res.statusCode = 404;
			res.statusMessage = 'Not found';
			res.end();
			return;
		}

		var furl = url.parse(ur.query.u);

		if(furl.protocol && furl.host) {
			if(furl.protocol === 'https:') {
				protocol = 'https';
			}
			ghost = furl.host;
			gpath = ur.query.u;
		} else {
			res.statusCode = 404;
			res.statusMessage = 'Not found';
			res.end();
			return;
		}
	} else if(typeof ur.query.item !== 'undefined') {
		itemid = ur.query.item;
		var ghost = 'offer.ebay.com';
		var gpath = '/ws/eBayISAPI.dll?ViewBidsLogin&item='+ur.query.item+'&rt=nc&_trksid=p2047675.l2564';
	} else {
		res.statusCode = 404;
		res.statusMessage = 'Not found';
		res.end();
		return;
	}

	// 随机UA
	var ua = '';
	var uidx = Math.floor(Math.random() * config.uas.length + 1)-1;
	if((uidx >= 0) && (typeof config.uas[uidx] !== 'undefined')) {
		ua = config.uas[uidx];	
	}
	if(ua === '') {
		ua = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0';
	}
	//console.log(ua);

	// 发送POST时需要提交的数据
	//var postData = querystring.stringify({
	//	'msg' : 'xxxx'
	//});

	// 这里的options对应HTTP的请求行和请求头信息(请求的cookie是请求头的组成部分)
	// 请求体的设置是调用clientRequest对象的write方法(可写流)
	var options = {
			host: ghost,
			path: gpath,
			method: 'GET',
			headers:{
				'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
				'Accept-Encoding': 'gzip,deflate',
				'Accept-Language': 'en-US,en;q=0.8',
				'Cache-Control': 'max-age=0',
				'Connection': 'keep-alive',
				//'Referer': '',
				'User-Agent': ua
			}
	};
	var q = http;
	if(protocol === 'https') {
		options['port'] = 443;
		q = https;
	}

	//var html = '';
	// 内部对外请求
	// reqc是一个clientRequest对象,它是一个可写流
	// 而resc是一个IncomeMessage对象,它是一个可读流
	var reqc = q.request(options, function(resc){
		//resc.setEncoding('utf-8');
		var chunks = [];
		// 数据可能分块返回
		resc.on('data', function(data){
			//html += data;
			chunks.push(data);
		});

		// 数据返回结束
		// 发起请求时发送了gzip,deflate,服务器端返回压缩数据
		// 由于压缩,网络传输时间明显下降
		resc.on('end', function(){
			// 传递状态码,比如302,404等
			res.statusCode = resc.statusCode;
			var buffer = Buffer.concat(chunks);
			var encoding = resc.headers['content-encoding'];
			if (encoding == 'gzip') {
				zlib.gunzip(buffer, function(err, decoded) {
					if (!err) {
						var json = parseHtml(decoded.toString(), itemid);
						var rej = {"success": 1, "message": '', "data": json};
					} else {
						var rej = {"success": 0, "message": '', "data": {}};
					}
					res.setHeader('Content-Type', 'application/json;charset=UTF-8'); 
					res.write(JSON.stringify(rej));
					res.end();
				});
			} else if (encoding == 'deflate') {
				zlib.inflate(buffer, function(err, decoded) {
					if (!err) {
						var json = parseHtml(decoded.toString(), itemid);
						var rej = {"success": 1, "message": '', "data": json};
					} else {
						var rej = {"success": 0, "message": '', "data": {}};
					}
					res.setHeader('Content-Type', 'application/json;charset=UTF-8');
					res.write(JSON.stringify(rej));
					res.end();
				});
			} else {
				if (!err) {
					var json = parseHtml(decoded.toString(), itemid);
					var rej = {"success": 1, "message": '', "data": json};
				} else {
					var rej = {"success": 0, "message": '', "data": {}};
				}
				res.setHeader('Content-Type', 'application/json;charset=UTF-8');
				res.write(JSON.stringify(rej));
				res.end();
			}

			chunks = null;
		});
	});

	// 设置超时,当超时时执行回调,终止请求
	reqc.setTimeout(10000, function(t) {
		var timeout = (new Date()).toLocaleString() + " Timeout";
		console.log(timeout);

		// 触发error事件 -> socket hang up
		reqc.abort();
	});
	// 发生错误
	reqc.on('error', function(e) {
		var err = (new Date()).toLocaleString() + " Error: "+e.message;
		console.log(err);
		//res.write('error');
		res.statusCode = 599;
		res.statusMessage = err;
		res.end();
	});
	// 发送数据
	//reqc.write(postData);

	//
	reqc.end();

}).listen(config.port, config.server);

/////////////////////////////////////
server.timeout = 20000;
server.on('listening', function() {
    var msg = (new Date()).toLocaleString() + " Server Listening";
	console.log(msg);
});
server.on('close', function() {
	var msg = (new Date()).toLocaleString() + " Server Closed";
	console.log(msg);
});

这个例子中,HTTP模块多数功能多已经使用到。类比其它语言,并不简单多少(甚至更加复杂),JS语法也不见得有何优势。实际上,之所以选择Node.js来做Http代理,原因只有一个,部署非常简单。

PHP GuzzleHttp使用文档

内容来自:http://docs.guzzlephp.org/en/latest/,这个Http库实现上可以用完美来形容,之前使用过Zend\Http库,感觉已经很不错了,不过看起来,GuzzleHttp更胜一筹。

Guzzlehttp/guzzle当前最新分支6.x,5.x是维护阶段,4.x之前已经终结。

6.x需要PHP5.5以上,依赖guzzlehttp下的psr7和promises包。它提供两种驱动支持,一是PHP的stream扩展,需要在php.ini中启用allow_url_fopen,二是cURL扩展。如果cURL没有安装,那么就使用PHP的stream扩展,也可以指定自己的驱动。

Composer安装:

 {
   "require": {
      "guzzlehttp/guzzle": "~6.0"
   }
}

开始开始:

use GuzzleHttp\Client;
// 客户端
$client = new Client([
    'base_uri' => 'https://foo.com/api/',
    'timeout'  => 2.0,
]);
// 发起请求
$response = $client->request('GET', 'test');
$response = $client->request('GET', '/root');

看明白这里的请求URL是关键,第一个发起的URL是https://foo.com/api/test,第二发起的URL是https://foo.com/root。(RFC 3986规范)

客户端Client构造函数接受的参数是base_uri,handler和任何请求参数(可以传递到request对象的参数)。这里的参数除了handler,都是可以覆盖的。handler参数的解释:
“(callable) Function that transfers HTTP requests over thewire. The function is called with a Psr7\Http\Message\RequestInterface and array of transfer options, and must return a GuzzleHttp\Promise\PromiseInterface that is fulfilled with a Psr7\Http\Message\ResponseInterface on success. “handler” is a constructor only option that cannot be overridden in per/request options. If no handler is provided, a default handler will be created that enables all of the request options below by attaching all of the default middleware to the handler.”

要理解这段话并不容易。大体上是说这个handler被Psr7\Http\Message\RequestInterface对象调用返回一个被Psr7\Http\Message\ResponseInterface填充的GuzzleHttp\Promise\PromiseInterface。一般我们理解应该是返回Response,这里返回一个Promise,引入了一个中间层,实际是为了可以产生异步调用而准备的。Promise可以是同步的,也可以是异步的。

看这个例子:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;

$handler = new CurlHandler();
$stack = HandlerStack::create($handler); // Wrap w/ middleware
$client = new Client(['handler' => $stack]);

可见,hanlder就是底层实际传输http内容的工具,比如curl,stream。(默认的hanlder就是优先使用curl,如果要强制使用curl,可以参考这个例子),可以给halder添加中间件。

发起请求:

$response = $client->get('http://httpbin.org/get');
$response = $client->delete('http://httpbin.org/delete');
$response = $client->head('http://httpbin.org/get');
$response = $client->options('http://httpbin.org/get');
$response = $client->patch('http://httpbin.org/patch');
$response = $client->post('http://httpbin.org/post');
$response = $client->put('http://httpbin.org/put');

#替代
$response = $client->request('GET',"");

也可以先创建一个请求对象,然后通过Client的send的方法发起请求:

use GuzzleHttp\Psr7\Request;

$request = new Request('PUT', 'http://httpbin.org/put');
$response = $client->send($request, ['timeout' => 2]);

对应,可以使用sendAsync()和requestAsync()发起异步请求:

use GuzzleHttp\Psr7\Request;

// Create a PSR-7 request object to send
$headers = ['X-Foo' => 'Bar'];
$body = 'Hello!';
$request = new Request('HEAD', 'http://httpbin.org/head', $headers, $body);

// Or, if you don't need to pass in a request instance:
$promise = $client->requestAsync('GET', 'http://httpbin.org/get');

如果是异步请求,可以使用then方法接收响应,或者异常:

use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;

$promise = $client->requestAsync('GET', 'http://httpbin.org/get');
$promise->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
);

有了异步的实现,那么就可以并行发起一批请求:

use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client(['base_uri' => 'http://httpbin.org/']);

// Initiate each request but do not block
$promises = [
    'image' => $client->getAsync('/image'),
    'png'   => $client->getAsync('/image/png'),
    'jpeg'  => $client->getAsync('/image/jpeg'),
    'webp'  => $client->getAsync('/image/webp')
];

// Wait on all of the requests to complete. Throws a ConnectException
// if any of the requests fail
$results = Promise\unwrap($promises);

// Wait for the requests to complete, even if some of them fail
$results = Promise\settle($promises)->wait();

// You can access each result using the key provided to the unwrap
// function.
echo $results['image']->getHeader('Content-Length');
echo $results['png']->getHeader('Content-Length');

或者使用Pool来进行并发请求:

use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;

$client = new Client();

$requests = function ($total) {
    $uri = 'http://127.0.0.1:8126/guzzle-server/perf';
    for ($i = 0; $i < $total; $i++) {
        yield new Request('GET', $uri);
    }
};

$pool = new Pool($client, $requests(100), [
    'concurrency' => 5,
    'fulfilled' => function ($response, $index) {
        // this is delivered each successful response
    },
    'rejected' => function ($reason, $index) {
        // this is delivered each failed request
    },
]);

// Initiate the transfers and create a promise
$promise = $pool->promise();

// Force the pool of requests to complete.
$promise->wait();

使用响应:

$code = $response->getStatusCode(); // 200
$reason = $response->getReasonPhrase(); // OK

// Check if a header exists.
if ($response->hasHeader('Content-Length')) {
    echo "It exists";
}

// Get a header from the response.
echo $response->getHeader('Content-Length');

// Get all of the response headers.
foreach ($response->getHeaders() as $name => $values) {
    echo $name . ': ' . implode(', ', $values) . "\r\n";
}

$body = $response->getBody();
// Implicitly cast the body to a string and echo it
echo $body;
// Explicitly cast the body to a string
$stringBody = (string) $body;
// Read 10 bytes from the body
$tenBytes = $body->read(10);
// Read the remaining contents of the body as a string
$remainingBytes = $body->getContents();

查询参数:

$response = $client->request('GET', 'http://httpbin.org?foo=bar');

$client->request('GET', 'http://httpbin.org', [
    'query' => ['foo' => 'bar']
]);

$client->request('GET', 'http://httpbin.org', ['query' => 'foo=bar']);

上传数据(数据直接作为body体):

// Provide the body as a string.
$r = $client->request('POST', 'http://httpbin.org/post', [
    'body' => 'raw data'
]);

// Provide an fopen resource.
$body = fopen('/path/to/file', 'r');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// Use the stream_for() function to create a PSR-7 stream.
$body = \GuzzleHttp\Psr7\stream_for('hello!');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// 传送JSON数据
$r = $client->request('PUT', 'http://httpbin.org/put', [
    'json' => ['foo' => 'bar']
]);

POST表单请求(如果是GET的表单,就是简单的查询字符串,不是这里讨论的内容)

$response = $client->request('POST', 'http://httpbin.org/post', [
    'form_params' => [
        'field_name' => 'abc',
        'other_field' => '123',
        'nested_field' => [	// 嵌套,checkbox多选
            'nested' => 'hello'
        ]
    ]
]);

// 上传文件
$response = $client->request('POST', 'http://httpbin.org/post', [
    'multipart' => [
        [
            'name'     => 'field_name',
            'contents' => 'abc'
        ],
        [
            'name'     => 'file_name',
            'contents' => fopen('/path/to/file', 'r')
        ],
        [
            'name'     => 'other_file',
            'contents' => 'hello',
            'filename' => 'filename.txt',
            'headers'  => [
                'X-Foo' => 'this is an extra header to include'
            ]
        ]
    ]
]);

Cookies

// Use a specific cookie jar
$jar = new \GuzzleHttp\Cookie\CookieJar;
$r = $client->request('GET', 'http://httpbin.org/cookies', [
    'cookies' => $jar
]);

// 如果要对所有请求使用cookies,可以在Client指定使用cookies
// Use a shared client cookie jar
$client = new \GuzzleHttp\Client(['cookies' => true]);
$r = $client->request('GET', 'http://httpbin.org/cookies');

重定向(GET请求和POST请求的重定向需要注意)
Guzzle自动跟踪重定向,可以明确关闭:

$response = $client->request('GET', 'http://github.com', [
    'allow_redirects' => false
]);
echo $response->getStatusCode();
// 301

异常:

use GuzzleHttp\Exception\ClientException;

try {
    $client->request('GET', 'https://github.com/_abc_123_404');
} catch (ClientException $e) {
    echo $e->getRequest();
    echo $e->getResponse();
}

请求对象可选项:

##############
allow_redirects
默认:
[
    'max'             => 5,
    'strict'          => false,
    'referer'         => true,
    'protocols'       => ['http', 'https'],
    'track_redirects' => false
]

#指定false关闭
$res = $client->request('GET', '/redirect/3', ['allow_redirects' => false]);
echo $res->getStatusCode();

##############
auth
$client->request('GET', '/get', ['auth' => ['username', 'password']]);
$client->request('GET', '/get', [
    'auth' => ['username', 'password', 'digest']
]);

##############
body 请求体内容

##############
cert

##############
cookies 可以在Client设置cookie为true以设置所有请求使用cookie

$jar = new \GuzzleHttp\Cookie\CookieJar();
$client->request('GET', '/get', ['cookies' => $jar]);

##############
connect_timeout
默认为0,表示不超时(无限等待)

##############
debug 可以输出调试信息,或把调试信息写入文件(fopen)

##############
decode_content
默认为ture,表示解码服务端回送的压缩的内容

##############
delay

##############
expect

##############
form_params

##############
headers

$client->request('GET', '/get', [
    'headers' => [
        'User-Agent' => 'testing/1.0',
        'Accept'     => 'application/json',
        'X-Foo'      => ['Bar', 'Baz']
    ]
]);

##############
http_errors
默认为true,表示出错时抛出异常

##############
json

##############
multipart

##############
on_headers

##############
query
$client->request('GET', '/get?abc=123', ['query' => ['foo' => 'bar']]);

##############
sink
保存请求体
$resource = fopen('/path/to/file', 'w');
$client->request('GET', '/stream/20', ['sink' => $resource]);

$resource = fopen('/path/to/file', 'w');
$stream = GuzzleHttp\Psr7\stream_for($resource);
$client->request('GET', '/stream/20', ['save_to' => $stream]);

##############
verify
默认为true,验证SSL证书

##############
timeout
默认为0,不超时。(请求超时)

当使用curl时(默认优先使用,如果指定curl参数,最后可以明确指定使用curl作为hanlder,否则无效):

$client->request('GET', '/', [
    'curl' => [
        CURLOPT_INTERFACE => 'xxx.xxx.xxx.xxx'
    ]
]);

HTTP BASIC认证操作实践

HTTP BASIC认证是HTTP层次内容,以下使用PHP来实现这个过程:

vi basic.php

<?php
$users = [
    "admin" => "xxxxxx"
];
$needAuth = true;
if(!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
    // 用户名和密码
    $user = trim($_SERVER['PHP_AUTH_USER']);
    $pwd = trim($_SERVER['PHP_AUTH_PW']);

    if(isset($users[$user]) && ($users[$user] === $pwd)) {
        $needAuth = false;
    }
}

if($needAuth) {
    header("Content-type:text/html;charset=utf-8");
    header('WWW-Authenticate: Basic realm="admin xxxxxx"');
    header('HTTP/1.0 401 Unauthorized');
    echo date("Y-m-d H:i:s")." -> Need Auth...";
    exit;
}

echo date("Y-m-d H:i:s")." -> Auth...";

http_basic_auth
对于一个小应用程序,如果需要做隐藏保护,这个方式会非常便利。不过这个认证方式更多见于API访问中。

查看发送的HTTP请求头:
http_basic_header
用户名和密码通过HTTP的一个请求头Authorization来传输的,内容是Basic YWRtaW46eHh4eHh4,第一个字符串Basic为认证方式,第二个字符串是用户名和密码冒号分隔的字符串的base64编码(admin:xxxxxx -> YWRtaW46eHh4eHh4)。

这个用户名和密码传递到服务器端,对于Nginx(Apache等),它可以首先处理,也可以继续转发到PHP,让PHP来处理(这里就是这个情况)。PHP接收这两个变量使用:

$_SERVER['PHP_AUTH_USER']
$_SERVER['PHP_AUTH_PW']

这两个变量是经过了base64解码之后得到的,这个解码应该是HTTP服务进行的,把得到的变量传递给PHP。注意,这里的base64编码目的不是在加密,而是方便传输。所有如果直接通过HTTP传输是不安全的(其它的一般用户名密码登录也一样),所以,对于API设计,为了安全,一般通过HTTPS传送数据。

BASCIC认证是HTTP层次的内容,所以对于Nginx这样的HTTP服务器软件,当然可以配置其进行BASIC认证,这样就不需要由PHP来处理。Nginx配置参考:

server {
    listen       80;
    server_name  xx.xx.xx.xx;
    root /var/www/xxx/public;
    index index.php

    error_page 404 /index.php;
    
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/conf.d/htpasswd;

    if (!-e $request_filename) {
        rewrite ^/(.+)$ /index.php last;
        break;
    }

    location / {
	root /var/www/xxx/public;
	try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* ^.+\.(css|js|jpeg|jpg|gif|png|ico|eot|ttf|woff|svg) {
        expires 30d;
    }

    location ~* \.(eot|ttf|woff|svg|html)$ {
        add_header Access-Control-Allow-Origin *;
    }

    location ~ .(php|php5)?$ {
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 256 32k;

        fastcgi_pass   127.0.0.1:9000;
	fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
   }
}

主要是添加auth_basic指令和auth_basic_user_file指令,auth_basic和PHP中的如下两行设置类似:

header('WWW-Authenticate: Basic realm="admin xxxxxx"');
header('HTTP/1.0 401 Unauthorized');

指令auth_basic的值直接对应Basic realm=”xxx”中的xxx值,表示是BASIC认证,认证的用户名和密码是auth_basic_user_file指定的密码文件,这个文件中保存的用户名密码可不是用base64编码的,它使用的是Hash算法,格式:

vfeelit:CQArTEgiT84So:注释

匹配过程大体应该是这样:获取HTTP的Authorization请求头,从中获取经过base64编码的字符串,解码获取用户名和密码,然后匹配用户名,再通过Hash算法得到密码的Hash值,最后和保存的Hash值进行比较。

这个密码文件产生(htpasswd或者openssl):

# printf "vfeelit:$(openssl passwd -crypt 123456)\n" >> conf.d/htpasswd
# cat conf.d/htpasswd 
vfeelit:DmZ3GXV9zFegY

Zend Framework 2.x 之 Zend\Http

Zend\Http

Overview
Zend\Http is a primary foundational component(基础组件) of Zend Framework. Since much of what PHP does is web-based, specifically HTTP, it makes sense感觉 to have a performant, extensible, concise简约 and consistent连续的 API to do all things HTTP. In nutshell简言之, there are several parts of Zend\Http:
> Context-less Request and Response classes that expose a fluent流畅的 API for introspecting several aspects of HTTP messages:
*Request line information and response status information
*Parameters, such as those found in POST and GET
*Message Body
*Headers
>A Client implementation with various adapters that allow for sending requests and introspecting responses.

Zend\Http Request, Response and Headers
The Request, Response and Headers portion一部分 of the Zend\Http component provides a fluent, object-oriented interface for introspecting information from all the various parts of an HTTP request or HTTP response. The two main objects are Zend\Http\Request and Zend\Http\Response. These two classes are “context-less”, meaning that they model a request or response in the same way whether it is presented by a client (to send a request and receive a response) or by a server (to receive a request and send a response). In other words, regardless of the context, the API remains the same for introspecting their various respective parts. Each attempts to fully model a request or response so that a developer can create these objects from a factory, or create and populate them manually.

1 The Request Class
Overview
The Zend\Http\Request object is responsible尽责 for providing a fluent API that allows a developer to interact with all the various parts of an HTTP request.

A typical HTTP request looks like this:
————————–
| METHOD | URI | VERSION |
————————–
| HEADERS |
————————–
| BODY |
————————–
In simplified terms, the request consists of a method, URI and HTTP version number which together make up the “Request Line.” Next come the HTTP headers, of which there can be 0 or more. After that is the request body, which is typically used when a client wishes to send data to the server in the form of an encoded file, or include a set of POST parameters, for example. More information on the structure and specification of a HTTP request can be found in RFC-2616 on the W3.org site.(这里描述的是HTTP协议基本概念)

Quick Start
Request objects can either be created from the provided fromString() factory, or, if you wish to have a completely empty object to start with, by simply instantiating the Zend\Http\Request class.(初始请求对象,看例子就好)

use Zend\Http\Request;

$request = Request::fromString(<<<EOS
POST /foo HTTP/1.1
\r\n
HeaderField1: header-field-value1
HeaderField2: header-field-value2
\r\n\r\n
foo=bar&
EOS
);

// OR, the completely equivalent

$request = new Request();
$request->setMethod(Request::METHOD_POST);
$request->setUri('/foo');
$request->getHeaders()->addHeaders(array(
    'HeaderField1' => 'header-field-value1',
    'HeaderField2' => 'header-field-value2',
));
$request->getPost()->set('foo', 'bar');

Configuration Options
No configuration options are available.

Available Methods 有一些列方法
比如isXXX方法(isGet isPost isXmlHttpRequest),不列举,看例子。

Examples
Generating a Request object from a string

use Zend\Http\Request;

$string = "GET /foo HTTP/1.1\r\n\r\nSome Content";
$request = Request::fromString($string);

$request->getMethod();    // returns Request::METHOD_GET
$request->getUri();       // returns Zend\Uri\Http object
$request->getUriString(); // returns '/foo'
$request->getVersion();   // returns Request::VERSION_11 or '1.1'
$request->getContent();   // returns 'Some Content'

Retrieving and setting headers

use Zend\Http\Request;
use Zend\Http\Header\Cookie;

$request = new Request();
$request->getHeaders()->get('Content-Type'); // return content type
$request->getHeaders()->addHeader(new Cookie(array('foo' => 'bar')));
foreach ($request->getHeaders() as $header) {
    echo $header->getFieldName() . ' with value ' . $header->getFieldValue();
}

每个Header对应Zend\Http\Header\***实例,每个Header实际对应HTTP请求中的请求头,格式是name:value,要取回请求行(就是Header),需要分别调用具体Header实例的getFieldName()和getFieldValue()。每个HTTP请求的第一行是请求行,区别与请求头。

Retrieving and setting GET and POST values

use Zend\Http\Request;

$request = new Request();

// getPost() and getQuery() both return, by default, a Parameters object, which extends ArrayObject
$request->getPost()->foo = 'Foo value';
$request->getQuery()->bar = 'Bar value';
$request->getPost('foo'); // returns 'Foo value'
$request->getQuery()->offsetGet('bar'); // returns 'Bar value'

Generating a formatted HTTP Request from a Request object

use Zend\Http\Request;

$request = new Request();
$request->setMethod(Request::METHOD_POST);
$request->setUri('/foo');
$request->getHeaders()->addHeaders(array(
    'HeaderField1' => 'header-field-value1',
    'HeaderField2' => 'header-field-value2',
));
$request->getPost()->set('foo', 'bar');
$request->setContent($request->getPost()->toString());
echo $request->toString();

/** Will produce:
POST /foo HTTP/1.1
HeaderField1: header-field-value1
HeaderField2: header-field-value2

foo=bar
*/

这个例子展示了如何以面向对象的方式得到HTTP请求原始信息。(getPost()获取POST数据容器,getQuery()获取GET查询容器),当是POST时,需要显式调用setContent()设置请求体??

The Response Class
Overview
The Zend\Http\Response class is responsible for providing a fluent API that allows a developer to interact with all the various parts of an HTTP response.

A typical HTTP Response looks like this:
—————————
| VERSION | CODE | REASON |
—————————
| HEADERS |
—————————
| BODY |
—————————
The first line of the response consists of the HTTP version, status code, and the reason string for the provided status code; this is called the Response Line. Next is a set of headers; there can be 0 or an unlimited number of headers. The remainder of the response is the response body, which is typically a string of HTML that will render on the client’s browser, but which can also be a place for request/response payload data typical of an AJAX request. More information on the structure and specification of an HTTP response can be found in RFC-2616 on the W3.org site.

Quick Start

Response objects can either be created from the provided fromString() factory, or, if you wish to have a completely empty object to start with, by simply instantiating the Zend\Http\Response class.

use Zend\Http\Response;
$response = Response::fromString(<<<EOS
HTTP/1.0 200 OK
HeaderField1: header-field-value
HeaderField2: header-field-value2

<html>
<body>
    Hello World
</body>
</html>
EOS);

// OR

$response = new Response();
$response->setStatusCode(Response::STATUS_CODE_200);
$response->getHeaders()->addHeaders(array(
    'HeaderField1' => 'header-field-value',
    'HeaderField2' => 'header-field-value2',
));
$response->setContent(<<<EOS
<html>
<body>
    Hello World
</body>
</html>
EOS
);

Configuration Options
No configuration options are available.

Available Methods
一些列跟响应相关的方法。

Examples

Generating a Response object from a string

use Zend\Http\Response;
$request = Response::fromString(<<<EOS
HTTP/1.0 200 OK
HeaderField1: header-field-value
HeaderField2: header-field-value2

<html>
<body>
    Hello World
</body>
</html>
EOS);

Generating a formatted HTTP Response from a Response object

use Zend\Http\Response;
$response = new Response();
$response->setStatusCode(Response::STATUS_CODE_200);
$response->getHeaders()->addHeaders(array(
    'HeaderField1' => 'header-field-value',
    'HeaderField2' => 'header-field-value2',
));
$response->setContent(<<<EOS
<html>
<body>
    Hello World
</body>
</html>
EOS);

直接是设置操作相应对象的机会看起来比较少。一般是取其返回的内容。

he Headers Class

Overview
The Zend\Http\Headers class is a container for HTTP headers(HTTP头容器). It is typically accessed as part of a Zend\Http\Request or Zend\Http\Response getHeaders() call.(getHeaders()方法放回这个类型对象) The Headers container will lazily load actual Header objects as to reduce the overhead of header specific parsing.

The Zend\Http\Header\* classes are the domain specific implementations for the various types of Headers that one might encounter during the typical HTTP request. If a header of unknown type is encountered, it will be implemented as a Zend\Http\Header\GenericHeader instance. See the below table for a list of the various HTTP headers and the API that is specific to each header type.(头对应Zend\Http\Header\*类,如果一个无法定位类型的头信息,那么它就是Zend\Http\Header\GenericHeader实例)

Quick Start

The quickest way to get started interacting with header objects is by getting an already populated Headers container from a request or response object.(从请求或响应对象获取请求头容器)

// $client is an instance of Zend\Http\Client

// You can retrieve the request headers by first retrieving
// the Request object and then calling getHeaders on it
$requestHeaders  = $client->getRequest()->getHeaders();

// The same method also works for retrieving Response headers
$responseHeaders = $client->getResponse()->getHeaders();

一个客户端就是一个请求和响应。

Zend\Http\Headers can also extract headers from a string:

$headerString = <<<EOB
Host: www.example.com
Content-Type: text/html
Content-Length: 1337
EOB;

$headers = Zend\Http\Headers::fromString($headerString);
// $headers is now populated with three objects
//   (1) Zend\Http\Header\Host
//   (2) Zend\Http\Header\ContentType
//   (3) Zend\Http\Header\ContentLength

Now that you have an instance of Zend\Http\Headers you can manipulate the individual headers it contains using the provided public API methods outlined in the “Available Methods” section.

Configuration Options
No configuration options are available.

Available Methods
……

Zend\Http\Header\HeaderInterface Methods
Headers是一个容器,容器中的单元都实现这个接口。

Zend\Http\Header\AbstractAccept Methods

Zend\Http\Header\AbstractDate Methods

Zend\Http\Header\AbstractLocation Methods

List of HTTP Header Types

把几乎所有的头,都封装了一遍,实在令人感觉其追求何其高大上。

Examples
Retrieving headers from a Zend\Http\Headers object

// $client is an instance of Zend\Http\Client
$response = $client->send();
$headers = $response->getHeaders();

// We can check if the Request contains a specific header by
// using the ``has`` method. Returns boolean ``TRUE`` if at least
// one matching header found, and ``FALSE`` otherwise
$headers->has('Content-Type');

// We can retrieve all instances of a specific header by using
// the ``get`` method:
$contentTypeHeaders = $headers->get('Content-Type');

There are three possibilities for the return value of the above call to the get method:

*If no Content-Type header was set in the Request, get will return false.
*If only one Content-Type header was set in the Request, get will return an instance of Zend\Http\Header\ContentType.
*If more than one Content-Type header was set in the Request, get will return an ArrayIterator containing one Zend\Http\Header\ContentType instance per header.

Adding headers to a Zend\Http\Headers object

$headers = new Zend\Http\Headers();

// We can directly add any object that implements Zend\Http\Header\HeaderInterface
$typeHeader = Zend\Http\Header\ContentType::fromString('Content-Type: text/html');
$headers->addHeader($typeHeader);

// We can add headers using the raw string representation, either
// passing the header name and value as separate arguments...
$headers->addHeaderLine('Content-Type', 'text/html');

// .. or we can pass the entire header as the only argument
$headers->addHeaderLine('Content-Type: text/html');

// We can also add headers in bulk using addHeaders, which accepts
// an array of individual header definitions that can be in any of
// the accepted formats outlined below:
$headers->addHeaders(array(

    // An object implementing Zend\Http\Header\HeaderInterface
    Zend\Http\Header\ContentType::fromString('Content-Type: text/html'),

    // A raw header string
    'Content-Type: text/html',

    // We can also pass the header name as the array key and the
    // header content as that array key's value
    'Content-Type' => 'text/html');

));

这个例子展示了添加头的方法。这里展示的方法何其强大。

Removing headers from a Zend\Http\Headers object
We can remove all headers of a specific type using the removeHeader method, which accepts a single object implementing Zend\Http\Header\HeaderInterface

// $headers is a pre-configured instance of Zend\Http\Headers

// We can also delete individual headers or groups of headers
$matches = $headers->get('Content-Type');

// If more than one header was found, iterate over the collection
// and remove each one individually
if ($matches instanceof ArrayIterator) {
    foreach ($headers as $header) {
        $headers->removeHeader($header);
    }
// If only a single header was found, remove it directly
} elseif ($matches instanceof Zend\Http\Header\HeaderInterface) {
    $headers->removeHeader($header);
}

// In addition to this, we can clear all the headers currently stored in
// the container by calling the clearHeaders() method
$matches->clearHeaders();

以上内容为HTTP首层封装,相对还是比较原始。

HTTP Client
Overview
Zend\Http\Client provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests.(提供一个指向HTTP请求的容易的接口,注意是请求) Zend\Http\Client supports the most simple features expected from an HTTP client, as well as some more complex features such as HTTP authentication and file uploads(认证与文件上传). Successful requests (and most unsuccessful ones too) return a Zend\Http\Response object, which provides access to the response’s headers and body (see this section).(成功请求后返回一个response对象)

Quick Start
The class constructor optionally accepts a URL as its first parameter (can be either a string or a Zend\Uri\Http object), and an array or Zend\Config\Config object containing configuration options. The send() method is used to submit the request to the remote server, and a Zend\Http\Response object is returned:(看例子就好)

use Zend\Http\Client;

$client = new Client('http://example.org', array(
    'maxredirects' => 0,
    'timeout'      => 30
));
$response = $client->send();

Both constructor parameters can be left out, and set later using the setUri() and setConfig() methods:(构造函数可以空,然后调用对应方法)

use Zend\Http\Client;

$client = new Client();
$client->setUri('http://example.org');
$client->setOptions(array(
    'maxredirects' => 0,
    'timeout'      => 30
));
$response = $client->send();

Zend\Http\Client can also dispatch requests using a separately configured request object (see the Zend\Http\Request manual page for full details of the methods available):

use Zend\Http\Client;
use Zend\Http\Request;

$request = new Request();
$request->setUri('http://example.org');

$client = new Client();

$response = $client->send($request);

如果要更加详细的控制请求,自定义一个请求对象。客户端参数通过$client->setOptions()设置,比如最大重定向,超时等。

Note:
Zend\Http\Client uses Zend\Uri\Http to validate URLs. See the Zend\Uri manual page for more information on the validation process. 依赖关系。

Configuration
The constructor and setOptions() method accepts an associative array of configuration parameters, or a Zend\Config\Config object. Setting these parameters is optional, as they all have default values.(设置这些参数是可选的,因为它们有默认值)….

Zend\Http\Client configuration parameters
Parameter Description Expected Values Default Value
maxredirects Maximum number of redirections to follow (0 = none) integer 5
strictredirects Whether to strictly follow the RFC when redirecting (see this section) boolean FALSE
useragent User agent identifier string (sent in request headers) string ‘Zend\Http\Client’
timeout Connection timeout (seconds) integer 10
httpversion HTTP protocol version (usually ‘1.1’ or ‘1.0’) string ‘1.1’
adapter Connection adapter class to use (see this section) mixed ‘Zend\Http\Client\Adapter\Socket’
keepalive Whether to enable keep-alive connections with the server. Useful and might improve performance if several consecutive requests to the same server are performed. boolean FALSE
storeresponse Whether to store last response for later retrieval with getLastResponse(). If set to FALSE, getLastResponse() will return NULL. boolean TRUE
encodecookies Whether to pass the cookie value through urlencode/urldecode. Enabling this breaks support with some web servers. Disabling this limits the range of values the cookies can contain. boolean TRUE
outputstream Destination for streaming of received data (options: string (filename), true for temp file, false/null to disable streaming) boolean FALSE
rfc3986strict Whether to strictly adhere to RFC 3986 (in practice, this means replacing ‘+’ with ‘%20’) boolean FALSE

注意这几个参数:maxredirects useragent timeout,当适配器为Zend\Http\Client\Adapter\Curl时,通过curloptions传递进去的CURLOPT_FOLLOWLOCATION CURLOPT_USERAGENT CURLOPT_TIMEOUT是无效的,意思是这些参考控制只能通过客户端传递给具体的适配器。

The options are also passed to the adapter class upon instantiation(这些参数传递到适配器实例), so the same array or Zend\Config\Config object) can be used for adapter configuration. See the Zend Http Client adapter section for more information on the adapter-specific options available.(这些参数实际是针对客户端本身的,比如控制超时,是否跟踪重定向等,是客户端的特征),客户端本身也提供了一些二次封装的方法,会对应到请求和响应对象。

Examples
Performing a Simple GET Request
Performing simple HTTP requests is very easily done:

use Zend\Http\Client;

$client = new Client('http://example.org');
$response = $client->send();

Using Request Methods Other Than GET
The request method can be set using setMethod(). If no method is specified, the method set by the last setMethod() call is used. If setMethod() was never called, the default request method is GET.

use Zend\Http\Client;

$client = new Client('http://example.org');

// Performing a POST request
$client->setMethod('POST');
$response = $client->send();

For convenience, Zend\Http\Request defines all the request methods as class constants, Zend\Http\Request::METHOD_GET, Zend\Http\Request::METHOD_POST and so on:

use Zend\Http\Client;
use Zend\Http\Request;

$client = new Client('http://example.org');

// Performing a POST request
$client->setMethod(Request::METHOD_POST);
$response = $client->send();

为了方便,Zend\Http\Request定义了所以的请求方法。

Setting GET parameters
Adding GET parameters to an HTTP request is quite simple, and can be done either by specifying them as part of the URL, or by using the setParameterGet() method. This method takes the GET parameters as an associative array of name => value GET variables.

use Zend\Http\Client;
$client = new Client();

// This is equivalent to setting a URL in the Client's constructor:
$client->setUri('http://example.com/index.php?knight=lancelot');

// Adding several parameters with one call
$client->setParameterGet(array(
   'first_name'  => 'Bender',
   'middle_name' => 'Bending',
   'last_name'   => 'Rodríguez',
   'made_in'     => 'Mexico',
));
// 看看这个方法实现 请求对象的二次封装
public function setParameterGet(array $query)
    {
        $this->getRequest()->getQuery()->fromArray($query);
        return $this;
    }

Setting POST Parameters
While GET parameters can be sent with every request method, POST parameters are only sent in the body of POST requests. Adding POST parameters to a request is very similar to adding GET parameters, and can be done with the setParameterPost() method, which is identical to the setParameterGet() method in structure.

use Zend\Http\Client;

$client = new Client();

// Setting several POST parameters, one of them with several values
$client->setParameterPost(array(
    'language'  => 'es',
    'country'   => 'ar',
    'selection' => array(45, 32, 80)
));

Note that when sending POST requests, you can set both GET and POST parameters. On the other hand, setting POST parameters on a non-POST request will not trigger an error, rendering it useless. Unless the request is a POST request, POST parameters are simply ignored.(如果是POST设置GET和POST参数都可以,如果是GET,设置POST参数不会出错,但是会被忽略)

Connecting to SSL URLs
If you are trying to connect to an SSL (https) URL and are using the default (Zend\Http\Client\Adapter\Socket) adapter, you may need to set the sslcapath configuration option in order to allow PHP to validate the SSL certificate:

use Zend\Http\Client;

$client = new Client('https://example.org', array(
   'sslcapath' => '/etc/ssl/certs'
));
$response = $client->send();

The exact path to use will vary depending on your Operating System. Without this you’ll get the exception “Unable to enable crypto on TCP connection” when trying to connect.(路径设置依赖操作系统)

Alternatively, you could switch to the curl adapter, which negotiates SSL connections more transparently(切换适配器):

use Zend\Http\Client;

$client = new Client('https://example.org', array(
   'adapter' => 'Zend\Http\Client\Adapter\Curl'
));
$response = $client->send();

A Complete Example

use Zend\Http\Client;

$client = new Client();
$client->setUri('http://www.example.com');
$client->setMethod('POST');
$client->setParameterPost(array(
   'foo' => 'bar'
));

$response = $client->send();

if ($response->isSuccess()) {
    // the POST was successful
}

or the same thing, using a request object:

use Zend\Http\Client;
use Zend\Http\Request;

$request = new Request();
$request->setUri('http://www.example.com');
$request->setMethod('POST');
$request->getPost()->set('foo', 'bar');

$client = new Client();
$response = $client->send($request);

if ($response->isSuccess()) {
    // the POST was successful
}

后面这个搞法,控制力更好。

HTTP Client – Connection Adapters
Overview
Zend\Http\Client is based on a connection adapter design. The connection adapter is the object in charge of performing the actual connection to the server, as well as writing requests and reading responses. This connection adapter can be replaced, and you can create and extend the default connection adapters to suite your special needs, without the need to extend or replace the entire HTTP client class, and with the same interface.(统一接口,适配器模式)

Currently, the Zend\Http\Client class provides four built-in connection adapters:

Zend\Http\Client\Adapter\Socket (default)
Zend\Http\Client\Adapter\Proxy
Zend\Http\Client\Adapter\Curl
Zend\Http\Client\Adapter\Test
(主要就两个)

The Zend\Http\Client object’s adapter connection adapter is set using the ‘adapter’ configuration option. When instantiating the client object, you can set the ‘adapter’ configuration option to a string containing the adapter’s name (eg. ‘Zend\Http\Client\Adapter\Socket’) or to a variable holding an adapter object (eg. new Zend\Http\Client\Adapter\Socket). You can also set the adapter later, using the Zend\Http\Client->setAdapter() method.

The Socket Adapter
The default connection adapter is the Zend\Http\Client\Adapter\Socket adapter – this adapter will be used unless you explicitly set the connection adapter. The Socket adapter is based on PHP‘s built-in fsockopen() function, and does not require any special extensions or compilation flags.(基于fsockopen()函数,这个是底层C Socket封装)

The Socket adapter allows several extra configuration options that can be set using Zend\Http\Client->setOptions() or passed to the client constructor.

SSL时需要指定证书位置。

Changing the HTTPS transport layer

// Set the configuration parameters
$config = array(
    'adapter'      => 'Zend\Http\Client\Adapter\Socket',
    'ssltransport' => 'tls'
);

// Instantiate a client object
$client = new Zend\Http\Client('https://www.example.com', $config);

// The following request will be sent over a TLS secure connection.
$response = $client->send();

The result of the example above will be similar to opening a TCP connection using the following PHP command:

fsockopen(‘tls://www.example.com’, 443)

Customizing and accessing the Socket adapter stream context
Zend\Http\Client\Adapter\Socket provides direct access to the underlying stream context used to connect to the remote server. This allows the user to pass specific options and parameters to the TCP stream, and to the SSL wrapper in case of HTTPS connections.

You can access the stream context using the following methods of Zend\Http\Client\Adapter\Socket:

>setStreamContext($context) Sets the stream context to be used by the adapter. Can accept either a stream context resource created using the stream_context_create() PHP function, or an array of stream context options, in the same format provided to this function. Providing an array will create a new stream context using these options, and set it.
>getStreamContext() Get the stream context of the adapter. If no stream context was set, will create a default stream context and return it. You can then set or get the value of different context options using regular PHP stream context functions.

Setting stream context options for the Socket adapter

// Array of options
$options = array(
    'socket' => array(
        // Bind local socket side to a specific interface
        'bindto' => '10.1.2.3:50505'
    ),
    'ssl' => array(
        // Verify server side certificate,
        // do not accept invalid or self-signed SSL certificates
        'verify_peer' => true,
        'allow_self_signed' => false,

        // Capture the peer's certificate
        'capture_peer_cert' => true
    )
);

// Create an adapter object and attach it to the HTTP client
$adapter = new Zend\Http\Client\Adapter\Socket();
$client = new Zend\Http\Client();
$client->setAdapter($adapter);

// Method 1: pass the options array to setStreamContext()
$adapter->setStreamContext($options);

// Method 2: create a stream context and pass it to setStreamContext()
$context = stream_context_create($options);
$adapter->setStreamContext($context);

// Method 3: get the default stream context and set the options on it
$context = $adapter->getStreamContext();
stream_context_set_option($context, $options);

// Now, perform the request
$response = $client->send();

// If everything went well, you can now access the context again
$opts = stream_context_get_options($adapter->getStreamContext());
echo $opts['ssl']['peer_certificate'];

Zend\Http\Client\Adapter\Socket内部使用PHP原生的Stream。

The Proxy Adapter
The cURL Adapter
cURL is a standard HTTP client library that is distributed with many operating systems and can be used in PHP via the cURL extension. It offers functionality for many special cases which can occur for a HTTP client and make it a perfect choice for a HTTP adapter. It supports secure connections, proxy, all sorts of authentication mechanisms and shines in applications that move large files around between servers.

Setting cURL options

$config = array(
    'adapter'   => 'Zend\Http\Client\Adapter\Curl',
    'curloptions' => array(CURLOPT_FOLLOWLOCATION => true),
);
$client = new Zend\Http\Client($uri, $config);

By default the cURL adapter is configured to behave exactly like the Socket Adapter and it also accepts the same configuration parameters as the Socket and Proxy adapters. You can also change the cURL options by either specifying the ‘curloptions’ key in the constructor of the adapter or by calling setCurlOption($name, $value). The $name key corresponds to the CURL_* constants of the cURL extension. You can get access to the Curl handle by calling $adapter->getHandle();

Transfering Files by Handle
You can use cURL to transfer very large files over HTTP by filehandle.

$putFileSize   = filesize("filepath");
$putFileHandle = fopen("filepath", "r");

$adapter = new Zend\Http\Client\Adapter\Curl();
$client = new Zend\Http\Client();
$client->setAdapter($adapter);
$client->setMethod('PUT');
$adapter->setOptions(array(
    'curloptions' => array(
        CURLOPT_INFILE => $putFileHandle,
        CURLOPT_INFILESIZE => $putFileSize
    )
));
$client->send();

The Test Adapter

Creating your own connection adapters

HTTP Client – Advanced Usage
HTTP Redirections
Zend\Http\Client automatically handles HTTP redirections, and by default will follow up to 5 redirections. This can be changed by setting the maxredirects configuration parameter.

According to the HTTP/1.1 RFC, HTTP 301 and 302 responses should be treated by the client by resending the same request to the specified location – using the same request method. However, most clients to not implement this and always use a GET request when redirecting. By default, Zend\Http\Client does the same – when redirecting on a 301 or 302 response, all GET and POST parameters are reset, and a GET request is sent to the new location. This behavior can be changed by setting the strictredirects configuration parameter to boolean TRUE:(客户端收到重定向时,标准说明应该是以同样的GET和POST参数重新提交,但是很多客户端没有实现,仅仅是重新发起GET请求,Zend\Http\Client也是如此,不过可以指定strictredirects为TRUE来实现)

Forcing RFC 2616 Strict Redirections on 301 and 302 Responses

// Strict Redirections
$client->setOptions(array('strictredirects' => true));

// Non-strict Redirections
$client->setOptions(array('strictredirects' => false));

You can always get the number of redirections done after sending a request using the getRedirectionsCount() method.

Adding Cookies and Using Cookie Persistence
Zend\Http\Client provides an easy interface for adding cookies to your request, so that no direct header modification is required. Cookies can be added using either the addCookie() or setCookies method. The addCookie method has a number of operating modes:

Setting Cookies Using addCookie()

 // Easy and simple: by providing a cookie name and cookie value
 $client->addCookie('flavor', 'chocolate chips');

 // By providing a Zend\Http\Header\SetCookie object
 $cookie = Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavor=chocolate%20chips');
 $client->addCookie($cookie);

 // Multiple cookies can be set at once by providing an
 // array of Zend\Http\Header\SetCookie objects
 $cookies = array(
     Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavorOne=chocolate%20chips'),
     Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavorTwo=vanilla'),
 );
 $client->addCookie($cookies);

The setCookies() method works in a similar manner, except that it requires an array of cookie values as its only argument and also clears the cookie container before adding the new cookies:

Setting Cookies Using setCookies()

// setCookies accepts an array of cookie values as $name => $value
 $client->setCookies(array(
     'flavor' => 'chocolate chips',
     'amount' => 10,
 ));

For more information about Zend\Http\Header\SetCookie objects, see this section.

Zend\Http\Client also provides a means for simplifying cookie stickiness – that is having the client internally store all sent and received cookies, and resend them on subsequent requests: Zend\Http\Cookies. This is useful, for example when you need to log in to a remote site first and receive and authentication or session ID cookie before sending further requests.

Enabling Cookie Stickiness

$headers = $client->getRequest()->getHeaders();
$cookies = new Zend\Http\Cookies($headers);

// First request: log in and start a session
$client->setUri('http://example.com/login.php');
$client->setParameterPost(array('user' => 'h4x0r', 'password' => 'l33t'));
$client->setMethod('POST');

$response = $client->getResponse();
$cookies->addCookiesFromResponse($response, $client->getUri());

// Now we can send our next request
$client->setUri('http://example.com/read_member_news.php');
$client->setCookies($cookies->getMatchingCookies($client->getUri()));
$client->setMethod('GET');

Setting Custom Request Headers
File Uploads
You can upload files through HTTP using the setFileUpload method. This method takes a file name as the first parameter, a form name as the second parameter(表单名称), and data as a third optional parameter. If the third data parameter is NULL, the first file name parameter is considered to be a real file on disk, and Zend\Http\Client will try to read this file and upload it. If the data parameter is not NULL(第三参数不为空,说明指定了内容), the first file name parameter will be sent as the file name(那么第一参数就是对应的文件名), but no actual file needs to exist on the disk. The second form name parameter is always required, and is equivalent to the “name” attribute of an tag, if the file was to be uploaded through an HTML form. A fourth optional parameter provides the file’s content-type. If not specified, and Zend\Http\Client reads the file from the disk, the mime_content_type function will be used to guess the file’s content type, if it is available. In any case, the default MIME type will be application/octet-stream.

Using setFileUpload to Upload Files

// Uploading arbitrary data as a file
$text = 'this is some plain text';
$client->setFileUpload('some_text.txt', 'upload', $text, 'text/plain');

// Uploading an existing file
$client->setFileUpload('/tmp/Backup.tar.gz', 'bufile');

// Send the files
$client->setMethod('POST');
$client->send();

In the first example, the $text variable is uploaded and will be available as $_FILES[‘upload’] on the server side. In the second example, the existing file /tmp/Backup.tar.gz is uploaded to the server and will be available as $_FILES[‘bufile’]. The content type will be guessed automatically if possible – and if not, the content type will be set to ‘application/octet-stream’.

Sending Raw POST Data
You can use a Zend\Http\Client to send raw POST data using the setRawBody() method. This method takes one parameter: the data to send in the request body. When sending raw POST data, it is advisable to also set the encoding type using setEncType().

Sending Raw POST Data

$xml = '<book>' .
       '  <title>Islands in the Stream</title>' .
       '  <author>Ernest Hemingway</author>' .
       '  <year>1970</year>' .
       '</book>';
$client->setMethod('POST');
$client->setRawBody($xml);
$client->setEncType('text/xml');
$client->send();

The data should be available on the server side through PHP‘s $HTTP_RAW_POST_DATA variable or through the php://input stream. 这个搞法常见于API设计中。

HTTP Authentication
Currently, Zend\Http\Client only supports basic HTTP authentication. This feature is utilized using the setAuth() method, or by specifying a username and a password in the URI. The setAuth() method takes 3 parameters: The user name, the password and an optional authentication type parameter. As mentioned, currently only basic authentication is supported (digest authentication support is planned).

Setting HTTP Authentication User and Password

// Using basic authentication
$client->setAuth('shahar', 'myPassword!', Zend\Http\Client::AUTH_BASIC);

// Since basic auth is default, you can just do this:
$client->setAuth('shahar', 'myPassword!');

// You can also specify username and password in the URI
$client->setUri('http://christer:secret@example.com');

Sending Multiple Requests With the Same Client
Zend\Http\Client was also designed specifically to handle several consecutive requests with the same object. This is useful in cases where a script requires data to be fetched from several places, or when accessing a specific HTTP resource requires logging in and obtaining a session cookie, for example(需要登录的特定资源).

When performing several requests to the same host, it is highly recommended to enable the ‘keepalive’ configuration flag. This way, if the server supports keep-alive connections, the connection to the server will only be closed once all requests are done and the Client object is destroyed. This prevents the overhead of opening and closing TCP connections to the server.

When you perform several requests with the same client, but want to make sure all the request-specific parameters are cleared, you should use the resetParameters() method. This ensures that GET and POST parameters, request body and headers are reset and are not reused in the next request.(参数清空)

Resetting parameters
Note that cookies are not reset by default when the resetParameters() method is used. To clean all cookies as well, use resetParameters(true), or call clearCookies() after calling resetParameters().

Another feature designed specifically for consecutive requests is the Zend\Http\Cookies object. This “Cookie Jar” allow you to save cookies set by the server in a request, and send them back on consecutive requests transparently. This allows, for example, going through an authentication request before sending the actual data-fetching request.

If your application requires one authentication request per user, and consecutive requests might be performed in more than one script in your application, it might be a good idea to store the Cookies object in the user’s session. This way, you will only need to authenticate the user once every session.

Performing consecutive requests with one client

// First, instantiate the client
$client = new Zend\Http\Client('http://www.example.com/fetchdata.php', array(
    'keepalive' => true
));

// Do we have the cookies stored in our session?
if (isset($_SESSION['cookiejar']) &&
    $_SESSION['cookiejar'] instanceof Zend\Http\Cookies) {

    $cookieJar = $_SESSION['cookiejar'];
} else {
    // If we don't, authenticate and store cookies
    $client->setUri('http://www.example.com/login.php');
    $client->setParameterPost(array(
        'user' => 'shahar',
        'pass' => 'somesecret'
    ));
    $response = $client->setMethod('POST')->send();
    $cookieJar = Zend\Http\Cookies::fromResponse($response);

    // Now, clear parameters and set the URI to the original one
    // (note that the cookies that were set by the server are now
    // stored in the jar)
    $client->resetParameters();
    $client->setUri('http://www.example.com/fetchdata.php');
}

// Add the cookies to the new request
$client->setCookies($cookieJar->getMatchingCookies($client->getUri()));
$response = $client->setMethod('GET')->send();

// Store cookies in session, for next page
$_SESSION['cookiejar'] = $cookieJar;

这个实例或者就是我本次阅读这个章节最终要的内容。

Data Streaming

PHP函数参考 – Web服务 – XML-RPC

PHP函数参考 – Web服务 – XML-RPC

从PHP4开始,XML-RPC就已经可用了,现在是PHP5.6都要到达稳定版本了,但是这个XML-RPC扩展的函数还声称是实验性的,实在令人不解。

实际上,要在PHP中实现XML-RPC,不一定要使用PHP自带的这个XML-RPC扩展。XML-RPC是构建在HTTP之上的,数据传递是依靠XML,所以使用PHP来自己实现也是没有问题的,而也的确存在这样的第三方库,不过这里想探讨的还是PHP自带的这个XML-RPC扩展。

由于需要解析XML,所以PHP的libxml扩展是必须的(默认就是启用的,除非明确使用disable-libxml),PHP的libxml扩展实际是libxml2库的包装器,所以需要确保安装了:

rpm -qa | grep xml
libxml2-2.7.6-14.el6_5.2.x86_64
libxml2-devel-2.7.6-14.el6_5.2.x86_64

在编译安装PHP时一般使用–with-libxml-dir=/usr来明确指定libxml安装的地方。

要在PHP中使用XML-RPC,还要在编译时添加–with-xmlrpc。

首先简单说明一下工作过程:客户端使用POST方法把一个XML文档提交过来,这个文档包含了要调用的方法和参数,然后服务端获取这个XML文档并解析出对应的方法和参数,然后调用对应的方法,把结果返回给客户端。对于客户端,只要能构建POST用的XML(当然要按照XML-RPC的XML构建规则)就可以,而没有限制需要使用什么语言。客户端的XML作为POST请求体的一部分,一般可以使用$GLOBALS[‘HTTP_RAW_POST_DATA’]获取原始的POST数据。

例子:

###xmlrpc_server.php
<?php
function xmlrpc_test_func($method, $params) {
        $prms = '';
        if(is_array($params[0])){
                $params = $params[0];
        }
        foreach($params as $pi=>$pv){
                $prms .= $pi."=>".$pv."--";
        }
        $prms = rtrim($prms,'--');
        $return = "Call the server method is $method, and params are:$prms\n";
        return $return;
}

$xmlrpc = xmlrpc_server_create();

xmlrpc_server_register_method($xmlrpc, "xmlrpc_test", "xmlrpc_test_func");

$request = $GLOBALS['HTTP_RAW_POST_DATA'];

$xmlrpc_response = xmlrpc_server_call_method($xmlrpc, $request, null);

header("Content-Type: text/xml");
echo $xmlrpc_response;

xmlrpc_server_destroy($xmlrpc);


###xmlrpc_client.php
<?php
function rpc_use_socket_call($host, $port, $rpc_server, $request) {
        $fp = fsockopen($host, $port);

        $query = "POST $rpc_server HTTP/1.0\nUser_Agent: XML-RPC Client\nHost: ".$host."\nContent-Type: text/xml\nContent-Length: ".strlen($request)."\n\n".$request."\n";

        if (!fputs($fp, $query, strlen($query)))
        {
                $errstr = "Write error";
                return false;
        }

        $contents = "";
        while (!feof($fp)){
                $contents .= fgets($fp);
        }
        fclose($fp);
        return $contents;
}

$host  = "nginx.vfeelit.com";
$port  = 80;
$rpc_server = "/xmlrpc_server.php";

$request = xmlrpc_encode_request("xmlrpc_test", array("test","data"=>"post data","key"=>"121256"));

$response = rpc_use_socket_call($host, $port, $rpc_server, $request);

print_r($response);

xmlrpc_server_create()建立XML-RPC服务端句柄,xmlrpc_server_register_method()注册供客户端调用的方法,第一参数是XML-RPC服务端句柄,第二参数是客户端端调用的方法名字,第三参数是客户端调用方法的映射方法,就是实际将在服务端被执行的方法。xmlrpc_server_call_method()第一参数是XML-RPC服务端句柄,第二参数来自客户端的XML字符串,第三个参数是自定义数据。这个方法会自动解码来自客户端的XML字符串,然后调用这个XML中指定的方法,在这里客户端调用的方法是xmlrpc_test,实际被影射到xmlrpc_test_func,这个方法稍微有点特殊,它的第一个参数是客户端实际调用的方法名字,第二个参数是来自客户端的参数,把方法的执行结果以XML结构的组织返回。

客户端中使用xmlrpc_encode_request()把要调用的方法和参数传递进去,然后这个方法把这些数据装换成一个XML字符串,通过SOCKET的方法,POST过去,不使用xmlrpc_encode_request()方法也可以,不过要按照要求组装XML。当然,不用SOCKET也是可以的,比如在浏览器端用JS也可以一样POST数据。

输出结果:

HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Sun, 03 Aug 2014 05:29:16 GMT
Content-Type: text/html
Connection: close
X-Powered-By: PHP/5.5.15

<?xml version="1.0" encoding="iso-8859-1"?>
<methodResponse>
<params>
 <param>
  <value>
   <string>Call the server method is xmlrpc_test, and params are:0=&#62;test--data=&#62;post data--key=&#62;121256&#10;</string>
  </value>
 </param>
</params>
</methodResponse>

这里看到,响应头被输出了。接下来是响应的内容,它是XML结构的字符串。所以客户端获取这个XML之后还需要解析这个XML获取数据。从整个流程下来看XML-RPC的使用实际是非常简单的。

在客户端代码中的:

$request = xmlrpc_encode_request("xmlrpc_test", array("test","data"=>"post data","key"=>"121256"));
//输出$request

直接输出:

<?xml version="1.0" encoding="iso-8859-1"?>
<methodCall>
<methodName>rpc_server</methodName>
<params>
 <param>
  <value>
   <struct>
    <member>
     <name>test</name>
     <value>
      <string>123</string>
     </value>
    </member>
    <member>
     <name>getdata</name>
     <value>
      <string>base64data</string>
     </value>
    </member>
   </struct>
  </value>
 </param>
</params>
</methodCall>

这个就是要发送的XML,如果自己组装,也要按照这样的规则,服务端也是按照同样的规则解析这个XML的。

对于轻量级别Web服务开发,PHP的XML-RPC还是简单实用的。不过由于它还是实验性的,版本升级上有一些风险,看起来使用SOAP可能更好一点。不过目前比较流行OAuth,这些PHP中都提供支持(OAuth需要安装PECL包)。

原创文章,转载务必保留出处。
永久链接:http://blog.ifeeline.com/1157.html

PHP多线程编程 之 Stackable详解

在Worker中,需要用到Stackable类型的对象,不过这个类型到底是什么东西,文档并没有详细说明。

/usr/local/php-5.5.15/bin/php --rc Threaded
Class [ <internal:pthreads> <iterateable> class Threaded implements Traversable, Countable ] {
...
/usr/local/php-5.5.15/bin/php --rc Stackable
Class [ <internal:pthreads> <iterateable> class Threaded implements Traversable, Countable ] {

查看Threaded和Stackable类的反射输出,发现它们是一模一样的。所以Stackable是Threaded的别名。

用一个例子验证一下:

class a{}
class_alias("a","b");
ReflectionClass::export("a");
ReflectionClass::export("b");

输出:

/usr/local/php-5.5.15/bin/php rr.php
Class [ <user> class a ] {
  @@ /root/rr.php 2-2
...
Class [ <user> class a ] {
  @@ /root/rr.php 2-2

继承Threaded的类需要实现run方法,除非这个类不是用来运行代码的(作为其它类的通用父类,比如Thread和Worker就是)。

看一个实例:

class Work extends Stackable {
        public function run() {
                if ($this->worker) {
                        printf("Running in %s\n", __CLASS__);
                } else printf("failed to get worker something is wrong ...\n");
                print_r($this);
        }
}
class ExampleWorker extends Worker {
        public $count = 0;
        public function __construct() { $this->count += 1; }
        public function run() {}
}
$worker = new ExampleWorker();
$work = new Work();

$worker->start();
$worker->stack($work);

$worker->shutdown();

例子中,在Stackable类Work内,调用了$this->worker判断Worker是否已经设置。先查看输出:

/usr/local/php-5.5.15/bin/php ts.php
Running in Work
Work Object
(
    [worker] => ExampleWorker Object
        (
            [count] => 1
        )

)

Work对象中有一个worker的属性指向Worker对象,这是何时设置的?官方文档有简单说明,大概是说Stackable对象被Worker压栈后在Stackable内可以调用Worker对应的方法,但是具体如何用,没有说明。通过实例可以推导,在Worker对象调用stack方法时,被压入的Stackable对象被修改了,它为之增加了一个worker属性,并引用到自己(Stackable::worker = $this)。

原创文章,转载务必保留出处。
永久链接:http://blog.ifeeline.com/1132.html

PHP多线程编程 之 基本概念(官方文档)

Share Nothing, Do Everything 🙂

PHP has an awesome amount of extensions and features, but was conceived at a time when Web Servers were far less powerful than they are today. Almost every web server ( and mobile phone ! ) has multiple cores, if not multiple CPU’s with multiple cores. PHP does not take advantage of this very well at all. While PHP is very fast to develop and express your ideas, when developing enterprising web applications we often find ourselves looking to other fully fledged languages and frameworks to write backend, or data-centric web applications because PHP isn’t able to take advantage of the hardware we have at our disposal. Being able to thread in PHP makes it a real candidate for enterprising applications, and it’ll make your personal home page be able to do things it simply didn’t have the time to do before.
PHP具有大量的扩展和功能,但是在构思它的时候Web服务器远没有今天那么强大。如今几乎每个Web服务器(和手机)都拥有多核心,如果不是多CPU带多核心。PHP根本没有利用到多核的优点。虽然PHP发展很快并激发你的灵感,当开发企业Web应用程序时,我们往往会寻找其他完全成熟的语言和框架来写后端或以数据为中心的Web应用程序,因为据我们所知PHP不能够充分利用硬件。能够支持线程能使PHP成为企业应用程序的候选者,并且它会让你的个人主页上可以做之前根本没有时间做的事情。

The Basics
pthreads is an Object Orientated API that allows user-land multi-threading in PHP. It includes all the tools you need to create multi-threaded applications targeted at the Web or the Console. PHP applications can create, read, write and synchronize with Threads, Workers and Stackables.
pthreads是一个面向对象的允许在PHP执行用户态多线程的API。它包含了所有你需要创建目标是Web或终端的多线程应用程序的工具。使用Threads,Workers和Stackable,PHP应用程序可以创建、读写和同步。
A Thread Object
The user can implement a thread by extending the Thread declaration provided by pthreads. Any members can be written and read by any context with a reference to the Thread, any context can also execute any public and protected methods. The run method of the implementation is executed in a separate thread when the start method of the implementation is called from the context ( that’s Thread or Process ) that created it. Only the context that creates a thread can start and join with it.
通过扩展pthreads提供的Thread定义,用户可以实现一个线程。带有对Thread引用的任意的上下文可以读写它的任何成员,任何的上下文也可以执行任何的它的公共的和受保护的方法。当实现类的start方法从创建它的上下文(那是线程或进程)被调用时,实现类的run方法在独立的线程中被执行。只有创建线程的上下文可以开始和join这个线程(初次接触肯定不知道join是何物,实际join就是等待线程返回)。
A Worker Object
A Worker Thread has a persistent state, and will be available from the call to start until the object goes out of scope, or is explicitly shutdown. Any context with a reference can pass objects of type Stackable to the Worker which will be executed by the Worker in a separate Thread. The run method of a Worker is executed before any objects on the stack, such that it can initialize resources that the Stackables to come may need.
一个Worker线程有一个持久化状态,并且从调用它的start方法后直到这个对象退出作用域都可用,除非它被明确关闭。任何的带有Worker对象的引用的上下文都可以传递Stackable类型的对象到Worker,这个Stackable类型的对象将被Worker放在一个独立的线程中执行。Worker的run方法在栈中的任何对象之前被执行,这样它可以初始化Stackable对象可能用到的资源。
A Stackable Object
A Stackable Object can read/write and execute the Worker Thread during the execution of it’s run method, additionally, any context with a reference to the Stackable can read, write and execute it’s methods before during and after execution.
Stackable对象的run方法执行时,它可以读写和执行Worker Thread(如何做到?Stackable对象压栈时把自身就是Worker传递给Stackable对象的$this->worker属性,这里没有说明),任何引用Stackable的上下文可以在执行之前、之间和之后读写和执行它的方法(这句话没有理解….)。
Synchronization
All of the objects that pthreads creates have built in synchronization in the form of ::wait and ::notify. Calling ::wait on an object will cause the context to wait for another context to call ::notify on the same object. This allows for powerful synchronization between Threaded Objects in PHP.
所有的pthreads创建的对象具有内置的以wait和notify形式的同步机制。在一个对象上调用wait将暂停上下去等待另一个上下文在相同的对象上调用notify(注意,是在另一个上下文调用,而且是同一个对象)。这允许在PHP中在Threaded对象之间进行功能强大的同步(一个场景是,创建两个线程对象,向需要同步的另一个对象传递引用)。
Wait, Threaded Objects ?
A Stackable, Thread or Worker can be thought of, and should be used as a Threaded stdClass: A Thread, Worker and Stackable all behave in the same way in any context with a reference.
Stackable,Thread或Worker可以认为和应该被用做一个Threaded StdClass(就是TMD Threaded是它们的父类):……..
Any objects that are intended for use in the multi-threaded parts of your application should extend the Stackable, Thread or Worker declaration, which means they must implement run but may not ever be executed; it will often be the case that Objects being used in a multi-threaded environment are intended for execution. Doing so means any context ( that’s Thread/Worker/Stackable/Process ) with a reference can read, write and execute the members of the Threaded Object before, during, and after execution.
任何的你打算在多线程中使用的对象都应该继承Stackable,Thead或Worker的声明,那意味着这些对象必须实现run方法,但是可能永不会被执行;它通常被认为在多线程环境中用于执行的情况(run方法在一个独立线程中运行)….
Method Modifiers
The protected methods of Threaded Objects are protected by pthreads, such that only one context may call that method at a time. The private methods of Threaded Objects can only be called from within the Threaded Object during execution.

Data Storage
As a rule of thumb, any data type that can be serialized can be used as a member of a Threaded object, it can be read and written from any context with a reference to the Threaded Object. Not every type of data is stored serially, basic types are stored in their true form. Complex types, Arrays, and Objects that are not Threaded are stored serially; they can be read and written to the Threaded Object from any context with a reference.
根据经验,任何可以被序列化的数据类型都可以用来作为Threaded对象的成员,它可被任何有引用到Threaded对象的上下文读写(只要有引用就可以读写,注意是任何上下文)。不是所有的数据类都是序列化存储的,基础类型使用它原来的形式存储。那些不是Threaded对象的复杂类型,数组和对象也是序列化存储的;可以在任何线程中把它们读写到Threaded对象中,只要上下文有Threaded对象的引用。
With the exception of Threaded Objects any reference used to set a member of a Threaded Object is separated from the reference in the Threaded Object; the same data can be read directly from the Threaded Object at any time by any context with a reference to the Threaded Object.
在Threaded对象中除了Threaded对象,任何用来设置Threaded对象的成员的引用都是从引用分离的(意思是用来设置Threaded对象属性的引用,不管是值还是什么,除了Threaded对象,都是用它的一份拷贝,理解这个是很重要的,比如你希望传递一个Threaded对象外的引用(非Threaded引用,比如数组)初始化Threaded对象,你务必知道这是个克隆操作)
Resources
The extensions and functionality that define resources in PHP are completely unprepared for this kind of environment; pthreads makes provisions for resources to be shared among contexts, however most types of resource WILL have problems. Most of the time, resources should NOT be shared among contexts, and when they are they should be basic types like streams and sockets.
PHP中扩展或功能定义的资源完全没有为这种类型为环境而准备;对于在上下文之间被共享的资源pthreads做了规定,然而大多数的资源类型将有问题。大多时候,资源不应该在上下文之间被共享,否则它们应该是基本的类型,比如streams和sockets。
Officially, resources remain unsupported.

A Work in Progress

pthreads was and is an experiment with pretty good results. Any of its limitations or features may change at any time; that is the nature of experimentation. It’s limitations – often imposed by the implementation – exist for good reason. The aim of pthreads is to provide a useable solution to multi-tasking in PHP at any level, in the environment which pthreads executes, some restrictions and limitations are necessary in order to provide a stable environment.

永久链接:http://blog.ifeeline.com/1127.html

HTTP协议简介

HTTP由两部分组成:请求和响应。当你在Web浏览器中输入一个URL时,浏览器将根据你的要求创建并发送请求,该请求包含所输入的URL以及一些与浏览器本身相关的信息。当服务器收到这个请求时将返回一个响应,该响应包括与该请求相关的信息以及位于指定URL(如果有的话)的数据。直到浏览器解析该响应并显示出网页(或其他资源)为止。

HTTP请求

HTTP请求的格式如下所示:

<request-line>
<headers>
<blank line>
<request-body>

在HTTP请求中,第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及使用的HTTP版本。紧接着是一个首部(header)小节,用来说明发送给服务器要使用的信息。在首部之后是一个空行,再此之后可以添加任意的其他数据[称之为主体(body)]。

在HTTP中,定义了多种请求类型,通常我们关心的只有GET请求和POST请求。只要在Web浏览器上输入一个URL,浏览器就将基于该URL向服务器发送一个GET请求,以告诉服务器获取并返回什么资源。对于www.vfeelit.com的GET请求如下所示:

GET / HTTP/1.1
Host: www.vfeelit.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie:
Connection: keep-alive

请求行的第一部分说明了该请求是GET请求。该行的第二部分是一个斜杠(/),用来说明请求的是该域名的根目录。该行的最后一部分说明使用的是HTTP 1.1版本。

首部Host将指出请求的目的地。结合Host和上一行中的斜杠(/),可以通知服务器请求的是www.vfeelit.com/。首部User-Agent,服务器端和客户端脚本都能够访问它,它是浏览器类型检测逻辑的重要基础。该信息由你使用的浏览器来定义,并且在每个请求中将自动发送。最后一行是首部Connection,通常将浏览器操作设置为Keep-Alive(当然也可以设置为其他值)。注意,在最后一个首部之后有一个空行。即使不存在请求主体,这个空行也是必需的。

要发送GET请求的参数,则必须将这些额外的信息附在URL本身的后面。其格式类似于:
URL?name1=value1&name2=value2&..&nameN=valueN

该信息称之为查询字符串(query string),它将会复制在HTTP请求的请求行中,如下所示:

GET /i.php?v=168 HTTP/1.1
Host: www.vfeelit.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie:
Connection: keep-alive

注意,为了将包含空格的文本作为URL的参数,需要编码处理其内容(如果通过socket自定义请求,需要自己编码),将空格替换成%20,这称为URL编码(URL encoding),常用于HTTP的许多地方。“名称—值”(name—value)对用 & 隔开。绝大部分的服务器端技术能够自动对请求主体进行解码,并为这些值的访问提供一些逻辑方式。当然,如何使用这些数据还是由服务器决定的。

另一方面,POST请求在请求主体中为服务器提供了一些附加的信息。通常,当填写一个在线表单并提交它时,这些填入的数据将以POST请求的方式发送给服务器。

以下就是一个典型的POST请求:

GET /i.php HTTP/1.1
Host: www.vfeelit.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Referer:
Cookie:
Content-Type: application/x-www-form-urlencoded
Content-Length: 40
Connection: Keep-Alive

v=168&t=ajax%20query

从上面可以发现,POST请求和GET请求之间有一些区别。首先,请求行开始处的GET改为了POST,以表示不同的请求类型。发现首部Host和User-Agent仍然存在,在后面有两个新行。其中首部Content-Type说明了请求主体的内容是如何编码的。浏览器始终以application/x-www-form-urlencoded的格式编码来传送数据,这是针对简单URL编码的MIME类型。首部Content-Length说明了请求主体的字节数。在首部Connection后是一个空行,再后面就是请求主体。与大多数浏览器的POST请求一样,这是以简单的“名称—值”对的形式给出的。你可以以同样的格式来组织URL的查询字符串参数。

下面是一些最常见的请求头:

Accept:		        浏览器可接受的MIME类型。
Accept-Charset:	        浏览器可接受的字符集。
Accept-Encoding:	浏览器能够进行解码的数据编码方式,比如gzip。Web服务器向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间。
Accept-Language:	浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。
Authorization:		授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。
Connection:		表示是否需要持久连接。
Content-Length:	        表示请求消息正文的长度。
Cookie:		        这是最重要的请求头信息之一。
From:			请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它。
Host:			初始URL中的主机和端口。
If-Modified-Since:	只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答。
Pragma:		        指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝。
Referer:		包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
User-Agent:		浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。
UA-Pixels,UA-Color,UA-OS,UA-CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。

HTTP响应

如下所示,HTTP响应的格式与请求的格式十分类似:

<status-line>
<headers>
<blank line>
<response-body>

正如你所见,在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况。以下就是一个HTTP响应的例子:

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 06 May 2013 00:23:23 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Pingback: http://blog.ifeeline.com/xmlrpc.php
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Pragma: no-cache
Content-Encoding: gzip

<html>
<head>
<title></title>
</head>
<body>
<!-- body goes here -->
</body>
</html>

在本例中,状态行给出的HTTP状态代码是200。状态行始终包含的是状态码和相应的简短消息,以避免混乱。最常用的状态码有:

200 (OK): 		找到了该资源,并且一切正常。
304 (NOT MODIFIED): 	该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。
401 (UNAUTHORIZED): 	客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。
403 (FORBIDDEN): 	客户端未能获得授权。这通常是在401之后输入了不正确的用户名或密码。
404 (NOT FOUND): 	在指定的位置不存在所申请的资源。

在状态行之后是一些首部。通常,服务器会返回一个名为Date的首部,用来说明响应生成的日期和时间(服务器通常还会返回一些关于其自身的信息,尽管并非是必需的)。接下来的两个首部大家应该熟悉,就是与POST请求中一样的Content-Type和Content-Length。在本例中,首部Content-Type指定了MIME类型HTML(text/html),其编码类型是UTF-8。响应主体所包含的就是所请求资源的HTML源文件(尽管还可能包含纯文本或其他资源类型的二进制数据)。浏览器将把这些数据显示给用户。

注意,这里并没有指明针对该响应的请求类型,不过这对于服务器并不重要。客户端知道每种类型的请求将返回什么类型的数据,并决定如何使用这些数据。

永久链接:http://blog.ifeeline.com/756.html