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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容

  • 网络编程 一.楔子 你现在已经学会了写python代码,假如你写了两个python文件a.py和b.py,分别去运...
    go以恒阅读 2,021评论 0 6
  • 大纲 一.Socket简介 二.BSD Socket编程准备 1.地址 2.端口 3.网络字节序 4.半相关与全相...
    VD2012阅读 2,344评论 0 5
  • 最近在学习Python看了一篇文章写得不错,是在脚本之家里的,原文如下,很有帮助: 一、网络知识的一些介绍 soc...
    qtruip阅读 2,717评论 0 6
  • http://python.jobbole.com/85231/ 关于专业技能写完项目接着写写一名3年工作经验的J...
    燕京博士阅读 7,579评论 1 118
  • 1. Socket地址数据类型及相关函数 IPv4和IPv6的地址格式定义在netinet/in.h中 IPv4地...
    執著我們的執著阅读 1,720评论 0 0