如何幹淨的在服務中實現socket長鏈接與服務器通信並處理相應的線程問題(有更新)

注:本文部分代碼改編自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 block
								e.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 block
					e.printStackTrace();
				}finally{
					try {
						writer.close();
						reader.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.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:heartbeat
client 1814681656:heartbeat
client 1814681656:heartbeat
client 103530884connect...
client 1814681656:heartbeat
client 103530884:heartbeat
client 1814681656:heartbeat
client 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:消息流的處理依舊有問題,以後會更單獨更新一個博客講一講消息流的處理。





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