如何干净的在服务中实现socket长链接与服务器通信并处理相应的线程问题(有更新)

来源:互联网 发布:java if 大小写 编辑:程序博客网 时间:2024/05/18 03:32

注:本文部分代码改编自csdn某作者,若您觉得侵权,请与我联系。

在我的上一篇文章中,简单了讲解了socket通信在客户端与服务器的大概思路。但是,在实际应用中,问题会变得复杂的多。如安卓端socket应该如何进行长链接,如何处理线程问题,如何保证连接一直都在,长链接在后台是如何运行的。这一系列问题必须通过一系列的实践才能得到解决。下面的就讲讲我的一些经验。

先附客户端的源码和服务器源码(用myeclipse搭建了一个简单的服务器),在代码后面会详细讲解各种注意点!

PS。希望各位不惧麻烦能将代码实际的跑一遍,加深理解。也防止因为我自己的疏忽而误导大家。

SocketService:(由于是在本人的项目上进行的实验,请忽略广播部分)

import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.lang.ref.WeakReference;import java.net.Socket;import java.net.UnknownHostException;import java.util.Arrays;import android.annotation.SuppressLint;import android.app.Service;import android.content.Intent;import android.os.Binder;import android.os.Handler;import android.os.IBinder;import android.os.RemoteException;import android.util.Log;public class SocketService extends Service {    private static final String TAG = "BackService";    /** 心跳检测时间  */    private static final long HEART_BEAT_RATE = 3 * 1000;    /** 主机IP地址  */    private static final String HOST = "10.0.2.2";    /** 端口号  */    public static final int PORT = 9898;    /** 消息广播  */    public static final String MESSAGE_ACTION = "com.message_ACTION";    private boolean isSuccess=false;//针对客户端主动断开连接    private boolean isconnected=false; //针对服务器,如果服务器主动断开链接,为false    private long current=0L;//表示服务器主动断开时间    private long sendTime = 0L;    /** 弱引用 在引用对象的同时允许对垃圾对象进行回收  */    private WeakReference<Socket> mSocket;    private ReadThread mReadThread;    private MyBackService iBackService = new MyBackService();    public class MyBackService extends Binder{        public boolean sendMessage(String message)  {            return sendMsg(message);        }    };    @Override    public IBinder onBind(Intent arg0) {        return (IBinder) iBackService;    }    @Override    public void onCreate() {        super.onCreate();        new InitSocketThread().start();    }    public void onDestroy(){        super.onDestroy();        mHandler.removeCallbacks(heartBeatRunnable);        Log.d("SocketService","end Service");    }    // 发送心跳包    private Handler mHandler = new Handler();    private Runnable heartBeatRunnable = new Runnable() {  //心跳一直在后台跑,防止主动断线和被动断线!!!        @Override        public void run() {            if (System.currentTimeMillis() - sendTime >= HEART_BEAT_RATE) {                Log.d("SocketService","heartbear is running");                 isSuccess = sendMsg("heartbeat");// 就发送一个\r\n过去, 如果发送失败,就重新初始化一个socket                  if (System.currentTimeMillis()-current>=10*HEART_BEAT_RATE)                      isconnected=false;//如果当前时间超过服务器断开时间时长为心跳频率的十倍,则重新连接                if (!isSuccess||!isconnected) {                    mReadThread.release();                    releaseLastSocket(mSocket);                    mHandler.removeCallbacks(heartBeatRunnable);                    Log.d("SocketService","重连1");                    new InitSocketThread().start();                    Log.d("SocketService","重连2");                }            }            mHandler.postDelayed(this,HEART_BEAT_RATE);           // stopSelf();//是否需要在杀进程后保持心跳重连机制,需要的话去除此行代码        }    };    public boolean sendMsg(String msg) {        if (null == mSocket || null == mSocket.get()) {            Log.d("SocketService","掉线");            return false;        }        Socket soc = mSocket.get();        if(soc.isClosed()||!soc.isConnected()||soc.isInputShutdown()||soc.isClosed()||soc.isOutputShutdown()){            Log.d("SocketService","socket连接客户端主动断开连接");            return false;        }        try {            if (!soc.isClosed() &&!soc.isOutputShutdown()) {                final OutputStream os = soc.getOutputStream();                final String message = msg + "\n";                new Thread(new Runnable() {                    @Override                    public void run() {                        try{                            os.write(message.getBytes());                            os.flush();                            Log.d("SocketService","send successfully");                        }catch (IOException e){                            isconnected=false;                        }                    }                }).start();                sendTime = System.currentTimeMillis();// 每次发送成功数据,就改一下最后成功发送的时间,节省心跳间隔时间                Log.i(TAG, "发送成功的时间:" + sendTime);                return true;            }        } catch (IOException e) {            e.printStackTrace();            return false;        }        return false;    }    // 初始化socket    private void initSocket() throws UnknownHostException, IOException {         Socket socket = new Socket(HOST, PORT);        if (socket.isConnected()&&!socket.isClosed()){   //防止初始化时断线            current=System.currentTimeMillis();            isconnected=true;        }        mSocket = new WeakReference<Socket>(socket);        mReadThread = new ReadThread(socket);        mReadThread.start();        mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功后,就准备发送心跳包        //mHandler.removeCallbacks(heartBeatRunnable);    }    // 释放socket    private void releaseLastSocket(WeakReference<Socket> mSocket) {        try {            if (null != mSocket) {                Socket sk = mSocket.get();                if (!sk.isClosed()) {                    sk.close();                }                sk = null;                mSocket = null;
                isconnected=false;
            }        } catch (IOException e) {            e.printStackTrace();        }    }    class InitSocketThread extends Thread {        @Override        public void run() {            super.run();            try {                initSocket();                Log.d("SocketService","init success");                //mHandler.removeCallbacks(heartBeatRunnable);                //                //mHandler.postDelayed(heartBeatRunnable,HEART_BEAT_RATE);            } catch (UnknownHostException e) {                e.printStackTrace();            } catch (IOException e) {                e.printStackTrace();            }        }    }    public class ReadThread extends Thread {        private WeakReference<Socket> mWeakSocket;        private boolean isStart = true;        public ReadThread(Socket socket) {            mWeakSocket = new WeakReference<Socket>(socket);        }        public void release() {            isStart = false;            releaseLastSocket(mWeakSocket);        }        @SuppressLint("NewApi")        @Override        public void run() {            super.run();            Socket socket = mWeakSocket.get();            if (null != socket) {                try {                    InputStream is = socket.getInputStream();                    byte[] buffer = new byte[1024 * 4];                    int length = 0;                    if(is.read()==-1)                        isStart=false;                    while (!socket.isClosed() && !socket.isInputShutdown()                            && isStart && ((length = is.read(buffer)) != -1)) {                        if (length > 0) {                            String message = new String(Arrays.copyOf(buffer,                                    length)).trim();                            Log.d(TAG, "收到服务器发送来的消息:"+message+"hahaha");                            Log.d("123456",message);                            // 收到服务器过来的消息,就通过Broadcast发送出去                            if (message!=""){                                if (message.equals("ok")) {// 处理心跳回复                                    Log.d("SocketService","心跳正常"+message);                                    current=System.currentTimeMillis();                                } else {                                    // 其他消息回复                                    Intent intent = new Intent(MESSAGE_ACTION);                                    intent.putExtra("message", message);                                    sendBroadcast(intent);                                    //接下来的工作,定义出一个json格式,对message进行解析,判断类型,发送特定广播                                    Log.d("SocketService","hellohello");                                    //没有断线后心跳一直运行,直到再次连接,掉线期间不应该进行任何网络请求                                }                            }                        }                    }                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }}



MainActivity:

import android.app.Notification;import android.app.NotificationManager;import android.content.BroadcastReceiver;import android.content.ComponentName;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.content.ServiceConnection;import android.graphics.BitmapFactory;import android.os.Handler;import android.os.IBinder;import android.os.Message;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.support.v7.app.NotificationCompat;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.CheckBox;import android.widget.EditText;import android.widget.ImageView;import com.bumptech.glide.Glide;import java.io.BufferedOutputStream;import java.io.BufferedWriter;import java.io.IOException;import java.io.OutputStreamWriter;import java.net.Socket;import okhttp3.OkHttpClient;import okhttp3.Response;public class MainActivity extends AppCompatActivity {     private Button userlogin;    private myreceiver mybroadcastreceiver;    private SocketService.MyBackService myBackService;    private ServiceConnection connection = new ServiceConnection() {        @Override        public void onServiceDisconnected(ComponentName name) {        }        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            myBackService=(SocketService.MyBackService)service;        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);      new Thread(new Runnable() {            @Override            public void run() {                startService(intent);            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                bindService(intent,connection,BIND_AUTO_CREATE);            }        }).start();        userlogin=(Button)findViewById(R.id.user_login);        userlogin.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                new Thread(new Runnable() {                    @Override                    public void run() {                        myBackService.sendMessage("this is mainactivirty\n");                    }                }).start();                Intent intent1=new Intent(MainActivity.this,test.class);                startActivity(intent1);            }        });    }    protected void onDestroy(){        super.onDestroy();        unbindService(connection);        unregisterReceiver(mybroadcastreceiver);        Log.d("MainActivity","unbindservice");    }}
     下面是服务器的代码:

public class Server {BufferedWriter writer=null;BufferedReader reader=null;public static void main(String[]args){Server serversocket=new Server();serversocket.start();}public void start(){ServerSocket server=null;Socket socket=null;try {server=new ServerSocket(9898);while(true){socket=server.accept();/* * 当没有客户端连接服务器时,accept方法会阻塞住 */System.out.println("client "+socket.hashCode()+"connect...");manageConnection(socket);}} catch (Exception e) {e.printStackTrace();}finally {try {socket.close();server.close();} catch (Exception e2) {e2.printStackTrace();}}}/* * 连接管理 * 每次客户端连接服务器是时都会生成一个socket,将socket传入manage进行处理和发送 */public void manageConnection(final Socket socket){new Thread(new Runnable(){public void run(){String string=null;try {reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); // 下面为测试代码,为了测试客户端的监听功能(客户端接受服务器主动发送数据)是否成功,定时发送心跳包 // 由于在匿名类中使用,writer需要设置为static或者全局变量/* new Timer().schedule(new TimerTask(){public void run(){try {writer.write("heart once...\n");writer.flush();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}},3000, 3000);*//* * 注意:主线程中需要加入while形成循环,否子运行一次就会推出接受客户端信息 * 同理,客户端在写消息的时候也需要注意这一点 */while(!(string=reader.readLine()).equals("bye")){if(!string.equals(""))System.out.println("client "+socket.hashCode()+":"+string);writer.write(string+"\n");writer.flush();}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}finally{try {writer.close();reader.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}).start();}}
下面讲一下长连接的思路:长链接放在android的服务里进行长时间运行,保证能随时接收消息。同时加入心跳机制和断线重连,保持连接稳定。

在干净实现socket长链接有以下注意点:

1:由于网络通信是耗时操作,而且服务与开启他的活动共用一个主线程,所以从服务器读取需要开启一个新的线程ReadThread。

2:由于需要保持长链接干净,所以一个客户端只允许存在一个与服务器通信的socket。此处普及一个android服务的知识:服务的onCreate方法只在创建时候被调用了一次,这说明:Service被启动时只调用一次onCreate()方法,如果服务已经被启动,在次启动的Service组件将直接调用onStartCommand()方法,通过这样的生命周期,可以根据自身需求将指定操作分配进onCreate()方法或onStartCommand()方法中。所以服务器所有关于socket的操作有应该放在一个在onCreate()方法中开启的线程里。并且向服务器发送信息也应该放在服务里,使用已经开启的socket,避免创建多余的socket。在activity里需要使用时使用bindservice()方法绑定一下。(不理解bindservice()的可以在csdn上搜一下,有很多详细的讲解)

3:注意第二点中的一句话,服务的onCreate方法只在创建时候被调用了一次。在启动服务后,后台心跳包和短线重连会一直运行。如果启动是用bindservice()启动,即将代码MainActivity中的startService()删除,那么启动后退出客户端再进入客户端,程序会另外创建一个socket长链接。如下所示:

client 1814681656connect...client 1814681656:heartbeatclient 1814681656:heartbeatclient 1814681656:heartbeatclient 103530884connect...client 1814681656:heartbeatclient 103530884:heartbeatclient 1814681656:heartbeatclient 103530884:heartbeat
这么一来,不断的推出进入会浪费很多资源。也会建立很多socket连接,这不符合我们建立干净长链接的目的。因此,第一次启动服务应使用startService()方法。使用这个方法启动服务后onCreate()执行,此后无论使用bindservice()或者startservice()启动服务,都不会建立新的socket服务。

4:启动服务等耗时的操作不应在主线程运行,都应该重新开一个线程运行。无论在服务或者活动中都如此。

5:我们的长链接理论上讲应该一直在后台运行。所以不需要人工使用stopservice()停止。但考虑到手机性能的问题,在关闭程序后后台服务依旧会跑,心跳极值和短线重连支持着这一点。那么如何做到在被杀进程后完全停止呢?你可以选择在heartbeat线程的最后面加stopself(),使得在被杀进程断线后心跳停止,不会执行短线重连。

6:关于习惯问题,有bindservice(),就得有unbindservice()。

7:借助heartbeat线程说一下,服务中开启的线程最好是在操作结束使用stopself()结束!
PS:关于代码有几点忘记说了!!!

处理心跳根管线重连之前没有考虑服务器主动断线

自己实现一个心跳检测,一定时间内未收到自定义的心跳包则标记为已断开。这是我认为最简单的想法!!!

1:用模拟器测试的话地址应该写10.0.2.2而不是127.0.0.1

2:模拟器测试我只会测试服务器主动断开socket后重连,而上述代码只针对客户端主动断开后重连。如果您实在5.17号之前看的话请重新看一下SocketService中的代码,我已经更新。

3:消息流的处理依旧有问题,以后会更单独更新一个博客讲一讲消息流的处理。





阅读全文
1 0