【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系列文章完成後給出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章