Java后端使用socketio,实现小程序答题pk功能
在使用socket.io跟前端通信过程中,出现了一系列问题,现做下记录。
一、功能需求是,在小程序端,用户可相互邀请,进入房间后进行答题PK。实现方法是,用户点击邀请好友,建立连接,查询当前是否有房间,有房间发送消息给两人,匹配成功,开始pk。没有房间新建房间返回,等待20秒,等待别人匹配。
代码如下,先看配置,在application.yml配置文件中增加如下配置
1 # host在本地测试可以设置为localhost或者本机IP,在Linux服务器跑可换成服务器IP
2 socketio:
3 host: 127.0.0.1 #监听的ip
4 port: 9999 #监听端口
5 # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
6 maxFramePayloadLength: 1048576
7 # 设置http交互最大内容长度
8 maxHttpContentLength: 1048576
9 # socket连接数大小(如只监听一个端口boss线程组为1即可)
10 bossCount: 1
11 workCount: 100
12 allowCustomRequests: true
13 # 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
14 upgradeTimeout: 1000000
15 # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
16 pingTimeout: 6000000
17 # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
18 pingInterval: 25000
复制代码
配置类
1 package com.cwn.wethink.remy.handler;
4 import com.corundumstudio.socketio.SocketConfig;
5 import org.springframework.beans.factory.annotation.Value;
6 import org.springframework.context.annotation.Bean;
7 import org.springframework.context.annotation.Configuration;
8
9 import com.corundumstudio.socketio.SocketIOServer;
10
11 /**
12 * @description:
13 * @author: m1575
14 * @create: 2020-11-11
15 **/
16 @Configuration
17 class SocketIOConfig {
18
19 @Value("${socketio.host}")
20 private String host;
21
22 @Value("${socketio.port}")
23 private Integer port;
24
25 @Value("${socketio.bossCount}")
26 private int bossCount;
27
28 @Value("${socketio.workCount}")
29 private int workCount;
30
31 @Value("${socketio.allowCustomRequests}")
32 private boolean allowCustomRequests;
33
34 @Value("${socketio.upgradeTimeout}")
35 private int upgradeTimeout;
36
37 @Value("${socketio.pingTimeout}")
38 private int pingTimeout;
39
40 @Value("${socketio.pingInterval}")
41 private int pingInterval;
42
43 /**
44 * 以下配置在上面的application.properties中已经注明
45 * @return
46 */
47 @Bean
48 public SocketIOServer socketIOServer() {
49 SocketConfig socketConfig = new SocketConfig();
50 socketConfig.setTcpNoDelay(true);
51 socketConfig.setSoLinger(0);
52 com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
53 config.setSocketConfig(socketConfig);
54 config.setHostname(host);
55 config.setPort(port);
56 config.setBossThreads(bossCount);
57 config.setWorkerThreads(workCount);
58 config.setAllowCustomRequests(allowCustomRequests);
59 config.setUpgradeTimeout(upgradeTimeout);
60 config.setPingTimeout(pingTimeout);
61 config.setPingInterval(pingInterval);
62 return new SocketIOServer(config);
63 }
64 }
复制代码
后台实现
1 package com.cwn.wethink.remy.handler;
2
3 import java.util.*;
4 import java.util.concurrent.ConcurrentHashMap;
5 import javax.annotation.PostConstruct;
6 import javax.annotation.PreDestroy;
7 import com.alibaba.fastjson.JSONObject;
8 import com.corundumstudio.socketio.*;
9 import com.cwn.wethink.pojo.entity.Question;
10 import com.cwn.wethink.remy.entity.PkAgainGame;
11 import com.cwn.wethink.remy.entity.PkAnswerTime;
12 import com.cwn.wethink.remy.entity.PkGroup;
13 import com.cwn.wethink.remy.entity.WxUserInfo;
14 import com.cwn.wethink.remy.mapper.PkMapper;
15 import com.cwn.wethink.remy.mapper.WxUserInfoMapper;
16 import com.cwn.wethink.remy.service.RemyCourseService;
17 import lombok.extern.slf4j.Slf4j;
18 import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.stereotype.Service;
20
21 @Service
22 @Slf4j
23 public class MessageEventHandler {
24
25 // 用来存已连接的客户端
26 private static Map<Long, SocketIOClient> clientMap = new ConcurrentHashMap<>();
27
28 @Autowired
29 private SocketIOServer socketIOServer;
30
31 @Autowired
32 private PkMapper pkMapper;
33
34 @Autowired
35 private WxUserInfoMapper wxUserInfoMapper;
36
37 @Autowired
38 private RemyCourseService remyCourseService;
39
40 /**
41 * Spring IoC容器创建之后,在加载SocketIOServiceImpl Bean之后启动
42 * @throws Exception
43 */
44 @PostConstruct
45 private void autoStartup() throws Exception {
46 start();
47 }
48
49 /**
50 * Spring IoC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
51 * @throws Exception
52 */
53 @PreDestroy
54 private void autoStop() throws Exception {
55 stop();
56 }
57
58 public void start() {
59 // 监听客户端连接,同级挑战比拼
60 socketIOServer.addConnectListener(client -> {
61 Long uid = Long.valueOf(getParamsByClient(client));
62 log.info("connect come in,uid:{}",uid);
63 //0为同级挑战,1为邀请好友pk
64 int type = 0;
65 //房间号
66 int pkId = 0;
67 //从请求的连接中拿出参数
68 Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
69 List<String> list = params.get("type");
70 if (list != null && list.size() > 0) {
71 type = Integer.valueOf(list.get(0));
72 }
73 List<String> list1 = params.get("pkId");
74 if (list1 != null && list1.size() > 0) {
75 pkId = Integer.valueOf(list1.get(0));
76 }
77 if (uid != null) {
78 if (clientMap.containsKey(uid)) {
79 log.info(uid + "is reconnect");
80 clientMap.remove(uid);
81 clientMap.put(uid , client);
82 }else{
83 clientMap.put(uid, client);
84 log.info("clientMap:"+clientMap);
85 joinSendMessage(uid , client , type , pkId);
86 }
87 }
88 });
89
90 // 监听客户端断开连接
91 socketIOServer.addDisconnectListener(client -> {
92 Long uid = Long.valueOf(getParamsByClient(client));
93 log.info("disconnect come in,uid:{}",uid);
94 if (uid != null) {
95 log.info("uid is not null come in,uid:{}",uid);
96 clientMap.remove(uid);
97 client.disconnect();
98 //退出通知对手
99 Long usrEntId = 0l;
100 PkGroup pkGroup = pkMapper.getPkGroupUserNewRoom(uid);
101 if(pkGroup != null){
102 log.info("pkGroup is not null come in,uid:{}",uid);
103 if(uid == pkGroup.getInviteUsrEntId()){
104 usrEntId = pkGroup.getAcceptUsrEntId();
105 }else if(uid == pkGroup.getAcceptUsrEntId()){
106 usrEntId = pkGroup.getInviteUsrEntId();
107 }
108 }
109 if(usrEntId != null && usrEntId != 0l){
110 log.info("usrEntId is not null come in,uid:{}",uid);
111 log.info("socketIOClient usrEntId:" + usrEntId);
112 JSONObject jsonObject = new JSONObject();
113 SocketIOClient socketIOClient = clientMap.get(usrEntId);
114 if(socketIOClient != null){
115 jsonObject.put("disconnect" , 1);
116 socketIOClient.sendEvent("ClientReceive" , jsonObject);
117 }
118 }
119 if(clientMap.get(usrEntId) == null || usrEntId == null){
120 if(pkGroup != null){
121 PkGroup updatePkGroup = pkMapper.getPkGroupById(pkGroup.getId());
122 updatePkGroup.setState(2);
123 pkMapper.updatePkGroup(updatePkGroup);
124 log.info("disconnect opponent is disconnect,uid:{}",uid);
125 }
126 }
127 }
128 log.info("disconnect is success,uid:{}",uid);
129 });
130
131 // 处理自定义的事件,与连接监听类似
132 // 此示例中测试的json收发 所以接收参数为JSONObject 如果是字符类型可以用String.class或者Object.class
133 socketIOServer.addEventListener("ServerReceive",JSONObject.class, (client, data, ackSender) -> {
134 JSONObject jsonObject = data;
135 if(data != null){
136 String uid = jsonObject.getString("usrEntId");
137 String action = jsonObject.getString("action");
138 if("getAI".equals(action)){
139 log.info("getAI come in,uid:{}",uid);
140 //和人机pk返回
141 botSendMessage(uid , client);
142 }else if("challenge".equals(action)){
143 log.info("challenge come in,uid:{}",uid);
144 //pk过程中每做一道题返回消息给两个人
145 int pkId = 0;
146 if(!"".equals(jsonObject.getString("pkId"))){
147 pkId = Integer.valueOf(jsonObject.getString("pkId"));
148 }
149 if(pkId == 0){
150 log.info("challenge pkId is 0");
151 return;
152 }
153 long usrEntId = -1;
154 if(!"".equals(jsonObject.getString("usrEntId"))){
155 usrEntId = Long.valueOf(jsonObject.getString("usrEntId"));
156 }
157 if(usrEntId == -1){
158 log.info("challenge usrEntId is -1");
159 return;
160 }
161 int answer = 0;
162 if(!"".equals(jsonObject.getString("answer"))){
163 answer = Integer.valueOf(jsonObject.getString("answer"));
164 }
165 int time = 0;
166 if(!"".equals(jsonObject.getString("time"))){
167 time = Integer.valueOf(jsonObject.getString("time"));
168 }
169 int queResId = 0;
170 if(!"".equals(jsonObject.getString("queResId"))){
171 queResId = Integer.valueOf(jsonObject.getString("queResId"));
172 }
173 int orderNum = 0;
174 if(!"".equals(jsonObject.getString("orderNum"))){
175 orderNum = Integer.valueOf(jsonObject.getString("orderNum"));
176 }
177 int option = 0;
178 if(!"".equals(jsonObject.getString("option"))){
179 option = Integer.valueOf(jsonObject.getString("option"));
180 }
181 PkAnswerTime pkAnswerNow = new PkAnswerTime();
182 pkAnswerNow.setPkGroupId(pkId);
183 pkAnswerNow.setUsrEntId(usrEntId);
184 pkAnswerNow.setAnswer(answer);
185 pkAnswerNow.setTime(time);
186 pkAnswerNow.setQueResId(queResId);
187 pkAnswerNow.setOrderNum(orderNum);
188 pkAnswerNow.setOption(option);
189 pkMapper.savePkAnswerTime(pkAnswerNow);
190 PkGroup pkGroup = pkMapper.getPkGroupById(pkId);
191 if(usrEntId == pkGroup.getInviteUsrEntId()){
192 long acceptUsrEntId = pkGroup.getAcceptUsrEntId();
193 judgeWinner(acceptUsrEntId , pkAnswerNow ,client);
194 }else if(usrEntId == pkGroup.getAcceptUsrEntId()){
195 long inviteUsrEntId = pkGroup.getInviteUsrEntId();
196 judgeWinner(inviteUsrEntId , pkAnswerNow ,client);
197 }
198 }else if("again".equals(action)){
199 log.info("again come in");
200 //再来一局
201 int pkId = Integer.valueOf(jsonObject.getString("pkId"));
202 log.info("pkId:"+pkId+"uid:"+uid);
203 againSendMessage(uid , pkId, client);
204 }else if("skill".equals(action)){
205 //使用技能
206 int pkId = Integer.valueOf(jsonObject.getString("pkId"));
207 //技能id
208 int infoId = Integer.valueOf(jsonObject.getString("info"));
209 skillSendMessage(uid , pkId , infoId);
210 }
211 }
212 });
213
214 socketIOServer.start();
215 log.info("socket.io初始化服务完成");
216 }
217
218 public void stop() {
219 if (socketIOServer != null) {
220 socketIOServer.stop();
221 socketIOServer = null;
222 }
223 log.info("socket.io服务已关闭");
224 }
225
226 /**
227 * 此方法为获取client连接中的参数,可根据需求更改
228 * @param client
229 * @return
230 */
231 private String getParamsByClient(SocketIOClient client) {
232 // 从请求的连接中拿出参数(这里的usrEntId必须是唯一标识)
233 Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
234 List<String> list = params.get("usrEntId");
235 if (list != null && list.size() > 0) {
236 return list.get(0);
237 }
238 return null;
239 }
240
241 private synchronized void joinSendMessage(long usrEntId , SocketIOClient client , int type , int pkId){
242 // 给客户端发送一条信息 发送ConnectReceive事件 需要客户端绑定此事件即可接收到消息
243 JSONObject jsonObject = new JSONObject();
244 Date date = new Date(new Date().getTime() - 20000);
245 PkGroup pkGroup = pkMapper.getPkGroupByState(usrEntId , type , date);
246 if(type != 0 && pkId != 0){
247 pkGroup = pkMapper.getPkGroupById(pkId);
248 }
249 if(type != 0 && pkId == 0){
250 pkGroup = null;
251 }
252 if(pkGroup != null){
253 pkGroup.setAcceptUsrEntId(usrEntId);
254 pkGroup.setState(1);
255 pkMapper.updatePkGroup(pkGroup);
256 long inviteUsrEntId = pkGroup.getInviteUsrEntId();
257 WxUserInfo invite = wxUserInfoMapper.queryWxUserInfoByUsrEntId(inviteUsrEntId);
258 List<Question> questions = remyCourseService.listGetRandomTopic(0);
259 jsonObject.put("state" , 1);
260 jsonObject.put("wxUserInfo" , invite);
261 jsonObject.put("questions" , questions);
262 jsonObject.put("pkId" , pkGroup.getId());
263 client.sendEvent("ConnectReceive",jsonObject);
264 SocketIOClient socketIOClient = clientMap.get(inviteUsrEntId);
265 WxUserInfo accept = wxUserInfoMapper.queryWxUserInfoByUsrEntId(usrEntId);
266 JSONObject acceptJson = new JSONObject();
267 acceptJson.put("state" , 1);
268 acceptJson.put("questions" , questions);
269 acceptJson.put("pkId" , pkGroup.getId());
270 acceptJson.put("wxUserInfo" , accept);
271 socketIOClient.sendEvent("ConnectReceive" , acceptJson);
272 }else{
273 PkGroup savePkGroup = new PkGroup();
274 savePkGroup.setInviteUsrEntId(usrEntId);
275 savePkGroup.setState(0);
276 savePkGroup.setCreateTime(new Date());
277 savePkGroup.setType(type);
278 pkMapper.savePkGroup(savePkGroup);
279 jsonObject.put("state" , 0);
280 jsonObject.put("pkId" , savePkGroup.getId());
281 client.sendEvent("ConnectReceive",jsonObject);
282 }
283 }
284
285 private synchronized void botSendMessage(String uid , SocketIOClient client){
286 JSONObject jsonObject = new JSONObject();
287 PkGroup pkGroup = pkMapper.getPkGroupByUsrEntIdToAI(Long.valueOf(uid));
288 if(pkGroup != null){
289 log.info("getAI pkGroup is not null come in,uid:{}",uid);
290 pkGroup.setAcceptUsrEntId(0l);
291 pkGroup.setState(1);
292 pkMapper.updatePkGroup(pkGroup);
293 List<Question> questions = remyCourseService.listGetRandomTopic(0);
294 jsonObject.put("state" , 1);
295 jsonObject.put("questions" , questions);
296 jsonObject.put("pkId" , pkGroup.getId());
297 client.sendEvent("AIReceive",jsonObject);
298 }
299 }
300
301 private synchronized void judgeWinner(long anotherEntId , PkAnswerTime pkAnswerNow, SocketIOClient client){
302 log.info("judgeWinner come in,anotherEntId:{}",anotherEntId);
303 int pkId = pkAnswerNow.getPkGroupId();
304 int orderNum = pkAnswerNow.getOrderNum();
305 int answer = pkAnswerNow.getAnswer();
306 int time = pkAnswerNow.getTime();
307 long usrEntId = pkAnswerNow.getUsrEntId();
308 int option = pkAnswerNow.getOption();
309 JSONObject json = new JSONObject();
310 PkAnswerTime pkAnswerTime = pkMapper.getPkAnswerTimeByParam(anotherEntId , pkId , orderNum);
311 if(pkAnswerTime != null){
312 log.info("judgeWinner pkAnswerTime is not null come in,pkAnswerTime:{}",pkAnswerTime);
313 PkGroup pkGroup = pkMapper.getPkGroupById(pkId);
314 if(orderNum == 5){
315 pkGroup.setState(2);
316 pkMapper.updatePkGroup(pkGroup);
317 }
318 long winUsrEntId = -1;
319 if(pkAnswerTime.getAnswer() == 1 && answer == 1){
320 if(time > pkAnswerTime.getTime()){
321 winUsrEntId = anotherEntId;
322 }else if(time < pkAnswerTime.getTime()){
323 winUsrEntId = usrEntId;
324 }else{
325 winUsrEntId = -1;
326 }
327 }else if(pkAnswerTime.getAnswer() == 1){
328 winUsrEntId = anotherEntId;
329 }else if(answer == 1){
330 winUsrEntId = usrEntId;
331 }else{
332 winUsrEntId = -1;
333 }
334 json.put("winUsrEntId" , winUsrEntId);
335 json.put("pkId" , pkId);
336 json.put("usrEntId" , anotherEntId);
337 json.put("answer" , pkAnswerTime.getAnswer());
338 json.put("time" , pkAnswerTime.getTime());
339 json.put("option" , pkAnswerTime.getOption());
340 client.sendEvent("challengeReceive",json);
341 if(anotherEntId != 0){
342 SocketIOClient socketIOClient = clientMap.get(anotherEntId);
343 JSONObject acceptJson = new JSONObject();
344 acceptJson.put("pkId" , pkId);
345 acceptJson.put("usrEntId" , usrEntId);
346 acceptJson.put("answer", answer);
347 acceptJson.put("time", time);
348 acceptJson.put("option",option);
349 acceptJson.put("winUsrEntId",winUsrEntId);
350 socketIOClient.sendEvent("challengeReceive" , acceptJson);
351 }
352 if(pkGroup.getInviteUsrEntId() == winUsrEntId){
353 if(pkGroup.getInviteNum() != null){
354 pkGroup.setInviteNum(pkGroup.getInviteNum() + 1);
355 }else{
356 pkGroup.setInviteNum(1);
357 }
358 }else if(pkGroup.getAcceptUsrEntId() == winUsrEntId){
359 if(pkGroup.getAcceptNum() != null){
360 pkGroup.setAcceptNum(pkGroup.getAcceptNum() + 1);
361 }else{
362 pkGroup.setAcceptNum(1);
363 }
364 }
365 pkMapper.updatePkNum(pkGroup);
366 }
367 }
368
复制代码
二、遇到的一些问题
1、最初在发送消息给两人时,有个人能收到消息,有个人收不到
刚开始clientMap是这样声明的:
private static Map<String, SocketIOClient> clientMap = new ConcurrentHashMap<>();
但我在查询用户SocketIOClient时使用的是long类型,所以一直没查到用户的SocketIOClient,也就发送消息发不过去。
后面改为
private static Map<Long, SocketIOClient> clientMap = new
ConcurrentHashMap<>();
这样声明还是获取不到,继而检查代码,开始返回给两个人的信息对象JSONObject,使用的同一个对象,虽然重新set了值,但还是返回了相同的对象。
1 JSONObject jsonObject = new JSONObject();
2 jsonObject.put("state" , 1);
3 jsonObject.put("wxUserInfo" , invite);
4 jsonObject.put("questions" , questions);
5 jsonObject.put("pkId" , pkGroup.getId());
6 client.sendEvent("ConnectReceive",jsonObject);
7 SocketIOClient socketIOClient = clientMap.get(inviteUsrEntId);
8 WxUserInfo accept = wxUserInfoMapper.queryWxUserInfoByUsrEntId(usrEntId);
9 jsonObject.put("state" , 1);
10 jsonObject.put("questions" , questions);
11 jsonObject.put("pkId" , pkGroup.getId());
12 jsonObject.put("wxUserInfo" , accept);
13 socketIOClient.sendEvent("ConnectReceive" , jsonObject);
复制代码
后改为重新new一个JSONObject对象,问题解决。
2、本地测试没问题,上了测试环境出现问题。连接时间超过1分钟,会自动断开连接。
后经查是服务器使用了nginx,nginx默认连接60s会断开连接。
需更改nginx配置,如下:
1 location /socket.io {
2 proxy_pass http://172.17.0.2:9999;
3 proxy_set_header Host $host;
4 proxy_next_upstream off;
5 proxy_buffering off;
6 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
7
8 proxy_http_version 1.1;
9 proxy_set_header Upgrade $http_upgrade;
10 proxy_set_header Connection "upgrade";
11 proxy_read_timeout 3600s;
12 }
复制代码
proxy_read_timeout默认为60s,若需要长时间连接,改大点。
三、前端代码示例
1 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
2 <script type="text/javascript">
3 const connectEvent = "ConnectReceive";
4 const aiEvent = "AIReceive";
5 const challengeEvent = "challengeReceive";
6 const sendEvent = "ServerReceive";
7 const againEvent = "AgainReceive";
8 const clientEvent = "ClientReceive";
9 const skillEvent = "SkillReceive";
10 var socket;
11 function socketClick() {
12 //连接服务器,返回房间、题目信息
13 socket = io.connect('http://127.0.0.1:9999', {
14 'force new connection': true,
15 'query': 'usrEntId=' + 41 + '&type=' + 1
16 })
17
18 socket.on(connectEvent, function (data) {
19 //data:{pkId: 31,state: 0} 房间号、状态为0表示创建了个房间,为1表示有房间并已加入
20 //data:{pkId: 31,questions:[{},{}...],state: 1,wxUserInfo:{openId:,unionId:,nickName:,headImg:,empiricalValue:,usrEntId:,createTime:,medal:}}
21 console.log(data)
22 })
23
24 socket.on(clientEvent, function (data) {
25 //data:{disconnect: 1} 对手退出了房间
26 console.log(data)
27 })
28
29 socket.on(skillEvent, function (data) {
30 //data:{usrEntId:42 , skill: 1 , info:1} 对手id、使用了技能,技能id
31 console.log(data)
32 })
33
34 socket.on(aiEvent, function (data) {
35 //data:{pkId: 31,questions:[{},{}...],state: 1} 房间号、题目集合、状态
36 console.log(data)
37 })
38
39 //每道题答完接收消息
40 socket.on(challengeEvent, function (data) {
41 //返回 data:{pkId: 31,winUsrEntId: 41,usrEntId:42,answer:1,time:3} 房间号、赢家id、对手id、答案1对0错、时间
42 console.log('ServerReceive成功')
43 })
44
45 socket.on(againEvent, function (data) {
46 //data:{state: 0,usrEntId: 41} state为0邀请信息、邀请再来一局玩家id
47 //data:{state: 1,wxUserInfo: {},questions:[{},{}...],pkId} state为1再来一局成功、对手信息、题目、房间号
48 console.log(data)
49 })
50
51 socket.on(clientEvent, function (data) {
52 //data:{disconnect: 1} 对手退出了房间
53 console.log(data)
54 })
55
56 //发送和人机pk消息到服务器
57 send({
58 action: 'getAI',
59 usrEntId: 41
60 })
61
62 //每道题答完发送消息到服务器
63 send({
64 action: 'challenge',
65 usrEntId: 41,
66 answer: 1,
67 time: 6,
68 pkId: 1,
69 orderNum: 1,
70 queResId: 1,
71 option: 3
72 })
73 //再来一局
74 send({
75 action: 'again',
76 usrEntId: 41,
77 pkId:1
78 })
79
80 //发送技能
81 send({
82 action: 'skill',
83 usrEntId: 41,
84 pkId:1
85 })
86
87 }
88 function send(data){
89 socket.emit(sendEvent,data);
90 }
91
92 function quitClick() {
93 var data={usrEntId:41};
94 socket.disconnect();
95 }
96 </script>
复制代码
- 191
- 0
- 0
回复 刷新
登录 后可发表评论