Android基於socket的五子棋雙人網路對戰實現
零、前言
1.很久以前在慕課網看過鴻洋的五子棋實現的視訊,由於是教學,功能比較簡單。 ofollow,noindex">詳情可見
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/> * 郵箱:[email protected]<br/> * 說明:客戶端連線時服務端回撥 */ public interface IAcceptCallback { /** * 連線成功回撥 */ void onConnect(String msg); /** * 連線錯誤回撥 * @param e 異常 */ void onError(Exception e); }
2.ServerHelper: 服務端執行緒---=建立伺服器端、監聽客戶端的連線、維護客戶端訊息集合
/** * 作者:張風捷特烈 * 時間:2018/11/2 0015:14:53 * 郵箱:[email protected] * 說明:服務端執行緒---=建立伺服器端、監聽客戶端的連線、維護客戶端訊息集合 */ 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 * 郵箱:[email protected] * 說明:連線的客戶端在此執行緒,用來向收集客戶端發來的資訊 */ 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 * 郵箱:[email protected] * 說明:用於服務端向所有客戶端傳送訊息 */ 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/> * 郵箱:[email protected]<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/> * 郵箱:[email protected]<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----看到這裡,我在此感謝你的喜歡與支援