For the first two, we have implemented the login screen and the main screen of the game. However, the data of the main interface of the game is written dead text in the front end. In this chapter, we add websocket component to the game module to realize the communication between the front and back end, so that the data of the front end can be dynamically obtained from the back end.
Add maven dependencies
In the POM of the Game module, we added three dependency packages as follows:
1 <! -- WebSocket component --> 2 <dependency> 3 <groupId>org. springFramework </groupId> 4 <artifactId> Spring-websocket </artifactId> 5 <version>5.1.6.RELEASE</version> 6 </dependency> 7 <dependency> 8 <groupId>org. springFramework </groupId> 9 <artifactId>spring- Messaging </artifactId> 10 <version>5.1.6.RELEASE</version> 11 </dependency> 12 <dependency> 13 <groupId>javax.websocket</groupId> 14 <artifactId>javax.websocket- API </artifactId> 15 <version>1.1</version> 16 <scope>provided</scope> 17 </dependency>Copy the code
2. Add MessageHub to the backend
Under com.idlewow.game.hub MessageHub, this class will be responsible for receiving websocket information from the client. The code is as follows:
1 @Component 2 @ServerEndpoint(value = "/hub", configurator = HttpSessionConfigurator.class) 3 public class MessageHub { 4 private static final Logger logger = LogManager.getLogger(MessageHub.class); 5 6 @Autowired 7 MessageHandler messageHandler; 8 @Autowired 9 CharacterService characterService; 10 11 @OnOpen 12 public void onOpen(Session session, EndpointConfig config) {13 Logger. info("[websocket][" + session.getid () + "] Establish connection "); 14 try { 15 HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getSimpleName()); 16 If (httpSession == null) {17 Logger. error("[websocket][" + session.getid () + "] failed to fetch httpSession! ); 18 throw new Exception(" Failed to get HttpSession!" ); 19 } 20 21 22 if (httpSession.getAttribute(GameWorld.SK_CharId) == null) { 23 logger.error("[websocket][" + Session.getid () + "] Get role Id null!" ); 24 Throw new Exception(" Get role ID null!" ); 25 } 26 27 String charId = httpSession.getAttribute(GameWorld.SK_CharId).toString(); 28 CommonResult commonResult = characterService.find(charId); 29 if (commonResult.isSuccess()) { 30 Character character = (Character) commonResult.getData(); 31 / * loaded successfully, add cache * / 32 GameWorld OnlineSession. Add (session); 33 GameWorld.OnlineCharacter.put(session.getId(), character); 34 GameWorld.MapCharacter.get(character.getMapId()).add(character); 35} else {36 Logger. error(" Failed to load role info! charId:" + charId + " message:" + commonResult.getMessage()); 37 Throw new Exception(" Failed to load role information!" ); 38} 39} catch (Exception ex) {40 logger.error("[websocket][" + session.getid () + "]) : "+ ex.getMessage(), ex); 41 this.closeSession(session, ex.getMessage()); 42 } 43 } 44 45 @OnMessage 46 public void onMessage(Session session, String message) {47 Logger. info("[websocket][" + session.getid () + "] receive message: "+ message); 48 messageHandler.handleMessage(session, message); 49 } 50 51 @OnClose 52 public void onClose(Session session) { 53 logger.info("[websocket][" + session.getId() + "] close connection "); 54 / * clear the cache * / 55 Character Character. = GameWorld OnlineCharacter. Get (session. The getId ()); 56 GameWorld.OnlineSession.remove(session); 57 GameWorld.OnlineCharacter.remove(session.getId()); 58 GameWorld.MapCharacter.get(character.getMapId()).remove(character); 59 } 60 61 @OnError 62 public void onError(Session session, Throwable t) {63 logger.error("[websocket][" + session.getid () + "] : "+ t.gettmessage (), t); 64 } 65 66 private void closeSession(Session session, String message) {67 try {68 logger.info("[websocket][" + session.getid () + "] closes connection, cause: "+ message); 69 CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, message); 70 session.close(closeReason); 71} catch (Exception ex) {72 logger.error("[websocket] closed connection Exception: "+ ex.getMessage(), ex); 73} 74} 75}Copy the code
MessageHub
Hub class mainly includes OnOpen, OnMessage, OnClose, OnError four methods.
When OnOpen establishes a connection, we get the role Id from HttpSession, load the role information, update the online data, and so on. Here we create a GameWorld class to store the global static data of the GameWorld, such as the online list.
When the OnMessage method receives the client data, we process the message uniformly in the MessageHandler.
OnClose and OnError correspond to closing the connection and exception occurrence events. When closing the connection, you need to clear the game character from the online list. When an exception occurs, we simply log for the time being.
Note: In the annotation for MesssageHub, we have configured it with a HttpSessionConfigurator. To get HttpSession data in the socket message. Without this configuration, HttpSession is not available. The code is as follows:
1 public class HttpSessionConfigurator extends SpringConfigurator { 2 @Override 3 public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { 4 HttpSession httpSession = (HttpSession) request.getHttpSession(); 5 sec.getUserProperties().put(HttpSession.class.getSimpleName(), httpSession); 6 super.modifyHandshake(sec, request, response); 8 7}}Copy the code
Define the message type
In socket communication, we must define the data structure of the message and prepare corresponding documents to facilitate the communication between the front and back ends.
Here we create the message class WowMessage and specify that it consists of header and Content. Header contains general parameters such as message type and request time. Content contains specific business data.
The UML diagram of the whole message class is as follows, which exemplifies four specific message types: LoadCache cache loading, Login Login message, Chat Chat message, Move map Move message.
4. Back-end message processing
Once the good news type is defined, we can process the corresponding message in the back end. The code is as follows:
In the handleMessage method, we determine what kind of message it is, based on the messageCode passed in the header, and pass it to the corresponding handler submethod.
For example, the handleMoveMessage method is used to process map movement. In this method, we change the current map ID in the character information cache data to the map ID after moving, remove this role from the original map online list, and add this role to the target map online list. And returns the target map information to the front end for display.
@Component
public class MessageHandler {
private static final Logger logger = LogManager.getLogger(MessageHandler.class);
@Autowired
CharacterService characterService;
@Autowired
WowMapService wowMapService;
@Autowired
MapMobService mapMobService;
@Autowired
MapCoordService mapCoordService;
/**
* 消息处理
*
* @param session session
* @param message 消息
*/
public void handleMessage(Session session, String message) {
WowMessage<?> wowMessage = JSONObject.parseObject(message, WowMessage.class);
WowMessageHeader header = wowMessage.getHeader();
String messageCode = header.getMessageCode();
switch (messageCode) {
case WowMessageCode.LoadCache:
this.handleLoadCacheMessage(session, (WowMessage<LoadCacheRequest>) wowMessage);
break;
case WowMessageCode.RefreshOnline:
this.handleRefreshOnlineMessage(session, (WowMessage<RefreshOnlineRequest>) wowMessage);
break;
case WowMessageCode.Login:
this.handleLoginMessage(session, (WowMessage<LoginRequest>) wowMessage);
break;
case WowMessageCode.Chat:
this.handleChatMessage(session, (WowMessage<ChatRequest>) wowMessage);
break;
case WowMessageCode.Move:
this.handleMoveMessage(session, (WowMessage<MoveRequest>) wowMessage);
break;
default:
break;
}
}
/**
* 给指定客户端发送消息
*
* @param session 客户端session
* @param message 消息内容
*/
private void sendOne(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
}
/**
* 给所有客户端发送消息
*
* @param message 消息内容
*/
private void sendAll(String message) {
try {
for (Session session : GameWorld.OnlineSession) {
session.getBasicRemote().sendText(message);
}
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
}
}
/**
* 登陆加载
*
* @param session session
* @param message 消息
*/
private void handleLoginMessage(Session session, WowMessage<LoginRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
LoginResponse response = new LoginResponse();
Character character = GameWorld.OnlineCharacter.get(session.getId());
String mapId = character.getMapId();
MapInfo mapInfo = this.loadMapInfo(mapId);
response.setMapInfo(mapInfo);
OnlineInfo onlineInfo = this.loadOnlineInfo(mapId);
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
}
/**
* 发送聊天
*
* @param session session
* @param message 消息
*/
private void handleChatMessage(Session session, WowMessage<ChatRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
ChatRequest request = message.getContent();
ChatResponse response = new ChatResponse();
response.setSendId(request.getSendId());
response.setSendName(request.getSendName());
response.setRecvId(request.getRecvId());
response.setRecvName(request.getRecvName());
response.setMessage(request.getMessage());
response.setChannel(request.getChannel());
WowMessage wowMessage = new WowMessage<>(header, response);
if (request.getChannel().equals(GameWorld.ChatChannel.WORLD)) {
this.sendAll(JSON.toJSONString(wowMessage));
} else if (request.getChannel().equals(GameWorld.ChatChannel.PRIVATE)) {
// todo 发送消息给指定玩家
} else if (request.getChannel().equals(GameWorld.ChatChannel.LOCAL)) {
// todo 发送消息给当前地图玩家
}
}
/**
* 加载缓存
*
* @param session session
* @param message 消息
*/
private void handleLoadCacheMessage(Session session, WowMessage<LoadCacheRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
LoadCacheResponse response = new LoadCacheResponse();
Map<String, Integer> levelExpMap = new HashMap<>();
for (Integer key : CacheUtil.levelExpMap.keySet()) {
levelExpMap.put(key.toString(), CacheUtil.levelExpMap.get(key));
}
response.setLevelExpMap(levelExpMap);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
}
/**
* 地图移动
*
* @param session session
* @param message 消息
*/
private void handleMoveMessage(Session session, WowMessage<MoveRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
MoveRequest request = message.getContent();
Character character = GameWorld.OnlineCharacter.get(session.getId());
String fromMapId = character.getMapId();
String destMapId = request.getDestMapId();
GameWorld.MapCharacter.get(fromMapId).remove(character);
GameWorld.MapCharacter.get(destMapId).add(character);
character.setMapId(destMapId);
MapInfo mapInfo = this.loadMapInfo(destMapId);
OnlineInfo onlineInfo = this.loadOnlineInfo(destMapId);
MoveResponse response = new MoveResponse();
response.setMapInfo(mapInfo);
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
}
/**
* 刷新在线列表
*
* @param session session
* @param message 消息
*/
private void handleRefreshOnlineMessage(Session session, WowMessage<RefreshOnlineRequest> message) {
WowMessageHeader header = message.getHeader();
header.setResponseTime(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
Character character = GameWorld.OnlineCharacter.get(session.getId());
String mapId = character.getMapId();
OnlineInfo onlineInfo = this.loadOnlineInfo(mapId);
RefreshOnlineResponse response = new RefreshOnlineResponse();
response.setOnlineInfo(onlineInfo);
WowMessage wowMessage = new WowMessage<>(header, response);
this.sendOne(session, JSON.toJSONString(wowMessage));
}
/**
* 读取地图信息
*
* @param mapId 地图ID
* @return
*/
private MapInfo loadMapInfo(String mapId) {
MapInfo mapInfo = new MapInfo();
CommonResult commonResult = wowMapService.find(mapId);
if (commonResult.isSuccess()) {
WowMap wowMap = (WowMap) commonResult.getData();
mapInfo.setWowMap(wowMap);
}
List<MapCoord> mapCoordList = mapCoordService.listByFromMapId(mapId);
mapInfo.setMapCoordList(mapCoordList);
return mapInfo;
}
/**
* 读取在线列表
*
* @param mapId 地图ID
* @return
*/
private OnlineInfo loadOnlineInfo(String mapId) {
OnlineInfo onlineInfo = new OnlineInfo();
List<MapMob> mapMobList = mapMobService.listByMapId(mapId);
onlineInfo.setMapMobList(mapMobList);
List<Character> mapCharacterList = GameWorld.MapCharacter.get(mapId);
onlineInfo.setMapCharacterList(mapCharacterList);
return onlineInfo;
}
}
Copy the code
MessageHandler
Fifth, front-end socket processing
Corresponding to the backend MessageHub, the front-end also needs a socket client, here we create a WowClient object, responsible for the outermost message processing logic.
1 const WowClient = function () { 2 this.cache = { 3 version: 0, 4 levelExpMap: [] 5 }; 6 this.cacheKey = "idlewow_client_cache"; 7 this.hubUrl = "ws://localhost:20010/hub"; 8 this.webSocket = new WebSocket(this.hubUrl); 9 this.websocket. onopen = function (event) {10 console.log(' webSocket establishes connection '); 11 wowClient.sendLogin(); 12 wowClient.loadCache(); 13}; 14 this.websocket. onMessage = function (event) {15 console.log(' webSocket received message: % C '+ event. 'color:green'); 16 var message = JSON.parse(event.data) || {}; 17 console.log(message); 18 wowClient.receive(message); 19}; 20 this.websocket. onclose = function (event) {21 console.log(' webSocket closes connection '); 22}; 23 this.websocket. onerror = function (event) {24 console.log(' webSocket error '); 25}; 26};Copy the code
In addition, the front end also needs to define message types,
1 const RequestMessage = function () { 2 this.header = { 3 messageCode: "", 4 requestTime: new Date(), 5 version: "1.0", 6}; 7 this.content = {}; 8}; 9 10 const MessageCode = {11 // pre-processing 12 LoadCache: "0010", 13 // system command 14 Login: "1001", 15 RefreshOnline: "1002", 16 // Player commands 17 Chat: "2001", 18 Move: "2002", 19 BattleMob: "2100" 20};Copy the code
The creation of concrete message processing logic and message entities is generated through the stereotype method. The complete js file is as follows:
1 const WowClient = function () {
2 this.cache = {
3 version: 0,
4 levelExpMap: []
5 };
6 this.cacheKey = "idlewow_client_cache";
7 this.hubUrl = "ws://localhost:20010/hub";
8 this.webSocket = new WebSocket(this.hubUrl);
9 this.webSocket.onopen = function (event) {
10 console.log('WebSocket建立连接');
11 wowClient.sendLogin();
12 wowClient.loadCache();
13 };
14 this.webSocket.onmessage = function (event) {
15 console.log('WebSocket收到消息:%c' + event.data, 'color:green');
16 var message = JSON.parse(event.data) || {};
17 console.log(message);
18 wowClient.receive(message);
19 };
20 this.webSocket.onclose = function (event) {
21 console.log('WebSocket关闭连接');
22 };
23 this.webSocket.onerror = function (event) {
24 console.log('WebSocket发生异常');
25 };
26 };
27
28 const RequestMessage = function () {
29 this.header = {
30 messageCode: "",
31 requestTime: new Date(),
32 version: "1.0"
33 };
34 this.content = {};
35 };
36
37 const MessageCode = {
38 // 预处理
39 LoadCache: "0010",
40 // 系统命令
41 Login: "1001",
42 RefreshOnline: "1002",
43 // 玩家命令
44 Chat: "2001",
45 Move: "2002",
46 BattleMob: "2100"
47 };
48
49 WowClient.prototype = {
50 //////////////////
51 //// 对外接口 ////
52 //////////////////
53 // 读取缓存
54 loadCache: function () {
55 let storage = localStorage.getItem(this.cacheKey);
56 let cache = storage ? JSON.parse(storage) : null;
57 if (!cache || (new Date().getTime() - cache.version) > 1000 * 60 * 60 * 24) {
58 this.sendLoadCache();
59 } else {
60 this.cache = cache;
61 }
62 },
63
64 //////////////////
65 //// 消息处理 ////
66 //////////////////
67
68 // 发送消息
69 send: function (message) {
70 let msg = JSON.stringify(message);
71 this.webSocket.send(msg);
72 },
73 // 接收消息
74 receive: function (message) {
75 switch (message.header.messageCode) {
76 case MessageCode.LoadCache:
77 this.recvLoadCache(message);
78 break;
79 case MessageCode.RefreshOnline:
80 this.recvRefreshOnline(message);
81 break;
82 case MessageCode.Login:
83 this.recvLogin(message);
84 break;
85 case MessageCode.Chat:
86 this.recvChat(message);
87 break;
88 case MessageCode.Move:
89 this.recvMove(message);
90 break;
91 case MessageCode.BattleMob:
92 this.recvBattleMob(message);
93 break;
94 default:
95 break;
96 }
97 },
98
99 // 读取缓存
100 sendLoadCache: function () {
101 this.send(new RequestMessage().loadCache());
102 },
103 recvLoadCache: function (message) {
104 this.cache.levelExpMap = message.content.levelExpMap;
105 this.cache.version = new Date().getTime();
106 localStorage.setItem(this.cacheKey, JSON.stringify(this.cache));
107 },
108 // 刷新在线列表
109 sendRefreshOnline: function () {
110 this.send(new RequestMessage().refreshOnline());
111 },
112 recvRefreshOnline: function (message) {
113 this.refreshOnlineInfo(message.content.onlineInfo);
114 },
115 // 登陆
116 sendLogin: function () {
117 this.send(new RequestMessage().login());
118 },
119 recvLogin: function (message) {
120 this.refreshMapInfo(message.content.mapInfo);
121 this.refreshOnlineInfo(message.content.onlineInfo);
122 },
123 // 聊天
124 sendChat: function () {
125 this.send(new RequestMessage().chat());
126 },
127 recvChat: function (message) {
128 let channel = "【当前】";
129 let content = "<p>" + channel + message.content.senderName + ": " + message.content.message + "</p>";
130 $('.msg-chat').append(content);
131 },
132 // 移动
133 sendMove: function (mapId) {
134 this.send(new RequestMessage().move(mapId));
135 },
136 recvMove: function (message) {
137 this.refreshMapInfo(message.content.mapInfo);
138 this.refreshOnlineInfo(message.content.onlineInfo);
139 },
140 // 战斗
141 sendBattleMob: function (mobId) {
142 this.send(new RequestMessage().battleMob(mobId));
143 },
144 recvBattleMob: async function (message) {
145 $('.msg-battle').html('');
146 let battleResult = message.content.battleResult;
147 if (battleResult.roundList) {
148 var rounds = battleResult.roundList;
149 for (var i = 0; i < rounds.length; i++) {
150 var round = rounds[i];
151 var content = "<p>【第" + round.round + "回合】</p>";
152 if (round.atkStage) {
153 content += "<p>" + round.atkStage.desc + "</p>";
154 }
155
156 if (round.defStage) {
157 content += "<p>" + round.defStage.desc + "</p>";
158 }
159
160 $('.msg-battle').append(content);
161 await this.sleep(1500);
162 }
163
164 $('.msg-battle').append("<p><strong>战斗结束," + battleResult.winName + " 获得胜利!</strong></p>");
165 if (battleResult.isPlayerWin) {
166 this.settlement(battleResult);
167 }
168
169 let that = this;
170 await this.sleep(5000).then(function () {
171 that.sendBattleMob(battleResult.atkId, battleResult.defId);
172 });
173 }
174 },
175
176 //////////////////
177 //// 辅助方法 ////
178 //////////////////
179
180 // 刷新地图信息
181 refreshMapInfo: function (mapInfo) {
182 let wowMap = mapInfo.wowMap;
183 let mapCoordList = mapInfo.mapCoordList;
184 $('#mapName').html(wowMap.name);
185 $('#mapDesc').html(wowMap.description);
186 $('#mapImg').attr('src', '/images/wow/map/' + wowMap.name + '.jpg');
187 let coordsHtml = '';
188 for (let index in mapCoordList) {
189 let mapCoord = mapCoordList[index];
190 coordsHtml += '<area shape="' + mapCoord.shape + '" coords="' + mapCoord.coord + '" onclick="wowClient.sendMove(\'' + mapCoord.destMapId + '\');" href="javascript:void(0);" alt="' + mapCoord.destMapName + '" title="' + mapCoord.destMapName + '"/>';
191 }
192
193 $('#map-coords').html(coordsHtml);
194 },
195 // 刷新在线列表
196 refreshOnlineInfo: function (onlineInfo) {
197 let mapCharacterList = onlineInfo.mapCharacterList;
198 let mapMobList = onlineInfo.mapMobList;
199 // 更新在线列表
200 $('#online-all').html('');
201 $('#online-player').html('');
202 $('#online-mob').html('');
203 for (let index in mapCharacterList) {
204 let mapCharacter = mapCharacterList[index];
205 let row = '<div class="layui-row"><div class="layui-col-md9"><label style="color: blue;">' + mapCharacter.name + '</label><label> - 等级:' + mapCharacter.level + '</label></div><div class="layui-col-md3"><button type="button" style="height:14px;line-height: 14px;">私聊</button></div></div>';
206 $('#online-all').append(row);
207 $('#online-player').append(row);
208 }
209
210 for (let index in mapMobList) {
211 let mapMob = mapMobList[index];
212 let row = '<div class="layui-row"><div class="layui-col-md9"><label style="color: red;">' + mapMob.name + '</label><label> - 等级:' + mapMob.level + '</label></div><div class="layui-col-md3"><button type="button" style="height:14px;line-height: 14px;" onclick="wowClient.sendBattleMob(\'' + mapMob.id + '\');">战斗</button><button type="button" style="height:14px;line-height:14px;" onclick="guaji();">挂机</button></div></div>';
213 $('#online-all').append(row);
214 $('#online-mob').append(row);
215 }
216 },
217 // 战斗结算
218 settlement: function (battleResult) {
219 $('.lbl-level').html(battleResult.settleLevel);
220 $('.lbl-exp').html(battleResult.settleExp);
221 },
222 // 休眠
223 sleep: function (milliseconds) {
224 let p = new Promise(function (resolve) {
225 setTimeout(function () {
226 resolve();
227 }, milliseconds)
228 });
229 return p;
230 },
231 // 关闭
232 close: function () {
233 this.webSocket.close();
234 }
235 };
236
237 RequestMessage.prototype = {
238 loadCache: function () {
239 this.header.messageCode = MessageCode.LoadCache;
240 },
241 login: function () {
242 this.header.messageCode = MessageCode.Login;
243 },
244 chat: function () {
245 this.header.messageCode = MessageCode.Chat;
246 this.content = {
247 senderId: charId,
248 senderName: charName,
249 receiverId: '',
250 receiverName: '',
251 message: $('#msg').val()
252 };
253 },
254 move: function (mapId) {
255 this.header.messageCode = MessageCode.Move;
256 this.content = {
257 destMapId: mapId
258 };
259 },
260 battleMob: function (mobId) {
261 this.header.messageCode = MessageCode.BattleMob;
262 this.content = {
263 mobId: mobId
264 };
265 },
266 refreshOnline: function () {
267 this.header.messageCode = MessageCode.RefreshOnline;
268 }
269 };
270
271 // wow客户端
272 window.wowClient = new WowClient();
273
274 // 关闭窗口
275 window.onbeforeunload = function (event) {
276 wowClient.close();
277 };
278
279 document.onkeydown = function (event) {
280 let e = event || window.event || arguments.callee.caller.arguments[0];
281 if (e.keyCode === 13 && document.activeElement.id === 'msg') {
282 wowClient.sendChat();
283 }
284 };
Copy the code
main.js
summary
This chapter mainly realizes the socket communication logic, the processing of the message involves the game’s business processing logic, only a simple talk about some.
In addition, because the interval is longer, the code cutting workload is larger. This chapter is only a rough cut of the completed code. Some changes to the source code, some of the major ones will be covered in this article, but others will not be covered here.
For some corners of the content, the code will change, but not reflected in the text, if there is a problem, you can leave a message for consultation.
Download source address: 545c.com/file/149603…
In this paper, the original address: www.cnblogs.com/lyosaki88/p…
Project Exchange Group: 329989095