标签归档:http

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代理,原因只有一个,部署非常简单。

Node.js HTTP服务器

> 创建HTTP服务器

var http = require('http');
var server = http.createServer(function(request, reponse){

}).listen('9999','127.0.0.1', function(){
	console.log('开始监听');
});

/////
var server = http.createServer();
server.on('request', function(request, reponse){

});
// listen方法第一参数为端口号,0表示随机端口
// 第三参数默认是511表示最大连接数量
server.listen('9999','127.0.0.1', function(){
	console.log('开始监听');
});
//listen方法将触发listening事件,如果不指定回调
server.on('listening', function(){
	console.log('开始监听');
});

// 显式关闭HTTP服务器,触发close事件,可以监听它做善后处理
server.close()
server.on('close', function(){});

// 如果端口冲突,将触发server的error事件
server.on('error', function(e){
	if(e.code == 'EADDRINUSE') {
	}
});
// 当有客户端连接进来时触发server的connection对象
// 回调参数为服务器端用于监听客户端请求的socket端口对象
server.on('connection', function(socket){
});

方法createServer()的回调中第一参数是http.IncomingMessage对象,此处代表一个客户端请求,第二个参数值为一个http.ServerResponse对象,代表一个服务器端响应对象。

方法createServer方法返回被创建的服务器对象,如果不在createServer方法中使用参数,也可以通过监听该方法创建的服务器对象的request事件(这种方式比较直观)

可以使用HTTP服务器的setTimeout方法来设置服务器的超时时间。当该超时时间超过之后,客户端不可继续利用本次与HTTP服务器建立的连接,下次向该HTTP服务器发出请求时必须重新建立连接

// 设置超时并给出超时时需要执行的回调
server.setTimeout(1000, function(socket) {
});
// 超时时会触发timeout事件 
server.on('timeout', function(socket){

});
// 也可以直接操作server的timeout属性
server.timeout = 1000;
console.log(server.timeout);

HTTP服务器拥有一个timeout属性,属性值为整数值,单位为毫秒,可用于查看或修改服务器的超时时间。

> 获取客户端请求信息
http.IncomingMessage用于读取客户端请求流中的数据,当从客户端请求流中读取到新的数据时触发data事件,当读取完客户端请求流中的数据时触发end事件。当该对象被用于读取客户端请求流中的数据时,有如下属性:

.method		GET POST PUT DELETE
.url		带请求查询的URL
.headers	客户端发送的请求头
.httpVersion
.trailers	客户端附加的HTTP头(end事件触发后才能读取)
.socket		socket对象

例子:

var http = require('http');

var server = http.createServer(function (req, res) {
	 //console.log(req);
	 console.log(req.method);
	 console.log(req.url);
	 console.log(req.headers);
	 // data触发主要用来接收客户端请求体的数据,比如POST的数据
	 req.on('data', function(data) {
		console.log(decodeURIComponent(data));
	 });

	 req.on('end', function(){
		console.log("客户端数据已经全部接收");
	 });
}).listen(9999, '127.0.0.1');

server.on('connection',function(socket){
	console.log('客户端连接已建立');
});

> 转换URL字符串与查询字符串
在Node.js中,提供了一个url模块与一个queryString模块,分别用来转换完整URL字符串与URL中的查询字符串。

querystring.parse('userName=Lulingniu&age=40&sex=male','&','=');
querystring.stringify({username:'vfeelit',age:'18',sex:'male'})
// 第二参数表示是否解析查询字符串为一个对象
// 内部实际使用querystring对象
url.parse('http:// user:pass@host.com:8080/users/user.php?userName=vv&age=18&sex=male#name1')
url.format(urlObj);
url.resolve(from, to); //可以简单看做是字符串替换

> 发送服务器响应
利用http.ServerResponse对象的writeHead方法来发送响应头

var http = require('http');
var server = http.createServer(function(req, res){
	res.writeHead(200, {'Content-Type':'text/plain', 'Access-Control-Allow-Origin':'http://127.0.0.1'});
	//也可以使用setHeader设置响应头 
	res.setHeader("Set-Cookie",["type=xx","language=xxxx"]);
	res.write("Hello");
	res.end();
});

使用了http.ServerResponse对象的setHeader方法设置响应头信息之后,可以使用http.ServerResponse对象的getHeader方法获取响应头中的某个字段值。使用http.ServerResponse对象的removeHeader方法删除一个响应字段。http.ServerResponse对象具有一个headersSent属性,当响应头已发送时,该属性值为true,当响应头未发送时,属性值为false。

HTTP服务器自动将服务器端当前时间作为响应头中的Date字段值发送给客户端。可以通过将http.ServerResponse对象的sendDate属性值设置为false的方法在响应头中删除Date字段。

可以通过http.ServerResponse对象的statusCode属性值获取或设置HTTP服务器返回的状态码。

可以使用http.ServerResponse对象的setTimeout方法设置响应超时时间。如果在指定时间内服务器没有做出响应,则响应超时,同时触发http.ServerResponse对象的timeout事件。

在http.ServerResponse对象的end方法被调用之前,如果连接中断,将触发http.ServerResponse对象的close事件。

> HTTP客户端
request方法返回一个http.ClientRequest对象,代表一个客户端请求。

当客户端请求获取到服务器响应流时,触发http.ClientRequest对象的response事件,可以不在request方法中使用callback参数,而是通过对http.ClientRequest对象的response事件进行监听并指定事件回调函数的方法来指定当获取到其他网站返回的响应流时执行的处理。

var http = require('http');
var option = {
	hostname: 'blog.ifeeline.com',
	port:80,
	path:'/',
	method:'GET'
};
//回调在reponse事件触发时执行
var req = http.request(options, function(res){
});
// http.request()方法返回一个http.clientRequest对象,调用end()方法将发起请求
// 数据返回后触发reponse事件,所以可以如下
req.on('reponse', function(res){
});
// res对象是一个http.IncomingMessage对象,表示发起请求的响应

// 可以调用http.clientRequest对象的write来发送数据,比如上次文件
// 这些内容实际就是HTTP请求中的请求体
req.write(chunk,[encoding])
// 必须调用end方法来结束请求(实际是发出请求)
// end中的参数(可空),类似最后执行一次write方法
req.end([chunk],[encoding])

使用http.ClientRequest对象的abort方法终止本次请求,如果在向目标网站请求数据的过程中发生了错误,将触发http.ClientRequest对象的error事件。在建立连接的过程中,当为该连接分配端口时,触发http.ClientRequest对象的socket事件。

如果使用GET方式向其他网站请求数据,也可以使用http模块中的get方法。

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

Laravel HTTP响应详解

请求与响应是HTTP协议里面的两个主要部分。实际响应跟请求一样,构造一个长字符串,返回客户端。一般,都不需要直接去控制细节,但是实际我们可以控制它。比如组装一个响应原始文本,然后经由服务器发送会客户端。

在MVC框架中,Response对象就代表这个响应。可以定制它。

Illuminate\Http\Response继承Symfony\Component\HttpFoundation\Response,使用了一个叫Illuminate\Http\ResponseTrait的Trait,看看这个Trait:

namespace Illuminate\Http;

trait ResponseTrait
{
    /**
     * Get the status code for the response.
     *
     * @return int
     */
    public function status()
    {
        return $this->getStatusCode();
    }

    /**
     * Get the content of the response.
     *
     * @return string
     */
    public function content()
    {
        return $this->getContent();
    }

    /**
     * Set a header on the Response.
     *
     * @param  string  $key
     * @param  string  $value
     * @param  bool    $replace
     * @return $this
     */
    public function header($key, $value, $replace = true)
    {
        $this->headers->set($key, $value, $replace);

        return $this;
    }

    /**
     * Add a cookie to the response.
     *
     * @param  \Symfony\Component\HttpFoundation\Cookie|mixed  $cookie
     * @return $this
     */
    public function withCookie($cookie)
    {
        if (is_string($cookie) && function_exists('cookie')) {
            $cookie = call_user_func_array('cookie', func_get_args());
        }

        $this->headers->setCookie($cookie);

        return $this;
    }
}

这几个方法都比较直观。status()查看响应状态码,就是200,404之类的。content()就是响应体。header()设置响应头。withCookie()设置响应cookie,就是Set-cookie响应头。这些是最常用的方法。

以上是PHP作为服务端,需要发送响应的场景。实际PHP也可以作为客户端,接收来自服务端的响应,这个时候需要操作的东西就多了,可以查看Symfony\Component\HttpFoundation\Response的实现,里面提供了大量方法(当然是针对不同场景的)。

Illuminate\Http\Response继承Symfony\Component\HttpFoundation\Response,它实现了什么特殊呢,除了使用Illuminate\Http\ResponseTrait之外,它还实现了setContent()方法,这个方法的特殊大概是这样的,如果传递给setContent()方法的对象是可以json_encode的(比如数组 或 实现了Jsonable接口),那么它就设置一个application/json响应头,并把内容转换成JSON字符串;否则判断是否是可以渲染的(实现Renderable接口对象),那么就渲染其内容,否则就调用父类的setContent()方法。

实际过程中,直接使用Reponse的情况还是不多的,因为它操作确实底层一点了(需要加一层封装)。

框架在初始化时会全局载入:

$vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
$vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',

最基础的全局函数在Foundation/helpers.php中定义,一些支持函数在Support/helpers.php中定义(主要是字符串 和 数组扩展,实际对应到Support\Arr.php和Str.php,还有一些工具方法,比如dd()等)。这里主要看看Foundation/helpers.php中的response()方法:

    function response($content = '', $status = 200, array $headers = [])
    {
        $factory = app('Illuminate\Contracts\Routing\ResponseFactory');

        if (func_num_args() === 0) {
            return $factory;
        }

        return $factory->make($content, $status, $headers);
    }

可以看到,如果参数为空,直接返回一个Illuminate\Contracts\Routing\ResponseFactory实例(如果指定了$content,就直接构建一个响应对象,这个就是最开始讨论的内容)。这个响应工厂就很有意思了,它顾名思义,就是用来生产不同的响应的。

这里先来个插曲:
app(‘Illuminate\Contracts\Routing\ResponseFactory’)的意思是从容器中取回这个对象,玛尼,这个东西什么时候进入容器的?

//启动流程中的步骤
$app = new Illuminate\Foundation\Application(realpath(__DIR__.'/../')); 
    // 构造函数
    public function __construct($basePath = null)
    {
        $this->registerBaseBindings();
 
        $this->registerBaseServiceProviders();
 
        $this->registerCoreContainerAliases();
 
        if ($basePath) {
            $this->setBasePath($basePath);
        }
    }
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));
        //注册4个key分别为router、url、redirect、Illuminate\Contracts\Routing\ResponseFactory的对象
        $this->register(new RoutingServiceProvider($this));
    }
}

好吧,这里只是产生了一个key而已,怎么对应上Illuminate\Routing\ResponseFactory呢,实际就是Laravel容器要负责的事情了。Laravel容器在寻找key对象的实例,没有就去bindings中查找,OK,答案是bingdings中定义了要实例化Illuminate\Routing\ResponseFactory对象(事情就是这么个事情了)。

然后,这个工厂类实现了Illuminate\Contracts\Routing\ResponseFactory接口:

<?php

namespace Illuminate\Contracts\Routing;

interface ResponseFactory
{
    public function make($content = '', $status = 200, array $headers = []);
    public function view($view, $data = [], $status = 200, array $headers = []);
    public function json($data = [], $status = 200, array $headers = [], $options = 0);
    public function jsonp($callback, $data = [], $status = 200, array $headers = [], $options = 0);
    public function stream($callback, $status = 200, array $headers = []);
    public function download($file, $name = null, array $headers = [], $disposition = 'attachment');
    public function redirectTo($path, $status = 302, $headers = [], $secure = null);
    public function redirectToRoute($route, $parameters = [], $status = 302, $headers = []);
    public function redirectToAction($action, $parameters = [], $status = 302, $headers = []);
    public function redirectGuest($path, $status = 302, $headers = [], $secure = null);
    public function redirectToIntended($default = '/', $status = 302, $headers = [], $secure = null);
}

Look,这些方法是不是很熟悉,view()啊,download()啊。

大概看看:

make()		直接返回原始Response对象。
view()		调用视图的make()方法渲染视图(view('xx',$data))
json()		返回一个JsonResponse()对象(看成JSON就可以了)
jsonp()		这个不解释了
stream()	返回一个流,这个方法可以详细看看
download()	返回一个二进制文件响应
redirectTo()	内部实际调用重定向求器

一些实例:

#可以直接这样干
Route::get('/', function()
{
    return 'Hello World';
});

#返回一个完整的 Response 实例时,你能够自定义响应的 HTTP 状态码以及响应头。
#Response 实例继承了 Symfony\Component\HttpFoundation\Response 类,
#它提供了很多方法来建立 HTTP 响应。
use Illuminate\Http\Response;

return (new Response($content, $status))
              ->header('Content-Type', $value);

#使用辅助方法 response。跟以上是一样的,只是包装了一下
return response($content, $status)
              ->header('Content-Type', $value);

#在响应送出视图
return response()->view('hello')->header('Content-Type', $type);

#附加 Cookies 到响应
return response($content)->withCookie(cookie('name', 'value'));

#链式方法
return response()->view('hello')->header('Content-Type', $type)
                 ->withCookie(cookie('name', 'value'));

#使用辅助方法 response 可以轻松的产生其他类型的响应实例。当你调用辅助方法 response 且不带任何参数时,将会返回 Illuminate\Contracts\Routing\ResponseFactory Contract 的实例。Contract 提供了一些有用的方法来产生响应
return response()->json(['name' => 'Abigail', 'state' => 'CA']);
return response()->json(['name' => 'Abigail', 'state' => 'CA'])
                 ->setCallback($request->input('callback'));

return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);
return response()->download($pathToFile)->deleteFileAfterSend(true);

大体上,这个工厂就生产这些东西。这里还是要打个岔,全局有提供:

    function redirect($to = null, $status = 302, $headers = [], $secure = null)
    {
        if (is_null($to)) {
            return app('redirect');
        }
	// 
        return app('redirect')->to($to, $status, $headers, $secure);
    }

这个redirect()方法为空时,返回一个Illuminate\Routing\Redirector对象,它是专门对付重定向而打造的。以上说的ResponseFactory内部实际使用的是相同的Redirector对象,它做二次封装而已。这个重定向器提供了如下用法:

#
return redirect('user/login');

#返回重定向并且加上快闪数据( Flash Data )
return redirect('user/login')->with('message', 'Login Failed');

#返回根据前一个 URL 的重定向(看起来有记录前一个链接,不知道是否有对方ajax这样的请求)
return redirect()->back();
return redirect()->back()->withInput();

#返回根据路由名称的重定向
#当调用辅助方法 redirect 且不带任何参数时,将会返回 Illuminate\Routing\Redirector 的实例,你可以对该实例调用任何的方法。
return redirect()->route('login'); //意思定位到此路由

#返回根据路由名称的重定向,并给予路由参数赋值
// 路由的 URI 为:profile/{id}
return redirect()->route('profile', [1]);
return redirect()->route('profile', ['user' => 1]);

#action表示定位到控制器的action
return redirect()->action('App\Http\Controllers\HomeController@index');
//如果你已经通过 URL::setRootControllerNamespace 注册了根控制器的命名空间,那么就不需要对 action() 方法内的控制器指定完整的命名空间。
return redirect()->action('App\Http\Controllers\UserController@profile', [1]);
return redirect()->action('App\Http\Controllers\UserController@profile', ['user' => 1]);

我需要说的是,它确实牛叉啊。

最后需要交代的是,redirect()和response()返回的对象,实际有一个Facade,分别是Redirect和Response(这个Response不是实际应该叫Response工厂才对,知道这回事即可):

Redirect    
redirect(容器key)    
Illuminate\Routing\Redirector(实例)

Response    
Illuminate\Contracts\Routing\ResponseFactory(容器key) 
Illuminate\Contracts\Routing\ResponseFactory(实例)

这大概就是大部分内容了。实例来自官方文档。

Laravel HTTP 请求详解

Laravel中的HTTP请求组件继承自Symfony\Component\HttpFoundation\Request,很多方法都是第二次包装。请求对象的产生:

// index.php
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

//
public static function capture()
{
    static::enableHttpMethodParameterOverride();

    return static::createFromBase(SymfonyRequest::createFromGlobals());
}

代码static::enableHttpMethodParameterOverride(); 就是启用请求方法欺骗,由于表单并只支持GET和POST方法,所以需要通过一个隐藏_method字段来模拟其它请求方法,可以查看其父类的getMethod()方法:

    public function getMethod()
    {
        if (null === $this->method) {
            $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));

            if ('POST' === $this->method) {
                if ($method = $this->headers->get('X-HTTP-METHOD-OVERRIDE')) {
                    $this->method = strtoupper($method);
                } elseif (self::$httpMethodParameterOverride) {
                    $this->method = strtoupper($this->request->get('_method', $this->query->get('_method', 'POST')));
                }
            }
        }

        return $this->method;
    }

可以看到,如果要模拟其它的请求,真实的方法必须是POST。首先接收一个请求X-HTTP-METHOD-OVERRIDE设置的值,然后才从请求中获取_method对应的值。

至于request对象的返回,主要是其父类的一个方法:

// $_GET $_POST attributes  $_COOKIES  $_FILES  $_SERVER  content 
    public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
    {
        $this->request = new ParameterBag($request);
        $this->query = new ParameterBag($query);
        $this->attributes = new ParameterBag($attributes);
        $this->cookies = new ParameterBag($cookies);
        $this->files = new FileBag($files);
        $this->server = new ServerBag($server);
        $this->headers = new HeaderBag($this->server->getHeaders());

        $this->content = $content;
        $this->languages = null;
        $this->charsets = null;
        $this->encodings = null;
        $this->acceptableContentTypes = null;
        $this->pathInfo = null;
        $this->requestUri = null;
        $this->baseUrl = null;
        $this->basePath = null;
        $this->method = null;
        $this->format = null;
    }

Laravel的Request再对其进行一次包装。参考如下用法即可。不过有两个方法需要注意,input()方法,如果是POST就会把查询和POST的参数都返回,all()方法会把$_FILE也加入。这里没有包含Cookie的数据,对于Cookie有专门的对应方法。

—————————————————————————————

// 如果在一个名空间中,需要导入Request,就是use一下
$name = Request::input('name');


// 依赖注入
<?php 
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class UserController extends Controller {

    /**
     * Store a new user.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        $name = $request->input('name');

        //
    }

}

// request对象有两个Facade
// Input       request     Illuminate\Http\Request
// Request     request     Illuminate\Http\Request

// 如果控制前需要传递参数,跟着依赖注入之后即可
<?php namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class UserController extends Controller {

    /**
     * Update the specified user.
     *
     * @param  Request  $request
     * @param  int  $id
     * @return Response
     */
    public function update(Request $request, $id)
    {
        //
    }

}

// 取数据
$name = Request::input('name');
// 取不到就是默认值
$name = Request::input('name', 'Sally');
// 是否有
if (Request::has('name'))
{
    //
}
// 请求是输入的所有数据
$input = Request::all();
// 取部分数据
$input = Request::only('username', 'password');
$input = Request::except('credit_card');
// 取数组形式的数据
$input = Request::input('products.0.name');

// 把当前数据全部存入Session
Request::flash();
// 将部分数据存入Session
Request::flashOnly('username', 'email');
Request::flashExcept('password');

// 重定向到form路由,并把当前所有输入数据传递过去()
return redirect('form')->withInput();
return redirect('form')->withInput(Request::except('password'));

// 获取上一次请求fresh的数据 
$username = Request::old('username');
// 在模板中使用
{{ old('username') }}

/////////////////////////////////////////////////////
// 取cookie
$value = Request::cookie('name');
// 响应cookie
$response = new Illuminate\Http\Response('Hello World');
$response->withCookie(cookie('name', 'value', $minutes));
$response->withCookie(cookie()->forever('name', 'value'));  //怪异

// ?
<?php namespace App\Http\Controllers;

use Cookie;
use Illuminate\Routing\Controller;

class UserController extends Controller
{
    /**
     * Update a resource
     *
     * @return Response
     */
     public function update()
     {
        Cookie::queue('name', 'value');

        return response('Hello World');
     }
}

// 文件上传
// file 方法返回的对象是 Symfony\Component\HttpFoundation\File\UploadedFile
$file = Request::file('photo');

if (Request::hasFile('photo'))
{
    //
}

if (Request::file('photo')->isValid())
{
    //
}

// 移动上传的文件
Request::file('photo')->move($destinationPath);
Request::file('photo')->move($destinationPath, $fileName);

// Request类也是继承Symfony\Component\HttpFoundation\Request
// 取得URI
$uri = Request::path();

if (Request::ajax())
{
    //
}

// 请求方法
$method = Request::method();

if (Request::isMethod('post'))
{
    //
}

// 检查路径特定格式
if (Request::is('admin/*'))
{
    //
}

// 取得请求URL
$url = Request::url();

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

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