运用场景

当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 署理服务,大约流程:

  1. 首先,客户端与 SOCKS5 署理服务器树立衔接。署理服务器监听来自客户端的衔接恳求,并在收到恳求时履行 key.isAcceptable() 中的办法。在这一步,署理服务器会承受客户端的衔接,并将客户端的 channel 注册为读事情到 selector,并传递标志 1。
  2. 客户端衔接成功后,会向署理服务器发送数据。此时,署理服务器履行 key.isReadable() 中的办法,初始传递的标志为 1。这时履行 negotiatedConnection 办法,用于洽谈衔接。洽谈完成后,标志被设置为 2。
  3. 接着,客户端发送要衔接的方针服务器的 IP 地址和端口数据包给署理服务器。署理服务器再次履行 key.isReadable() 中的办法,此时标志为 2。履行 connectServer 办法,解析方针服务器的 IP 地址和端口,测验与方针服务器树立衔接。
  4. 因为channel是设置为非堵塞的,衔接进程需求注册到 selector。这时署理服务器会履行 key.isConnectable() 中的办法,假如没有反常,说明衔接成功树立。此时,两边的channel都注册到 selector,而且 attach 参数是对方的channel
  5. 最终,两边通道有数据时,会写入对方的通道中,完成数据交流和传输。

你能够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

看看成果没啥问题。

运用Java NIO 简略完成socks5署理服务

最终咱们用浏览器去测验,直接运用火狐浏览器,他直接能够设置socks署理。其他浏览器得装插件,费事点在windows下设置署理脚本。

下面是在火狐浏览器设置socks5署理,打开火狐浏览器到设置页面

运用Java NIO 简略完成socks5署理服务

我测验了拜访b站和百度没啥问题。

总结

你会发现Selector是用一个线程在跑,按理来说读到数据应该用多线程去跑,但是发现读到数据后面操作都不会堵塞线程,假如加多线程又会导致数据乱序写入channel,得用Future等待上一次数据写完才干写,那不如多开几个线程履行Selector给事情注册,或者运用AIO编写代码。netty框架有现成得,主要是自己完成。

代码: Socks5Server-01 断续/learn-demo – 码云 – 开源中国 (gitee.com)