基于javax的websocket服务端实现,含心跳机制

websocket连接类

package com.dnn.controller.inter;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.apache.http.impl.entity.EntitySerializer;

import com.dnn.entity.WebSocketEntity;
import com.dnn.model.TbAdminMember;
import com.dnn.service.TbAdminMemberService;
import com.dnn.service.TbDataGrouprecordService;
import com.dnn.utils.jfinal.BaseController;
import com.jfinal.aop.Clear;

import net.sf.json.JSONObject;

@Clear
@ServerEndpoint("/webSocket/{userId}")
public class WebSocketController extends BaseController {
    
    protected TbDataGrouprecordService tbDataGrouprecordService=new TbDataGrouprecordService();
    protected TbAdminMemberService tbAdminMemberService=new TbAdminMemberService();
    private static boolean isHeart=false;
    private static final Set<WebSocketEntity> connections = new CopyOnWriteArraySet<WebSocketEntity>();
    
    /**
     * 
     * @Description: 连接方法
     * @param @param userId
     * @param @param session   
     * @return void  
     * @throws IOException 
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    @OnOpen
    public synchronized void onOpen(@PathParam("userId") String userId, Session session) throws IOException {
        TbAdminMember member=tbAdminMemberService.findInfo(new TbAdminMember().setId(userId));
        if(null==member){ 
            logger.debug("发现未知生物");
            return;
        }
        addUser(member, session);
        if(connections.size()==1 && !isHeart){
            isHeart=true;
            startHeart();
        }
    }

    /**
     * 
     * @Description: 收到消息执行
     * @param @param userId
     * @param @param message
     * @param @param session
     * @param @throws IOException   
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    @OnMessage
    public synchronized void onMessage(@PathParam("userId") String userId, String message, Session session) throws IOException {
        logger.info(message);
        JSONObject jsonObject = JSONObject.fromObject(message);
        if(jsonObject.has("secret") && jsonObject.getString("secret").equals("ping")){//心跳
            logger.info("收到"+userId+"的心跳"+message);
            //如果收到了心跳 这里设置isHeart为true
            WebSocketEntity entity=getUserEntity(userId);
            if(null!=entity){
                entity.setHeart(true);
            }
        }else{//普通对话
            boolean res=tbDataGrouprecordService.addGroupRecord(jsonObject);
            logger.warn("保存记录:"+res);
            SimpleDateFormat sdfTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date = new Date();
            String datetime = sdfTime.format(date);//获取当前时间
            jsonObject.put("intime", datetime);
            message=tbAdminMemberService.addUserInfo(jsonObject);
            sendMsg(message);
        }
    }

    /**
     * 
     * @Description: 链接错误执行
     * @param @param userId
     * @param @param session
     * @param @param error   
     * @return void  
     * @throws IOException 
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    @OnError
    public synchronized void onError(@PathParam("userId") String userId, Session session, Throwable error) throws IOException {
        logger.debug(userId+":发生了错误");
        removeUser(userId,new CloseReason(CloseCodes.NO_EXTENSION, "客户端异常"));
        error.printStackTrace();
    }
    
    @OnClose
    public synchronized void onClose(@PathParam("userId") String userId,Session session,CloseReason reason){
        logger.debug(userId+":退出了链接");
        removeUser(userId,reason);
    }

    /**
     * 
     * @Description: 获取在线人数
     * @param @return   
     * @return int  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    private synchronized int getUserOnlineNum(){
        return connections.size();
    }
    
    /**
     * 
     * @Description: 获取在线人数列表
     * @param @return   
     * @return Set<String>  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    @SuppressWarnings("unused")
    private synchronized Set<WebSocketEntity> getUserOnline(){
        return connections;
    }
    
    /**
     * 
     * @Description: 用户上线
     * @param @param member
     * @param @param session   
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    private synchronized void addUser(TbAdminMember member, Session session){       
        WebSocketEntity entity=getUserEntity(member.getId());
        if(null==entity){
            connections.add(new WebSocketEntity(member, session));
        }else{
            entity.setSession(session);
            entity.setMemberHead(member.getHead());
            entity.setMemberName(member.getName());
            logger.debug("用户"+entity.getMemberName()+"上线了,当前人数为:"+getUserOnlineNum());
        }
    
    }
    
    /**
     * 
     * @Description: 根据userId获取实体类
     * @param @param userId
     * @param @return   
     * @return WebSocketEntity  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月22日
     */
    private static WebSocketEntity getUserEntity(String userId){
        WebSocketEntity entity=null;
        if(connections.size()==0)
            return entity;
        for (WebSocketEntity webSocketEntity : connections) {
            if(webSocketEntity.getUserId().contentEquals(userId)){
                entity=webSocketEntity;
                break;
            }
        }
        return entity;
    }
    
    /**
     * 
     * @Description: 用户下线
     * @param @param userId
     * @param @param reason   
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月23日
     */
    private void removeUser(String userId, CloseReason reason) {
        WebSocketEntity entity=getUserEntity(userId);
        if(null!=entity){       
            try {
                if(entity.getSession().isOpen()){
                    entity.getSession().close(reason);
                }
                connections.remove(entity);
            } catch (IOException e) {
                
                logger.info(e.toString());
                e.printStackTrace();
            }
        }
        logger.debug("当前人数:"+connections.size());
        
    }
    

    /**
     * 
     * @param 发送心跳包
     * @Description: 服务端群发消息
     * @param @param message
     * @param @throws IOException   
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    public synchronized void sendPing(String message) throws IOException{
        if(connections.size()<=0)
            return;
        for (WebSocketEntity webSocketEntity : connections) {   
            synchronized (webSocketEntity) {
                webSocketEntity.setTimeStr(getTimeInMillis());
                webSocketEntity.setHeart(false);  
                ((Session) webSocketEntity.getSession()).getBasicRemote().sendText(message);
            }
        }
    }
    
    /**
     * 
     * @Description: 发消息
     * @param @param message
     * @param @throws IOException   
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月11日
     */
    public synchronized void sendMsg(String message) throws IOException{
        if(connections.size()<=0)
            return;
        for (WebSocketEntity entity : connections) {
            synchronized (entity) {
                ((Session) entity.getSession()).getBasicRemote().sendText(message); // 回复用户
            }
        }
    }
    
    
    
    /**
     * 
     * @Description: 启动心跳包
     * @param    
     * @return void  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月10日
     */
    private synchronized void startHeart(){
        ExamineHeartThread examineHeart =new ExamineHeartThread();
        Thread examineThread=new Thread(examineHeart);
        
        KeepHeartThread keepHeart=new KeepHeartThread();
        Thread keepThread=new Thread(keepHeart);
        
        
        keepThread.start(); 
        examineThread.start();
        
    }
    
    /**
     * 
     * @Description: 获取时间戳
     * @param @return   
     * @return long  
     * @throws
     * @author 黑暗料理界扛把子
     * @date 2018年5月22日
     */
    private static long getTimeInMillis(){
        Calendar c = Calendar.getInstance();
        c.set(Calendar.SECOND,c.get(Calendar.SECOND)+8);
        return c.getTimeInMillis();
    }
    
   /**
    * 
    * @author 黑暗料理界扛把子
    *
    * @Description server发送心跳包 10秒一次
    */
    private class KeepHeartThread implements Runnable {
        
        @Override
        public void run() {  
            JSONObject heartJson=new JSONObject();
            heartJson.put("type", "0");
            heartJson.put("secret", "heart_keep");
            while (true) {              
                try {
                    logger.debug("发送心跳包当前人数为:"+getUserOnlineNum());
                    sendPing(heartJson.toString());
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
            
    }
    
    /**
     * 
     * @author 黑暗料理界扛把子
     *
     * @Description 检测是否收到client心跳 每秒一次
     */
    private class ExamineHeartThread implements Runnable{

        @Override
        public void run() {
            while (true) {
                try {
                    long timeMillins=System.currentTimeMillis();
                    for (WebSocketEntity entity : connections) {
                        logger.debug(timeMillins);
                        logger.info(entity.getTimeStr());
                        logger.debug(timeMillins>entity.getTimeStr());
                        if(!entity.isHeart() && entity.getTimeStr()!=0 && timeMillins>entity.getTimeStr()){
                            logger.debug(entity.getMemberName()+"挂了");
                            onClose(entity.getUserId(),entity.getSession(),new CloseReason(CloseCodes.NORMAL_CLOSURE, "没有收到心跳"));
                        }
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
} 
   

WebSocketEntity实体类

package com.dnn.entity;

import javax.websocket.Session;

import com.dnn.model.TbAdminMember;

public class WebSocketEntity {

    private String userId;//用户id
    private Session session;
    private String memberName;//用户姓名
    private String memberHead;//头像
    private long timeStr;//记录下次发送时间的时间戳
    private boolean isHeart=false;//是否收到了心跳
    
    public boolean isHeart() {
        return isHeart;
    }

    public void setHeart(boolean isHeart) {
        this.isHeart = isHeart;
    }

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }

    public String getMemberHead() {
        return memberHead;
    }

    public void setMemberHead(String memberHead) {
        this.memberHead = memberHead;
    }   

    public long getTimeStr() {
        return timeStr;
    }

    public void setTimeStr(long timeStr) {
        this.timeStr = timeStr;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public Session getSession() {
        return session;
    }
    
    public void setSession(Session session) {
        this.session = session;
    }


    public WebSocketEntity(String userId, Session session, String memberName, String memberHead) {
        super();
        this.userId = userId;
        this.session = session;
        this.memberName = memberName;
        this.memberHead = memberHead;
    }

    public WebSocketEntity(TbAdminMember member, Session session) {
        super();
        this.userId = member.getId();
        this.session = session;
        this.memberName = member.getName();
        this.memberHead = member.getHead();
    }

    @Override
    public String toString() {
        return "WebSocketEntity [userId=" + userId + ", session=" + session + ", memberName=" + memberName
                + ", memberHead=" + memberHead + ", timeStr=" + timeStr + ", isHeart=" + isHeart + "]";
    }

    @Override
    public int hashCode() {
        return this.userId.length();
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof WebSocketEntity)){
            return false;
        }
        if(obj==this){
            return true;
        }
        return this.userId.equals(((WebSocketEntity)obj).userId);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容