使用WebSocket实现与客户的即时聊天功能

本项目的源代码地址:https://github.com/Alexshi5/learn-parent/tree/master/learn-javaweb/f1chapter10-websocket

本项目的前导文章:JavaWeb高级编程(十)—— 在应用程序中使用WebSocket进行交互

        通常聊天有两种实现方式:

        聊天室 —— 它有超过两个参与者,通常最大数量没有上限;

        私聊 —— 它通常只有两个参与者,其他人都无法看到聊天的内容。

        无论是私聊还是聊天室,服务器端的实现基本上都是相同的:服务器接受连接,关联所有相关的连接,并将进入的消息发送到相关的连接中。它们之间最大的区别就是关联彼此连接的数目。

下面是项目的代码示例:

1、使用的Maven依赖和版本号


        
            com.fasterxml.jackson.core
            jackson-databind
        

        
            org.apache.commons
            commons-lang3
        

        
            jstl
            jstl
        

        
            javax.servlet
            javax.servlet-api
            provided
        

        
            javax.servlet.jsp
            javax.servlet.jsp-api
            provided
        

        
            javax.websocket
            javax.websocket-api
        
    

1.0

2.9.2

1.2

2.3.1
4.0.0

3.6

2、创建简单的登录页面login.jsp和处理登录请求的LoginServlet

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>


    用户登录


用户登录

账号或密码错误,请重新尝试!
用户:
密码:
package com.mengfei.chat;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;

/**
 * author Alex
 * date 2018/12/30
 * description 用于用户登录的Servlet
 */
@WebServlet(name = "loginServlet",urlPatterns = "/login")
public class LoginServlet extends HttpServlet{
    private static final Map userDatabase = new Hashtable<>();

    static {
        userDatabase.put("customer001", "customer001");
        userDatabase.put("customer002", "customer002");
        userDatabase.put("service001", "service001");
        userDatabase.put("service002", "service002");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        HttpSession session = request.getSession();
        if(request.getParameter("logout") != null)
        {
            session.invalidate();
            response.sendRedirect("login");
            return;
        }
        else if(session.getAttribute("username") != null)
        {
            request.getRequestDispatcher("/WEB-INF/jsp/product.jsp")
                    .forward(request, response);
            return;
        }

        request.setAttribute("loginFailed", false);
        request.getRequestDispatcher("/WEB-INF/jsp/login.jsp")
                .forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        HttpSession session = request.getSession();
        if(session.getAttribute("username") != null)
        {
            request.getRequestDispatcher("/WEB-INF/jsp/product.jsp")
                    .forward(request, response);
            return;
        }

        String username = request.getParameter("username");
        String password = request.getParameter("password");
        if(username == null || password == null ||
                !LoginServlet.userDatabase.containsKey(username) ||
                !password.equals(LoginServlet.userDatabase.get(username)))
        {
            request.setAttribute("loginFailed", true);
            request.getRequestDispatcher("/WEB-INF/jsp/login.jsp")
                    .forward(request, response);
        }
        else
        {
            session.setAttribute("username", username);
            request.changeSessionId();
            request.getRequestDispatcher("/WEB-INF/jsp/product.jsp")
                    .forward(request, response);
        }
    }
}

 3、创建一个简单的商品列表页面product.jsp

        客户可以在此页面联系客服,客服人员可以在此页面查看发起聊天会话请求的列表。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>


    商品列表
    


商品列表

衣服

鞋子

外套


查看会话列表

联系客服

退出登录

4、创建一个POJO类ChatMessage

package com.mengfei.chat;

import java.util.Date;

/**
 * author Alex
 * date 2018/12/29
 * description 一个简单的聊天消息POJO
 */
public class ChatMessage
{
    //当前时区的时间
    private Date timestamp;
    //消息类型
    private Type type;
    //用户名
    private String username;
    //消息内容
    private String content;

    public Date getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(Date timestamp) {
        this.timestamp = timestamp;
    }

    public Type getType()
    {
        return type;
    }

    public void setType(Type type)
    {
        this.type = type;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getContent()
    {
        return content;
    }

    public void setContent(String content)
    {
        this.content = content;
    }

    public static enum Type
    {
        STARTED, JOINED, ERROR, LEFT, TEXT
    }
}

5、创建一个编(解)码器类ChatMessageCodec

        它将使用Jackson数据处理器编码和解码消息,编码方法将接受一个ChatMessage和一个OutputStream,通过将它转换成json对消息进行编码,并将它写入OutputStream中;方法decode完成的任务则刚好相反,它是根据所提供的InputStream读取反序列化Json的ChatMessage。

 注意:

① 只要提供的解码器能够将进入的文本或二进制消息转换成对象,那么就可以指定任意的Java对象作为参数;

② 只要提供的编码器能够将对象转换成文本或二进制消息,那么就可以使用RemoteEndpoint.Basic或RemoteEndpoint.Async的sendObject方法发送任何对象;

③ 实现Encoder.Binary、Encoder.BinaryStream、Encoder.Text或者Encoder.TextStream,并在解码器属性@ClientEndpoint或@ServerEndpoint中指定它们的类,通过这种方式可以提供解码器;

④ 可以实现Decoder.Binary、Decoder.BinaryStream、Decoder.Text或Decoder.TextStream,并使用终端注解的decoders特性为消息提供解码器。

package com.mengfei.chat;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.websocket.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;

/**
 * author Alex
 * date 2018/12/29
 * description 一个编解码器类
 */
public class ChatMessageCodec
        implements Encoder.BinaryStream,
        Decoder.BinaryStream
{
    private static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        MAPPER.findAndRegisterModules();
        MAPPER.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
    }

    @Override
    public void encode(ChatMessage chatMessage, OutputStream outputStream)
            throws EncodeException, IOException
    {
        try
        {
            ChatMessageCodec.MAPPER.writeValue(outputStream, chatMessage);
        }
        catch(JsonGenerationException | JsonMappingException e)
        {
            throw new EncodeException(chatMessage, e.getMessage(), e);
        }
    }

    @Override
    public ChatMessage decode(InputStream inputStream)
            throws DecodeException, IOException
    {
        try
        {
            return ChatMessageCodec.MAPPER.readValue(
                    inputStream, ChatMessage.class
            );
        }
        catch(JsonParseException | JsonMappingException e)
        {
            throw new DecodeException((ByteBuffer)null, e.getMessage(), e);
        }
    }

    @Override
    public void init(EndpointConfig endpointConfig) { }

    @Override
    public void destroy() { }
}

6、创建ChatSession类

       服务器终端将使用此类将请求聊天的客户关联到客服人员,它包含了消息的打开和聊天中众多消息的发送。

package com.mengfei.chat;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import javax.websocket.Session;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * author Alex
 * date 2018/12/29
 * description 聊天会话类,用于关联客户与客服人员的关系类
 */
public class ChatSession
{
    //聊天会话ID
    private long chatSessionId;
    //客户登录的用户名
    private String customerUsername;
    //客户的WebSocket会话
    private Session customer;
    //客服登录的用户名
    private String customerServiceUsername;
    //客服的WebSocket会话
    private Session customerService;
    //创建的消息
    private ChatMessage creationMessage;
    //聊天日志
    private final List chatLog = new ArrayList<>();

    public long getChatSessionId() {
        return chatSessionId;
    }

    public void setChatSessionId(long chatSessionId) {
        this.chatSessionId = chatSessionId;
    }

    public String getCustomerUsername()
    {
        return customerUsername;
    }

    public void setCustomerUsername(String customerUsername)
    {
        this.customerUsername = customerUsername;
    }

    public Session getCustomer()
    {
        return customer;
    }

    public void setCustomer(Session customer)
    {
        this.customer = customer;
    }

    public String getCustomerServiceUsername() {
        return customerServiceUsername;
    }

    public void setCustomerServiceUsername(String customerServiceUsername) {
        this.customerServiceUsername = customerServiceUsername;
    }

    public Session getCustomerService() {
        return customerService;
    }

    public void setCustomerService(Session customerService) {
        this.customerService = customerService;
    }

    public ChatMessage getCreationMessage()
    {
        return creationMessage;
    }

    public void setCreationMessage(ChatMessage creationMessage)
    {
        this.creationMessage = creationMessage;
    }

    @JsonIgnore
    public void log(ChatMessage message)
    {
        this.chatLog.add(message);
    }

    @JsonIgnore
    public void writeChatLog(File file) throws IOException
    {
        ObjectMapper mapper = new ObjectMapper();
        mapper.findAndRegisterModules();
        mapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

        try(FileOutputStream stream = new FileOutputStream(file))
        {
            mapper.writeValue(stream, this.chatLog);
        }
    }
}

7、创建聊天服务器终端类ChatEndpoint

        它接收了聊天连接并进行适当的协调,该类中的内部类EndpointConfigurator重写了modifyHandshake方法,在握手的时候,该方法将被调用并暴露出底层的HTTP请求,从该请求中可以得到HttpSession对象,通过这个对象可以判断用户是否已经登录,如果用户已经登录还可以关闭WebSocket会话。当会话无效时,sessionDestroyed方法将被调用,并且终端也会终止该聊天会话。

        当新的握手完成时,onOpen方法将被调用,它首先检查HttpSession是否被关联到了Session,以及用户是否已经登录。如果聊天会话ID为0(即请求创建新的会话),那么它将会创建新的聊天会话并添加到等待会话列表中;如果聊天会话ID大于0,客服人员将被加入到被请求的会话中,消息也将同时发送到两个客户端。

        当onMessage从某个客户端收到消息时,它将同时把消息发送到两个客户端。

        当会话被关闭引起错误时或者HttpSession被销毁时,一个消息将被发送到另一个用户,通知他聊天已经结束了,并关闭两个连接。

package com.mengfei.chat;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.websocket.*;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * author Alex
 * date 2018/12/30
 * description 聊天服务器终端
 */
@ServerEndpoint(value = "/chat/{chatSessionId}",
                encoders = ChatMessageCodec.class,
                decoders = ChatMessageCodec.class,
                configurator = ChatEndpoint.EndpointConfigurator.class)
@WebListener
public class ChatEndpoint implements HttpSessionListener{
    //HTTP会话的键
    private static String HTTP_SESSION_PROPERTY = "http_session";
    //WebSocket会话的键
    private static String WEBSOCKET_SESSION_PROPERTY = "websocket_session";
    //聊天会话序列ID
    private static long chatSessionIdSequence = 1L;
    //聊天会话序列ID的同步锁
    private static final Object chatSessionIdSequenceLock = new Object();
    //聊天会话的Map集合,以会话序列ID为键
    private static final Map chatSessions = new Hashtable<>();
    //与WebSocket会话关联的聊天会话Map集合,以WebSocket会话对象为键
    private static final Map sessions = new Hashtable<>();
    //与WebSocket会话关联的HttpSession会话Map集合,以WebSocket会话对象为键
    private static final Map httpSessions = new Hashtable<>();
    //等待聊天会话列表
    public static final List waitingChatSessionList = new ArrayList<>();

    @OnOpen
    public void onOpen(Session session, @PathParam("chatSessionId") long chatSessionId){
        //从WebSocket会话的的配置属性中获取设置的HttpSession对象
        HttpSession httpSession = (HttpSession)session.getUserProperties().get(HTTP_SESSION_PROPERTY);
        try {
            //检查httpSession是否为空,以及用户是否登录
            if(httpSession == null || httpSession.getAttribute("username") == null){
                //关闭WebSocket会话
                session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY,"请先登录!"));
                return;
            }
            String username = (String)httpSession.getAttribute("username");
            //向WebSocket会话的属性中设置用户名
            session.getUserProperties().put("username",username);

            //创建消息对象
            ChatMessage message = new ChatMessage();
            message.setTimestamp(new Date());
            message.setUsername(username);

            ChatSession chatSession;
            if(chatSessionId == 0){//请求创建新的聊天会话,并添加到等待会话列表中
                message.setType(ChatMessage.Type.STARTED);
                message.setContent(username + "启动聊天会话");

                chatSession = new ChatSession();
                //添加单机环境的同步块
                synchronized (ChatEndpoint.chatSessionIdSequenceLock){
                    chatSession.setChatSessionId(ChatEndpoint.chatSessionIdSequence++);
                }
                chatSession.setCustomer(session);
                chatSession.setCustomerUsername(username);
                chatSession.setCreationMessage(message);

                ChatEndpoint.waitingChatSessionList.add(chatSession);
                ChatEndpoint.chatSessions.put(chatSession.getChatSessionId(),chatSession);
            }else {//客服将被加入到请求的会话中,消息也将同时发送到两个客户端
                message.setType(ChatMessage.Type.JOINED);
                message.setContent(username + "加入聊天会话");

                //通过chatSessionId从聊天会话集合中获取聊天会话对象
                chatSession = ChatEndpoint.chatSessions.get(chatSessionId);
                chatSession.setCustomerService(session);
                chatSession.setCustomerServiceUsername(username);
                //移除等待聊天会话列表中的对象
                ChatEndpoint.waitingChatSessionList.remove(chatSession);
                //给客服人员推送消息
                session.getBasicRemote().sendObject(chatSession.getCreationMessage());
                session.getBasicRemote().sendObject(message);
            }

            ChatEndpoint.sessions.put(session,chatSession);
            ChatEndpoint.httpSessions.put(session,httpSession);
            //在当前的HTTP请求会话属性中添加关联的WebSocket会话
            this.getSessionsForHttpSession(httpSession).add(session);
            chatSession.log(message);
            //给客户推送消息
            chatSession.getCustomer().getBasicRemote().sendObject(message);
        }catch (IOException | EncodeException e){
            this.onError(session, e);
        }
    }

    @OnMessage
    public void onMessage(Session session, ChatMessage message)
    {
        //通过WebSocket会话来获取聊天会话
        ChatSession c = ChatEndpoint.sessions.get(session);
        //通过聊天会话来获取聊天的另外一个参与者
        Session other = this.getOtherSession(c, session);
        if(c != null && other != null)
        {
            c.log(message);
            try
            {
                //向两边发送消息
                session.getBasicRemote().sendObject(message);
                other.getBasicRemote().sendObject(message);
            }
            catch(IOException | EncodeException e)
            {
                this.onError(session, e);
            }
        }
    }

    //WebSocket会话关闭
    @OnClose
    public void onClose(Session session, CloseReason reason)
    {
        if(reason.getCloseCode() == CloseReason.CloseCodes.NORMAL_CLOSURE)
        {
            ChatMessage message = new ChatMessage();
            message.setUsername((String)session.getUserProperties().get("username"));
            message.setType(ChatMessage.Type.LEFT);
            message.setTimestamp(new Date());
            message.setContent(message.getUsername() + "退出聊天");
            try
            {
                Session other = this.close(session, message);
                if(other != null){
                    other.close();
                }
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }
        }
    }


    @OnError
    public void onError(Session session, Throwable e)
    {
        ChatMessage message = new ChatMessage();
        message.setUsername((String)session.getUserProperties().get("username"));
        message.setType(ChatMessage.Type.ERROR);
        message.setTimestamp(new Date());
        message.setContent(message.getUsername() + "由于出现异常退出聊天");
        try
        {
            Session other = this.close(session, message);
            if(other != null)
                other.close(new CloseReason(
                        CloseReason.CloseCodes.UNEXPECTED_CONDITION, e.toString()
                ));
        }
        catch(IOException ignore) { }
        finally
        {
            try
            {
                session.close(new CloseReason(
                        CloseReason.CloseCodes.UNEXPECTED_CONDITION, e.toString()
                ));
            }
            catch(IOException ignore) { }
        }
    }

    //HttpSession会话销毁
    @Override
    public void sessionDestroyed(HttpSessionEvent event)
    {
        HttpSession httpSession = event.getSession();
        if(httpSession.getAttribute(WEBSOCKET_SESSION_PROPERTY) != null)
        {
            ChatMessage message = new ChatMessage();
            message.setUsername((String)httpSession.getAttribute("username"));
            message.setType(ChatMessage.Type.LEFT);
            message.setTimestamp(new Date());
            message.setContent(message.getUsername() + "退出登录");
            for(Session session:new ArrayList<>(this.getSessionsForHttpSession(httpSession)))
            {
                try
                {
                    session.getBasicRemote().sendObject(message);
                    Session other = this.close(session, message);
                    if(other != null){
                        other.close();
                    }
                }
                catch(IOException | EncodeException e)
                {
                    e.printStackTrace();
                }
                finally
                {
                    try
                    {
                        session.close();
                    }
                    catch(IOException ignore) { }
                }
            }
        }
    }

    @Override
    public void sessionCreated(HttpSessionEvent event) { /* do nothing */ }

    @SuppressWarnings("unchecked")
    private synchronized ArrayList getSessionsForHttpSession(HttpSession httpSession)
    {
        try
        {
            //一个HttpSession可能关联多个WebSocket会话
            if(httpSession.getAttribute(WEBSOCKET_SESSION_PROPERTY) == null)
                httpSession.setAttribute(WEBSOCKET_SESSION_PROPERTY, new ArrayList<>());

            return (ArrayList)httpSession.getAttribute(WEBSOCKET_SESSION_PROPERTY);
        }
        catch(IllegalStateException e)
        {
            return new ArrayList<>();
        }
    }

    private Session close(Session s, ChatMessage message)
    {
        ChatSession c = ChatEndpoint.sessions.get(s);
        Session other = this.getOtherSession(c, s);
        ChatEndpoint.sessions.remove(s);
        HttpSession h = ChatEndpoint.httpSessions.get(s);
        if(h != null){
            this.getSessionsForHttpSession(h).remove(s);
        }
        if(c != null)
        {
            c.log(message);
            ChatEndpoint.waitingChatSessionList.remove(c);
            ChatEndpoint.chatSessions.remove(c.getChatSessionId());
            try
            {
                c.writeChatLog(new File("D:/logs/chat." + c.getChatSessionId() + ".log"));
            }
            catch(Exception e)
            {
                System.err.println("无法写入聊天日志信息!");
                e.printStackTrace();
            }
        }
        if(other != null)
        {
            ChatEndpoint.sessions.remove(other);
            h = ChatEndpoint.httpSessions.get(other);
            if(h != null){
                this.getSessionsForHttpSession(h).remove(s);
            }
            try
            {
                other.getBasicRemote().sendObject(message);
            }
            catch(IOException | EncodeException e)
            {
                e.printStackTrace();
            }
        }
        return other;
    }

    //通过当前聊天会话中的参与者来获取另外一个参与者的WebSocket会话
    private Session getOtherSession(ChatSession c, Session s)
    {
        return c == null ? null :
                (s == c.getCustomer() ? c.getCustomerService() : c.getCustomer());
    }

    public static class EndpointConfigurator extends ServerEndpointConfig.Configurator{
        @Override
        public void modifyHandshake(ServerEndpointConfig config,
                                    HandshakeRequest request,
                                    HandshakeResponse response) {
            super.modifyHandshake(config, request, response);
            //从底层的HTTP请求中获取到HttpSession对象,并设置到当前WebSocket会话的的配置属性中
            config.getUserProperties().put(HTTP_SESSION_PROPERTY,request.getHttpSession());
        }
    }
}

8、创建ChatServlet

        它的任务相当简单,主要是管理聊天会话的显示、创建和加入,方法Post设置了Expires和Cache-Control头,用于保证浏览器不会缓存该聊天页面。代码如下:

package com.mengfei.chat;

import org.apache.commons.lang3.math.NumberUtils;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * author Alex
 * date 2018/12/29
 * description 主要是管理聊天会话的显示、创建和加入,方法Post设置了Expires和Cache-Control头,
 * 用于保证浏览器不会缓存该聊天页面
 */
@WebServlet(name = "chatServlet",urlPatterns = "/chat")
public class ChatServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String action = request.getParameter("action");
        if("list".equals(action))
        {
            request.setAttribute("waitingChatSessionList", ChatEndpoint.waitingChatSessionList);
            request.getRequestDispatcher("/WEB-INF/jsp/list.jsp")
                    .forward(request, response);
        }
        else {
            request.getRequestDispatcher("/WEB-INF/jsp/product.jsp")
                    .forward(request, response);
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setHeader("Expires", "Thu, 1 Jan 1970 12:00:00 GMT");
        response.setHeader("Cache-Control","max-age=0, must-revalidate, no-cache");

        String action = request.getParameter("action");
        String view = null;
        switch(action)
        {
            case "new":
                //客户发起的会话请求,即新创建的聊天会话ID均设为0
                request.setAttribute("chatSessionId", 0);
                view = "chat";
                break;
            case "join":
                String id = request.getParameter("chatSessionId");
                if(id == null || !NumberUtils.isDigits(id))
                    response.sendRedirect("chat?list");
                else
                {
                    request.setAttribute("chatSessionId", Long.parseLong(id));
                    view = "chat";
                }
                break;
            default:
                request.getRequestDispatcher("/WEB-INF/jsp/product.jsp")
                        .forward(request, response);
                break;
        }

        if(view != null) {
            request.getRequestDispatcher("/WEB-INF/jsp/" + view + ".jsp")
                    .forward(request, response);
        }
    }
}

9、创建聊天会话列表页面list.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>


    会话列表
    


会话列表

没有等待的客户会话请求!
${s.customerUsername}
退出登录

10、创建聊天室页面chat.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>


    聊天室
    
    


聊天室


退出登录

11、测试

        可以开启两个浏览器进行聊天测试,访问http://127.0.0.1:8082/login先进行用户登录,再根据客户和客服角色的不同发送不同的消息,测试结果如下:

使用WebSocket实现与客户的即时聊天功能_第1张图片

你可能感兴趣的:(开发实践)