【socket】- 客户端源码分析

简介

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

构造体

  • Socket()

    void setImpl() {
        if (factory != null) {
            impl = factory.createSocketImpl();
            checkOldImpl();
        } else {
            impl = new SocksSocketImpl();
        }
        if (impl != null)
            impl.setSocket(this);
    }
    
    1. factory
      创建Socket具体实现的工厂对象,调用setSocketImplFactory方法进行设置,注意该变量只允许设置一次,设置多次会导致异常。一般情况是不需要设置的,这里将会使用默认的实现类SocksSocketImpl。里面保存了服务器的地址(server)和端口号(serverPort)。
  • Socket(Proxy proxy)
    创建具有代理功能的Socket对象。

    1. Proxy
      包含了两个属性,Type和SocketAddress。Type有三种类型,分别是:

      • DIRECT:表示直接连接或缺省代理
      • HTTP:高级协议(如HTTP或FTP)的代理
      • SOCKS:表示SOCKS(V4或V5)代理。

      SocketAddress具体实现类是InetSocketAddress,存储地址相关的变量和地址相关的操作。里面存储了三个属性值。分别是:

      • hostname:Socket地址的主机名
      • addr:Socket地址的IP地址
      • port:Socket地址的端口号

      addr是InetAddress对象,具体实现类有Inet4Address和Inet6Address,InetAddress存储了4个属性值,其中family指定地址族类型,例如,IPv4地址是AF_INET和IPv6地址是AF_INET6。

绑定(bind)

无连接的socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。使用bind函数时,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。
Bind()函数在成功被调用时返回0;出现错误时返回"-1"并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。

有连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息,无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。(当然也有特殊情况,linux系统中rlogin命令应当调用bind函数绑定一个未用的保留端口号,还有当客户端需要用指定的网络设备接口和端口号进行通信等等)
总之:

  1. 需要在建连前就知道端口的话,需要 bind
  2. 需要通过指定的端口来通讯的话,需要 bind

连接(connect)

public void connect(SocketAddress endpoint, int timeout) throws IOException {
   if (!created)
        createImpl(true);
   if (!oldImpl)
        impl.connect(epoint, timeout);
   else if (timeout == 0) {
       if (epoint.isUnresolved())
           impl.connect(addr.getHostName(), port);
       else
           impl.connect(addr, port);
    } else
        throw new UnsupportedOperationException("SocketImpl.connect(addr, timeout)");
    connected = true;
    bound = true;
}
  1. createImpl(boolean stream)
    创建流或数据报套接字,stream为true时表示创建的是TCP socket,为false代表创建的是UDP socket。

接下来看一下,socket具体实现类里面的connect。

  • connect(SocketAddress endpoint, int timeout)
    1. 创建套接字并将其连接到指定端口和地址上
      privilegedConnect(server, serverPort, remainingMillis(deadlineMillis)).

    2. 通过socket输出流创建BufferedOutputStream 对象向输出流写socket协议。

        if (useV4) {
            // SOCKS Protocol version 4 doesn't know how to deal with
            // DOMAIN type of addresses (unresolved addresses here)
            if (epoint.isUnresolved())
                throw new UnknownHostException(epoint.toString());
            connectV4(in, out, epoint, deadlineMillis);
            return;
        }
      

      如果使用的是SOCKS Protocol version 4,这调用connectV4方法。

      private void connectV4(InputStream in, OutputStream out,
                           InetSocketAddress endpoint,
                           long deadlineMillis) throws IOException {
        ...
        out.write(PROTO_VERS4);
        out.write(CONNECT);
        out.write((endpoint.getPort() >> 8) & 0xff);
        out.write((endpoint.getPort() >> 0) & 0xff);
        out.write(endpoint.getAddress().getAddress());
        String userName = getUserName();
        try {
            out.write(userName.getBytes("ISO-8859-1"));
        } catch (java.io.UnsupportedEncodingException uee) {
            assert false;
        }
        out.write(0);
        out.flush();
        byte[] data = new byte[8];
        int n = readSocksReply(in, data, deadlineMillis);      
        ...
      }
      

      下面看一下SOCKS Protocol version 4协议的具体内容。参考:http://www.openssh.com/txt/socks4.protocol。下面只讲解connect的协议,bind过程自行查看上面连接文档。

      1. connect过程
        客户端连接到SOCKS服务器,当它想要与服务器建立连接时,客户端发送CONNECT请求。客户端在请求包中包含IP地址和目标主机端口号,userid,格式如下。

         +----+----+----+----+----+----+----+----+----+----+....+----+
         | VN | CD | DSTPORT |      DSTIP        | USERID       |NULL|
         +----+----+----+----+----+----+----+----+----+----+....+----+
            1    1      2              4           variable       1        
        

        上面数字表示占用多少个字节。VN是SOCKS协议版本号,对应4. CD是
        SOCKS命令代码,connect对应1。 NULL是一个字节所有为零。

        如果请求被授予,则SOCKSserver建立与目标主机的指定端口的连接。
        建立此连接后或当请求被拒绝或操作失败时,将回复数据包发送到客户端。数据包格式如下:

          +----+----+----+----+----+----+----+----+
           | VN | CD | DSTPORT |      DSTIP  |
          +----+----+----+----+----+----+----+----+
             1    1      2              4
        

        上面数字表示占用多少个字节。VN是回复代码的版本,应为0(从Android Socket源码里面看,0或者4都是可以的). CD是结果,有以下取值:

        1. 90: 请求成功
        2. 91: 请求失败或者被拒绝
        3. 92: 请求被拒绝因为SOCKS服务器无法连接到客户端
        4. 93: 请求被拒绝,用户ID不符
        5. 其它值 :请求失败

      下面看一下SOCKS Protocol version 5协议的具体内容。参考:https://tools.ietf.org/html/rfc1928。下面只讲解connect的协议,bind过程自行查看上面连接文档。

      1. 客户端连接到服务器,并发送版本标识符/方法选择消息:

         +----+----------+----------+
         |VER | NMETHODS | METHODS  |
         +----+----------+----------+
         | 1  |    1     | 1 to 255 |
         +----+----------+----------+
        

        VER:协议版本,设置为5
        NMETHODS:METHODS占用的字节数
        METHODS:方法参数集合,取值可以看下面 METHOD的取值

      2. 服务器从METHODS给出的方法中选择之一并发送METHOD选择消息给客户端,格式如下:

         +----+--------+
         |VER | METHOD |
         +----+--------+
         | 1  |   1    |
         +----+--------+
        

        如果选择的METHOD是'FF',则没有列出的方法,客户端是可以接受的,客户端必须关闭连接。METHOD值如下:

        X表示单个8位字节
        -----------------------------------
        X'00' NO AUTHENTICATION REQUIRED
        X'01' GSSAPI
        X'02' USERNAME/PASSWORD
        X'03' to X'7F' IANA ASSIGNED
        X'80' to X'FE' RESERVED FOR PRIVATE METHODS
        X'FF' NO ACCEPTABLE METHODS
        
      3. SOCKS请求形成如下:

        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+
        

        取值如下:

        VER    protocol version: X'05'
        CMD
        CONNECT X'01'
        BIND X'02'
        UDP ASSOCIATE X'03'
        RSV    RESERVED
        ATYP   address type of following address
        IP V4 address: X'01'
        DOMAINNAME: X'03'
        IP V6 address: X'04'
        DST.ADDR       desired destination address
        DST.PORT desired destination port in network octet order
        

        回复格式如下:

        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+
        

        取值如下:

        VER    protocol version: X'05'
        REP    Reply field:
               X'00' succeeded
               X'01' general SOCKS server failure
               X'02' connection not allowed by ruleset
               X'03' Network unreachable
               X'04' Host unreachable
               X'05' Connection refused
               X'06' TTL expired
               X'07' Command not supported
               X'08' Address type not supported
               X'09' to X'FF' unassigned
        RSV    RESERVED(must be set to X'00')
        ATYP   address type of following address
        IP V4 address: X'01'
        DOMAINNAME: X'03'
        IP V6 address: X'04'
        BND.ADDR       server bound address
        BND.PORT       server bound port in network octet order
        
      4. Socket认证
        SOCKS Protocol Version 5实现将在后面具体分析。

Socket客户端实现

    override fun connect(ip: String, port: Int) {
        lock.lock()
        if (isConnected()){
            disConnect(false)
        }
        connectState = SState.STATE_CONNECTING
        this.ip = ip
        this.port = port
        Log.i(TAG,"connecting  ip=$ip , port = $port")
        try {
            while (true){
                try {
                    socket = Socket()
                    if (null == socket){
                        throw (Exception("connect failed,unknown error"))
                    }

                    val address = InetSocketAddress(ip,port)
                    socket!!.bind(address)
                    socket!!.keepAlive = false
                    //inputStream read 超时时间
                    socket!!.soTimeout = 2 * 3 * 60 * 1000
                    socket!!.tcpNoDelay = true
                    if (socket!!.isConnected){
                        dataInputStream = DataInputStream(socket!!.getInputStream())
                        dataOutputStream = DataOutputStream(socket!!.getOutputStream())
                        connectState = SState.STATE_CONNECTED
                        this.sCallback.onConnect()
                        break
                    }else{
                        throw (Exception("connect failed,unknown error"))
                    }
                }catch (e:Exception){
                    cRetryPolicy?.retry(e)
                    Thread.sleep(5*1000)
                    Log.i(TAG,"connect IOException =${e.message} , and retry count = ${cRetryPolicy?.getCurrentRetryCount()}")
                }
            }
        }catch (e:Exception){
            e.printStackTrace()
            Log.i(TAG,"connect IOException =  ${e.message}")
            connectState = SState.STATE_CONNECT_FAILED
            sCallback.onConnectFailed(e)
        }finally {
            lock.unlock()
        }
        if (connectState == SState.STATE_CONNECTED){
            receiveData()
        }
    }

完整代码将在Socket系列文章完成后给出。

你可能感兴趣的:(【socket】- 客户端源码分析)