Android socket与服务器通信及心跳连接的实现
在项目中,有如下需求:Android客户端向服务器发送数据,收到服务器返回的数据发送成功标识后,客户端即与服务器建立数据一来一往的心跳连接,若服务器端断开时,客户端接收到通知,关闭Service停止发送数据;代码如下:
- public class BackService extends Service {
- private static final String TAG = "BackService";
- /** 心跳检测时间 */
- private static final long HEART_BEAT_RATE = 3* 1000;
- /** 主机IP地址 */
- private static String HOST = "192.168.1.30";
- /** 端口号 */
- public static final int PORT =10801;
- /** 消息广播 */
- public static final String MESSAGE_ACTION = "org.feng.message_ACTION";
- /** 心跳广播 */
- public static final String HEART_BEAT_ACTION = "org.feng.heart_beat_ACTION";
- private long sendTime = 0L;
- private Socket socket;
- private ReadThread mReadThread;
- private InputStream is;//输入流
- private int count;//读取的字节长度
- private IBackService.Stub iBackService = new IBackService.Stub() {
- @Override
- public boolean sendMessage(String message) throws RemoteException {
- return sendMsg(message);
- }
- };
- @Override
- public IBinder onBind(Intent arg0) {
- return (IBinder) iBackService;
- }
- @Override
- public void onCreate() {
- super.onCreate();
- HOST= (String) Utils.getShare(this, ConfigUrl.SERVER_IP,"ip");
- Log.i(TAG,"ip-->"+HOST);
- Log.i(TAG,"port-->"+PORT);
- new InitSocketThread().start();
- }
- // 发送心跳包
- private Handler mHandler = new Handler();
- private Runnable heartBeatRunnable = new Runnable() {
- @Override
- public void run() {
- ReadThread thread=new ReadThread(socket);
- thread.start();
- }
- };
- public boolean sendMsg(String msg) {
- if (null == socket) {
- return false;
- }
- try {
- if (!socket.isClosed() && !socket.isOutputShutdown()) {
- OutputStream os = socket.getOutputStream();
- os.write(msg.getBytes());
- os.flush();
- sendTime = System.currentTimeMillis();// 每次发送成功数据,就改一下最后成功发送的时间,节省心跳间隔时间
- Log.i(TAG, "发送成功的时间:" + sendTime+" 内容-->"+msg);
- } else {
- return false;
- }
- } catch (IOException e) {
- e.printStackTrace();
- Intent intent = new Intent(HEART_BEAT_ACTION);
- intent.putExtra("message", "exit");
- sendBroadcast(intent);
- Log.i(TAG,"send-->"+e.getMessage());
- return false;
- }
- return true;
- }
- // 初始化socket
- private void initSocket() throws UnknownHostException, IOException {
- socket = new Socket(HOST, PORT);
- socket.setSoTimeout(13000);//?
- if (socket.isConnected()){//连接成功
- if (sendMsg("mdc_"+Utils.getShare(this,ConfigUrl.DEVICE_ID,"手机设备id"))){//发送成功
- //接收服务器返回的信息
- mReadThread = new ReadThread(socket);
- mReadThread.start();
- }
- }
- }
- // 释放socket
- private void releaseLastSocket(Socket mSocket) {
- try {
- if (null != mSocket) {
- if (!mSocket.isClosed()) {
- is.close();
- mSocket.close();
- }
- mSocket = null;
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- class InitSocketThread extends Thread {
- @Override
- public void run() {
- super.run();
- try {
- initSocket();
- } catch (UnknownHostException e) {
- e.printStackTrace();
- Log.i(TAG,"socket-->"+e.getMessage());
- Log.i(TAG,"连接失败");
- Intent intent = new Intent(HEART_BEAT_ACTION);
- intent.putExtra("message", "fail");
- sendBroadcast(intent);
- } catch (IOException e) {
- e.printStackTrace();
- Log.i(TAG,"socket-->"+e.getMessage());
- Intent intent = new Intent(HEART_BEAT_ACTION);
- intent.putExtra("message", "fail");
- sendBroadcast(intent);
- }
- }
- }
- public class ReadThread extends Thread {
- private Socket rSocket;
- private boolean isStart = true;
- public ReadThread(Socket socket) {
- rSocket=socket;
- }
- public void release() {
- isStart = false;
- releaseLastSocket(rSocket);
- }
- @SuppressLint("NewApi")
- @Override
- public void run() {
- super.run();
- String line="";
- if (null != rSocket) {
- while (isStart&&!rSocket.isClosed()&&!rSocket.isInputShutdown()){
- try {
- Log.i(TAG,"开始读取消息");
- is=rSocket.getInputStream();
- count=is.available();
- byte[] data=new byte[count];
- is.read(data);
- line=new String(data);
- if (line!=null){
- Log.i(TAG, "收到服务器发送来的消息:"+line);
- if ("mdc_ok".equals(line)||"mdc_exist".equals(line)||"exist".equals(line)){
- sendMsg("connect");
- mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功后,就准备发送心跳
- return;
- }else if ("mdc_connect".equals(line)){//服务器发送继续接收的消息
- boolean isSuccess=sendMsg("connect");
- if (isSuccess){//成功发送,接收回执信息
- mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功后,就准备发送心跳
- }else {//发送失败,处理善后工作
- mHandler.removeCallbacks(heartBeatRunnable);
- release();
- releaseLastSocket(socket);
- }
- return;
- }else if ("mdc_connectexit".equals(line)||"exit".equals(line)){//断开连接消息
- Intent intent = new Intent(HEART_BEAT_ACTION);
- intent.putExtra("message", "exit");
- sendBroadcast(intent);
- return;
- }
- }else if (line==null){
- Log.i(TAG, "服务器发送过来的消息是空的");
- }
- } catch (IOException e) {
- Log.i(TAG,"Read-->"+e.getClass().getName());
- e.printStackTrace();
- continue;
- }
- }
- }
- }
- }
- @Override
- public void onDestroy() {
- super.onDestroy();
- Log.i(TAG,"onDestroy");
- mHandler.removeCallbacks(heartBeatRunnable);
- mReadThread.release();
- releaseLastSocket(socket);
- }
- }
以上代码只是Service中的代码亲测可用;在开发过程中也遇到很多问题,有些已解答,有些仍未解决,在此记录,i希望有了解的可以告知以下。
1.当 OutputStream os,调用os.close()或者 InputStream os,调用is.close()时,会将socket关闭,开始时调用os.close()导致scoket关闭,在后面读取服务器的消息时不断抛出socket closed异常,经检查后发现此处有问题;若想将输出/入流关闭,可调用 socket.shutdownInput();/socket.shutdownOutput();此时socket不会关闭;
2.在ReadThread线程中,读取服务器消息时开始使用BufferedReader in=new BufferedReader(new InputStreamReader(rSocket.getInputStream())); String line=in.readLine();然后一直抛出超时异常,使用Debug模式调试后发现,line当中已经读取到正确的内容,但是readLine()方法在读到“\n”标识符后才会完成读取,不然会一直等待读取直到抛出超时异常,并且readLIne()是阻塞线程,未完成读取时程序一直阻塞,无法继续向下进行,直到超时进入catch中。此时应该使用字节读取。
3.由于客户端一直在轮询发送并接收服务起的消息,当服务器端socket主动断开时,客户端也要断开,但是在开发中,服务器端的socket断开时,客户端一直在while循环中读取消息,使用字节读取时,调用到is.read(data);方法也不会抛出异常(但是好多文章中都写此时会抛出异常,但是我这里没有抛出,原因未知),而是一直读取字节长度为0 的空数据却不结束,使用socket的isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()等判断socket与服务器的连接状态均无效,原因在于上这些方法都是访问socket在内存驻留的状态,而非与服务器的实时状态,因此判断是无效的,以下三张截图:
此为客户端与服务器刚建立连接,此时未通消息
此为客户端向服务器发送消息成功后
此为服务器socket断开后的情况
从图中可以看到,三种情况下各方法返回的状态均相同,无法作为判断客户端与服务器实时连接情况的方法使用,网上查阅资料有网友提到,可以使用socket.sendUrgentData(0xFF);方法,向服务器发送数据,检测服务器是否断开连接,类似于ping,且该方法可往输出流发送一个字节的数据,只要对方Socket的SO_OOBINLINE属性没有打开,就会自动舍弃这个字节,而SO_OOBINLINE属性默认情况下就是关闭的,这样防止向服务器发送无效数据,若服务器断开了,则会在报出客户端报出异常,此时即可得知服务器的socket是否处于连接状态;本以为找到一个好方法,使用时才发现,即使服务器处于正常的连接状态,也会抛出异常(有好多文章说此方法可行,然而我没有搞通,不知是不是使用方法出错了,有了解相关情况的望告知),最后只有让服务器在断开socket时,发送一个标识符,即在代码中mdc_connectexit及exit(之所以会用这两个是因为服务器在发送数据时,会出现“粘包”的情况)。