注:本文部分代码改编自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:消息流的处理依旧有问题,以后会更单独更新一个博客讲一讲消息流的处理。