公司项目有即时通讯的需求,完成之后整理一下贴出来,文章最后有GitHub上Android项目地址。
目录
为什么是WebSocket
Http是单向的,不能服务器主动给客户端发送数据,如果使用轮询的方式,将浪费流量和服务器的资源。WebSocket可以弥补这一缺点。在WebSocket中,只需要服务器和浏览器通过HTTP协议进行一个握手的动作,即可单独建立一条TCP的通信通道进行数据的传送。
服务器端代码
spring-mvc需要引入
javax.websocket-api 这个包
<!-- websocket -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
spring-boot需要引入
spring-boot-starter-websocket 这个包
<dependency>
<!-- websocket -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
新建Class类 - WebSocketService,添加@ServerEndPoint注解,设置你自己的value值。
其中,onOpen建立连接时调用,onClose连接关闭时调用,onError发生错误时调用,onMessage接收到信息是调用
@ServerEndpoint("/client/yourWebsocketServer")
public class WebSocketService {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//线程安全Set,存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketService> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/** 示例 , 从 spring 上下文 获取 bean ,进行数据库交互**/
private static ClientJsonMessageServiceImpl clientJsonMessageService = (ClientJsonMessageServiceImpl) ContextLoader.getCurrentWebApplicationContext().getBean("clientJsonMessageServiceImpl");
/** 连接打开调用的方法 **/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
webSocketSet.remove(this); //从set中删除
userSessionMap.remove(connectUserId);
subOnlineCount(); //在线数减1
System.out.println(connectUserId + " 退出!当前在线人数为" + getOnlineCount());
}
/**
* 接受消息调用的方法,安卓端只能发String , 服务器端需要转换成实体类接
* **/
@OnMessage
public void onMessage(String message , Session session) {
this.session = session;
try {
ClientChatMessage clientMessage = JSON.parseObject(message , new TypeReference<ClientChatMessage>() {});
/** 业务逻辑 **/
switch (clientMessage.getMessageType()){
case "beat":{
/** 心跳包检测 **/
session.getBasicRemote.sendText(JSON.toJSONString(clientMessage));
break;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable error) {
try {
webSocketSet.remove(this);
System.out.println(connectUserId + "连接发生错误, 当前在线人数: " + getOnlineCount());
System.out.println(error.getMessage());
}catch (Exception e){
error.printStackTrace();
}
}
private static synchronized int getOnlineCount() {
return onlineCount;
}
private static synchronized void addOnlineCount() {
WebSocketService.onlineCount++;
}
private static synchronized void subOnlineCount() {
WebSocketService.onlineCount--;
}
}
安卓端的实现
- [Android Studio]新建安卓项目 WSDemo
- 将项目改为Project查看目录结构,将 autobahn-0.5.0.jar (该包在github项目中,或自行百度)添加到WSDemo的libs目录下,右键jar包,选择 Add as Library
- 建立连接。
项目要求连接的稳定性,切换Activity不会影响WebSocket的连接,以及在连接断掉的情况下能够自动重连。
这里的思路是WebSocketService继承Service类,将连接放在WebSocketService中。应用需要使用WebSocket的时候,可以通过 WebSocketService.XX的方式获取连接。
实现自动重连需要加入心跳包。
心跳包的思路是,在发送或者接收到服务器返回信息的时候,记录时间,并与之前的时间对比, 超过时间限制,断开连接,并重新获取新的连接。
以下是这两个部分的代码
- WebSocketService类
public class WebSocketService extends Service {
public static WebSocketConnection webSocketConnection;
public MyWebSocket myWebSocket;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
/** 建立连接 **/
myWebSocket = new MyWebSocket();
myWebSocket.run();
}
}
要在清单文件中声明该服务
<application>
<service android:name=".service.WebSocketService"/>
</application>
- MyWebSocket类(2019.06.10更新)
public class MyWebSocket implements Runnable{
private static final String TAG = "MyWebSocket";
/** 心跳包间隔 **/
private static final int BEATSPACE = 2000;
/** 重连间隔 **/
private static final int RECONNECTSPACE = 5000;
/** 最长的无连接时间 **/
private static final int BEAT_MAX_UNCON_SPACE = 10000;
/** 上一次接受到心跳包反馈信息的时间 **/
private static long lastBeatSuccessTime;
/** 心跳包定时 **/
private static Timer beatTimer = null;
/** 心跳包定时任务 **/
private static TimerTask beatTimerTask = null;
/** WebSocket重连 **/
private static Timer wsReconnectTimer = null;
private static TimerTask wsReconnectTimerTask = null;
private static final int DELAY = 1000;
@Override
public void run() {
WebSocketService.webSocketConnection = new WebSocketConnection(){
@Override
public void sendTextMessage(String payload) {
super.sendTextMessage(payload);
}
};
startReconnect();
}
private void connect(){
try{
WebSocketService.webSocketConnection.connect(WebSocketUtil.ROOT_URL , getHandler());
}catch (WebSocketException e){
e.printStackTrace();
}
}
private WebSocketHandler getHandler(){
return new WebSocketHandler(){
@Override
public void onOpen(){
super.onOpen();
/** 关闭重连 **/
stopReconnect();
/** 建立连接时开启心跳包 **/
startBeat();
}
@Override
public void onClose(int code , String reason){
super.onClose(code , reason);
Log.i(TAG , code + " , " + reason);
}
@Override
public void onTextMessage(String payLoad){
/** payLoad - 接收到的信息 , 将其转成实体类 , 方便业务处理**/
WebSocketMessageBean webSocketMessageBean = JSON.parseObject(payLoad , new TypeReference<WebSocketMessageBean>(){});
Log.i(TAG , "接收到返回数据 : " + webSocketMessageBean);
/**
*
*
* 这部分写自己的业务逻辑
*
* 业务逻辑
*
*
*
* */
}
};
}
private void startBeat(){
/** 定时器 **/
beatTimer = new Timer();
beatTimerTask = new TimerTask() {
@Override
public void run() {
long thisBeatTime = new Date().getTime();
/** 消息实体 **/
WebSocketMessageBean webSocketMessageBean = new WebSocketMessageBean();
webSocketMessageBean.setMessageType(MESSAGETYPE.BEAT);
/** 只能发送String **/
if(WebSocketService.webSocketConnection != null && WebSocketService.webSocketConnection.isConnected()){
WebSocketService.webSocketConnection.sendTextMessage(JSON.toJSONString(webSocketMessageBean));
lastBeatSuccessTime = thisBeatTime;
}else{
if(lastBeatSuccessTime != 0 && (thisBeatTime - lastBeatSuccessTime) > BEAT_MAX_UNCON_SPACE){
/** 判断 当前时间 距离 上次连接时间 已经超过 设定的 最长未连接时间 ,开启重连**/
stopSendBeat();
startReconnect();
}
}
}
};
beatTimer.schedule(beatTimerTask , DELAY , BEATSPACE);
}
private void stopSendBeat(){
try{
if(beatTimer != null){
beatTimer.cancel();
beatTimer = null;
}
}catch (Exception e){
e.printStackTrace();
}
}
private void startReconnect(){
wsReconnectTimer = new Timer();
wsReconnectTimerTask = new TimerTask() {
@Override
public void run() {
connect();
}
};
wsReconnectTimer.schedule(wsReconnectTimerTask , DELAY , RECONNECTSPACE);
}
private void stopReconnect(){
lastBeatSuccessTime = new Date().getTime();
try{
if(wsReconnectTimer != null){
wsReconnectTimer.cancel();
wsReconnectTimer = null;
}
}catch (Exception e){
e.printStackTrace();
}
}
}
修改部分解决了在没有网络的情况下无法启动重连以及心跳包发送出错的问题。
此外,还需要在清单文件中加入网络权限。做完这一步,基本的准备工作就做完了。
接下来,启动APP,MainActivity中发消息,断网重连分别测试
这是启动APP后Logcat的输出
可以看到一直在发送心跳包,并且服务器可以接收到心跳包信息
在MainActivity中测试是否可以发消息
在服务器端接收到如下信息
发送没有问题
断网重连测试
可以看到 Network is unreachable,重新联网,可以创建新的连接。
WebSocket使用还是比较简单的,但是在心跳包重连这部分我觉得可能还存在问题,后期会再修改。如果有什么问题,欢迎评论,或者cheng9112@yahoo.com