Android开发Android开发Android技术知识

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

2018-10-29  本文已影响20人  e4e52c116681

零、前言

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----看到这里,我在此感谢你的喜欢与支持

上一篇下一篇

猜你喜欢

热点阅读