用websocket实现实时聊天功能

最近想实现网页版的仿QQ聊天工具,本来想用ajax实现的,但是一想到要一直轮询,就感觉有点蠢。后来在网上找到了websocket相关的资料,就拿来跟大家分享下(不是很熟练,现在只实现了群聊,单聊的前端不会写了。但可以跟大家说说思路)。
服务器端代码:
首先要创建类WebSocketConfig实现ServerApplicationConfig接口,ServerApplicationConfig项目启动时会自动启动,类似与ContextListener.是webSocket的核心配置

import java.util.Set;

import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;

public class WebSocketConfig implements ServerApplicationConfig{

    @Override
    public Set> getAnnotatedEndpointClasses(Set> channel) {
        System.out.println("Endpoint扫描到的数量"+channel.size());
        return channel;
    }

    @Override
    public Set getEndpointConfigs(Set> channel) {
        System.out.println("实现EndPoint接口的类数量:"+channel.size());
        return null;
    }

}
第二步:在webSocket的服务程序类上面加上注解@ServerEndPoint("/groupChat")表示的连接路径是:ws://${serverIp}:8000/avod/groupChat;
onopen是打开连接时的响应事件,onmessage 是发送数据时的响应事件,onclose是关闭连接时的响应事件。
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.google.gson.Gson;

@ServerEndpoint("/groupChat")
public class GroupChatSocket {

    //存放socket
    private static Map socketMap = new HashMap<>();
    //存放用户名(userLoginName)
    private static List userNameList = new ArrayList<>();


    private Session session;
    private String userName;
    private Gson gson = new Gson();

    @OnOpen
    public void openSocket(Session session){

        this.session = session;
        //var url="ws://${serverIp}:8000/avod/groupChat?currentUser=${session_user.loginName}";?后面的值就是session传递的值
        //reqString的值为:currentUser=${session_user.loginName}
        String reqString = this.session.getQueryString();
        this.userName = reqString.split("=")[1];
        socketMap.put(this.userName, this);


    }

    @OnMessage
    public void receive(Session session,String msgDataJson){
        MsgData msgData = gson.fromJson(msgDataJson, MsgData.class);

        Message message = new Message();
        message.setMsgSender(this.userName);
        message.setSendDate(new Date().toLocaleString());
        message.setMsgContent(msgData.getMsgContent());

        if(msgData.getIsFristJoin().equals("Y")){
            userNameList.add(this.userName);
            message.setHintMessage(this.userName+"加入群聊!!!!");
            message.setUserNameList(userNameList);
            broadcast(socketMap,gson.toJson(message),this.userName);
        }
        else{
            message.setUserNameList(userNameList);
            broadcast(socketMap,gson.toJson(message));
        }





    }



    @OnClose
    public void closeSocket(Session session){
        socketMap.remove(this.userName, this);
        userNameList.remove(this.userName);
        if(userNameList.size()==1||userNameList.size()>1){
            Message message = new Message();
            message.setHintMessage(this.userName+"退出了群聊!!!!");
            System.out.println(this.userName+"退出了群聊!!!!");
            message.setUserNameList(userNameList);

            broadcast(socketMap, gson.toJson(message));
        }

    }


    //广播聊天内容
    private void broadcast(Map socketMap, String message) {
        for(Iterator iterator = socketMap.values().iterator();iterator.hasNext();){
            GroupChatSocket socket = iterator.next();
            try {
                socket.session.getBasicRemote().sendText(message);
            } catch (IOException e) {

                e.printStackTrace();
            }
        }

    }


    //广播提示信息,除了自己以外的都要广播
    private void broadcast(Map socketMap, String message,String userName) {
        for(Iterator iterator = socketMap.values().iterator();iterator.hasNext();){
            GroupChatSocket socket = iterator.next();
            if(socketMap.get(userName) == socket){
                Message msg = gson.fromJson(message, Message.class);
                msg.setHintMessage(null);
                try {
                    socket.session.getBasicRemote().sendText(gson.toJson(msg));
                } catch (IOException e) {
                    // TODO 自动生成的 catch 块
                    e.printStackTrace();
                }
                continue;
                }
                try {
                    socket.session.getBasicRemote().sendText(message);
                } catch (IOException e) {

                    e.printStackTrace();
                }
            }

        }
}
前端代码:groupWS.readyState可以获取websocket的状态:
    根据readyState属性可以判断webSocket的连接状态,该属性的值可以是下面几种:
0 :对应常量CONNECTING (numeric value 0), 正在建立连接连接,还没有完成。The connection has not yet been established.
1 :对应常量OPEN (numeric value 1),连接成功建立,可以进行通信。The WebSocket connection is established and communication is possible.
2 :对应常量CLOSING (numeric value 2),连接正在进行关闭握手,即将关闭。The connection is going through the closing handshake.
3 : 对应常量CLOSED (numeric value 3),连接已经关闭或者根本没有建立。The connection has been closed or could not be opened.

打开连接:打开连接会触发后台@onopen注解下的方法
if(groupWS==null||groupWS==""||groupWS.readyState==3){
            var url="ws://${serverIp}:8000/avod/groupChat?currentUser=${session_user.loginName}";
            if ('WebSocket' in window) {
                groupWS = new WebSocket(url);
            } else if ('MozWebSocket' in window) {
                groupWS = new MozWebSocket(url);
            } else {
                alert('WebSocket is not supported by this browser.');
                return;
            }
刚连接的时候有握手操作,所以会有些延迟。如果想在一连接上就发消息的话,最好先判断下连接状态(连接成功建立,再发消息)。
var findInterval = setInterval(function (){
                console.log(groupWS.readyState);
                if(groupWS.readyState==1){
                    var msgData = JSON.stringify({chatType : "groupChat",isFristJoin : "Y"});
                    groupWS.send(msgData);
                    clearInterval(findInterval);
                }
            }, 500); 
消息展示:展示消息的消息是后台广播出来的数据,即broadcast()的执行结果。
            //展示消息
            groupWS.onmessage=function(event){
                eval("var message="+event.data+";");
                console.info(message);
                }
发送消息:发生的消息为String类型,如果想传一个实体对象到后台,需要先转换为json字符串,可以用JSON.stringify()来转换。消息发送会触发后台的@onmessage注解下的方法。
function sendMesg(){        
        var type = "groupChat";
        var msg = $("#mesgContent").val().trim();
        var joinFlag = "N";
        var msgData = JSON.stringify({chatType : type,msgContent : msg,isFristJoin : joinFlag});
        groupWS.send(msgData);
        $("#mesgContent").val(""); 
    }
关闭:关闭会触发后台@onclose注解下的方法
    groupWS.close(); 
下面来看效果图:
![这里写图片描述](https://img-blog.csdn.net/20180225003126444?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvWkhBTkdMSV9XT1JC/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

总结下思路:当点击群聊的图标的时候,打开连接,并将userName为key,当前对象为value,加入socketMap中,并发送一条消息,表示加入聊天室,并广播给在聊天室中除了自己的所有人,同时将userName加入userNameList中。聊天时,广播给在聊天室中的所有人。关闭聊天是,socketMap移除userName为key的value,同时userNameList也移除userName,广播给在聊天室中的所有人。
单聊的代码如下:
@ServerEndpoint("/singleChat")
public class SingleChatSocket {

    //存放socket
    private static Map socketMap = new HashMap<>();
    //存放用户请求的map(主要用来获取用户的头像),以用户名(userName)为key,以对应的请求该用户聊天的好友集合(userList)为value
    private static Map> requestMap = new HashMap<>();
    //以用户名(userName)为key,以对应的与该用户聊天的好友集合(chatList)为value
    private static Map> chatMap = new HashMap<>();

    private static final String SENDER_CODE = "PING";
    private static final String RECEIVER_AGREE_CODE = "PONG";
    private static final String RECEIVER_REJECT_CODE = "GUN";

    private Session session;
    private String userName;
    private Gson gson = new Gson();

    @OnOpen
    public void openSocket(Session session){

        this.session = session;

        String reqString = this.session.getQueryString();
        this.userName = reqString.split("=")[1];
        socketMap.put(this.userName, this);


    }

    @OnMessage
    public void receive(Session session,String msgDataJson){
        MsgData msgData = gson.fromJson(msgDataJson, MsgData.class);


        Message message = new Message();
        message.setMsgSender(this.userName);
        message.setSendDate(new Date().toLocaleString());
        message.setMsgContent(msgData.getMsgContent());

        String msgReceiver = msgData.getMsgReceiver();
        SingleChatSocket onlineChatSocket = socketMap.get(msgReceiver);
        //是否首次加入(只有点击“发消息”及点击消息提示的弹窗时,isFristJoin的值才为"Y",其他情景下为"N")
        if(msgData.getIsFristJoin().equals("Y")){
            //如果响应码为"PING",即点击“发消息”按钮时触发,发给消息接收者(msgReceiver)即可,等待其回应
            if(SENDER_CODE.equals(msgData.getResponseCode())){
                message.setResponseCode(SENDER_CODE);

                if(onlineChatSocket.session!=null){
                    //普通类调用Service
                    UserServiceImpl userServiceImpl = (UserServiceImpl)SpringBeanFactoryUtils.getBean("userServiceImpl");
                    User user = userServiceImpl.findUserByName(this.userName);
                    String path = user.getHeadPath();
                    user.setImgPath(path);
                    if(requestMap.get(msgReceiver)!=null){
                        List list = requestMap.get(msgReceiver);
                        list.add(gson.toJson(user));
                        requestMap.put(msgReceiver, list);
                    }else{
                        List list = new ArrayList<>();
                        list.add(gson.toJson(user));
                        requestMap.put(msgReceiver, list);
                    }
                    message.setUserList(requestMap.get(msgReceiver));
                    broadcast(onlineChatSocket.session,gson.toJson(message));
                }
            }else if(RECEIVER_AGREE_CODE.equals(msgData.getResponseCode())){
                /*如果响应码为"PONG",即同意进行聊天,这时将对应的值更新进chatMap中(同时考虑以发送方为key的情况及
                以接收方为key的情况),同时前端弹出聊天界面(包括发送方和接收方)*/
                message.setResponseCode(RECEIVER_AGREE_CODE);
                if(requestMap.get(this.userName)!=null){
                    List list = requestMap.get(this.userName);
                    list.removeAll(list);
                    requestMap.put(this.userName,list);
                }
                if(onlineChatSocket.session!=null){
                    flushChatMap(chatMap,this.userName,msgReceiver);
                    //接收方显示当前的聊天队列(userList)
                    message.setUserNameList(chatMap.get(msgReceiver));
                    broadcast(onlineChatSocket.session,gson.toJson(message));
                }
            }else if(RECEIVER_REJECT_CODE.equals(msgData.getResponseCode())){
                //如果响应码为"GUN",即不同意进行聊天,发给消息接收者(msgReceiver)即可
                message.setResponseCode(RECEIVER_REJECT_CODE);
                if(onlineChatSocket.session!=null){
                    broadcast(onlineChatSocket.session,gson.toJson(message));
                }
            }


        }else{
            //正常聊天情景下,isFristJoin的值才为"N",发给消息接收者(msgReceiver)即可
            if(chatMap.get(this.userName).contains(msgReceiver)){
                message.setUserNameList(chatMap.get(this.userName));
                if(onlineChatSocket.session!=null){
                    broadcast(onlineChatSocket.session,gson.toJson(message));
                }
            }
            /*else{
                message.setMsgContent("");
                message.setHintMessage(msgReceiver+"已离开");
            }*/
        }


        }








    @OnClose
    public void closeSocket(Session session){
        socketMap.remove(this.userName, this);
        List receiverList = chatMap.get(this.userName);
        if(receiverList!=null){
            if(receiverList.size()==1||receiverList.size()>1){
                Message message = new Message();
                message.setMsgSender(this.userName);
                message.setHintMessage(this.userName+"下线了!!");

                //broadcast(socketMap, gson.toJson(message));
                broadcast(receiverList,gson.toJson(message));
            }
            chatMap.remove(this.userName);
        }



    }


    private void broadcast(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    private void broadcast(List receiverList,String message){
        for(String receiver:receiverList){
            Message msg = gson.fromJson(message, Message.class);
            msg.setUserNameList(chatMap.get(receiver));
            SingleChatSocket onlineChatSocket = socketMap.get(receiver);
            if(onlineChatSocket!=null){
                broadcast(onlineChatSocket.session,gson.toJson(message));
            }


        }
    }


    private void flushChatMap(Map> chatMap,String msgSender,String msgReceiver){

        if(chatMap.get(msgSender)!=null){
            List list = chatMap.get(msgSender);
            list.add(msgReceiver);
            chatMap.put(msgSender, list);
            flushChatMap(chatMap,msgReceiver,msgSender);
        }else{
            List list = new ArrayList<>();
            list.add(msgReceiver);
            chatMap.put(msgSender, list);
            flushChatMap(chatMap,msgReceiver,msgSender);
        }

    }

}
单聊的思路用口头说有点啰嗦,大家看代码及下面的流程图即可:
![这里写图片描述](https://img-blog.csdn.net/20180225003521503?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvWkhBTkdMSV9XT1JC/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

由于自己的前端编码水平有限,单聊的前端界面不会写,不过后台数据已经可以用了(已验证),会前端的可以试试实现。

你可能感兴趣的:(javaweb程序设计,websocket,javascript)