精华内容
参与话题
问答
  • TCP建立连接的过程

    千次阅读 2018-08-03 16:13:59
    TCP建立连接的过程 转载:https://blog.csdn.net/caoyan_12727/article/details/52081269    在可靠的TCP网络通信中,客户端和服务器端通信建立连接的过程可简单表述为三次握手(建立连接的阶段)和四次挥手(释放...

                                            TCP建立连接的过程

    转载:https://blog.csdn.net/caoyan_12727/article/details/52081269

     

     在可靠的TCP网络通信中,客户端和服务器端通信建立连接的过程可简单表述为三次握手(建立连接的阶段)和四次挥手(释放连接阶段),下图是这两个阶段的一个完整的表述:

    其状态图可以表示为,

    在TCP连接建立的时候,存在一个如下的有限状态机:

           在状态转化图中,其中客户端的状态转移用带箭头的粗实线表示,服务器端的状态转换用带箭头的粗虚线表示。带箭头的细线表示一些不常见的事件,如复位、同时打开、同时关闭等。关于有限状态图可以参考博客http://blog.csdn.net/lycb_gz/article/details/8515062,里面的细节都将的很清楚;如果要深入理解TCP连接建立和释放的过程就需要结合socket编程里的connect(),socket(),bind(),listen(),send(),close()等函数。

    从图中看到,三次握手对应的Berkeley Socket API:connect, listen, accept   3个,connect用在客户端,另外2个用在服务端。对于TCP/IP protocol stack来说,TCP层的tcp_in&tcp_out也参与这个过程。我们这里只讨论这3个应用层的API干了什么事情。

    (1) connect()
    发送了一个SYN,收到Server的SYN+ACK后,代表连接完成。发送最后一个ACK是protocol stack,tcp_out完成的。
    (2)listen()
    在server这端,准备了一个未完成的连接队列,保存只收到SYN_C的socket结构;还准备了已完成的连接队列,即保存了收到了最后一个ACK的socket结构。
    (3)accept()
    应用进程调用accept的时候,就是去检查上面说的已完成的连接队列,如果队列里有连接,就返回这个连接;如果没有,即空的,blocking方试调用,就睡眠等待;客户端调用connect函数之后就发起完成TCP的三次握手,客户端调用connect后,由内核中的TCP协议完成TCP的三次握手,close操作会完成四次挥手。

    其中accept发生在三次握手之后。
    第一次握手:客户端发送syn包(syn=j)到服务器。
    第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个ASK包(ask=k)。
    第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)。
    三次握手完成后,客户端和服务器就建立了tcp连接。这时可以调用accept函数获得此连接。

    我们如何判断有一个建立链接请求或一个关闭链接请求:
    建立链接请求:
    1、connect将完成三次握手,accept所监听的fd上,产生读事件,表示有新的链接请求,但此时accept函数并没有调用,在内核中维持了一个完成连接的队列;           
    关闭链接请求:
    1、close将完成四次挥手,如果有一方关闭sockfd,对方将感知到有读事件,如果read读取数据时,返回0,即读取到0个数据,表示有断开链接请求。(在操作系统中已经这么定义) 关闭链接过程中的TCP状态和SOCKET处理,及可能出现的问题:
     

    1. TIME_WAIT
    TIME_WAIT 是主动关闭 TCP 连接的那一方出现的状态,系统会在 TIME_WAIT 状态下等待 2MSL(maximum segment lifetime  )后才能释放连接(端口)。通常约合 4 分钟以内。TIME_WAIT 状态等待 2MSL 的意义:
    1、确保连接可靠地关闭; 即防止最后一个ACK丢失。
    2、避免产生套接字混淆(同一个端口对应多个套接字)。
     为什么说可以用来避免套接字混淆呢?一方close发送了关闭链接请求,对方的应答迟迟到不了(例如网络原因),导致TIME_WAIT超时,此时这个端口又可用了,我们在这个端口上又建立了另外一个socket链接。 如果此时对方的应答到了,怎么处理呢?其实这个在TCP层已经处理了,由于有TCP序列号,所以内核TCP层,就会将包丢掉,并给对方发包,让对方将sockfd关闭。所以应用层是没有关系的。即我们用socket API编写程序,就不用处理。
    注意:TIME_WAIT是指操作系统的定时器会等2MSL,而主动关闭sockfd的一方,并不会阻塞。(即应用程序在close时,并不会阻塞)。当主动方关闭sockfd后,对方可能不知道这个事件。那么当对方(被动方)写数据,即send时,将会产生错误,即errno为: ECONNRESET。服务器产生大量 TIME_WAIT 的原因:(一般我们不这样开发Server,但是web服务器等这种多客户端的Server,是需要在完成一次请求后,主动关闭连接的,否则可能因为句柄不够用,而造成无法提供服务。)服务器存在大量的主动关闭操作,需关注程序何时会执行主动关闭(如批量清理长期空闲的套接字等操作)。一般我们自己写的服务器进行主动断开连接的不多,除非做了空闲超时之类的管理。(TCP短链接是指,客户端发送请求给服务器,客户端收到服务器端的响应后,关闭链接)。
     

    2. CLOSE_WAIT
    CLOSE_WAIT 是被动关闭 TCP 连接时产生的,如果收到另一端关闭连接的请求后,本地(Server端)不关闭相应套接字就会导致本地套接字进入这一状态。
    (如果对方关闭了,没有收到关闭链接请求,就是下面的不正常情况)按TCP状态机,我方收到FIN,则由TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果我方不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。如果存在大量的 CLOSE_WAIT,则说明客户端并发量大,且服务器未能正常感知客户端的退出,也并未及时 close 这些套接字。(如果不及时处理,将会出现没有可用的socket描述符的问题,原因是sockfd耗尽)。
    正常情况下:一方关闭sockfd,另外一方将会有读事件产生, 当recv数据时,如果返回值为0,表示对端已经关闭。此时我们应该调用close,将对应的sockfd也关闭掉。
    不正常情况下:一方关闭sockfd,另外一方并不知道,(比如在close时,自己断网了,对方就收不到发送的数据包)。此时,如果另外一方在对应的sockfd上写send或读recv数据。
    recv时,将会返回0,表示链接已经断开。
    send时, 将会产生错误,errno为ECONNRESET。

     

    3.close()函数和shutdown()函数的区别:

    首先我们来看看close()函数的原型:

     

    
     头文件:#include <unistd.h>
    
    定义函数:int close(int fd);

     

    close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。如果是主动调用close()则会发起内核TCP协议的四次挥手,断开连接.

     

    再来看看shutdown()函数的原型:

     

    
     int shutdown(int sockfd,int howto); //返回成功为0,出错为-1.
    
    该函数的行为依赖于howto的值
    
    1.SHUT_RD:值为0,关闭连接的读这一半。
    
    2.SHUT_WR:值为1,关闭连接的写这一半。
    
    3.SHUT_RDWR:值为2,连接的读和写都关闭。
    
    终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。

     

    当调用SHUT_RD的时候,套接字sockfd的读端将会关闭,不能调用接受数据的函数,这对于协议层没有影响。然和当前在sockfd读端的数据缓冲区的数据都会被舍弃掉,进程将不能对该套接字发起读操作,对TCP套接字调用SHUT_RD将会导致协议层将接收到的数据无声的丢掉!!!如果想要继续接受数据都要重置链接;
    当调用SHUT_WR的时候,对于tcp套接字来说,这意味着会在所有数据发送出并得到接受端确认后产生一个FIN包。而此时套接字的状态会由ESTABLISHED变成FIN_WAIT_1,然后对方发送一个 ACK包作为回应,套接字又变成FIN_WAIT_2。如果对方也关闭了连接则对方会发出FIN,我方会回应一个ACK并将套接字置为 TIME_WAIT。

     

    4.如何判断socket连接断开:

    非阻塞模式,如果暂时没有数据,返回的值也会是<=0的,如果用阻塞模式的话,返回<=0的值是可以认为socket已经无效了。当使用 select()函数测试一个socket是否可读时,如果select()函数返回值为1,且使用recv()函数读取的数据长度为0 时,就说明该socket已经断开。经过代码试验,如果进程受到一些信号时,例如:EINTR,recv()返回值小于等于0时,这是就需要判断 errno是否等于 EINTR , 如果errno == EINTR 则说明recv函数是由于程序接收到信号后返回的,socket连接还是正常的,不应close掉socket连接。如果write,我觉得还有一些情况需要考虑,那就是写的太快的时候,有可能buffer写满了,errno是EAGAIN,可以根据实际需要,如果errno是EAGAIN的话,再写几次。

     

    展开全文
  • WebSocket安卓客户端实现详解(一)--连接建立与重连

    万次阅读 多人点赞 2017-06-10 00:07:03
    今年在公司第一个需求就是基于websocket写一个客户端消息中心,现在已经上线很久了在司机这种网络环境平均一天重连8次,自认为还是不错的.当时写的时候那个心酸啊,主要因为第一次写都不知道该从哪下手,没有方向....

    今年在公司第一个需求就是基于websocket写一个客户端消息中心,现在已经上线很久了在司机这种网络环境平均一天重连8次,自认为还是不错的.当时写的时候那个心酸啊,主要因为第一次写都不知道该从哪下手,没有方向.所以这里我将尽可能详细的跟大家分享出来.

    本篇内容会比较多,先来段舞蹈热身下.

    我准备按如下顺序来讲解

    1. 整体流程的一个概括了解大体思路.

    2. 把大体流程细化,逐步去实现.

    前言

    这里特别说明下因为WebSocket服务端是公司线上项目所以这里url和具体协议我全部抹去了,但我会尽力给大家讲明白并且demo我都是测试过,还望各位看官见谅

    我们先粗犷的讲下流程,掌握个大概的方向,然后在深入讲解细节的实现.这里先解答一个疑惑,为啥我们这要用WebSocket而不是Socket呢,因为WebSocket是一个应用层协议很多东西都规定好了我们直接按他的规定来用就好,而Socket是传输层和应用层的一个抽象层很多东西我们还得自己规定相对来说会比较麻烦,所以这里我们用的WebSocket.

    既然WebSocket是一个应用层协议,我们肯定不可能自己去实现,所以第一步是需要找一个实现了该协议的框架,这里我用的nv-websocket-client,api我就不介绍了,库中readme已经详细的介绍了,后面我就直接使用了.

    关于通讯协议为了方便,这里我们使用的是json.

    接下来我们先简单描述下我们将要做的事情

    用户登录流程

    第一步用户输入账号密码登录成功后,我们将会通过websocket协议建立连接,当连接失败回调的时候我们尝试重连,直到连接成功,当然这个尝试重连的时间间隔我是根据重连失败次数按一定规则写的具体后面再说.

    第二步当连接建立成功后,我们需要在后台通过长连接发送请求验证该用户的身份也就是上图的授权,既然前面用户登录都成功了一般情况下授权是不会失败的,所以这里对于授权失败并未处理,授权成功后我们开启心跳,并且发送同步数据请求到服务端获取还未收到的消息.

    客户端发送请求流程

    第一步将请求参数封装成请求对象,然后添加超时任务并且将该请求的回调添加到回调集合.

    这里有点需要说明下,封装请求参数的时候这里额外添加了两个参数seqId和reqCount,这里我们是通过长连接请求当服务端响应的时候为了能够找到对应的回调,所以每个请求我们都需要传给服务端一个唯一标识来标识该请求,这里我用的seqId,请求成功后服务端再把seqId回传,我们再通过这个seqId作为key从回调集合中找到对应的回调.而reqCount的话主要针对请求超时的情况,如果请求超时,第二次请求的时候就把reqCount++在放入request中,我们约定同一个请求次数大于三次时候走http补偿通道,那么当request中的reqCount>3的时候我们就通过http发送该请求,然后根据响应回调对应结果.

    第二步开始请求,成功或者失败的话通过seqId找到对应回调执行并从回调集合中移除该回调,然后取消超时任务.如果超时的话根据seqId拿到对应的回调并从回调集合中移除该回调,然后判断请求次数如果小于等于3次再次通过websocket尝试请求,如果大于3次通过http请求,根据请求成功失败情况执行对应回调.

    服务端主动推送消息流程

    先说明下这里服务端推送的消息仅仅是个事件,不携带具体消息.

    第一步根据notify中事件类型找到对应的处理类,一般情况下这里需要同步对应数据.

    第二步然后用eventbus通知对应的ui界面更新

    第三步如果需要ack,发送ack请求

    上面只是一个概括,对于心跳,重连,发送请求这里有不少细节需要注意的下一节我们将详细讲解

    具体实现

    理论说完了,接下来我们将一步步实现客户端代码.首先我们添加依赖

        compile 'com.neovisionaries:nv-websocket-client:2.2'
    

    然后创建一个单利的WsManager管理websocket供全局调用,

    public class WsManager {
    
        private static WsManager mInstance;
    
        private WsManager() {
        }
    
        public static WsManager getInstance(){
            if(mInstance == null){
                synchronized (WsManager.class){
                    if(mInstance == null){
                        mInstance = new WsManager();
                    }
                }
            }
            return mInstance;
        }
    }
    

    建立连接

    然后添加建立连接代码,这里关于WebSocket协议的操作用的都是nv-websocket-client,我也加上了详细的注释,实在不理解可以去读一遍readme文件.

    public class WsManager {
        private static WsManager mInstance;
        private final String TAG = this.getClass().getSimpleName();
    
        /**
         * WebSocket config
         */
        private static final int FRAME_QUEUE_SIZE = 5;
        private static final int CONNECT_TIMEOUT = 5000;
        private static final String DEF_TEST_URL = "测试服地址";//测试服默认地址
        private static final String DEF_RELEASE_URL = "正式服地址";//正式服默认地址
        private static final String DEF_URL = BuildConfig.DEBUG ? DEF_TEST_URL : DEF_RELEASE_URL;
        private String url;
    
        private WsStatus mStatus;
        private WebSocket ws;
        private WsListener mListener;
    
        private WsManager() {
        }
    
        public static WsManager getInstance(){
            if(mInstance == null){
                synchronized (WsManager.class){
                    if(mInstance == null){
                        mInstance = new WsManager();
                    }
                }
            }
            return mInstance;
        }
    
        public void init(){
            try {
              /**
               * configUrl其实是缓存在本地的连接地址
               * 这个缓存本地连接地址是app启动的时候通过http请求去服务端获取的,
               * 每次app启动的时候会拿当前时间与缓存时间比较,超过6小时就再次去服务端获取新的连接地址更新本地缓存
               */
                String configUrl = "";
                url = TextUtils.isEmpty(configUrl) ? DEF_URL : configUrl;
                ws = new WebSocketFactory().createSocket(url, CONNECT_TIMEOUT)
                    .setFrameQueueSize(FRAME_QUEUE_SIZE)//设置帧队列最大值为5
                    .setMissingCloseFrameAllowed(false)//设置不允许服务端关闭连接却未发送关闭帧
                    .addListener(mListener = new WsListener())//添加回调监听
                    .connectAsynchronously();//异步连接
                setStatus(WsStatus.CONNECTING);
                Logger.t(TAG).d("第一次连接");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    
        /**
         * 继承默认的监听空实现WebSocketAdapter,重写我们需要的方法
         * onTextMessage 收到文字信息
         * onConnected 连接成功
         * onConnectError 连接失败
         * onDisconnected 连接关闭
         */
        class WsListener extends WebSocketAdapter{
            @Override
            public void onTextMessage(WebSocket websocket, String text) throws Exception {
                super.onTextMessage(websocket, text);
                Logger.t(TAG).d(text);
            }
    
    
            @Override
            public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
                throws Exception {
                super.onConnected(websocket, headers);
                Logger.t(TAG).d("连接成功");
                setStatus(WsStatus.CONNECT_SUCCESS);
            }
    
    
            @Override
            public void onConnectError(WebSocket websocket, WebSocketException exception)
                throws Exception {
                super.onConnectError(websocket, exception);
                Logger.t(TAG).d("连接错误");
                setStatus(WsStatus.CONNECT_FAIL);
            }
    
    
            @Override
            public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer)
                throws Exception {
                super.onDisconnected(websocket, serverCloseFrame, clientCloseFrame, closedByServer);
                Logger.t(TAG).d("断开连接");
                setStatus(WsStatus.CONNECT_FAIL);
            }
        }
    
        private void setStatus(WsStatus status){
            this.mStatus = status;
        }
    
        private WsStatus getStatus(){
            return mStatus;
        }
    
        public void disconnect(){
            if(ws != null)
            ws.disconnect();
        }
    }
    
    public enum WsStatus {
        CONNECT_SUCCESS,//连接成功
        CONNECT_FAIL,//连接失败
        CONNECTING;//正在连接
    }
    

    从注释我们可以知道,这里我们是app启动的时候通过http请求获取WebSocket连接地址,如果获取失败就走本地默认的url建立连接.并且内部自己维护了一个websocket状态后面发送请求和重连的时候会用上.

    其实获取连接地址这个地方是可以优化的,就是app启动的时候先比较上次获取的时间如果大于6小时就通过http请求获取websocket的连接地址,这个地址应该是个列表,然后存入本地,连接的时候我们可以先ping下地址,选择耗时最短的地址接入.如果连不上我们在连耗时第二短的地址以此类推.但这里我们就以简单的方式做了.

    至于建立连接代码在哪调用的话,我选择的是主界面onCreate()的时候,因为一般能进入主界面了,就代表用户已经登录成功.

    WsManager.getInstance().init();
    

    断开连接的话在主界面onDestroy()的时候调用

    WsManager.getInstance().disconnect();
    

    重连

    建立连接有成功就有失败,对于失败情况我们需要重连,那么下面我们分别说明重连的时机,重连的策略和当前是否应该重连的判断.

    对于重连的时机有如下几种情况我们需要尝试重连

    1. 应用网络的切换.具体点就是可用网络状态的切换,比如4g切wifi连接会断开我们需要重连.

    2. 应用回到前台的时候,判断如果连接断开我们需要重连,这个是尽量保持当应用再前台的时候连接的稳定.

    3. 收到连接失败或者连接断开事件的时候,这个没什么好解释.

    4. 心跳连续3次失败时候.当然这个连续失败3次是自己定义的,大伙可以根据自己app的情况定制.

    等会我们先展示前三种情况,心跳失败这个在后面我们把客户端发送请求讲完再说.

    上面把需要重连的情景说了,现在讲讲具体的重连策略.

    这里我定义了一个最小重连时间间隔min和一个最大重连时间间隔max,当重连次数小于等于3次的时候都以最小重连时间间隔min去尝试重连,当重连次数大于3次的时候我们将重连地址替换成默认地址DEF_URL,将重连时间间隔按min*(重连次数-2)递增最大不不超过max.

    还有最后一个当前是否应该重连的判断

    1. 用户是否登录,可以通过本地是否有缓存的用户信息来判断.因为重连成功后我们需要将用户信息通过WebSocket发送到服务器进行身份验证所以这里必须登录成功.

    2. 当前连接是否可用,这个通过nv-websocket-client库中的api判断ws.isOpen().

    3. 当前不是正在连接状态,这里我们根据自己维护的状态来判断getStatus() != WsStatus.CONNECTING.

    4. 当前网络可用.

    下面我们show code.跟之前相同的代码这里就省略了

    public class WsManager {
    
        .....省略部分跟之前代码一样.....
    
        /**
         * 继承默认的监听空实现WebSocketAdapter,重写我们需要的方法
         * onTextMessage 收到文字信息
         * onConnected 连接成功
         * onConnectError 连接失败
         * onDisconnected 连接关闭
         */
        class WsListener extends WebSocketAdapter {
            @Override
            public void onTextMessage(WebSocket websocket, String text) throws Exception {
                super.onTextMessage(websocket, text);
                Logger.t(TAG).d(text);
            }
    
    
            @Override
            public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
                throws Exception {
                super.onConnected(websocket, headers);
                Logger.t(TAG).d("连接成功");
                setStatus(WsStatus.CONNECT_SUCCESS);
                cancelReconnect();//连接成功的时候取消重连,初始化连接次数
            }
    
    
            @Override
            public void onConnectError(WebSocket websocket, WebSocketException exception)
                throws Exception {
                super.onConnectError(websocket, exception);
                Logger.t(TAG).d("连接错误");
                setStatus(WsStatus.CONNECT_FAIL);
                reconnect();//连接错误的时候调用重连方法
            }
    
    
            @Override
            public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer)
                throws Exception {
                super.onDisconnected(websocket, serverCloseFrame, clientCloseFrame, closedByServer);
                Logger.t(TAG).d("断开连接");
                setStatus(WsStatus.CONNECT_FAIL);
                reconnect();//连接断开的时候调用重连方法
            }
        }
    
    
        private void setStatus(WsStatus status) {
            this.mStatus = status;
        }
    
    
        private WsStatus getStatus() {
            return mStatus;
        }
    
    
        public void disconnect() {
            if (ws != null) {
                ws.disconnect();
            }
        }
    
    
        private Handler mHandler = new Handler();
    
        private int reconnectCount = 0;//重连次数
        private long minInterval = 3000;//重连最小时间间隔
        private long maxInterval = 60000;//重连最大时间间隔
    
    
        public void reconnect() {
            if (!isNetConnect()) {
                reconnectCount = 0;
                Logger.t(TAG).d("重连失败网络不可用");
                return;
            }
    
            //这里其实应该还有个用户是否登录了的判断 因为当连接成功后我们需要发送用户信息到服务端进行校验
            //由于我们这里是个demo所以省略了
            if (ws != null &&
                !ws.isOpen() &&//当前连接断开了
                getStatus() != WsStatus.CONNECTING) {//不是正在重连状态
    
                reconnectCount++;
                setStatus(WsStatus.CONNECTING);
    
                long reconnectTime = minInterval;
                if (reconnectCount > 3) {
                    url = DEF_URL;
                    long temp = minInterval * (reconnectCount - 2);
                    reconnectTime = temp > maxInterval ? maxInterval : temp;
                }
    
                Logger.t(TAG).d("准备开始第%d次重连,重连间隔%d -- url:%s", reconnectCount, reconnectTime, url);
                mHandler.postDelayed(mReconnectTask, reconnectTime);
            }
        }
    
    
        private Runnable mReconnectTask = new Runnable() {
    
            @Override
            public void run() {
                try {
                    ws = new WebSocketFactory().createSocket(url, CONNECT_TIMEOUT)
                        .setFrameQueueSize(FRAME_QUEUE_SIZE)//设置帧队列最大值为5
                        .setMissingCloseFrameAllowed(false)//设置不允许服务端关闭连接却未发送关闭帧
                        .addListener(mListener = new WsListener())//添加回调监听
                        .connectAsynchronously();//异步连接
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
    
    
        private void cancelReconnect() {
            reconnectCount = 0;
            mHandler.removeCallbacks(mReconnectTask);
        }
    
    
        private boolean isNetConnect() {
            ConnectivityManager connectivity = (ConnectivityManager) WsApplication.getContext()
                .getSystemService(Context.CONNECTIVITY_SERVICE);
            if (connectivity != null) {
                NetworkInfo info = connectivity.getActiveNetworkInfo();
                if (info != null && info.isConnected()) {
                    // 当前网络是连接的
                    if (info.getState() == NetworkInfo.State.CONNECTED) {
                        // 当前所连接的网络可用
                        return true;
                    }
                }
            }
            return false;
        }
    }
    

    上面代码通过handler实现了一定时间间隔的重连,然后我们在WsListener监听中的onConnectError()onDisconnected()调用了reconnect()实现重连,onConnected()中调用了cancelReconnect()取消重连并初始化重连次数.

    所以当需要重连的时候我们调用reconnect()方法,如果失败onConnectError()onDisconnected()回调会再次调用reconnect()实现重连,如果成功onConnected()中会调用cancelReconnect()取消重连并初始化重连次数.

    并且这里我们已经实现了需要重连的情景3,收到连接失败或者连接断开事件的时候进行重连.

    接下来我们实现情景1和2

    1. 应用网络的切换.具体点就是可用网络状态的切换,比如4g切wifi连接会断开我们需要重连.

    2. 应用回到前台的时候,判断如果连接断开我们需要重连,这个是尽量保持当应用再前台的时候连接的稳定.

    对于可用网络的切换这里通过广播来监听实现重连

    public class NetStatusReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
    
                // 获取网络连接管理器
                ConnectivityManager connectivityManager
                    = (ConnectivityManager) WsApplication.getContext()
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
                // 获取当前网络状态信息
                NetworkInfo info = connectivityManager.getActiveNetworkInfo();
    
                if (info != null && info.isAvailable()) {
                    Logger.t("WsManager").d("监听到可用网络切换,调用重连方法");
                    WsManager.getInstance().reconnect();//wify 4g切换重连websocket
                }
    
            }
        }
    }
    

    应用回到前台情况的重连.

    通过Application.ActivityLifecycleCallbacks实现app前后台切换监听如下

    public class ForegroundCallbacks implements Application.ActivityLifecycleCallbacks {
    
        public static final long CHECK_DELAY = 600;
        public static final String TAG = ForegroundCallbacks.class.getName();
        private static ForegroundCallbacks instance;
        private boolean foreground = false, paused = true;
        private Handler handler = new Handler();
        private List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
        private Runnable check;
    
        public static ForegroundCallbacks init(Application application) {
            if (instance == null) {
                instance = new ForegroundCallbacks();
                application.registerActivityLifecycleCallbacks(instance);
            }
            return instance;
        }
    
        public static ForegroundCallbacks get(Application application) {
            if (instance == null) {
                init(application);
            }
            return instance;
        }
    
        public static ForegroundCallbacks get(Context ctx) {
            if (instance == null) {
                Context appCtx = ctx.getApplicationContext();
                if (appCtx instanceof Application) {
                    init((Application) appCtx);
                }
                throw new IllegalStateException(
                        "Foreground is not initialised and " +
                                "cannot obtain the Application object");
            }
            return instance;
        }
    
        public static ForegroundCallbacks get() {
    
            return instance;
        }
    
        public boolean isForeground() {
            return foreground;
        }
    
        public boolean isBackground() {
            return !foreground;
        }
    
        public void addListener(Listener listener) {
            listeners.add(listener);
        }
    
        public void removeListener(Listener listener) {
            listeners.remove(listener);
        }
    
        @Override
        public void onActivityResumed(Activity activity) {
            paused = false;
            boolean wasBackground = !foreground;
            foreground = true;
            if (check != null)
                handler.removeCallbacks(check);
            if (wasBackground) {
    
                for (Listener l : listeners) {
                    try {
                        l.onBecameForeground();
                    } catch (Exception exc) {
    
                    }
                }
            } else {
    
            }
        }
    
        @Override
        public void onActivityPaused(Activity activity) {
            paused = true;
    
            if (check != null)
                handler.removeCallbacks(check);
            handler.postDelayed(check = new Runnable() {
                @Override
                public void run() {
                    if (foreground && paused) {
                        foreground = false;
                        for (Listener l : listeners) {
                            try {
                                l.onBecameBackground();
                            } catch (Exception exc) {
    
                            }
                        }
                    } else {
    
                    }
                }
            }, CHECK_DELAY);
        }
    
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }
    
        @Override
        public void onActivityStarted(Activity activity) {
        }
    
        @Override
        public void onActivityStopped(Activity activity) {
        }
    
        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }
    
        @Override
        public void onActivityDestroyed(Activity activity) {
        }
    
        public interface Listener {
            public void onBecameForeground();
    
            public void onBecameBackground();
        }
    }
    

    然后在application中初始化该监听,当应用回到前台的时候尝试重连

    public class WsApplication extends Application {
    
    
        @Override
        public void onCreate() {
            super.onCreate();
            initAppStatusListener();
        }
    
        private void initAppStatusListener() {
            ForegroundCallbacks.init(this).addListener(new ForegroundCallbacks.Listener() {
                @Override
                public void onBecameForeground() {
                    Logger.t("WsManager").d("应用回到前台调用重连方法");
                    WsManager.getInstance().reconnect();
                }
    
                @Override
                public void onBecameBackground() {
    
                }
            });
        }
    }
    

    到这里连接的建立和重连讲完了,还剩客户端发送请求和服务端主动通知消息.

    本来我准备一篇把WebSocket客户端实现写完的,现在才一半就已经这么多了,索性分为几篇算了,下篇我们将介绍 WebSocket安卓客户端实现详解(二)–客户端发送请求.

    这里附上本篇的源码
    WebSocket安卓客户端实现详解(一)–连接建立与重连源码传送门

    展开全文
  • Linux创建连接

    千次阅读 2018-06-19 20:31:53
    创建了软连接,在/home/root上创建了一个到/usr/local/tomcat的连接。相当于快捷方式 ln -d /usr/local/tomcat /home/root 创建了硬连接 其他参数: -f : 链结时先将与 dist 同档名的档案删除  -d : 允许系统...
    ln -s /usr/local/tomcat /home/root
    创建了软连接,在/home/root上创建了一个到/usr/local/tomcat的连接。相当于快捷方式
    
    ln -d /usr/local/tomcat /home/root
    创建了硬连接
    
    其他参数:
        -f : 链结时先将与 dist 同档名的档案删除
      -d : 允许系统管理者硬链结自己的目录
      -i : 在删除与 dist 同档名的档案时先进行询问
      -n : 在进行软连结时,将 dist 视为一般的档案
      -s : 进行软链结(symbolic link)
      -v : 在连结之前显示其档名
      -b : 将在链结时会被覆写或删除的档案进行备份
      -S SUFFIX : 将备份的档案都加上 SUFFIX 的字尾
      -V METHOD : 指定备份的方式
      --help : 显示辅助说明
      --version : 显示版本

    硬连接

    硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。


    软连接

    软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。

    展开全文
  • 简述TCP建立连接和释放连接

    千次阅读 2018-07-14 12:09:50
    第二次握手:服务端收到数据包后由同步号SYN=1知道客户端请求建立连接,服务端将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给客户端以确认连接请求,服务端进入SYN_RCV...

     

    三次握手建立链接:

     

     

    第一次握手:客户端将同步号SYN置为1,随机产生一个值seq=x,将该数据包发送给服务端,客户端进入SYN_SENT状态,等待服务端确认。
    第二次握手:服务端收到数据包后由同步号SYN=1知道客户端请求建立连接,服务端将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给客户端以确认连接请求,服务端进入SYN_RCVD状态。
    第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务端,服务端检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,客户端和服务端进入ESTABLISHED状态.
    完成三次握手,成功建立链接,进行数据传输。

    四次挥手释放链接:

     

     

     

    第一次挥手:客户端发送FIN=1,用来关闭客户端到服务端的数据传送,随机产生一个seq=u,将该数据包发送给服务端,客户端进入FIN_WAIT_1状态。
    第二次挥手:服务端收到结束标志FIN=1后,发送确认标志ACK=1给客户端,确认号ack=u+1,随机产生一个seq=v,将该数据包发送给客户端,服务端进入CLOSE_WAIT状态。
    第三次挥手:服务端发送结束标志FIN=1,用来关闭服务端到客户端的数据传送,并发送确认标志ACK=1,随机产生一个seq=w,ack=u+1,将该数据包发送给客户端,服务端进入LAST_ACK状态。
    第四次挥手:客户端收到FIN后,客户端进入TIME_WAIT状态,接着发送确认标志ACK=1给服务端,确认号seq=u+1,服务端进入CLOSED状态。
    完成四次挥手,成功断开连接。

     

    数据包状态:

     

     

     

    流量图:

    附:

    展开全文
  • TCP协议建立连接的过程

    千次阅读 2018-03-01 23:45:26
    TCP是一个协议,那这个协议是如何定义的,它的数据格式是什么样子的呢?要进行更深层次的剖析,就 需要了解,甚至是熟记TCP协议中每个字段的含义。哦,来吧。上面就是TCP协议头部的格式,由于它太重要了,是理解其它...
  • TCP/IP 建立连接、断开连接的过程

    千次阅读 2018-08-06 14:36:40
    TCP/IP 建立连接、断开连接的过程    以下内容来自:http://www.cnblogs.com/rootq/articles/1377355.html   tcp协议和udp协议的差别 TCP UDP 是否连接 面向连接 面向非连接 传输可靠性 可靠 不可靠...
  • TCP建立连接/断开连接状态详解

    千次阅读 2017-03-19 21:44:53
    TCP连接的建立可以简单的称为三次...第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送
  • TCP/IP 建立连接的过程

    千次阅读 2018-08-24 09:13:33
    第一次握手:建立连接时,客户端发送连接请求到服务器,并进入SYN_SEND状态,等待服务器确认; 第二次握手:服务器收到客户端连接请求,向客户端发送允许连接应答,此时服务器进入SYN_RECV状态; 第三次握手:...
  • 建立连接

    2016-11-26 06:26:02
    建立连接,意思是一个控件发射一个信号,该信号与某个函数建立关连。比如:点击窗体里一个按钮,关闭该窗体。按钮点击时发射clicked()信号,该信号与quit()建立连接,实现关闭窗体。通俗的讲,就是点击按钮后执行...
  • TCP建立连接三次握手和释放连接四次握手

    万次阅读 多人点赞 2016-09-14 14:55:09
    TCP建立连接三次握手和释放连接四次握手  【尊重原创,转载请注明出处】http://blog.csdn.net/guyuealian/article/details/52525724  在谈及TCP建立连接和释放连接过程,先来简单认识一下TCP报文段首部格式...
  • TCP建立连接和关闭连接的过程

    千次阅读 2018-03-07 18:11:56
    先来一张图看看整个连接和关闭的过程: 各个状态的意义如下: LISTEN:侦听来自远方TCP端口的连接请求; SYN-SENT:在发送连接请求后等待匹配的连接请求; SYN-RECEIVED:在收到和发送一个连接请求后等待对...
  • https 建立连接过程

    万次阅读 多人点赞 2016-05-26 22:00:16
    思考问题的顺序学技术时,总是会问什么?这里也不例外,https为什么会存在,它有什么优点,又有什么缺点?为什么网站有的用http,有的用https?如果不能很好的回答,就往下看吧。...容易被监听 ...被伪装 ...
  • 由于之前用webSocket比较少,一直以为在创建连接的时候,不能传递参数,直到有一天看源码的时候,发现了新大陆,下面分享一下 1、在被websocket映射的Java类中的注解如下: @ServerEndpoint("/websocket/{...
  • TCP建立连接的过程

    万次阅读 2016-08-18 17:33:46
    在可靠的TCP网络通信中,客户端和服务器端通信建立连接的过程可简单表述为三次握手(建立连接的阶段)和四次挥手(释放连接阶段),下图是这两个阶段的一个完整的表述: 其状态图可以表示为,  下面我们从建立连接...
  • 看到了一道面试题:“为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?为什么不能用两次握手进行连接?”,想想最近也到金三银四了,所以就查阅了相关资料,整理出来了这篇文章,希望对你们有所帮助。 ...
  • TCP建立连接和拆除连接的过程

    万次阅读 2016-06-05 10:12:43
    " src="http://s3.51cto.com/wyfs02/M01/77/11/wKiom1ZiSO7DKJh_AAClFg1mtgc653.png" title="360反馈意见截图16370622317666.png" alt="wKiom1ZiSO7DKJh_AAClFg1mtgc653.png" />2、TCP连接建立时三次
  • 建立连接理解: 1、TCP的流量控制 2、TCP使用窗口机制进行流量控制 3、什么是窗口? 连接建立时,各端分配一块缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端接收方发送的确认信息中包含了自己剩余的...
  • TCP是面向连接的、可靠的进程到进程通信的协议。它提供的是全双工(双向可传输)的服务,每个TCP都有发送缓存和接受缓存,用来临时存储数据。 1、TCP报文段: TCP把若干个字节构成一个分组,称为报文段(segment)。...
  • MySQL与eclipse建立连接

    千次阅读 2017-10-06 16:21:36
    2).mysql与eclipse建立连接。 3).测试连接是否成功。 1) ctrl+R —— 输入:cmd —— 回车 输入命令:mysql -u root -p 【进入MySQL数据库】 输入密码:****** 【输入密码】 输入命令:create databas
  • TCP建立连接和断开连接过程

    千次阅读 2017-08-24 16:32:45
    假设Client端发起中断连接请求,也就是发送FIN报文。 Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。 所以你先发送...

空空如也

1 2 3 4 5 ... 20
收藏数 1,483,932
精华内容 593,572
关键字:

建立连接