Android 使用Socket(TCP协议)实现监控(类似于及时通讯)服务器端使用Walkman

最近的项目要实现一台平板控制其余平板的功能(类即时通讯),主要用tcp或者udp进行开发,因为要保证一台机的命令要“发到”其余的机器上,我采取的是数据接收安全的TCP。思路如下图:

Android 使用Socket(TCP协议)实现监控(类似于及时通讯)服务器端使用Walkman_第1张图片

1.客户端负责发送与“接收”(其实不是服务端向客户端发送数据而是客户端自己在服务器读取),当然前提服务器提供URL与端口(PORT)。

2.难点:1,客户端与服务器断了能够重连(即当客户端断网了或者服务器崩了,客户端能显示此时断了并能够重连)即心跳感应

                   

                  由客户端在一定的时间向服务器消息比如“ping”,服务器会在一定的时间内返回pong,如果在这段的时间内客户端没有接受到pong,则判定socket断了。这里我使用                   了github上别人封装好的socket,并修改了一些东西。(本人是一级代码搬运工,擅长快速修改运用第三方代码,使之完美契合项目)。

                  附上链接: https://github.com/vilyever/AndroidSocketClient     主要是对里面的clientsocket类做了修改:

package utils;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.Toast;

import com.alibaba.fastjson.JSON;
import com.wyt.hcy.aiyixue_teacher.WelcomeActivity;

import Jsonbean.TeacherLoginRequest;
import application.App;
import contacts.Contacts;
import contacts.SendOrderBean;
import databse.DbUtils;
import socketclient.SocketClient;
import socketclient.helper.SocketClientDelegate;
import socketclient.helper.SocketClientReceivingDelegate;
import socketclient.helper.SocketClientSendingDelegate;
import socketclient.helper.SocketPacket;
import socketclient.helper.SocketPacketHelper;
import socketclient.helper.SocketResponsePacket;
import socketclient.util.CharsetUtil;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;


/**
 * TestClient
 * Created by vilyever on 2016/7/26.
 * Feature:
 */
public class ClientSocket {

    public interface IsConnectedListener {
        void sendMes(SocketClient client);
    }
    private IsConnectedListener listener;
    public void setListener(IsConnectedListener listener) {
        this.listener = listener;
    }
    private Context context;
    public ClientSocket(Context context) {
        this.context = context;
    }
    final ClientSocket self = this;
    public void connect() {
        self.getLocalSocketClient().connect();
    }
    private SocketClient localSocketClient;
    public SocketClient getLocalSocketClient() {
        if (this.localSocketClient == null) {
            this.localSocketClient = new SocketClient();
            __i__setupAddress(this.localSocketClient);
            __i__setupEncoding(this.localSocketClient);
            __i__setupConstantHeartBeat(this.localSocketClient);
            __i__setupReadByLengthForSender(this.localSocketClient);
            __i__setupReadByLengthForReceiver(this.localSocketClient);
            this.localSocketClient.registerSocketClientDelegate(new SocketClientDelegate() {
                @Override
                public void onConnected(SocketClient client) {
                    Toast.makeText(context, "服务器已连接", Toast.LENGTH_SHORT).show();
                    DbUtils dbUtils=DbUtils.getInstance(context);
                    String userno = dbUtils.getTeacher().getUserno();
                    TeacherLoginRequest teacherLoginRequest = new TeacherLoginRequest(userno);

                    SendOrderBean sendOrderBean = new SendOrderBean("teacherLoginRequest", JSON.toJSONString(teacherLoginRequest), System.currentTimeMillis() + "");

                    client.sendString(JSON.toJSONString(sendOrderBean));
                    if (listener != null) {
                        listener.sendMes(client);
                    }

                }

                @Override
                public void onDisconnected(final SocketClient client) {
                    Toast.makeText(context, "服务器断开连接", Toast.LENGTH_SHORT).show();

                    new AsyncTask() {
                        @Override
                        protected Void doInBackground(Void... params) {
                            try {
                                Thread.sleep(3 * 1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                            client.connect();

                            return null;
                        }

                        @Override
                        protected void onPostExecute(Void aVoid) {
                            super.onPostExecute(aVoid);

                        }
                    }.execute();
                }

                @Override
                public void onResponse(final SocketClient client, @NonNull SocketResponsePacket responsePacket) {
                      Log.i("onResponse", "SocketClient: onResponse: " + responsePacket.hashCode() + " 【" + responsePacket.getMessage() + "】 " + " isHeartBeat: " + responsePacket.isHeartBeat() + " " + Arrays.toString(responsePacket.getData()));
                    if (responsePacket.isHeartBeat()) {
                        return;
                    }

                    Toast.makeText(context,responsePacket.getMessage(),Toast.LENGTH_LONG).show();

                    Intent intent = new Intent();
                    intent.setAction("SOCKET_MESSAGE");
                    Bundle bundle = new Bundle();
                    bundle.putString("message", responsePacket.getMessage());
                    intent.putExtra("data", bundle);
                    App.localBroadcastManager.sendBroadcast(intent);

                }
            });
            this.localSocketClient.registerSocketClientSendingDelegate(new SocketClientSendingDelegate() {

                @Override
                public void onSendPacketBegin(SocketClient client, SocketPacket packet) {
                    byte[] data = packet.getData();

                    Log.i("onSend", "SocketClient: 数据发送开始: " + new String(data, 0, data.length));
                }

                @Override
                public void onSendPacketCancel(SocketClient client, SocketPacket packet) {
                    Log.i("onSend", "SocketClient: 发送数据暂停: " + packet.hashCode());
                }

                @Override
                public void onSendingPacketInProgress(SocketClient client, SocketPacket packet, float progress, int sendedLength) {
                    Log.i("onSend", "SocketClient: 数据发送进度: " + packet.hashCode() + " : " + progress + " : " + sendedLength);
                }

                @Override
                public void onSendPacketEnd(SocketClient client, SocketPacket packet) {
                    Log.i("onSend", "SocketClient: 数据发送结束: " + packet.hashCode() + " " + packet.getMessage());
                }
            });
            this.localSocketClient.registerSocketClientReceiveDelegate(new SocketClientReceivingDelegate() {
                @Override
                public void onReceivePacketBegin(SocketClient client, SocketResponsePacket packet) {
                    byte[] data = packet.getData();
                    if (data != null) {
                        Log.i("onReceive", "SocketClient: 开始接受数据: " + new String(data, 0, data.length));

                    }

                }

                @Override
                public void onReceivePacketEnd(SocketClient client, SocketResponsePacket packet) {
                    byte[] data = packet.getData();
                    Log.i("onReceive", "SocketClient: 接受到的数据 " + new String(data, 0, data.length));
                }

                @Override
                public void onReceivePacketCancel(SocketClient client, SocketResponsePacket packet) {
                    Log.i("onReceive", "SocketClient: 接受数据失败 " + packet.hashCode());
                }

                @Override
                public void onReceivingPacketInProgress(SocketClient client, SocketResponsePacket packet, float progress, int receivedLength) {
                    Log.i("onReceive", "SocketClient: 接受数据进度 " + packet.hashCode() + " : " + progress + " : " + receivedLength);
                }
            });
        }
        return this.localSocketClient;
    }

    /* Overrides */


    /* Delegates */


    /* Private Methods */

    /**
     * 设置远程端地址信息
     */
    private void __i__setupAddress(SocketClient socketClient) {
        socketClient.getAddress().setRemoteIP(Contacts.SOCKETIP); // 远程端IP地址
        socketClient.getAddress().setRemotePort(Contacts.SOCKETPORT); // 远程端端口号
        socketClient.getAddress().setConnectionTimeout(Contacts.SOCKETCONNECTTIMEOUT); // 连接超时时长,单位毫秒
    }

    /**
     * 设置自动转换String类型到byte[]类型的编码
     * 如未设置(默认为null),将不能使用{@link SocketClient#sendString(String)}发送消息
     * 如设置为非null(如UTF-8),在接受消息时会自动尝试在接收线程(非主线程)将接收的byte[]数据依照编码转换为String,在{@link SocketResponsePacket#getMessage()}读取
     */
    private void __i__setupEncoding(SocketClient socketClient) {
        socketClient.setCharsetName(CharsetUtil.UTF_8); // 设置编码为UTF-8
    }

    private void __i__setupConstantHeartBeat(SocketClient socketClient) {
        /**
         * 设置自动发送的心跳包信息
         */
        socketClient.getHeartBeatHelper().setDefaultSendData(CharsetUtil.stringToData("{\"type\":\"pong\"}", CharsetUtil.UTF_8));

        /**
         * 设置远程端发送到本地的心跳包信息内容,用于判断接收到的数据包是否是心跳包
         * 通过{@link SocketResponsePacket#isHeartBeat()} 查看数据包是否是心跳包
         *
         *
         */


        socketClient.getHeartBeatHelper().setDefaultReceiveData(CharsetUtil.stringToData("{\"type\":\"ping\"}", CharsetUtil.UTF_8));
        socketClient.getHeartBeatHelper().setHeartBeatInterval(10 * 1000); // 设置自动发送心跳包的间隔时长,单位毫秒
        socketClient.getHeartBeatHelper().setSendHeartBeatEnabled(true); // 设置允许自动发送心跳包,此值默认为false
    }


    private void __i__setupReadByLengthForSender(SocketClient socketClient) {


        /**
         * 设置包长度转换器
         * 即每次发送数据时,将包头以外的数据长度转换为特定的byte[]发送个远程端用于解析还需要读取多少长度的数据
         *
         * 例:socketClient.sendData(new byte[]{0x01, 0x02})的步骤为
         * 1. socketClient向远程端发送包头(如果设置了包头信息)
         * 2. socketClient要发送的数据为{0x01, 0x02},长度为2(若设置了包尾,还需加上包尾的字节长度),通过此转换器将int类型的2转换为4字节的byte[],远程端也照此算法将4字节的byte[]转换为int值
         * 3. socketClient向远程端发送转换后的长度信息byte[]
         * 4. socketClient向远程端发送正文数据{0x01, 0x02}
         * 5. socketClient向远程端发送包尾(如果设置了包尾信息)
         *
         * 此转换器用于第二步
         *
         * 使用{@link socketclient.helper.SocketPacketHelper.ReadStrategy.AutoReadByLength}必须设置此项
         * 用于分隔多条消息
         */

        //  socketClient.getSocketPacketHelper().setSendHeaderData(null);

  /*      socketClient.getSocketPacketHelper().setSendPacketLengthDataConvertor(new SocketPacketHelper.SendPacketLengthDataConvertor() {
            @Override
            public byte[] obtainSendPacketLengthDataForPacketLength(SocketPacketHelper helper, int packetLength) {
                *//**
         * 简单将int转换为byte[]
         *//*

                byte[] data = new byte[4];
                data[3] = (byte) (packetLength & 0xFF);
                data[2] = (byte) ((packetLength >> 8) & 0xFF);
                data[1] = (byte) ((packetLength >> 16) & 0xFF);
                data[0] = (byte) ((packetLength >> 24) & 0xFF);
                return data;
            }
        });*/

        /**
         * 根据连接双方协议设置自动发送的包头数据
         * 每次发送数据包(包括心跳包)都会在发送包内容前自动发送此包头
         * 若无需包头可删除此行
         */

        // socketClient.getSocketPacketHelper().setSendHeaderData(CharsetUtil.stringToData(null, CharsetUtil.UTF_8));

        /**
         * 根据连接双方协议设置自动发送的包尾数据
         * 每次发送数据包(包括心跳包)都会在发送包内容后自动发送此包尾
         *
         * 若无需包尾可删除此行
         * 注意:
         * 使用{@link socketclient.helper.SocketPacketHelper.ReadStrategy.AutoReadByLength}时不依赖包尾读取数据
         */
        socketClient.getSocketPacketHelper().setSendTrailerData(CharsetUtil.stringToData("\n", CharsetUtil.UTF_8));

        /**
         * 设置分段发送数据长度
         * 即在发送指定长度后通过 {@link SocketClientSendingDelegate#onSendingPacketInProgress(SocketClient, SocketPacket, float, int)}回调当前发送进度
         * 注意:回调过于频繁可能导致设置UI过于频繁从而导致主线程卡顿
         *
         * 若无需进度回调可删除此二行,删除后仍有【发送开始】【发送结束】的回调
         */
        socketClient.getSocketPacketHelper().setSendSegmentLength(8); // 设置发送分段长度,单位byte
        socketClient.getSocketPacketHelper().setSendSegmentEnabled(true); // 设置允许使用分段发送,此值默认为false

        /**
         * 设置发送超时时长
         * 在发送每个数据包时,发送每段数据的最长时间,超过后自动断开socket连接
         * 通过设置分段发送{@link SocketPacketHelper#setSendSegmentEnabled(boolean)} 可避免发送大数据包时因超时断开,
         *
         * 若无需限制发送时长可删除此二行
         */
        socketClient.getSocketPacketHelper().setSendTimeout(30 * 1000); // 设置发送超时时长,单位毫秒
        socketClient.getSocketPacketHelper().setSendTimeoutEnabled(true); // 设置允许使用发送超时时长,此值默认为false
    }

    private void __i__setupReadByLengthForReceiver(SocketClient socketClient) {
        /**
         * 设置读取策略为自动读取指定长度
         */
        socketClient.getSocketPacketHelper().setReadStrategy(SocketPacketHelper.ReadStrategy.AutoReadToTrailer);

        /**
         * 设置包长度转换器
         * 即每次接收数据时,将远程端发送到本地的长度信息byte[]转换为int,然后读取相应长度的值
         *
         * 例:自动接收远程端所发送的socketClient.sendData(new byte[]{0x01, 0x02})【{0x01, 0x02}为将要接收的数据】的步骤为
         * 1. socketClient接收包头(如果设置了包头信息)(接收方式为一直读取到与包头相同的byte[],即可能过滤掉包头前的多余信息)
         * 2. socketClient接收长度为{@link SocketPacketHelper#getReceivePacketLengthDataLength()}(此处设置为4)的byte[],通过下面设置的转换器,将byte[]转换为int值,此int值暂时称为X
         * 3. socketClient接收长度为X的byte[]
         * 4. socketClient接收包尾(如果设置了包尾信息)(接收方式为一直读取到与包尾相同的byte[],如无意外情况,此处不会读取到多余的信息)
         * 5. socketClient回调数据包
         *
         * 此转换器用于第二步
         *
         * 使用{@link socketclient.helper.SocketPacketHelper.ReadStrategy.AutoReadByLength}必须设置此项
         * 用于分隔多条消息
         */
        socketClient.getSocketPacketHelper().setReceivePacketLengthDataLength(4);
        socketClient.getSocketPacketHelper().setReceivePacketDataLengthConvertor(new SocketPacketHelper.ReceivePacketDataLengthConvertor() {
            @Override
            public int obtainReceivePacketDataLength(SocketPacketHelper helper, byte[] packetLengthData) {
                /**
                 * 简单将byte[]转换为int
                 */
                int length = (packetLengthData[3] & 0xFF) + ((packetLengthData[2] & 0xFF) << 8) + ((packetLengthData[1] & 0xFF) << 16) + ((packetLengthData[0] & 0xFF) << 24);

                return length;
            }
        });

        /**
         * 根据连接双方协议设置的包头数据
         * 每次接收数据包(包括心跳包)都会先接收此包头
         *
         * 若无需包头可删除此行
         */
        socketClient.getSocketPacketHelper().setReceiveHeaderData(CharsetUtil.stringToData("\t", CharsetUtil.UTF_8));

        /**
         * 根据连接双方协议设置的包尾数据
         *
         * 若无需包尾可删除此行
         * 注意:
         * 使用{@link socketclient.helper.SocketPacketHelper.ReadStrategy.AutoReadByLength}时不依赖包尾读取数据
         */
        socketClient.getSocketPacketHelper().setReceiveTrailerData(CharsetUtil.stringToData("\n", CharsetUtil.UTF_8));

        /**
         * 设置接收超时时长
         * 在指定时长内没有数据到达本地自动断开
         *
         * 若无需限制接收时长可删除此二行
         */
        socketClient.getSocketPacketHelper().setReceiveTimeout(120 * 1000); // 设置接收超时时长,单位毫秒
        socketClient.getSocketPacketHelper().setReceiveTimeoutEnabled(true); // 设置允许使用接收超时时长,此值默认为false
    }


}

                2.发送与接收的数据格式定义:(包头+数据+包尾)

                为什么要定义包头与包尾:1.本人初次开发的时候没有使用这个socket框架,而是自己写的,因为tcp发送读取就那么几句话而已,可实际测试的时候会发现接受的时候会产生数据欠缺,这主要是客户端与服务器的交流发送是流的形式,当你socket建立连接的时候,就好比建立了管道,数据好比水流,给数据建立特定的开头与结尾,这样才能保证准确的拿到数据。一般的数据这里也就是字符串格式应该定义为:【包头】【特定位数指定数据长度】【json字符串】【包尾】。这里我给出我一开始做的例子:一开始我没有给定数据长度而是一位一位的读取,我的包头为\t 包尾是\n:

 public void getMsg() {
        try {
            if (socket != null) {
                is = socket.getInputStream();
                byte[] b = new byte[1];
                int count = is.read(b);
                while (is != null && count != -1) {
                    String cha = new String(b, 0, b.length);
                    // Log.i("服务器发送的完整命令:::", cha);
                    if (cha.equals("\t")) {
                        sb = new StringBuffer();
                        count = is.read(b);
                        Log.i("*****", "***************");
                    } else if (cha.equals("\n")) {
                        if (sb != null) {
                            Log.i("服务器发送的完整命令:::", sb.toString());
                            if (sb.toString().equals("{\"type\":\"ping\"}")) {
                                try{
                                    Classroom receiveOrderBean = JSON.parseObject(sb.toString(), Classroom.class);
                                }catch (com.alibaba.fastjson.JSONException ex){
                                    Log.i("解析出错", sb.toString());

                                }

                             /*   String type = receiveOrderBean.getType();
                                Log.i("服务器发送的完整命令2:::", type+" ");*/
                                CONNECTEDTIMEOUT = 20;
                            }else{



                                Intent intent = new Intent();
                                intent.setAction("SERVICE_MESSAGE");
                                Bundle bundle = new Bundle();
                                bundle.putString("message", sb.toString());
                                intent.putExtra("data", bundle);
                                App.localBroadcastManager.sendBroadcast(intent);


                            }


                               /*     if (handler != null) {
                                        Message msg = Message.obtain();
                                        msg.what = -1;
                                        msg.obj = sb.toString();
                                        handler.sendMessage(msg);
                                    }*/

                            sb = null;
                            count = is.read(b);
                        } else {
                            count = is.read(b);
                            //sb = new StringBuffer();
                        }

                    } else {
                        if (sb != null) {
                            sb.append(cha);
                        }
                        count = is.read(b);
                    }

                }


            }


        } catch (IOException e) {
            e.printStackTrace();
        }

    /*
    *         }
        });*/


    }


发送的数据与接收的数据一般要与服务器约定好,这里我发送接收的数据都是json字符串,发送命令字符串里包含三部分:type(命令类型)+time(发送时间)+data(发送的信息),接收命令包含:type(对应命令服务器的返回类型)+time(时间)+data(返回字符串)。


这里由于php端使用的是walkman,walkman默认要求发送数据的结尾为\n,也就是包尾,所以在clientsocket里面设置了\t为包头,\n为包尾,这样发送数据的时候只要发我们想要发的json字符串就可以了。当然还要设置心跳:我发给服务器的数据ping与接收的数据pong,这个框架好的一点是可以检测要发送的进度。



异常的分析处理:   walkman接收数据的时候开头出现乱码:

                                   这里主要是clientsocket里发送数据的方法里将string数据转成了byte[],(字节流与字符流的问题),导致walkman接收的时候开头乱码(个人认为,毕竟两边都                                    是框架,别人写的,自己功力尚浅,胡乱猜测)

                                  【解决办法】将clientsocket里这个方法注释掉:

                                   

  socketClient.getSocketPacketHelper().setSendPacketLengthDataConvertor(new SocketPacketHelper.SendPacketLengthDataConvertor() {
            @Override
            public byte[] obtainSendPacketLengthDataForPacketLength(SocketPacketHelper helper, int packetLength) {
      /*          *
         * 简单将int转换为byte[]*/
         

                byte[] data = new byte[4];
                data[3] = (byte) (packetLength & 0xFF);
                data[2] = (byte) ((packetLength >> 8) & 0xFF);
                data[1] = (byte) ((packetLength >> 16) & 0xFF);
                data[0] = (byte) ((packetLength >> 24) & 0xFF);
                return data;
            }
        });

                                   



 
  









                3.客户端1向客户端2发送指令,如果此时客户端1与服务器保持连接,但客户端2与服务器此时是断连的,此时的逻辑处理(服务器)


                   client1要想向client2发送消息,client2没有与服务器连接,自然收不到消息,此时消息必然是存在服务器中的。只要在socketclient里建立连接之后的方法里向服务器发送一条数据(此数据即用户登录),即让服务器知道这个连进去的socket是你,因为服务器端是有你连进去的监听的,一连进去,服务器就会跟去你的账号查找有没有你没收到的消息,有就会发送,这也就是每次与服务器进行socket连接的时候,客户端发送的第一条消息(除心跳外)就是用户登录。





工具类:

          socket在android里坑定是放在线程里,接收消息我这里使用本地广播接收的,这里提供一个单例模式的工具类:socket坑定是连接之后才能发消息的,不然报错

package utils;

import android.content.Context;
import android.util.Log;

import socketclient.SocketClient;

/**
 * Created by hcy on 2016/11/9.
 */

public class SingleSocket {


    private static SingleSocket uniqueSocket;

    private ClientSocket socket;

    private Context context;

    private SocketClient client;


    private SingleSocket(Context context) {
        this.context = context;

        socket = new ClientSocket(context);
        socket.connect();
        socket.setListener(new ClientSocket.IsConnectedListener() {
            @Override
            public void sendMes(SocketClient cli) {

                Log.i("DDDDDPPPPP", "ddd");
                client = cli;
            }
        });

    }


    public void  disConnectedSocket(){
        if (client!=null){
            client.disconnect();

        }
    }


    public static synchronized SingleSocket getUniqueSocket(Context context) {
        if (uniqueSocket == null) {
            uniqueSocket = new SingleSocket(context);
        }

        return uniqueSocket;
    }


    public void sendMessage(String str) {

        if (client != null) {

            client.sendString(str);
        }
    }

}







      



你可能感兴趣的:(android,socket,walkman服务器,通讯,心跳,Android)