簡介
https://github.com/GramYang/landlord_client
這是一個斗地主游戲的Android前端demo,對應(yīng)landlord_go后端。你可以把它當(dāng)作一個Android使用原生socket連接go的后端的范例。
特點(diǎn)
使用原生socket+protobuf與后端通信,輕便簡捷珊随。
牌桌采用自定義view設(shè)計(jì),更加簡潔(不會套殼)。
個人寫前端沒什么設(shè)計(jì)天賦蝇更,頁面比較挫。宛逗。
自定義view
CircleImageView
CardsPack
繼承自recyclerview坎匿。
adapter
其中持有一個CardsPackAdapter,CardsPackAdapter持有兩個list代表持有的牌和打出去的牌,牌在點(diǎn)擊后從持有的牌中刪除并添加到打出去的牌中替蔬。
使用
在GameActivity中告私,首先初始化LinearLayoutManager:
LinearLayoutManager layoutManagerLeft = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,
false);
layoutManagerLeft.setSmoothScrollbarEnabled(false);
然后將LinearLayoutManager和傳遞數(shù)據(jù)和標(biāo)記位的CardsPackAdapter傳遞進(jìn)CardsPack實(shí)例中:
rivalLeftCardsOut.setLayoutManager(layoutManagerLeft);
leftAdapter = rivalLeftCardsOut.new CardsPackAdapter(this, 1, leftCardsOut);
rivalLeftCardsOut.setAdapter(leftAdapter);
網(wǎng)絡(luò)庫
網(wǎng)絡(luò)庫在OkSocket的基礎(chǔ)上進(jìn)行了修改。
使用
ConnectionInfo loginInfo = new ConnectionInfo(Constants.loginHost, Constants.loginPort);
loginManager = OkSocket.open(loginInfo);
loginManager.registerReceiver(new LoginHandler());
loginManager.connect();
ConnectionInfo用來存放ip和port承桥,調(diào)用OkSocket.open()返回一個IConnectionManager實(shí)例驻粟,調(diào)用registerReceiver()傳入連接后的回調(diào)handler實(shí)例,最后調(diào)用connect()進(jìn)行socket的連接凶异。
- LoginHandler
這是一個SocketActionAdapter的實(shí)現(xiàn)類蜀撑,SocketActionAdapter的實(shí)現(xiàn)類會在socket返回信息后分發(fā)時逐個遍歷,因此可以有多個SocketActionAdapter的實(shí)現(xiàn)類去實(shí)現(xiàn)相同的方法剩彬,他們都會被調(diào)用酷麦。
流程分析
OkSocket.open(info)
返回一個IConnectionManager實(shí)例,該實(shí)例以info為key存入一個map緩存喉恋,第一次調(diào)用的時候調(diào)用new ConnectionManagerImpl(info)沃饶。ConnectionManagerImpl構(gòu)造器中設(shè)置了一些標(biāo)記位,提取了ip和port轻黑。
ConnectionManagerImpl.connect()
- 綁定ActionHandler實(shí)例
ActionHandler實(shí)現(xiàn)了SocketActionAdapter的三個方法糊肤,被存入在一個列表中,在讀寫線程解析完數(shù)據(jù)后會使用這個列表來遍歷處理數(shù)據(jù)氓鄙。如何選擇不同的方法來處理不同情況下的數(shù)據(jù)馆揉,就是靠的key了,其實(shí)現(xiàn)在ActionDispatcher玖详。
這里的ActionHandler主要處理IOThread啟動把介、斷開,連接失敗蟋座。
- 綁定DefaultReconnectManager實(shí)例
DefaultReconnectManager同樣實(shí)現(xiàn)了ISocketActionListener拗踢,也算是handler的一種。
在attach方法中向臀,持有傳入的mConnectionManager技扼,以及其中的mPulseManager(這個時候可能是空的,但是過幾毫秒就不是空的了)瓜客,將DefaultReconnectManager實(shí)例也傳入到ISocketActionListener列表中谷扣。DefaultReconnectManager實(shí)現(xiàn)了ISocketActionListener的三個方法,主要實(shí)現(xiàn)了斷線重連的邏輯芹彬。
- 生成ConnectionManagerImpl.ConnectionThread實(shí)例蓄髓,并Start
其中,socket實(shí)例連接指定地址并設(shè)置超時舒帮,然后生成PulseManager實(shí)例会喝,生成IOThreadManager實(shí)例并startEngine()陡叠。連接成功就發(fā)送action_connection_success,拋異常則發(fā)送action_connection_failed和異常實(shí)例肢执。
- 生成PulseManager實(shí)例
上面生成了PulseManager的實(shí)例枉阵,其pulse()需要你在設(shè)置mSendable后自己手動調(diào)用。
在pulse()中预茄,設(shè)置好心跳頻率后兴溜,開啟mPulseThread,其線程模式是DUPLEX耻陕。run()中拙徽,超時了就斷開,否則就發(fā)送心跳包淮蜈,發(fā)送的動作是通過IConnectionManager實(shí)現(xiàn)的斋攀。
心跳是在一個線程中定期調(diào)用ConnectionManagerImpl的send方法實(shí)現(xiàn)的。
- IOThreadManager.startEngine()
IOThreadManager的構(gòu)造器持有socket的兩個流梧田,以及mActionDispatcher淳蔼。然后獲取mReaderProtocol,也就是ltv協(xié)議三個部分長度的解析方法裁眯。初始化ReaderImpl和WriterImpl鹉梨。
startEngine()中生成DuplexWriteThread和DuplexReadThread實(shí)例,然后啟動穿稳。
- DuplexWriteThread和DuplexReadThread
就是裝包和拆包的過程存皂,無限執(zhí)行WriterImpl和ReaderImpl的write()和read()。
IConnectionManager.send()
調(diào)用IOThreadManager的send()逢艘,向WriteImpl的LinkedBlockingQueue中添加消息旦袋,WriteImpl的write()則會不停的從LinkedBlockingQueue中取消息處理。
這里增加了一個sendAfterConnect()它改,用一個信號量控制在connect()中的邏輯執(zhí)行完后再異步的調(diào)用send()
ReaderImpl.read()
從socket的inputStream中讀取流數(shù)據(jù)后疤孕,拆包填充OriginalData,調(diào)用
mStateSender.sendBroadcast(IOAction.ACTION_READ_COMPLETE, originalData)
進(jìn)行分發(fā)央拖,這里的mStateSender就是ActionDispatcher祭阀。
ActionDispatcher的sendBroadcast()中將action和originalData存入
ActionDispatcher.ActionBean實(shí)例,然后存入LinkedBlockingQueue中
ActionDispatcher中的DispatchThread靜態(tài)實(shí)例啟動鲜戒,其run()中從LinkedBlockingQueue取出ActionBean實(shí)例专控,然后遍歷ActionDispatcher的mResponseHandlerList(就是上面封裝的ISocketActionListener列表)
ActionDispatcher.dispatchActionToListener()
"action_read_complete":ReaderImpl.read()解析成功后發(fā)送
"action_read_thread_start":DuplexReadThread的beforeLoop()
"action_write_thread_start":DuplexWriteThread的beforeLoop()
"action_read_thread_shutdown":DuplexReadThread的loopFinish()
"action_write_thread_shutdown":DuplexWriteThread的loopFinish()
"action_pulse_request":WriterImpl的write()
"action_write_complete":WriterImpl的write()
"action_disconnection":DisconnectThread運(yùn)行完之后
"action_connection_success":ConnectionThread連接成功
"action_connection_failed":ConnectionThread連接失敗拋出異常
游戲邏輯
- 進(jìn)入房間后準(zhǔn)備
準(zhǔn)備:發(fā)送new ReadyRequest(true)
解除整備:發(fā)送new CancelReadyRequest(true)
所有玩家準(zhǔn)備后開始搶地主
- 開始搶地主
onGrabLandlordResponse()中顯示三張地主牌,顯示手中的牌遏餐,顯示搶地主面板
選擇搶地主:選擇加倍
選擇不搶地主:順移到右邊的玩家搶地主伦腐,如果其他兩個玩家都不搶地主,強(qiáng)制成為地主失都,選擇加倍
- 選擇加倍
onEndGrabLandlordResponse()中地主顯示加倍選擇面板蔗牡,在地主選擇加倍后其他兩個玩家選擇加倍應(yīng)答面板
不選擇加倍:直接開始游戲
選擇加倍:其他玩家根據(jù)倍數(shù)判斷是否加倍颖系,有一個人不同意加倍,則加倍取消辩越,開始游戲
- 開始游戲
onCardsOutResponse()中出牌和接牌
對牌的判斷和處理邏輯都在前端
第一個牌出完的玩家觸發(fā)結(jié)束游戲
- 結(jié)束游戲
onEndGameResponse()中彈窗顯示戰(zhàn)績,你可以選擇繼續(xù)也可以選擇退出房間
與后端的協(xié)議適配
后端的拆包和封包協(xié)議都采用的是ltv格式信粮,length-type-value黔攒。內(nèi)容長度如下:
public class ReaderProtocol1 implements IReaderProtocol {
@Override
public int getHeaderLength() {
return 2;
}
@Override
public int getTypeLength() {
return 2;
}
@Override
public int getBodyLength(byte[] header, ByteOrder byteOrder) {
if (header == null || header.length < getHeaderLength()) {
return 0;
}
ByteBuffer bb = ByteBuffer.wrap(header);
bb.order(byteOrder);
return bb.getShort() - 2;
}
}
封包示例
public class JsonReq implements ISendable {
private int jsonType;
private byte[] content;
public JsonReq(int jsonType, byte[] content) {
this.jsonType = jsonType;
this.content = content;
}
public int getJsonType() {
return jsonType;
}
public byte[] getContent() {
return content;
}
@Override
public byte[] parse() {
Landlord.JsonREQ.Builder builder = Landlord.JsonREQ.newBuilder();
builder.setJsonType(jsonType);
builder.setContent(ByteString.copyFrom(content));
Landlord.JsonREQ req = builder.build();
byte[] body = req.toByteArray();
ByteBuffer bb = ByteBuffer.allocate(4+body.length);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putShort((short)(body.length+2));
bb.putShort(Constants.JSON_REQ_TYPE);
bb.put(body);
return bb.array();
}
}
特別注意:后端采用的是小端序列,因此前端也要用小端序列强缘,而Java默認(rèn)的是大端序列督惰。