服務端
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 }
注:僅供學習參考