【Spring Boot】整合Netty Socket.IO通訊框架

空涅發表於2019-01-10

服務端

 1 @Configuration
 2 public class NettySocketConfig {
 3 
 4     private static final Logger logger = LoggerFactory.getLogger(NettySocketConfig.class);
 5 
 6     @Bean
 7     public SocketIOServer socketIOServer() {
 8         //建立Socket,並設定監聽埠
 9         com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
10         // 設定主機名,預設是0.0.0.0
11         config.setHostname("192.168.8.107");
12         // 設定監聽埠
13         config.setPort(9096);
14         // 協議升級超時時間(毫秒),預設10000。HTTP握手升級為ws協議超時時間
15         config.setUpgradeTimeout(10000);
16         // Ping訊息間隔(毫秒),預設25000。客戶端向伺服器傳送一條心跳訊息間隔
17         config.setPingInterval(60000);
18         // Ping訊息超時時間(毫秒),預設60000,這個時間間隔內沒有接收到心跳訊息就會傳送超時事件
19         config.setPingTimeout(180000);
20         // 這個版本0.9.0不能處理好namespace和query引數的問題。所以為了做認證必須使用全域性預設名稱空間
21         config.setAuthorizationListener(new AuthorizationListener() {
22             @Override
23             public boolean isAuthorized(HandshakeData data) {
24                 // 可以使用如下程式碼獲取使用者密碼資訊
25                 //String username = data.getSingleUrlParam("username");
26                 //String password = data.getSingleUrlParam("password");
27                 //logger.info("連線引數:username=" + username + ",password=" + password);
28                 //ManagerInfo managerInfo = managerInfoService.findByUsername(username);
29                 //
30                 //String salt = managerInfo.getSalt();
31                 //String encodedPassword = ShiroKit.md5(password, username + salt);
32                 //// 如果認證不通過會返回一個Socket.EVENT_CONNECT_ERROR事件
33                 //return encodedPassword.equals(managerInfo.getPassword());
34 
35                 return true;
36             }
37         });
38 
39         final SocketIOServer server = new SocketIOServer(config);
40         System.out.println("注入SocketIOServer");
41         return server;
42     }
43 
44     @Bean
45     public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
46         return new SpringAnnotationScanner(socketServer);
47     }
48 }

 

  1 @Component
  2 public class MessageEventHandler {
  3 
  4     private static final Logger logger = LoggerFactory.getLogger(MessageEventHandler.class);
  5 
  6     /**
  7      * 伺服器socket物件
  8      */
  9     public static SocketIOServer socketIoServer;
 10 
 11     /**
 12      * 客戶端集合
 13      */
 14     static ArrayList<UUID> listClient = new ArrayList<>();
 15 
 16     /**
 17      * 超時時間
 18      */
 19     static final int limitSeconds = 60;
 20 
 21     @Autowired
 22     public LoginService loginService;
 23 
 24     /**
 25      * 初始化訊息事件處理器
 26      *
 27      * @param server 伺服器socket物件
 28      */
 29     @Autowired
 30     public MessageEventHandler(SocketIOServer server) {
 31         logger.info("初始化SOCKET訊息事件處理器");
 32         this.socketIoServer = server;
 33     }
 34 
 35     /**
 36      * 客戶端發起連線時觸發
 37      *
 38      * @param client 客戶端Socket物件資訊
 39      */
 40     @OnConnect
 41     public void onConnect(SocketIOClient client) {
 42         logger.info("客戶端{}已連線", client.getSessionId());
 43         listClient.add(client.getSessionId());
 44     }
 45 
 46     /**
 47      * 客戶端斷開連線時觸發
 48      *
 49      * @param client 客戶端Socket物件資訊
 50      */
 51     @OnDisconnect
 52     public void onDisconnect(SocketIOClient client) {
 53         logger.info("客戶端{}斷開連線", client.getSessionId());
 54         if (listClient.contains(client.getSessionId())) {
 55             listClient.remove(client.getSessionId());
 56         }
 57     }
 58 
 59 
 60     /**
 61      * 客戶端傳送訊息時觸發
 62      *
 63      * @param client  客戶端Socket物件資訊
 64      * @param request AckRequest 回撥物件
 65      * @param data    訊息資訊實體
 66      */
 67     @OnEvent(value = SocketConstants.SocketEvent.MESSAGE)
 68     public void onEvent(SocketIOClient client, AckRequest request, MessageInfo data) {
 69         System.out.println("發來訊息:" + data.getMsgContent());
 70         socketIoServer.getClient(client.getSessionId()).sendEvent("messageevent", "back data");
 71     }
 72 
 73     /**
 74      * 效驗連線事件並儲存客戶端資訊
 75      *
 76      * @param client  客戶端Socket物件資訊
 77      * @param data    客戶端資料
 78      * @param request AckRequest 回撥物件
 79      */
 80     @OnEvent(value = SocketConstants.SocketEvent.HEALTH_CHECK)
 81     public void onEventByHealthCheck(SocketIOClient client, String data, AckRequest request) {
 82         //logger.info("客戶端{}效驗連線請求", client.getSessionId());
 83         ////解析請求資料
 84         //HealthCheckRequest healthCheckRequest = JSON.parseObject(data, HealthCheckRequest.class);
 85         //if (healthCheckRequest != null) {
 86         //    //儲存客戶端資訊
 87         //    SocketInstance instance = SocketInstance.getSocketInstance();
 88         //    System.out.println(data);
 89         //    instance.insertSocketClient(healthCheckRequest.getEnCode(), client);
 90         //    logger.info("客戶端{}效驗連線響應:{}", client.getSessionId(), "OK");
 91         //    //響應客戶端
 92         //    request.sendAckData("OK");
 93         //}
 94     }
 95 
 96     /**
 97      * 登入事件
 98      *
 99      * @param client  客戶端Socket物件資訊
100      * @param data    客戶端資料
101      * @param request AckRequest 回撥物件
102      */
103     @OnEvent(value = SocketConstants.SocketEvent.LOGIN)
104     public void onEventByLogin(SocketIOClient client, String data, AckRequest request) {
105         logger.info("客戶端{}登入請求:{}", client.getSessionId(), data);
106         AppResponseBase appResponseBase = new AppResponseBase(0, "通訊成功");
107         //業務響應物件
108         LoginResponse loginResponse = null;
109         try {
110             //解析請求資料
111             LoginRequest loginRequest = JSON.parseObject(data, LoginRequest.class);
112             if (loginRequest == null) {
113                 throw new AppException(AppResultCode.LoginAnalysis_Fail);
114             }
115             //呼叫登陸介面
116             loginResponse = loginService.appLogin(loginRequest);
117             if (loginResponse == null) {
118                 throw new AppException(AppResultCode.LoginCloud_Fail);
119             }
120             if (EnumResult.Success.equals(loginResponse.getResultCode())) {
121                 //儲存客戶端Socket資訊
122                 SocketInstance instance = SocketInstance.getSocketInstance();
123                 instance.insertSocketClient(loginRequest.getEnCode(), client);
124             }
125         } catch (AppException ex) {
126             loginResponse = new LoginResponse(ex.getAppResultCode().getCode(), ex.getAppResultCode().getMsg());
127         } catch (Exception ex) {
128             loginResponse = new LoginResponse(AppResultCode.Exceptions.getCode(), AppResultCode.Exceptions.getMsg());
129             ex.printStackTrace();
130         }
131         appResponseBase.setRespData(loginResponse);
132         String result = JSON.toJSONString(appResponseBase);
133         logger.info("客戶端{}登入響應:{}", client.getSessionId(), result);
134         //響應客戶端
135         request.sendAckData(result);
136     }
137 
138     /**
139      * 交易下單事件
140      * @param callPayRequest 下單請求資訊實體
141      * @return
142      */
143     public static String sendByPayEvent(CallPayRequest callPayRequest) {
144         String result = "";
145         //獲取客戶端資訊
146         SocketInstance instance = SocketInstance.getSocketInstance();
147         SocketIOClient client = instance.getClientSocket(callPayRequest.getEnCode());
148         if (client != null) {
149             //請求報文
150             String requestParam = JSON.toJSONString(callPayRequest);
151             //請求下單
152             client.sendEvent(SocketConstants.SocketEvent.PAY, new AckCallback<String>(String.class) {
153                 @Override
154                 public void onSuccess(String s) {
155                     //響應資訊
156                     System.out.println("ack from client: " + client.getSessionId() + " data: " + s.toString());
157                 }
158             }, requestParam);
159 
160         } else {
161             //客戶端已斷開連線
162 
163         }
164         return result;
165     }
166 }

 

 1 @Component
 2 @Order(value = 1)
 3 public class MyCommandLineRunner implements CommandLineRunner {
 4 
 5     private final SocketIOServer server;
 6 
 7     @Autowired
 8     public MyCommandLineRunner(SocketIOServer server) {
 9         System.out.println("初始化MyCommandLineRunner");
10         this.server = server;
11     }
12 
13     @Override
14     public void run(String... args) {
15         try {
16             server.start();
17             System.out.println("socket.io啟動成功!");
18         } catch (Exception ex) {
19             ex.printStackTrace();
20         }
21     }
22 }

 

 1 public class SocketConstants {
 2 
 3     /**
 4      * Socket事件類
 5      */
 6     public class SocketEvent {
 7 
 8         /**
 9          * 效驗連線狀況
10          */
11         public static final String HEALTH_CHECK = "HEALTH_CHECK";
12 
13         /**
14          * 訊息接收事件名稱
15          */
16         public static final String MESSAGE = "message";
17 
18         /**
19          * 登入事件名稱
20          */
21         public static final String LOGIN = "LOGIN";
22 
23         /**
24          * 獲取交易要素事件名稱
25          */
26         public static final String QUERY_PAY_FIELDS = "QUERY_PAY_FIELDS";
27 
28         /**
29          * 建立訂單事件名稱
30          */
31         public static final String CREATE_ORDER = "CREATE_ORDER";
32 
33         /**
34          * 監控訂單狀態事件名稱
35          */
36         public static final String CHECK_ORDER_STATUS = "CHECK_ORDER_STATUS";
37 
38         /**
39          * 獲取訂單事件名稱
40          */
41         public static final String QUERY_ORDER = "QUERY_ORDER";
42 
43         /**
44          * 支付事件名稱
45          */
46         public static final String PAY = "PAY";
47     }
48 }

 

 1 public class SocketInstance {
 2 
 3     /**
 4      * 客戶端Socket連線物件容器
 5      */
 6     private static Map<String, SocketIOClient> socketClients = null;
 7 
 8     /**
 9      * 私有構造
10      */
11     private SocketInstance() {
12         //從快取中獲取socketClients
13         socketClients = new HashMap<>();
14     }
15 
16     /**
17      * 定義一個私有的內部類,在第一次用這個巢狀類時,會建立一個例項。而型別為SocketInstanceHolder的類,只有在SocketInstance.getSocketInstance()中呼叫,
18      * 由於私有的屬性,他人無法使用SocketInstanceHolder,不呼叫SocketInstance.getSocketInstance()就不會建立例項。
19      * 優點:達到了lazy loading的效果,即按需建立例項。
20      * 無法適用於分散式叢集部署
21      */
22     private static class SocketInstanceHolder {
23         /**
24          * 建立全域性唯一例項
25          */
26         private final static SocketInstance instance = new SocketInstance();
27     }
28 
29     /**
30      * 獲取全域性唯一例項
31      *
32      * @return SocketInstance物件
33      */
34     public static SocketInstance getSocketInstance() {
35         return SocketInstanceHolder.instance;
36     }
37 
38     /**
39      * 新增客戶端連線到容器
40      *
41      * @param encode         裝置En號
42      * @param socketIOClient 客戶端socket物件
43      */
44     public void insertSocketClient(String encode, SocketIOClient socketIOClient) {
45         SocketIOClient oldSocketIOClient = socketClients.get(encode);
46         if (oldSocketIOClient != null) {
47             try {
48                 //關閉客戶端連線
49                 oldSocketIOClient.disconnect();
50             } catch (Exception ex) {
51                 ex.printStackTrace();
52             }
53         }
54         socketClients.put(encode, socketIOClient);
55     }
56 
57     /**
58      * 獲取客戶端Socket物件
59      *
60      * @param encode 裝置encode
61      * @return 客戶端Socket物件
62      */
63     public SocketIOClient getClientSocket(String encode) {
64         return socketClients.get(encode);
65     }
66 }

 

 Android客戶端

  1 public class SocketClient {
  2 
  3     /**
  4      * 最大重連次數
  5      */
  6     private int maxReConnectionCount = 5;
  7 
  8     /**
  9      * 重連次數
 10      */
 11     private int reConnectionCount = 0;
 12 
 13     /**
 14      * 等待框物件
 15      */
 16     private static ProgressDialog progressdialog;
 17 
 18     /**
 19      * 提示框
 20      */
 21     private static AlertDialog.Builder dialogExitBuilder;
 22 
 23     /**
 24      * Toast提示物件
 25      */
 26     private static Toast toast;
 27 
 28     /**
 29      * Socket客戶端物件資訊
 30      */
 31     public static Socket socket;
 32 
 33     /**
 34      * 主頁面物件,每個頁面onCreate時必須設定,可在每個頁面監控Socket連線狀況
 35      */
 36     public static Context nowContext;
 37 
 38     /**
 39      * Socket連線提示handler(等待框)
 40      */
 41     Handler dialogMessageHandler = new Handler() {
 42         @Override
 43         public void handleMessage(Message msg) {
 44             super.handleMessage(msg);
 45             Bundle bundle = msg.getData();
 46             String message = bundle.getString(MessageUtil.MESSAGE);
 47             setDialogMessage(message);
 48         }
 49     };
 50 
 51     /**
 52      * Socket連線失敗退出提示handler(提示框)
 53      */
 54     Handler dialogExitHandler = new Handler() {
 55         @Override
 56         public void handleMessage(Message msg) {
 57             super.handleMessage(msg);
 58             Bundle bundle = msg.getData();
 59             String message = bundle.getString(MessageUtil.MESSAGE);
 60             dialogExit(message);
 61         }
 62     };
 63 
 64     /**
 65      * Socket連線提示handler(Toast)
 66      */
 67     Handler toastMessageHandler = new Handler() {
 68         @Override
 69         public void handleMessage(Message msg) {
 70             super.handleMessage(msg);
 71             Bundle bundle = msg.getData();
 72             String message = bundle.getString(MessageUtil.MESSAGE);
 73             showToast(message, Toast.LENGTH_SHORT);
 74         }
 75     };
 76 
 77     /**
 78      * 等待框
 79      *
 80      * @param message 提示文字
 81      */
 82     private static void setDialogMessage(String message) {
 83         if (progressdialog == null) {
 84             progressdialog = new ProgressDialog(nowContext);
 85         }
 86         progressdialog.setTitle("學通寶收銀");
 87         progressdialog.setMessage(message);
 88         progressdialog.setCancelable(false);
 89         progressdialog.show();
 90     }
 91 
 92     /**
 93      * 退出提示框
 94      *
 95      * @param message 提示文字
 96      */
 97     private void dialogExit(String message) {
 98         //初始化退出builder
 99         if (dialogExitBuilder == null) {
100             dialogExitBuilder = new AlertDialog.Builder(nowContext);
101         }
102         dialogExitBuilder.setMessage(message);
103         dialogExitBuilder.setTitle("提示");
104         dialogExitBuilder.setIcon(R.mipmap.warning);
105         dialogExitBuilder.setPositiveButton("確認", new DialogInterface.OnClickListener() {
106             public void onClick(DialogInterface dialog, int which) {
107                 dialog.dismiss();
108                 //引數用作狀態碼;根據慣例,非 0 的狀態碼錶示異常終止。
109                 System.exit(0);
110             }
111         });
112         dialogExitBuilder.create().show();
113     }
114 
115     /**
116      * Toast訊息提醒
117      *
118      * @param text     標題
119      * @param duration 時長
120      */
121     public void showToast(String text, int duration) {
122         //只建立一次
123         if (toast == null) {
124             toast = Toast.makeText(nowContext, text, duration);
125         } else {
126             toast.setText(text);
127             toast.setDuration(duration);
128         }
129         toast.show();
130     }
131 
132     public void startSocket() throws URISyntaxException {
133         //初始化Socket配置
134         IO.Options options = new IO.Options();
135         options.transports = new String[]{"websocket"};
136         options.reconnectionAttempts = maxReConnectionCount; // 設定一個重連的最大嘗試次數,超過這個值後Socket.io會使用所有允許的其他連線方式嘗試重連,直到最終失敗。
137         options.reconnectionDelay = 500;  //為Socket.io的重連設定一個時間間隔,內部會在多次重連嘗試時採用該值的指數值間隔,用來避免效能損耗(500 > 1000 > 2000 > 4000 > 8000)
138         options.reconnection = true;      //當連線終止後,是否允許Socket.io自動進行重連
139         options.timeout = 9000;           //連線超時時間(ms)
140         options.forceNew = true;
141         options.query = "appid=cn.xuetongbao.xtbpay";
142         socket = IO.socket("http://192.168.8.107:9096/", options);
143         //連線成功
144         socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
145             @Override
146             public void call(Object... args) {
147                 //重連機制
148                 if (reConnectionCount > 0) {
149                     //連線儲存客戶端資訊
150                     DeviceInfoInstance instance = DeviceInfoInstance.getSocketInstance();
151                     HealthCheckRequest healthCheckRequest = new HealthCheckRequest();
152                     healthCheckRequest.setEnCode(instance.getDeviceInfo().getEnCode());
153                     socket.emit(SocketConstants.SocketEvent.HEALTH_CHECK, RequestUtil.createObject(healthCheckRequest), (Ack) args1 -> {
154                         System.out.println("args1:" + args1.toString());
155                     });
156                 }
157                 System.out.println("連線成功...");
158                 toastMessageHandler.sendMessage(MessageUtil.createMessage("伺服器連線成功"));
159                 //關閉等待框
160                 if (progressdialog != null) {
161                     progressdialog.dismiss();
162                 }
163             }
164         });
165 
166         //連線失敗事件
167         socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() {
168             @Override
169             public void call(Object... args) {
170                 System.out.println("Socket.EVENT_CONNECT_ERROR");
171                 System.out.println("reConnectionCount:" + reConnectionCount);
172                 if (reConnectionCount >= maxReConnectionCount) {
173                     dialogExitHandler.sendMessage(MessageUtil.createMessage("伺服器連線失敗,請稍後再試"));
174                 } else {
175                     dialogMessageHandler.sendMessage(MessageUtil.createMessage("伺服器連線失敗,正在重新連線..."));
176                 }
177             }
178         });
179 
180         //連線中事件
181         socket.on(Socket.EVENT_RECONNECTING, new Emitter.Listener() {
182             @Override
183             public void call(Object... args) {
184                 reConnectionCount++;
185                 System.out.println("Socket.EVENT_RECONNECTING");
186                 dialogMessageHandler.sendMessage(MessageUtil.createMessage("正在連線伺服器..."));
187             }
188         });
189 
190         //連線超時事件
191         socket.on(Socket.EVENT_CONNECT_TIMEOUT, new Emitter.Listener() {
192             @Override
193             public void call(Object... args) {
194                 System.out.println("Socket.EVENT_CONNECT_TIMEOUT");
195                 if (nowContext != null) {
196                     dialogMessageHandler.sendMessage(MessageUtil.createMessage("與伺服器連線超時,正在重新建立連線..."));
197                     socket.connect();
198                 }
199             }
200         });
201 
202         //心跳包
203         socket.on(Socket.EVENT_PING, new Emitter.Listener() {
204             @Override
205             public void call(Object... args) {
206                 System.out.println("Socket.EVENT_PING");
207             }
208         });
209         //心跳包
210         socket.on(Socket.EVENT_PONG, new Emitter.Listener() {
211             @Override
212             public void call(Object... args) {
213                 System.out.println("Socket.EVENT_PONG");
214             }
215         });
216 
217         //訊息接收事件
218         socket.on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
219             @Override
220             public void call(Object... args) {
221                 System.out.println("-----------接受到訊息啦--------" + Arrays.toString(args));
222             }
223         });
224 
225         //連線斷開事件
226         socket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {
227             @Override
228             public void call(Object... args) {
229                 reConnectionCount = 0;
230                 System.out.println("客戶端斷開連線啦。。。");
231                 if (nowContext != null) {
232                     dialogMessageHandler.sendMessage(MessageUtil.createMessage("似乎與伺服器斷開連線,正在重新建立連線..."));
233                     socket.connect();
234                 }
235             }
236         });
237 
238         //交易事件
239         socket.on(SocketConstants.SocketEvent.PAY, new Emitter.Listener() {
240             @Override
241             public void call(Object... args) {
242                 Object data = args[0];
243                 Object ackCallBack = args[1];
244                 System.out.println("接收到服務端交易下單訊息" + data);
245                 CallPayRequest callPayRequest = JSON.parseObject(data.toString(), CallPayRequest.class);
246                 if (callPayRequest != null) {
247 
248                 }
249                 //data
250                 CallPayResponse callPayResponse = new CallPayResponse();
251                 callPayResponse.setResultCode(AppResultCode.Success.getCode());
252                 callPayResponse.setResultMsg(AppResultCode.Success.getMsg());
253 
254                 //響應服務端
255                 ((Ack) ackCallBack).call(JSON.toJSONString(callPayResponse));
256             }
257         });
258         System.out.println("準備連線伺服器...");
259         socket.connect();
260     }
261 }

 

 注:僅供學習參考

相關文章