运用场景
当A端无法直接拜访C端网站,而B端能够拜访C端网站,一起A端又能够拜访B端网站时,咱们能够在B端布置一个socks5署理服务。这样,A端就能够经过B端的署理服务,间接地拜访C端网站。这种办法有效地处理了A端无法直接拜访C端网站的问题。
socks5协议
1.客户端衔接恳求(客户端发送到服务器):
客户端发送的第一个数据包,用于洽谈版别和认证办法。
数据包格局:
---- ---------- ----------
| VER| NMETHODS | METHODS |
---- ---------- ----------
| 1 | 1 | 1 to 255 |
---- ---------- ----------
- VER:SOCKS版别,当前为 5。
- NMETHODS:认证办法数量。
- METHODS:认证办法列表,每个字节代表一种认证办法,可选值包括 0x00(无认证)和其他认证办法。
2.服务器回应版别洽谈(服务器发送到客户端):
服务器回应客户端的版别洽谈恳求,挑选一个认证办法。
数据包格局:
---- --------
| VER| METHOD |
---- --------
| 1 | 1 |
---- --------
- VER:SOCKS版别,当前为 5。
- METHOD:挑选的认证办法,假如为 0xFF 表明无可承受的办法。
3.客户端向服务器发送认证信息(客户端发送到服务器):
客户端依据服务器挑选的认证办法,向服务器发送认证信息。
数据包格局:
---- ------ ----------
| VER| ULEN | UNAME |
---- ------ ----------
| 1 | 1 | 1 to 255 |
---- ------ ----------
- VER:SOCKS版别,当前为 5。
- ULEN:用户名的长度。
- UNAME:用户名,长度由 ULEN 指定。
4.服务器回应认证成果(服务器发送到客户端):
服务器依据客户端发送的认证信息进行验证,并回应认证成果。
数据包格局:
---- --------
| VER| STATUS |
---- --------
| 1 | 1 |
---- --------
- VER:SOCKS版别,当前为 5。
- STATUS:认证成果,0 表明成功,其他值表明失败。
5.树立衔接恳求(客户端发送到服务器):
客户端发送树立衔接恳求,恳求与方针服务器树立衔接。
数据包格局:
---- ----- ------- ------ ---------- ----------
| VER| CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---- ----- ------- ------ ---------- ----------
| 1 | 1 | X'00' | 1 | Variable | 2 |
---- ----- ------- ------ ---------- ----------
- VER:SOCKS版别,当前为 5。
- CMD:命令,1 表明衔接,2 表明绑定。
- RSV:保存字段,有必要为 0x00。
- ATYP:地址类型,1 表明 IPv4 地址,3 表明域名,4 表明 IPv6 地址。
- DST.ADDR:方针地址,长度和类型由 ATYP 指定。
- DST.PORT:方针端口,2 个字节表明。
6.服务器回应树立衔接成果(服务器发送到客户端):
服务器依据客户端的衔接恳求,向客户端发送树立衔接的成果。
数据包格局:
---- ----- ------- ------ ---------- ----------
| VER| REP | RSV | ATYP | BND.ADDR | BND.PORT |
---- ----- ------- ------ ---------- ----------
| 1 | 1 | X'00' | 1 | Variable | 2 |
---- ----- ------- ------ ---------- ----------
- VER:SOCKS版别,当前为 5。
- REP:应对,0 表明成功,其他值表明失败。
- RSV:保存字段,有必要为 0x00。
- ATYP:地址类型,1 表明 IPv4 地址,3 表明域名,4 表明 IPv6 地址。
- BND.ADDR:绑定地址,长度和类型由 ATYP 指定。
- BND.PORT:绑定端口,2 个字节表明。
个人了解
socks5协议比较简略,就是客户端供给socks版别和支撑认证办法,然后服务端挑选socks版别和认证办法,客户端就依照认证办法供给数据发送给服务端,服务端就开端认证数据。客户端收到验证成功,就会发送ip和端口等信息给服务端,最终就是客户端和服务端交流数据。
NIO 和 Selector
Java NIO(New I/O,即新输入/输出)是Java供给的一种根据通道(Channel)和缓冲区(Buffer)的I/O操作办法,与传统的输入/输出流比较,Java NIO 供给了更灵敏、更高效的I/O操作办法。
我这里运用java NIO
(零复制),依照正常逻辑在window系统拿到数据得先复制到java虚拟机上,java运用才干操作,NIO
就少了复制到java虚拟机上操作(少了中间商),这样就削减数据在内核态和用户态之间进行频繁的复制。在配合运用Selector
经过挑选器来完成单线程一起处理多个通道的 I/O 操作,正常一个socket
有读和写,读是得用一个线程去死循环读数据,假如不运用Selector
那不是每个socket都得创立一个读线程(也能够自己运用线程池)。
TCP
服务端 ServerSocketChannel
类,注册事情:SelectionKey.OP_ACCEPT
(承受衔接事情) ->
SelectionKey.OP_READ
(读取数据事情)
TCP
客户端 ServerSocketChannel
类,注册事情:SelectionKey.OP_CONNECT
(衔接事情) -> SelectionKey.OP_READ
(读取数据事情)
运用Selector
得设置Channel.configureBlocking(false)
(设置通道非堵塞),selector.select()
堵塞获取事情,isAcceptable
有客户端衔接服务端触发,isConnectable()
客户端衔接触发,key.isReadable()
channel通道有数据触发。
ByteBuffer
支撑读和写操作,channel.read(byteBuffer)
是将数据写入byteBuffer中,得调用ByteBuffer.flip()
来切换读形式,因为数据写入byteBuffer游标跟着移动到后面,你想读取之前写入的数据那不是得把游标初始为0后读取数据。
具体代码
当咱们拜访一个网站,经过咱们的 SOCKS5 署理服务,大约流程:
- 首先,客户端与 SOCKS5 署理服务器树立衔接。署理服务器监听来自客户端的衔接恳求,并在收到恳求时履行
key.isAcceptable()
中的办法。在这一步,署理服务器会承受客户端的衔接,并将客户端的channel
注册为读事情到selector
,并传递标志 1。 - 客户端衔接成功后,会向署理服务器发送数据。此时,署理服务器履行
key.isReadable()
中的办法,初始传递的标志为 1。这时履行negotiatedConnection
办法,用于洽谈衔接。洽谈完成后,标志被设置为 2。 - 接着,客户端发送要衔接的方针服务器的 IP 地址和端口数据包给署理服务器。署理服务器再次履行
key.isReadable()
中的办法,此时标志为 2。履行connectServer
办法,解析方针服务器的 IP 地址和端口,测验与方针服务器树立衔接。 - 因为
channel
是设置为非堵塞的,衔接进程需求注册到selector
。这时署理服务器会履行key.isConnectable()
中的办法,假如没有反常,说明衔接成功树立。此时,两边的channel
都注册到selector
,而且attach
参数是对方的channel
。 - 最终,两边通道有数据时,会写入对方的通道中,完成数据交流和传输。
你能够debug的一下断点,看看代码履行流程,不杂乱。
public static void main(String[] args) throws IOException {
//创立一个tcp服务端
int port = 1080;
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("tcp服务端发动成功,端口:" port);
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
try{
if (key.isAcceptable()) {
//服务端接收客户端恳求
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ,1);
} else if(key.isConnectable()){
//本地衔接
SocketChannel socketChannel = (SocketChannel) key.channel();
Object[] attachment = (Object[])key.attachment();
SocketChannel clientChannel = (SocketChannel)attachment[0];
byte[] replyData = (byte[])attachment[1];
try {
// 完成衔接[这里会呈现超时反常]
socketChannel.finishConnect();
//两个channel交流数据
socketChannel.register(selector,SelectionKey.OP_READ,clientChannel);
//从头注册事情
clientChannel.register(selector, SelectionKey.OP_READ,socketChannel);
//告诉客户端衔接成功
clientChannel.write(ByteBuffer.wrap(replyData));
} catch (Exception ex) {
ex.printStackTrace();
socketChannel.close();
clientChannel.close();
}
}else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
Object attachment = key.attachment();
ByteBuffer buffer = ByteBuffer.allocate(1024);
try{
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
socketChannel.close();
} else if (bytesRead > 0) {
buffer.flip();
if(attachment instanceof Integer){
//协议和认证确认,然后衔接
int i = (Integer) attachment;
if(i == 1){
//第一步 洽谈衔接
negotiatedConnection(socketChannel,buffer);
key.attach(2);
}else if(i == 2){
//第二步 衔接客户端发送的ip和端口
connectServer(selector,socketChannel,buffer);
key.attach(3);
}
}else {
//一个channel写入另一个channel
SocketChannel clientChannel = (SocketChannel) attachment;
if(clientChannel.isConnected()){
clientChannel.write(buffer);
}
}
}
}catch (Exception e){
e.printStackTrace();
socketChannel.close();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
selector.selectedKeys().clear();
}
}
connectServer
办法,判断socks版别和是否支撑不验证。
private static void negotiatedConnection(SocketChannel socketChannel,ByteBuffer buffer) throws IOException {
//SOCKS版别
byte version = buffer.get();
//认证办法数量
byte nmethods = buffer.get();
//认证办法
boolean noAuth = false;
byte[] methods = new byte[nmethods];
for (byte b = 0; b < nmethods; b ) {
methods[b] = buffer.get();
//无需验证
if(methods[b] == 0){
noAuth = true;
}
}
//协议是socks5,支撑无需求验证
if(version == 5 && noAuth){
//回复客户端,版别5,无需验证
byte[] reply = {5,0};
socketChannel.write(ByteBuffer.wrap(reply));
}else{
//不支撑,关闭衔接
socketChannel.close();
}
}
connectServer
办法,解分出ip和端口而且创立客户端衔接,然后把衔接事情注册到select
。
private static void connectServer(Selector selector,SocketChannel socketChannel, ByteBuffer buffer) throws IOException {
//SOCKS版别
byte version = buffer.get();
//CMD:命令,1 表明衔接,2 表明绑定。
byte cmd = buffer.get();
//保存字段,有必要为 0x00。
byte rsv = buffer.get();
//地址类型,1 表明 IPv4 地址,3 表明域名,4 表明 IPv6 地址
byte atyp = buffer.get();
String host = "";
if(atyp == 1){
//ipv4
byte[] bt = new byte[4];
buffer.get(bt);
host = InetAddress.getByAddress(bt).getHostAddress();
}else if(atyp == 3){
//域名
byte len = buffer.get();
byte[] bt = new byte[len];
buffer.get(bt);
host = new String(bt);
}else if(atyp == 4){
//ipv6
byte[] bt = new byte[16];
buffer.get(bt);
host = InetAddress.getByAddress(bt).getHostAddress();
}
int port = Short.toUnsignedInt(buffer.getShort());
System.out.println("host:" host ",port:" port);
InetSocketAddress address = new InetSocketAddress(host, port);
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(address);
byte[] replyData = Arrays.copyOf(buffer.array(), buffer.position());
//应对成功
replyData[1] = 0;
Object[] array = new Object[2];
array[0] = socketChannel;
array[1] = replyData;
clientChannel.register(selector,SelectionKey.OP_CONNECT,array);
}
测验
先简略运用curl
设置SOCKS5署理,在cmd
下履行下面代码:
curl -x socks5://127.0.0.1:1080 http://www.baidu.com
看看成果没啥问题。
最终咱们用浏览器去测验,直接运用火狐浏览器,他直接能够设置socks署理。其他浏览器得装插件,费事点在windows下设置署理脚本。
下面是在火狐浏览器设置socks5署理,打开火狐浏览器到设置页面
我测验了拜访b站和百度没啥问题。
总结
你会发现Selector是用一个线程在跑,按理来说读到数据应该用多线程去跑,但是发现读到数据后面操作都不会堵塞线程,假如加多线程又会导致数据乱序写入channel,得用Future
等待上一次数据写完才干写,那不如多开几个线程履行Selector
给事情注册,或者运用AIO编写代码。netty框架有现成得,主要是自己完成。