Android基于socket的五子棋双人网络对战实现
2018-10-29 本文已影响20人
e4e52c116681
零、前言
五子棋.png1.很久以前在慕课网看过鸿洋的五子棋实现的视频,由于是教学,功能比较简单。详情可见
2.然后我基于此拓展了一些功能,比如音效、自定义网格数,选择图片设置背景、截图、悔棋等。
3.最想做的当然是联网对战啦,当时实力不济,只好暂放,现在回来看看,感觉可以做。
4.核心是在每次绘制时将坐标点传给服务端,然后服务端再将数据发送给两个手机,在视图上显示。
5.该应用可以开启服务端,也可以连接服务端,具体如下:
本文着重于介绍:
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.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的CSDN | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持