重新建立以后的套接字死亡

时间:2016-12-15 01:44:16

标签: android sockets

我有一个客户端服务器模型,客户端在android上运行。它使用以下代码建立其tls套接字:。

(客户端登录和重新登录所做的一切)

public class LoginAsync extends AsyncTask<Boolean, String, Boolean>
protected Boolean doInBackground(Boolean... params)
{
    try
    {
        //only handle 1 login request at a time
        synchronized(loginLock)
        {
            if(tryingLogin)
            {
                Utils.logcat(Const.LOGW, tag, "already trying a login. ignoring request");
                onPostExecute(false);
                return false;
            }
            tryingLogin = true;
        }


        //http://stackoverflow.com/a/34228756
        //check if server is available first before committing to anything
        //  otherwise this process will stall. host not available trips timeout exception
        Socket diag = new Socket();
        diag.connect(new InetSocketAddress(Vars.serverAddress, Vars.commandPort), TIMEOUT);
        diag.close();

        //send login command
        Vars.commandSocket = Utils.mkSocket(Vars.serverAddress, Vars.commandPort, Vars.expectedCertDump);
        String login = Utils.currentTimeSeconds() + "|login|" + uname + "|" + passwd;
        Vars.commandSocket.getOutputStream().write(login.getBytes());

        //read response
        byte[] responseRaw = new byte[Const.BUFFERSIZE];
        int length = Vars.commandSocket.getInputStream().read(responseRaw);

        //on the off chance the socket crapped out right from the get go, now you'll know
        if(length < 0)
        {
            Utils.logcat(Const.LOGE, tag, "Socket closed before a response could be read");
            onPostExecute(false);
            return false;
        }

        //there's actual stuff to process, process it!
        String loginresp = new String(responseRaw, 0, length);
        Utils.logcat(Const.LOGD, tag, loginresp);

        //process login response
        String[] respContents = loginresp.split("\\|");
        if(respContents.length != 4)
        {
            Utils.logcat(Const.LOGW, tag, "Server response imporoperly formatted");
            onPostExecute(false); //not a legitimate server response
            return false;
        }
        if(!(respContents[1].equals("resp") && respContents[2].equals("login")))
        {
            Utils.logcat(Const.LOGW, tag, "Server response CONTENTS imporperly formated");
            onPostExecute(false); //server response doesn't make sense
            return false;
        }
        long ts = Long.valueOf(respContents[0]);
        if(!Utils.validTS(ts))
        {
            Utils.logcat(Const.LOGW, tag, "Server had an unacceptable timestamp");
            onPostExecute(false);
            return false;
        }
        Vars.sessionid = Long.valueOf(respContents[3]);

        //establish media socket
        Vars.mediaSocket = Utils.mkSocket(Vars.serverAddress, Vars.mediaPort, Vars.expectedCertDump);
        String associateMedia = Utils.currentTimeSeconds() + "|" + Vars.sessionid;
        Vars.mediaSocket.getOutputStream().write(associateMedia.getBytes());

        Intent cmdListenerIntent = new Intent(Vars.applicationContext, CmdListener.class);
        Vars.applicationContext.startService(cmdListenerIntent);

        onPostExecute(true);
        return true;
    }
    catch (CertificateException c)
    {
        Utils.logcat(Const.LOGE, tag, "server certificate didn't match the expected");
        onPostExecute(false);
        return false;
    }
    catch (Exception i)
    {
        Utils.dumpException(tag, i);
        onPostExecute(false);
        return false;
    }
}

使用mksocket实用程序函数:

public static Socket mkSocket(String host, int port, final String expected64) throws CertificateException
{
    TrustManager[] trustOnlyServerCert = new TrustManager[]
    {new X509TrustManager()
            {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String alg)
                {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String alg) throws CertificateException
                {
                    //Get the certificate encoded as ascii text. Normally a certificate can be opened
                    //  by a text editor anyways.
                    byte[] serverCertDump = chain[0].getEncoded();
                    String server64 = Base64.encodeToString(serverCertDump, Base64.NO_PADDING & Base64.NO_WRAP);

                    //Trim the expected and presented server ceritificate ascii representations to prevent false
                    //  positive of not matching because of randomly appended new lines or tabs or both.
                    server64 = server64.trim();
                    String expected64Trimmed = expected64.trim();
                    if(!expected64Trimmed.equals(server64))
                    {
                        throw new CertificateException("Server certificate does not match expected one.");
                    }

                }

                @Override
                public X509Certificate[] getAcceptedIssuers()
                {
                    return null;
                }

            }
    };
    try
    {
        SSLContext context;
        context = SSLContext.getInstance("TLSv1.2");
        context.init(new KeyManager[0], trustOnlyServerCert, new SecureRandom());
        SSLSocketFactory mkssl = context.getSocketFactory();
        Socket socket = mkssl.createSocket(host, port);
        socket.setKeepAlive(true);
        return socket;
    }
    catch (Exception e)
    {
        dumpException(tag, e);
        return null;
    }
}

以下是成功登录时启动的命令侦听器服务:

    public class CmdListener extends IntentService
protected void onHandleIntent(Intent workIntent)
{
    //  don't want this to catch the login resposne
    Utils.logcat(Const.LOGD, tag, "command listener INTENT SERVICE started");

    while(inputValid)
    {
        String logd = ""; //accumulate all the diagnostic message together to prevent multiple entries of diagnostics in log ui just for cmd listener
        try
        {//the async magic here... it will patiently wait until something comes in

            byte[] rawString = new byte[Const.BUFFERSIZE];
            int length = Vars.commandSocket.getInputStream().read(rawString);
            if(length < 0)
            {
                throw new Exception("input stream read failed");
            }
            String fromServer = new String(rawString, 0, length);
            String[] respContents = fromServer.split("\\|");
            logd = logd +  "Server response raw: " + fromServer + "\n";

            //check for properly formatted command
            if(respContents.length != 4)
            {
                Utils.logcat(Const.LOGW, tag, "invalid server response");
                continue;
            }

            //verify timestamp
            long ts = Long.valueOf(respContents[0]);
            if(!Utils.validTS(ts))
            {
                Utils.logcat(Const.LOGW, tag, "Rejecting server response for bad timestamp");
                continue;
            }

            //just parse and process commands here. not much to see

        }
        catch (IOException e)
        {
            Utils.logcat(Const.LOGE, tag, "Command socket closed...");
            Utils.dumpException(tag, e);
            inputValid = false;
        }
        catch(NumberFormatException n)
        {
            Utils.logcat(Const.LOGE, tag, "string --> # error: ");
        }
        catch(NullPointerException n)
        {
            Utils.logcat(Const.LOGE, tag, "Command socket null pointer exception");
            inputValid = false;
        }
        catch(Exception e)
        {
            Utils.logcat(Const.LOGE, tag, "Other exception");
            inputValid = false;
        }
    }
    //only 1 case where you don't want to restart the command listener: quitting the app.
    //the utils.quit function disables BackgroundManager first before killing the sockets
    //that way when this dies, nobody will answer the command listener dead broadcast

    Utils.logcat(Const.LOGE, tag, "broadcasting dead command listner");
    try
    {
        Intent deadBroadcast = new Intent(Const.BROADCAST_BK_CMDDEAD);
        sendBroadcast(deadBroadcast);
    }
    catch (Exception e)
    {
        Utils.logcat(Const.LOGE, tag, "couldn't broadcast dead command listener... leftover broadacast from java socket stupidities?");
        Utils.dumpException(tag, e);
    }
}

这是后台管理员,当你从wifi切换到lte,lte到wifi,或者当你从地铁出来的时候从无到有,都会告诉你:

public class BackgroundManager extends BroadcastReceiver
{
private static final String tag = "BackgroundManager";

@Override
public void onReceive(final Context context, Intent intent)
{
    if(Vars.applicationContext == null)
    {
        //sometimes intents come in when the app is in the process of shutting down so all the contexts won't work.
        //it's shutting down anyways. no point of starting something
        return;
    }

    AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

    if(Vars.uname == null || Vars.passwd == null)
    {
        //if the person hasn't logged in then there's no way to start the command listener
        //  since you won't have a command socket to listen on
        Utils.logcat(Const.LOGW, tag, "user name and password aren't available?");
    }

    String action = intent.getAction();
    if(action.equals(ConnectivityManager.CONNECTIVITY_ACTION))
    {
        manager.cancel(Vars.pendingRetries);
        new KillSocketsAsync().execute();

        if(Utils.hasInternet())
        {
            //internet reconnected case
            Utils.logcat(Const.LOGD, tag, "internet was reconnected");
            new LoginAsync(Vars.uname, Vars.passwd).execute();
        }
        else
        {
            Utils.logcat(Const.LOGD, tag, "android detected internet loss");
        }
        //command listener does a better of job of figuring when the internet died than android's connectivity manager.
        //android's connectivity manager doesn't always get subway internet loss
    }
    else if (action.equals(Const.BROADCAST_BK_CMDDEAD))
    {
        String loge = "command listener dead received\n";

        //cleanup the pending intents and make sure the old sockets are gone before making new ones
        manager.cancel(Vars.pendingRetries);
        new KillSocketsAsync().execute(); //make sure everything is good and dead

        //all of this just to address the stupid java socket issue where it might just endlessly die/reconnect
        //initialize the quick dead count and timestamp if this is the first time
        long now = System.currentTimeMillis();
        long deadDiff =  now - Vars.lastDead;
        Vars.lastDead = now;
        if(deadDiff < Const.QUICK_DEAD_THRESHOLD)
        {
            Vars.quickDeadCount++;
            loge = loge + "Another quick death (java socket stupidity) occured. Current count: " + Vars.quickDeadCount + "\n";
        }

        //with the latest quick death, was it 1 too many? if so restart the app
        //https://stackoverflow.com/questions/6609414/how-to-programatically-restart-android-app
        if(Vars.quickDeadCount == Const.QUICK_DEAD_MAX)
        {
            loge = loge + "Too many quick deaths (java socket stupidities). Restarting the app\n";
            Utils.logcat(Const.LOGE, tag, loge);
            //self restart, give it a 5 seconds to quit
            Intent selfStart = new Intent(Vars.applicationContext, InitialServer.class);
            int pendingSelfId = 999;
            PendingIntent selfStartPending = PendingIntent.getActivity(Vars.applicationContext, pendingSelfId, selfStart, PendingIntent.FLAG_CANCEL_CURRENT);
            manager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+Const.RESTART_DELAY, selfStartPending);

            //hopefully 5 seconds will be enough to get out
            Utils.quit();
            return;
        }
        else
        { //app does not need to restart. still record the accumulated error messages
            Utils.logcat(Const.LOGE, tag, loge);
        }

        //if the network is dead then don't bother
        if(!Utils.hasInternet())
        {
            Utils.logcat(Const.LOGD, tag, "No internet detected from commnad listener dead");
            return;
        }

        new LoginAsync(Vars.uname, Vars.passwd).execute();
    }
    else if (action.equals(Const.ALARM_ACTION_RETRY))
    {
        Utils.logcat(Const.LOGD, tag, "login retry received");

        //no point of a retry if there is no internet to try on
        if(!Utils.hasInternet())
        {
            Utils.logcat(Const.LOGD, tag, "no internet for sign in retry");
            manager.cancel(Vars.pendingRetries);
            return;
        }

        new LoginAsync(Vars.uname, Vars.passwd).execute();

    }
    else if(action.equals(Const.BROADCAST_LOGIN_BG))
    {
        boolean ok = intent.getBooleanExtra(Const.BROADCAST_LOGIN_RESULT, false);
        Utils.logcat(Const.LOGD, tag, "got login result of: " + ok);

        Intent loginResult = new Intent(Const.BROADCAST_LOGIN_FG);
        loginResult.putExtra(Const.BROADCAST_LOGIN_RESULT, ok);
        context.sendBroadcast(loginResult);

        if(!ok)
        {
            Utils.setExactWakeup(Const.RETRY_FREQ, Vars.pendingRetries);
        }
    }
}
}

服务器正在选择系统调用以侦听其已建立的套接字。它接受使用此代码的新套接字(Linux上的C)

            incomingCmd = accept(cmdFD, (struct sockaddr *) &cli_addr, &clilen);
        if(incomingCmd < 0)
        {
            string error = "accept system call error";
            postgres->insertLog(DBLog(Utils::millisNow(), TAG_INCOMINGCMD, error, SELF, ERRORLOG, DONTKNOW, relatedKey));
            perror(error.c_str());
            goto skipNewCmd;
        }
        string ip = inet_ntoa(cli_addr.sin_addr);

        //setup ssl connection
        SSL *connssl = SSL_new(sslcontext);
        SSL_set_fd(connssl, incomingCmd);
        returnValue = SSL_accept(connssl);

        //in case something happened before the incoming connection can be made ssl.
        if(returnValue <= 0)
        {
            string error = "Problem initializing new command tls connection from " + ip;
            postgres->insertLog(DBLog(Utils::millisNow(), TAG_INCOMINGCMD, error, SELF, ERRORLOG, ip, relatedKey));
            SSL_shutdown(connssl);
            SSL_free(connssl);
            shutdown(incomingCmd, 2);
            close(incomingCmd);
        }
        else
        {
            //add the new socket descriptor to the client self balancing tree
            string message = "new command socket from " + ip;
            postgres->insertLog(DBLog(Utils::millisNow(), TAG_INCOMINGCMD, message, SELF, INBOUNDLOG, ip, relatedKey));
            clientssl[incomingCmd] = connssl;
            sdinfo[incomingCmd] = SOCKCMD;
            failCount[incomingCmd] = 0;
        }

我遇到的问题是当客户端从最近使用过的ip地址重新连接到服务器时,客户端上的套接字似乎总是在创建后死掉。如果我再次重试,它会再次死亡。让它连接的唯一方法是让Android应用程序杀死并重新启动。

示例:在家里的wifi上,地址为192.168.1.101。连接好的。切换到地址24.157.18.90上的LTE。将我重新连接到服务器确定。回到家,得到192.168.1.101。套接字总是会死,直到应用程序自行终止。或者,如果我在外面,我放宽了LTE,因为我乘坐地铁,当我出来时,我遇到了同样的问题。请注意,每次都会创建一个新套接字。它不会以某种方式试图挽救旧的。套接字创建似乎也成功了。只要客户想要对它进行读取,java就说套接字已关闭。

我将所有相关代码放入未经模糊处理的原始表单中,因为这是我的业余爱好项目。我不明白为什么会这样。

1 个答案:

答案 0 :(得分:0)

    // http://stackoverflow.com/a/34228756
    //check if server is available first before committing to anything
    //  otherwise this process will stall. host not available trips timeout exception
    Socket diag = new Socket();
    diag.connect(new InetSocketAddress(Vars.serverAddress, Vars.commandPort), TIMEOUT);
    diag.close();

这是由这三个无意义的代码行引起的。服务器获得连接并立即read()结果为零。

建立连接只是为了关闭它,然后假设你可以打开另一个连接没有价值。您应该使用刚刚建立的连接。通常,确定任何资源是否可用的正确方法是尝试以正常方式使用它。上述技术与预测未来的尝试无法区分。