一、SOCKS代理
全能代理,就像有很多跳线的转接板,它只是简单地将一端的系统连接到另外一端。支持多种协议,包括http、ftp请求及其它类型的请求。它分socks 4 和socks 5两种类型,socks 4只支持TCP协议而socks 5支持TCP/UDP协议,还支持各种身份验证机制等协议。其标准端口为1080。
1.1 原理
socks代理相应的采用socks协议的代理服务器就是SOCKS服务器,是一种通用的代理服务器。Socks是个电路级的底层网关,是DavidKoblas在1990年开发的,此后就一直作为Internet RFC标准的开放标准。Socks不要求应用程序遵循特定的操作系统平台,Socks 代理与应用层代理、 HTTP 层代理不同,Socks代理只是简单地传递数据包,而不必关心是何种应用协议(比如FTP、HTTP和NNTP请求)。所以,Socks代理比其他应用层代理要快得多。它通常绑定在代理服务器的1080端口上。如果您在企业网或校园网上,需要透过防火墙或通过代理服务器访问Internet就可能需要使用SOCKS。一般情况下,对于拨号上网用户都不需要使用它。注意,浏览网页时常用的代理服务器通常是专门的http代理,它和SOCKS是不同的。因此,您能浏览网页不等于您一定可以通过SOCKS访问Internet。 常用的防火墙,或代理软件都支持SOCKS,但需要其管理员打开这一功能。如果您不确信您是否需要SOCKS或是否有SOCKS可用,请与您的网络管理员联系。为了使用socks,您需要了解一下内容:
① SOCKS服务器的IP地址
② SOCKS服务所在的端口
③ 这个SOCKS服务是否需要用户认证?如果需要,您要向您的网络管理员申请一个用户和口令
知道了上述信息,您就可以把这些信息填入“网络配置”中,或者在第一次登记时填入,您就可以使用socks代理了。
1.2 实际应用
在实际应用中SOCKS代理可以用作为:电子邮件、新闻组软件、网络传呼ICQ、网络聊天MIRC和使用代理服务器打游戏等等各种应用软件当中。
二、HTTP普通代理&隧道代理
2.1 普通代理
第一种 Web 代理原理特别简单:
HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。
下面这张图片来自于《HTTP 权威指南》,直观地展示了上述行为:
假如我通过代理访问 A 网站,对于 A 来说,它会把代理当做客户端,完全察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。当然代理也可以修改 HTTP 请求头部,通过 X-Forwarded-IP 这样的自定义头部告诉服务端真正的客户端 IP。但服务器无法验证这个自定义头部真的是由代理添加,还是客户端修改了请求头,所以从 HTTP 头部字段获取 IP 时,需要格外小心。这部分内容可以参考我之前的《HTTP 请求头中的 X-Forwarded-For》这篇文章。
给浏览器显式的指定代理,需要手动修改浏览器或操作系统相关设置,或者指定 PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置,还有些浏览器支持 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自动发现协议)。显式指定浏览器代理这种方式一般称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文做一些修改,来规避老旧代理服务器的一些问题,这部分内容可以参考我之前的《Http 请求头中的 Proxy-Connection》这篇文章。
还有一种情况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种情况一般被称之为反向代理,它可以用来隐藏服务器 IP 及端口。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。反向代理是 Web 系统最为常见的一种部署方式,例如本博客就是使用 Nginx 的 proxy_pass 功能将浏览器请求转发到背后的 Node.js 服务。
了解完第一种代理的基本原理后,我们用 Node.js 实现一下它。只包含核心逻辑的代码如下:
var http = require('http');
var net = require('net');
var url = require('url');
function request(cReq, cRes) {
var u = url.parse(cReq.url);
var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};
var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});
cReq.pipe(pReq);
}
http.createServer().on('request', request).listen(8888, '0.0.0.0');
以上代码运行后,会在本地 8888 端口开启 HTTP 代理服务,这个服务从请求报文中解析出请求 URL 和其他必要参数,新建到服务端的请求,并把代理收到的请求转发给新建的请求,最后再把服务端响应返回给浏览器。修改浏览器的 HTTP 代理为 127.0.0.1:8888 后再访问 HTTP 网站,代理可以正常工作。
但是,使用我们这个代理服务后,HTTPS 网站完全无法访问,这是为什么呢?答案很简单,这个代理提供的是 HTTP 服务,根本没办法承载 HTTPS 服务。那么是否把这个代理改为 HTTPS 就可以了呢?显然也不可以,因为这种代理的本质是中间人,而 HTTPS 网站的证书认证机制是中间人劫持的克星。普通的 HTTPS 服务中,服务端不验证客户端的证书,中间人可以作为客户端与服务端成功完成 TLS 握手;但是中间人没有证书私钥,无论如何也无法伪造成服务端跟客户端建立 TLS 连接。当然如果你拥有证书私钥,代理证书对应的 HTTPS 网站当然就没问题了。
HTTP 抓包神器 Fiddler 的工作原理也是在本地开启 HTTP 代理服务,通过让浏览器流量走这个代理,从而实现显示和修改 HTTP 包的功能。如果要让 Fiddler 解密 HTTPS 包的内容,需要先将它自带的根证书导入到系统受信任的根证书列表中。一旦完成这一步,浏览器就会信任 Fiddler 后续的「伪造证书」,从而在浏览器和 Fiddler、Fiddler 和服务端之间都能成功建立 TLS 连接。而对于 Fiddler 这个节点来说,两端的 TLS 流量都是可以解密的。
如果我们不导入根证书,Fiddler 的 HTTP 代理还能代理 HTTPS 流量么?实践证明,不导入根证书,Fiddler 只是无法解密 HTTPS 流量,HTTPS 网站还是可以正常访问。这是如何做到的,这些 HTTPS 流量是否安全呢?这些问题将在下一节揭晓。
2.2 隧道代理
第二种 Web 代理的原理也很简单:
HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。
下面这张图片同样来自于《HTTP 权威指南》,直观地展示了上述行为:
假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的,下图中的抓包信息显示了这种场景:
可以看到,浏览器与代理进行 TCP 握手之后,发起了 CONNECT 请求,报文起始行如下:
CONNECT imququ.com:443 HTTP/1.1 对于 CONNECT 请求来说,只是用来让代理创建 TCP 连接,所以只需要提供服务器域名及端口即可,并不需要具体的资源路径。代理收到这样的请求后,需要与服务端建立 TCP 连接,并响应给浏览器这样一个 HTTP 报文:
HTTP/1.1 200 Connection Established 浏览器收到了这个响应报文,就可以认为到服务端的 TCP 连接已经打通,后续直接往这个 TCP 连接写协议数据即可。通过 Wireshark 的 Follow TCP Steam 功能,可以清楚地看到浏览器和代理之间的数据传递:
可以看到,浏览器建立到服务端 TCP 连接产生的 HTTP 往返,完全是明文,这也是为什么 CONNECT 请求只需要提供域名和端口:如果发送了完整 URL、Cookie 等信息,会被中间人一览无余,降低了 HTTPS 的安全性。HTTP 代理承载的 HTTPS 流量,应用数据要等到 TLS 握手成功之后通过 Application Data 协议传输,中间节点无法得知用于流量加密的 master-secret,无法解密数据。而 CONNECT 暴露的域名和端口,对于普通的 HTTPS 请求来说,中间人一样可以拿到(IP 和端口很容易拿到,请求的域名可以通过 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),所以这种方式并没有增加不安全性。
了解完原理后,再用 Node.js 实现一个支持 CONNECT 的代理也很简单。核心代码如下:
var http = require('http');
var net = require('net');
var url = require('url');
function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);
var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});
cSock.pipe(pSock);
}
http.createServer().on('connect', connect).listen(8888, '0.0.0.0');
以上代码运行后,会在本地 8888 端口开启 HTTP 代理服务,这个服务从 CONNECT 请求报文中解析出域名和端口,创建到服务端的 TCP 连接,并和 CONNECT 请求中的 TCP 连接串起来,最后再响应一个 Connection Established 响应。修改浏览器的 HTTP 代理为 127.0.0.1:8888 后再访问 HTTPS 网站,代理可以正常工作。
最后,将两种代理的实现代码合二为一,就可以得到全功能的 Proxy 程序了,全部代码在 50 行以内(当然异常什么的基本没考虑,这是我博客代码的一贯风格):
var http = require('http');
var net = require('net');
var url = require('url');
function request(cReq, cRes) {
var u = url.parse(cReq.url);
var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};
var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});
cReq.pipe(pReq);
}
function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);
var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});
cSock.pipe(pSock);
}
http.createServer()
.on('request', request)
.on('connect', connect)
.listen(8888, '0.0.0.0');
需要注意的是,大部分浏览器显式配置了代理之后,只会让 HTTPS 网站走隧道代理,这是因为建立隧道需要耗费一次往返,能不用就尽量不用。但这并不代表 HTTP 请求不能走隧道代理,我们用 Node.js 写段程序验证下(先运行前面的代理服务):
var http = require('http');
var options = {
hostname : '127.0.0.1',
port : 8888,
path : 'imququ.com:80',
method : 'CONNECT'
};
var req = http.request(options);
req.on('connect', function(res, socket) {
socket.write('GET / HTTP/1.1\r\n' +
'Host: imququ.com\r\n' +
'Connection: Close\r\n' +
'\r\n');
socket.on('data', function(chunk) {
console.log(chunk.toString());
});
socket.on('end', function() {
console.log('socket end.');
});
});
req.end();
这段代码运行完,结果如下:
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Thu, 19 Nov 2015 15:57:47 GMT
Content-Type: text/html
Content-Length: 178
Connection: close
Location: https://imququ.com/
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
socket end.
可以看到,通过 CONNECT 让代理打开到目标服务器的 TCP 连接,用来承载 HTTP 流量也是完全没问题的。
最后,HTTP 的认证机制可以跟代理配合使用,使得必须输入正确的用户名和密码才能使用代理,这部分内容比较简单,这里略过
三、socket通讯代码实战
1. 添加依赖和权限
app的build.gradle下添加okhttp依赖
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
AndroidManifest.xml文件添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
2. 添加布局文件
activity_main.xml文件中代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start"
android:textSize="16sp" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/start2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start2"
android:textSize="16sp" />
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
3. MainActivity逻辑实现
package aidl_customer.com.wj.http_socket;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
/**
* okhttp是3.5以后才添加对WebSocket的支持
*/
public class MainActivity extends AppCompatActivity {
private Button start;
private TextView text;
private Button start2;
private TextView text2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start = (Button) findViewById(R.id.start);
text = (TextView) findViewById(R.id.text);
start2 = (Button) findViewById(R.id.start2);
text2 = (TextView) findViewById(R.id.text2);
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
connect();
}
});
start2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(3, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("ws://echo.websocket.org")
.build();
WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
}
});
}
});
}
/**
*WebSocket官网就提供了相应url可以测试
*/
private void connect() {
//建立连接
EchoWebSocketListener listener = new EchoWebSocketListener();
Request request = new Request.Builder()
.url("ws://echo.websocket.org")
.build();
OkHttpClient client = new OkHttpClient();
client.newWebSocket(request, listener);
client.dispatcher().executorService().shutdown();
}
/**
* 重写了WebSocketListener中的几个方法,这几个方法很好理解,是用来异步回调的,
* 这里简单说一下:onOpen当WebSocket和远程建立连接时回调;两个onMessage就是接收到消息时回调,
* 只是消息内容的类型不同;onClosing是当远程端暗示没有数据交互时回调(即此时准备关闭,但连接还没有关闭);
* onClosed就是当连接已经释放的时候被回调;onFailure当然是失败时被回调(包括连接失败,发送失败等)。
*
* send用来发送消息;close用来关闭连接
*/
private final class EchoWebSocketListener extends WebSocketListener {
@Override
public void onOpen(WebSocket webSocket, Response response) {
webSocket.send("hello world");
webSocket.send("welcome");
webSocket.send(ByteString.decodeHex("adef"));
webSocket.close(1000, "再见");
}
@Override
public void onMessage(WebSocket webSocket, String text) {
output("onMessage: " + text);
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
output("onMessage byteString: " + bytes);
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
webSocket.close(1000, null);
output("onClosing: " + code + "/" + reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
output("onClosed: " + code + "/" + reason);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
output("onFailure: " + t.getMessage());
}
}
private void output(final String content) {
runOnUiThread(new Runnable() {
@Override
public void run() {
text.setText(text.getText().toString() + content + "n");
}
});
}
}
以上就是socket通讯代码实战与原理介绍;更多有关OKhttp技术或者Android高级进阶技术,可参考《Android核心技术手册》点击查看更多学习知识资料。
温馨提示:
(1)EchoWebSocketListener继承OkHttp中的抽象类WebSocketListener,重写了几个方法,是用来异步回调的,这里简单说一下:onOpen当WebSocket和远程建立连接时回调;两个onMessage就是接收到消息时回调,只是消息内容的类型不同;onClosing是当远程端暗示没有数据交互时回调(即此时准备关闭,但连接还没有关闭);onClosed就是当连接已经释放的时候被回调;onFailure当然是失败时被回调(包括连接失败,发送失败等)。
(2)send用来发送消息;close用来关闭连接
(3)WebSocket官网就提供了相应url可以测试
(4)关闭连接方式,OkHttp提供两个方法来关闭连接:
- 1)close webSocket.close(0, “bye”);请求服务器优雅地关闭连接然后等待确认。在关闭之前,所有已经在队列中的消息将被传送完毕。 既然涉及到交互,那么socket可能不会立即关闭。如果初始化和关闭连接是和Activity的生命周期绑定的(比如onPause/onResume),有一些消息可能是在close被调用之后接收到,所以这需要小心去处理。
- 2)cancel;cancel更加残忍:它会丢弃所有已经在队列中的消息然后残忍地关闭socket。这样也有优点:不必等待家政(housekeeping)和已在队列中消息的传送。然而,选择cancel还是close取决于使用场景。