美文网首页Android开发Android开发Android技术知识
Android基于socket的五子棋双人网络对战实现

Android基于socket的五子棋双人网络对战实现

作者: e4e52c116681 | 来源:发表于2018-10-29 21:40 被阅读20次

    零、前言

    1.很久以前在慕课网看过鸿洋的五子棋实现的视频,由于是教学,功能比较简单。详情可见
    2.然后我基于此拓展了一些功能,比如音效、自定义网格数,选择图片设置背景、截图、悔棋等。
    3.最想做的当然是联网对战啦,当时实力不济,只好暂放,现在回来看看,感觉可以做。
    4.核心是在每次绘制时将坐标点传给服务端,然后服务端再将数据发送给两个手机,在视图上显示。
    5.该应用可以开启服务端,也可以连接服务端,具体如下:

    五子棋.png
    本文着重于介绍:

    1.通过文件记录点位和打开时复原数据
    2.基于TCP的Socket实现两个手机间的数据交互,完成两个手机的网络对战
    3.五子棋的具体实现比较基础,就不在这贴了,会说明一下重要的方法接口,文尾附上github源码地址,可自行查看

    网络对战的流程概要:
    流程概览.png
    五子棋的接口(public)方法
    start();//重新开局
    backStep();//悔棋
    
    getCurrentPos()//获取落点
    getWhites()//获取白子集合
    getBlacks()//获取黑子集合
    
    //根据点位来设置棋盘
    public void setPoints(ArrayList<Point> whites, ArrayList<Point> blacks)
    
    结束回调接口:OnGameOverListener :void gameOver(boolean isWhiteWin)
    绘制回调接口:OnDrawListener:void drawing(boolean isWhite)
    
    最终效果实现一次点击,两个手机同步显示
    最终效果.png

    一、将坐标字符化存储在SD卡

    1.坐标字符化:

    以左上角为(0,0)点,将ArrayList<Point>以x1,y1-x2,y2-...的形式变为字符串

    public class ParseUtils {
        /**
         * 将黑棋和白棋的数据写入文件:格式x1,y1-x2,y2
         *
         * @param pos 棋坐标列表
         */
        public static String point2String(List<Point> pos) {
            //白棋字落点符串
            StringBuilder sbPos = new StringBuilder();
    
            for (Point p : pos) {
                sbPos.append(p.x).append(",").append(p.y).append("-");
            }
            return sbPos.toString();
        }
    }
    
    2.OnDrawListener监听方法下:写入到文件
    //配置信息
    public class CfgCons {
        public final static String SAVE_WHITE_PATH = "五子棋/数据保存/白棋.txt";
        public static final String SAVE_BLACK_PATH = "五子棋/数据保存/黑棋.txt";
    }
    
     /**
      * 将黑棋和白棋的数据写入文件
      *
      * @param whites 白棋坐标列表
      * @param blacks 黑棋坐标列表
      */
     public void savePoint2File(List<Point> whites, List<Point> blacks) {
         String whiteStr = ParseUtils.point2String(whites);
         String blackStr = ParseUtils.point2String(blacks);
         //写入到SD卡中的封装函数(自行处理)
         FileHelper.get().writeFile2SD(CfgCons.SAVE_WHITE_PATH, whiteStr);
         FileHelper.get().writeFile2SD(CfgCons.SAVE_BLACK_PATH, blackStr);
     }
    
    数据本地化.png
    3.解析数据回显
    /**
     * 从字符串解析出坐标点
     *
     * @param pointStr 坐标字符串
     */
    public static ArrayList<Point> parseData(String pointStr) {
        ArrayList<Point> points;
        if (pointStr != null) {
            points = new ArrayList<>();
            String[] strings = pointStr.split("-");
            for (String s : strings) {
                if (s.split(",").length >= 2) {
                    int x = Integer.parseInt(s.split(",")[0].trim());
                    int y = Integer.parseInt(s.split(",")[1].trim());
                    points.add(new Point(x, y));
                }
            }
            return points;
        }
        return null;
    }
    
    4.回显:设置与刷新

    在进入是可以看一下是否有数据,有就回显,这样及时销毁Activity也不用担心

    public void updateView(ArrayList<Point> white, ArrayList<Point> black) {
        mIWuzi.setPoints(white, black);
        mIWuzi.invalidate();
    }
    

    二、服务端的实现

    每当点击时,将落点数据发送给服务端,然后服务端在将数据传送给两个客户端。

    落点数据双向共享.png
    1.IAcceptCallback:客户端连接时服务端回调
    /**
     * 作者:张风捷特烈<br/>
     * 时间:2018/11/2 0018:11:17<br/>
     * 邮箱:1981462002@qq.com<br/>
     * 说明:客户端连接时服务端回调
     */
    public interface IAcceptCallback {
        /**
         * 连接成功回调
         */
        void onConnect(String msg);
    
        /**
         * 连接错误回调
         * @param e 异常
         */
        void onError(Exception e);
    }
    
    
    2.ServerHelper: 服务端线程---=创建服务器端、监听客户端的连接、维护客户端消息集合
    /**
     * 作者:张风捷特烈
     * 时间:2018/11/2 0015:14:53
     * 邮箱:1981462002@qq.com
     * 说明:服务端线程---=创建服务器端、监听客户端的连接、维护客户端消息集合
     */
    public class ServerHelper extends Thread {
    
        //ServerSocket服务
        private ServerSocket mServerSocket;
        // 监听端口
        public static final int PORT = 8080;
        //维护客户端集合,记录客户端线程
        final Vector<ClientThread> mClients;
        //维护消息集合
        final Vector<String> msgs;
        //监听服务端连接的回调
        private IAcceptCallback mAcceptCallback;
        //向所有客户端发送消息的Runnable
        private final BroadCastTask mBroadCastTask;
        
        public ServerHelper() {
            mClients = new Vector<>();//实例化客户端集合
            msgs = new Vector<>();//实例化消息集合
            try {
                mServerSocket = new ServerSocket(PORT);//实例化Socket服务
            } catch (IOException e) {
                e.printStackTrace();
            }
            //创建广播线程并启动:这里只是在启动服务端时创建线程,不会频繁创建,不需要创建线程池
            mBroadCastTask = new BroadCastTask(this);
            new Thread(mBroadCastTask).start();
        }
    
        @Override
        public void run() {
            while (true) {
                try {
                    //socket等待客户端连接
                    Socket socket = mServerSocket.accept();
                    //走到这里说明有客户端连接了,该客户端的Socket流即为socket,
                    ClientThread clientThread = new ClientThread(socket, this);
                    clientThread.start();
                    //设置连接的回调
                    if (mAcceptCallback != null) {
                        Poster.newInstance().post(() -> {
                            String ip = socket.getInetAddress().getHostAddress();
                            mAcceptCallback.onConnect(ip);
                        });
                    }
                    mClients.addElement(clientThread);
                } catch (IOException e) {
                    e.printStackTrace();
                    mAcceptCallback.onError(e);
                }
            }
        }
    
        /**
         * 开启服务发热方法
         * @param iAcceptCallback 客户端连接监听
         * @return 自身
         */
        public ServerHelper open(IAcceptCallback iAcceptCallback) {
            mAcceptCallback = iAcceptCallback;
            new Thread(this).start();
            return this;
        }
    
        /**
         * 关闭服务端和发送线程
         */
        public void close() {
            try {
                mServerSocket.close();
                mBroadCastTask.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            mServerSocket = null;
        }
    }
    
    3.ClientThread:连接的客户端在此线程,用来向收集客户端发来的信息
    /**
     * 作者:张风捷特烈
     * 时间:2018/10/15 0015:14:57
     * 邮箱:1981462002@qq.com
     * 说明:连接的客户端在此线程,用来向收集客户端发来的信息
     */
    public class ClientThread extends Thread {
    
        //持有服务线程引用
        private ServerHelper mServerHelper;
        //输入流----接收客户端数据
        private DataInputStream dis = null;
        //输出流----用于向客户端发送数据
        DataOutputStream dos = null;
    
        public ClientThread(Socket socket, ServerHelper serverHelper) {
    
            mServerHelper = serverHelper;
            try {
                //通过传入的socket获取读写流
                dis = new DataInputStream(socket.getInputStream());
                dos = new DataOutputStream(socket.getOutputStream());
                //服务端发送连接成功反馈
                dos.writeUTF("~连接服务器成功~!");
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("ClientThread IO ERROR");
            }
        }
    
        @Override
        public void run() {
            while (true) {
                try {
                    //此处读取客户端的消息,并加入消息集合
                    String msg = dis.readUTF();
                    mServerHelper.msgs.addElement(msg);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    4.BroadCastTask:用于服务端向所有客户端发送消息
    /**
     * 作者:张风捷特烈
     * 时间:2018/11/3 0015:15:11
     * 邮箱:1981462002@qq.com
     * 说明:用于服务端向所有客户端发送消息
     */
    public class BroadCastTask implements Runnable {
        
        //服务端线程
        private ServerHelper mServerHelper;
        //停止标志
        private boolean isRunning = true;
    
        public BroadCastTask(ServerHelper serverHelper) {
            mServerHelper = serverHelper;
        }
    
        @Override
        public void run() {
            while (isRunning) {
                try {//每隔200毫秒,间断的监听客户端的发送消息情况
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String str;
    
                if (mServerHelper.msgs.isEmpty()) {////当消息为空时,不执行下面
                    continue;
                }
                str = mServerHelper.msgs.firstElement();
    
                for (ClientThread client : mServerHelper.mClients) {
                    //获取所有的客户端线程,将信息写出
                    try {
                        client.dos.writeUTF(str);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    mServerHelper.msgs.removeElement(str);
                }
            }
        }
    
        public void close() {
            isRunning = false;
        }
    }
    
    5.使用:在需要打开服务器的事件下写:(我这里是长按背景)
    mRlRoot.setOnLongClickListener(v -> {
                if (mServerHelper != null) {
                    return false;
                }
                new Thread(() -> {
                    mServerHelper = new ServerHelper().open(new IAcceptCallback() {
                        @Override
                        public void onConnect(String msg) {
                            ToastUtil.showAtOnce(MainActivity.this, msg);
                        }
                        @Override
                        public void onError(Exception e) {
                        }
                    });
                }).start();
                return false;
            });
    

    三、服务端的实现:

    1. 客户端连接时客户端的回调
    /**
     * 作者:张风捷特烈<br/>
     * 时间:2018/9/18 0018:11:17<br/>
     * 邮箱:1981462002@qq.com<br/>
     * 说明:客户端连接时客户端的回调
     */
    public interface IConnCallback {
        /**
         * 开始连接时回调
         */
        void onStart();
        /**
         * 连接错误回调
         *
         * @param e 异常
         */
        void onError(Exception e);
        /**
         * 连接成功回调
         */
        void onFinish(String msg);
    
        //给一个默认的接口对象--也可以在不写,在用时判断非空
        DefaultCnnCallback DEFAULT_CONN_CALLBACK = new DefaultCnnCallback();
    
        /**
         * 默认的连接时回调
         */
        class DefaultCnnCallback implements IConnCallback {
            @Override
            public void onStart() {
    
            }
            @Override
            public void onError(Exception e) {
    
            }
            @Override
            public void onFinish(String msg) {
    
            }
        }
    }
    
    2.ClientHelper:客户端的辅助类(用于连接,发送数据)
    /**
     * 作者:张风捷特烈<br/>
     * 时间:2018/10/29 0029:13:37<br/>
     * 邮箱:1981462002@qq.com<br/>
     * 说明:客户端的辅助类(用于连接,发送)
     */
    public class ClientHelper {
        private Socket mSocket;
        private boolean isConned;
        private DataInputStream dis;
        private DataOutputStream dos;
        private String mIp;
        private int mPort;
        private ExecutorService mExecutor;
    
        public ClientHelper(String ip, int port) {
            mIp = ip;
            mPort = port;
        }
    
        /**
         * 发送所有落点的位置到服务端
         */
        public void writePos2Service(ArrayList<Point> whites, ArrayList<Point> blacks) {
            new Thread(() -> {
                if (isConned) {
                    try {
                        String whiteStr = ParseUtils.point2String(whites);
                        String blackStr = ParseUtils.point2String(blacks);
                        dos.writeUTF(whiteStr + "#" + blackStr);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
        public DataInputStream getDis() {
            return dis;
        }
    
        /**
         * 连接到服务器
         *
         * @param callback 连接回调
         */
        public void conn2Server(IConnCallback callback) {
            if (isConned) {//已经连接了,就不执行下面
                return;
            }
            if (callback == null) {
                callback = IConnCallback.DEFAULT_CONN_CALLBACK;
            }
            final IConnCallback finalCallback = callback;
            //开始回调:onStart函数
            finalCallback.onStart();
            //使用AsyncTask来实现异步通信(子线程-->主线程)
            new AsyncTask<Void, Void, String>() {
                @Override//子线程运行:耗时操作
                protected String doInBackground(Void... voids) {
                    try {
                        //通过ip和端口连接到到服务端
                        mSocket = new Socket(mIp, mPort);
                        //通过mSocket拿到输入、输出流
                        dis = new DataInputStream(mSocket.getInputStream());
                        dos = new DataOutputStream(mSocket.getOutputStream());
                        //这里通过输入流获取连接时服务端发送的信息,并返回到主线程
                        return dis.readUTF();
                    } catch (IOException e) {//异常处理及回调
                        e.printStackTrace();
                        finalCallback.onError(null);
                        isConned = false;
                        return null;
                    }
                }
    
                @Override//此处是主线程,可进行UI操作
                protected void onPostExecute(String msg) {
                    if (msg == null) {
                        //错误回调:onError函数
                        finalCallback.onError(null);
                        isConned = false;
                        return;
                    }
                    //成功的回调---此时onFinish在主线程
                    finalCallback.onFinish(msg);
                    isConned = true;
                }
            }.execute();
        }
    }
    
    3.客户端的使用:
    1).创建客户端对象
    //注意ip是打开服务端手机的ip地址,可在设置-->关于手机-->状态信息下查看
    mClientHelper = new ClientHelper("192.168.43.39", 8080)
    
    2).在五子棋绘制监听器中发送位置消息:setOnDrawListener里(即每次落子都会向服务器发送消息)
    mClientHelper.writePos2Service(mIWuzi.getWhites(), mIWuzi.getBlacks());
    
    3).让Activity继承Runnable,实现接收服务器数据的轮回线程
    @Override
    public void run() {
        while (true) {
            try {//每隔200毫秒,间断的监听客户端的发送消息情况
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                msgFromServer = mClientHelper.getDis().readUTF();
                runOnUiThread(() -> {
                    //一旦有消息传来,此处会处理,更新UI
                    ToastUtil.showAtOnce(MainActivity.this, msgFromServer);
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    4).在需要的地方,执行连接,并启动轮回线程(这里是按钮长按)
    fab.setOnLongClickListener(v -> {
        mClientHelper.conn2Server(new IConnCallback() {
            @Override
            public void onStart() {
                L.d("onStart" + L.l());
            }
            @Override
            public void onError(Exception e) {
                L.d("onError" + L.l());
            }
            @Override
            public void onFinish(String msg) {
                //已在主线程
                ToastUtil.show(MainActivity.this, msg);
                //开启接收服务器数据的轮回线程
                new Thread(MainActivity.this).start();
                L.d("onConnect" + L.l());
            }
        });
        return true;
    });
    

    四、将接收到的点绘制到界面上:

    1.思路很简单,就是在弹吐司的地方将服务器数据解析,在设置给界面(刷新)即可。
    msgFromServer = mClientHelper.getDis().readUTF();
    runOnUiThread(() -> {
        String[] split = msgFromServer.split("#");
        if (split.length > 0) {
            ArrayList<Point> whitePoints = ParseUtils.parseData(split[0]);
            ArrayList<Point> blackPoints = new ArrayList<>();
            if (split.length > 1) {
                blackPoints = ParseUtils.parseData(split[1]);
            }
            drawByServer = true;//是从服务器绘制的
            mIWuzi.setPoints(whitePoints, blackPoints);
            ToastUtil.showAtOnce(MainActivity.this, msgFromServer);
        }
    });
    
    2.不过有个坑点:

    重绘过后又会调用绘制监听,发送消息,然后循环了,导致一直闪
    在这加了一个boolean标识:drawByServer,来标记是否是从服务端绘制的,在绘制监听中:

    if (drawByServer) {
        drawByServer = false;
        return;
    }
    

    好了,基本上就这样,通过写这个案例,对线程、回调和异步通信、socket网络编程都有了更深的理解。


    后记:捷文规范

    1.本文成长记录及勘误表
    项目源码 日期 备注
    V0.1--五子棋 2018-11-3 Android基于socket的五子棋双人网络对战实现
    2.更多关于我
    笔名 QQ 微信 爱好
    张风捷特烈 1981462002 zdl1994328 语言
    我的github 我的简书 我的CSDN 个人网站
    3.声明

    1----本文由张风捷特烈原创,转载请注明
    2----欢迎广大编程爱好者共同交流
    3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
    4----看到这里,我在此感谢你的喜欢与支持

    相关文章

      网友评论

        本文标题:Android基于socket的五子棋双人网络对战实现

        本文链接:https://www.haomeiwen.com/subject/tsiitqtx.html