注:本文部分代碼改編自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:消息流的處理依舊有問題,以後會更單獨更新一個博客講一講消息流的處理。