Spring+Websocket實現伺服器與Andoird端通訊
本部落格伺服器端內容參考於部落格:http://www.cnblogs.com/3dianpomian/p/5902084.html。
寫這篇部落格的原因是在網上查閱了很多資料,關於websocket的介紹和程式碼很多,但是很少有統一給出伺服器端和Andorid端的具體實現的,在這裡給出我的解決方案,希望可以幫助大家。如有疑問,歡迎留言。
伺服器端
第一步:使用Maven(不會自行百度,這個很實用)自動更新新增依賴包
<dependency>
<groupId>org.springframework</groupId>
<artifactId >spring-websocket</artifactId>
<version>${srping.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${srping.version}</version >
</dependency>
另外還有配置Sping的相關包,不在此全部列出。
第二步:配置WebSocket的入口,編寫WebSocketConfig類實現WebSocketConfigurer 介面
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(),"/websocket/socketServer.do" ).addInterceptors(new SpringWebSocketHandlerInterceptor());
registry.addHandler(webSocketHandler(), "/sockjs/socketServer.do").addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();
}
@Bean
public TextWebSocketHandler webSocketHandler(){
return new SpringWebSocketHandler();
}
}
第三步:定義一個SpringWebSocketHandler類繼承TextWebSocketHandler,這個類是用來處理Websocket連線建立、斷開,訊息傳送的邏輯的,這個是訊息處理的核心程式碼
public class SpringWebSocketHandler extends TextWebSocketHandler {
private static final ArrayList<WebSocketSession> users;//這個會出現效能問題,最好用Map來儲存,key用userid
private static Logger logger = Logger.getLogger(SpringWebSocketHandler.class);
static {
users = new ArrayList<WebSocketSession>();
}
public SpringWebSocketHandler() {
// TODO Auto-generated constructor stub
}
/**
* 連線成功時候,會觸發頁面上onopen方法
*/
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// TODO Auto-generated method stub
System.out.println("connect to the websocket success......當前數量:"+users.size());
users.add(session);
//這塊會實現自己業務,比如,當用戶登入後,會把離線訊息推送給使用者
// TextMessage returnMessage = new TextMessage("你將收到的離線");
// session.sendMessage(returnMessage);
}
/**
* 關閉連線時觸發
*/
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.debug("websocket connection closed......");
String username= (String) session.getAttributes().get("WEBSOCKET_USERNAME");
System.out.println("使用者"+username+"已退出!");
users.remove(session);
System.out.println("剩餘線上使用者"+users.size());
}
/**
* js呼叫websocket.send時候,會呼叫該方法
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
logger.debug("message:"+message.getPayload().toString());
TextMessage returnMessage = new TextMessage(message.getPayload().toString());
//session.sendMessage(returnMessage);
//sendMessageToUser("123",returnMessage);
sendMessageToUsers(returnMessage);
}
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if(session.isOpen()){session.close();}
logger.debug("websocket connection closed......");
users.remove(session);
}
public boolean supportsPartialMessages() {
return false;
}
/**
* 給某個使用者傳送訊息
*
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : users) {
if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 給所有線上使用者傳送訊息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (WebSocketSession user : users) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第四步:攔截器配置
從WebSocketConfig中可以看到在註冊WebSocket通道時,不僅設定了入口地址,還配置了攔截器,攔截器可以實現握手之前和之後的邏輯操作,這裡配置的攔截器主要用於儲存使用者名稱以便於在Handler中定向傳送訊息。
public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
// TODO Auto-generated method stub
System.out.println("Before Handshake");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName區分WebSocketHandler,以便定向傳送訊息
String userName = (String) session.getAttribute("SESSION_USERNAME");
if (userName==null) {
userName="default-system";
}
attributes.put("WEBSOCKET_USERNAME",userName);
}
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
第五步:配置Websocket連線前臺頁面
網頁端連線Websocket和推送訊息的介面就是這裡
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript">
var msgDiv = document.getElementById("#msgDiv");
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/bank_pm/websocket/socketServer.do");
}
else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://localhost:8080/bank_pm/websocket/socketServer.do");
}
else {
websocket = new SockJS("http://localhost:8080/bank_pm/sockjs/socketServer.do");
}
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
function onOpen(openEvt) {
//alert(openEvt.Data+"onOpen");
}
function onMessage(evt) {
alert(evt.data+"onMessage");
}
function onError() {
alert("出錯"+"onError");
}
function onClose() {
alert("關閉"+"onClose");
}
function doSend() {
if (websocket.readyState == websocket.OPEN) {
var msg = document.getElementById("inputMsg").value;
websocket.send(msg); //呼叫後臺handleTextMessage方法
alert("傳送成功!");
} else {
alert("連線失敗!");
}
}
window.close=function(){
websocket.onclose();
}
</script>
<body align="center">
<h3>訊息推送</h3>
請輸入:<textarea rows="8" cols="50" id="inputMsg" name="inputMsg"></textarea>
<button onclick="doSend();">傳送</button>
<hr/>
<textarea rows="10" cols="70" id="msgDiv"></textarea>
</body>
</html>
第六步:登入介面與登入的Controller配置
因為我們傳送訊息是需要傳送給登入使用者的,所以在這裡補充登入的相關程式碼:
(1)登入頁面,地址可自己換成自己的:
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body align="center">
<h2>銀行績效管理系統</h2>
<form action="${pageContext.request.contextPath}/login.action" method="post">
登入名:<input type="text" name="username"/><br/>
密碼 : <input type="text" name="password"/><br/>
<input type="submit" id="login_btn_id" value="登入"/>
</form>
</body>
</html>
(2)登入的Controller編寫:主要是在登入是用HttpSession儲存了使用者名稱,為攔截器獲取使用者名稱做了準備。
@ResponseBody
@RequestMapping(value="/login.action",method={RequestMethod.POST,RequestMethod.GET})
public User checkUserAndPassword(
@RequestParam(value="username",required=true) String username,
@RequestParam(value="password",required=true) String password,User user,HttpServletRequest request) throws Exception{
User u = new User();
//登入成功
if((u = userService.checkUsernameAndPassword(user)) != null){
HttpSession session = request.getSession(true);
session.setAttribute("SESSION_USERNAME", user.getUsername());
return u;
};
//登入失敗,返回Null
return null;
}
另:web.xml中的servlet和filter中新增非同步支援
<async-supported>true</async-supported>
Android端
現在,伺服器端通過前臺介面websocket.jsp可以傳送和接收訊息了,那麼Android端是如何接收和傳送訊息的呢?同樣,Android端的處理方法也是用到了WebSocket。
第一步:Android專案中匯入JavaWebSocket_fat.jar包,放到專案的libs目錄下,記得新增檔案依賴哦。
編寫ManActivity測試程式碼
實現思路:在導好包後,在MainActivity中使用WebSocketClient這個類來進行Websocket網路通訊,詳細解釋見程式碼。點選andorid介面上的TextView實現網路連線,在網頁上推送訊息,介面上就會以Toast形式彈出傳送過來的訊息。
MainActivity:
public class MainActivity extends AppCompatActivity {
private WebSocketClient mWebSocketClient;
//注意更改成為你的伺服器地址,格式:"ws://ip:port/專案名字/websocket入口地址
private String address = "ws://192.168.1.115:8080/bank_pm/websocket/socketServer.do";
private TextView tv;
private Handler handler = new Handler(){
public void handleMessage(Message msg){
if(msg.what == 0x111){
String news = "";
news = msg.getData().getString("news");
Toast.makeText(MainActivity.this, "收到訊息:"+news, Toast.LENGTH_SHORT).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
initSocketClient();
mWebSocketClient.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
});
}
private void initSocketClient() throws URISyntaxException {
if(mWebSocketClient == null) {
mWebSocketClient = new WebSocketClient(new URI(address)) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
//連線成功
Log.i("LOG","opened connection");
}
‘
@Override
public void onMessage(String s) {
//服務端訊息來了
Log.i("LOG","received:" + s);
Message msg = Message.obtain();
msg.what = 0x111;
Bundle bundle = new Bundle();
bundle.putString("news",s);
msg.setData(bundle);
handler.sendMessage(msg);
}
@Override
public void onClose(int i, String s, boolean remote) {
//連線斷開,remote判定是客戶端斷開還是服務端斷開
Log.i("LOG","Connection closed by " + ( remote ? "remote peer" : "us" ) + ", info=" + s);
//
closeConnect();
}
@Override
public void onError(Exception e) {
Log.i("LOG","error:" + e);
}
};
}
}
//斷開連線
private void closeConnect() {
try {
mWebSocketClient.close();
}
catch(Exception e) {
e.printStackTrace();
}
finally {
mWebSocketClient = null;
}
}
//向伺服器傳送訊息的方法
private void sendMsg(String msg) {
mWebSocketClient.send(msg);
}
}
注意:websocket的網路連線自己已經是用了執行緒的,所以不用我們再去寫執行緒操作了。android專案記得新增網路許可權。
PS:OK,終於初步實現了這個東西,以後還是要多擠出時間寫部落格,向大牛邁進!