使用WebSocket实现与客户的即时聊天功能
本项目的源代码地址:https://github.com/Alexshi5/learn-parent/tree/master/learn-javaweb/f1chapter10-websocket
本项目的前导文章:JavaWeb高级编程(十)—— 在应用程序中使用WebSocket进行交互
通常聊天有两种实现方式:
聊天室 —— 它有超过两个参与者,通常最大数量没有上限;
私聊 —— 它通常只有两个参与者,其他人都无法看到聊天的内容。
无论是私聊还是聊天室,服务器端的实现基本上都是相同的:服务器接受连接,关联所有相关的连接,并将进入的消息发送到相关的连接中。它们之间最大的区别就是关联彼此连接的数目。
下面是项目的代码示例:
1、使用的Maven依赖和版本号
[code]<dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> </dependency> </dependencies>
[code]<!--websocket--> <websocket.version>1.0</websocket.version> <!-- jackson工具 --> <jackson.version>2.9.2</jackson.version> <!-- jstl标签库 --> <jstl.version>1.2</jstl.version> <!-- jsp和servlet --> <jsp-api.version>2.3.1</jsp-api.version> <servlet-api.version>4.0.0</servlet-api.version> <!-- jdk的补充工具jar包 --> <commons-lang3.version>3.6</commons-lang3.version>
2、创建简单的登录页面login.jsp和处理登录请求的LoginServlet
[code]<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>用户登录</title> </head> <body> <h1>用户登录</h1> <c:if test="${loginFailed}"> 账号或密码错误,请重新尝试! </c:if> <form method="post" action="login"> 用户:<input name="username"><br> 密码:<input name="password"><br> <input type="submit" value="登录"> </form> </body> </html>
[code]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<String, String> 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
客户可以在此页面联系客服,客服人员可以在此页面查看发起聊天会话请求的列表。
[code]<%@ 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" %> <html> <head> <title>商品列表</title> <script type="text/javascript" src="js/jquery-3.2.1.js"></script> </head> <body> <h1>商品列表</h1> <h3>衣服</h3> <h3>鞋子</h3> <h3>外套</h3> <c:if test="${fn:contains(sessionScope.username,'service')}"> <br> <a href="chat?action=list">查看会话列表</a> </c:if> <c:if test="${fn:contains(sessionScope.username,'customer')}"> <br> <a href="javascript:void(0)" οnclick="newChat()">联系客服</a> </c:if> <br> <a href="login?logout=true">退出登录</a> </body> </html> <script> function newChat() { hiddenFormSubmit('chat',{action:'new'}); } function hiddenFormSubmit(url,fields) { var form = $('<form id="mapForm" method="post"></form>') .attr({ action: url, style: 'display: none;' }); for(var key in fields) { if(fields.hasOwnProperty(key)) form.append($('<input type="hidden">').attr({ name: key, value: fields[key] })); 7ff7 } $('body').append(form); form.submit(); } </script>
4、创建一个POJO类ChatMessage
[code]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特性为消息提供解码器。
[code]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<ChatMessage>, Decoder.BinaryStream<ChatMessage> { 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类
服务器终端将使用此类将请求聊天的客户关联到客服人员,它包含了消息的打开和聊天中众多消息的发送。
[code]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<ChatMessage> 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被销毁时,一个消息将被发送到另一个用户,通知他聊天已经结束了,并关闭两个连接。
[code]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<Long,ChatSession> chatSessions = new Hashtable<>(); //与WebSocket会话关联的聊天会话Map集合,以WebSocket会话对象为键 private static final Map<Session,ChatSession> sessions = new Hashtable<>(); //与WebSocket会话关联的HttpSession会话Map集合,以WebSocket会话对象为键 private static final Map<Session,HttpSession> httpSessions = new Hashtable<>(); //等待聊天会话列表 public static final List<ChatSession> 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<Session> getSessionsForHttpSession(HttpSession httpSession) { try { //一个HttpSession可能关联多个WebSocket会话 if(httpSession.getAttribute(WEBSOCKET_SESSION_PROPERTY) == null) httpSession.setAttribute(WEBSOCKET_SESSION_PROPERTY, new ArrayList<>()); return (ArrayList<Session>)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头,用于保证浏览器不会缓存该聊天页面。代码如下:
[code]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
[code]<%@ 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" %> <html> <head> <title>会话列表</title> <script type="text/javascript" src="js/jquery-3.2.1.js"></script> </head> <body> <h1>会话列表</h1> <c:if test="${fn:length(waitingChatSessionList) == 0}"> 没有等待的客户会话请求!<br> </c:if> <c:if test="${fn:length(waitingChatSessionList) > 0}"> <c:forEach items="${waitingChatSessionList}" var="s"> <a href="javascript:void(0)" οnclick="joinChat(${s.chatSessionId});">${s.customerUsername}</a> </c:forEach> </c:if> <br> <a href="login?logout=true">退出登录</a> </body> </html> <script> function joinChat(chatSessionId) { hiddenFormSubmit('chat',{action:'join',chatSessionId:chatSessionId}); } function hiddenFormSubmit(url,fields) { var form = $('<form id="mapForm" method="post"></form>') .attr({ action: url, style: 'display: none;' }); for(var key in fields) { if(fields.hasOwnProperty(key)) form.append($('<input type="hidden">').attr({ name: key, value: fields[key] })); } $('body').append(form); form.submit(); } </script>
10、创建聊天室页面chat.jsp
[code]<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %> <html> <head> <title>聊天室</title> <script type="text/javascript" src="js/jquery-3.2.1.js"></script> <style> #messageContainer { width: 300px; float: left; } #messageArea { height: 75px; width:280px; } #chatLog div.informational { font-style: italic; color: #AAA; } #chatLog div.error { font-weight: bold; color: #C00; } #chatLog span.user-me { font-weight: bold; color: #0A0; } #chatLog span.user-you { font-weight: bold; color: #55F; } </style> </head> <body> <h1>聊天室</h1> <div id="chatContainer"> <div id="chatLog"> </div> <div id="messageContainer"> <textarea id="messageArea"></textarea> </div> <div id="buttonContainer"> <button οnclick="send();">发送消息</button> <button οnclick="disconnect();">退出聊天</button> </div> </div> <div id="modalError" style="display: none"> <div id="modalErrorBody">出现了一个异常</div> </div> <br> <a href="login?logout=true">退出登录</a> </body> </html> <script type="text/javascript" language="javascript"> var send, disconnect; $(document).ready(function() { var modalError = $("#modalError"); var modalErrorBody = $("#modalErrorBody"); var chatLog = $('#chatLog'); var messageArea = $('#messageArea'); var username = '${sessionScope.username}'; var otherJoined = false; if(!("WebSocket" in window)) { modalErrorBody.text('该浏览器不支持WebScoket通信,请更换其他浏览器继续尝试!'); modalError.show(); return; } var infoMessage = function(msg) { chatLog.append($('<div>').text(getFormatDate(new Date()) + ': ' + msg)); }; infoMessage('正在连接聊天终端服务器,请稍候......'); var objectMessage = function(message) { var log = $('<div>'); var date = message.timestamp == null ? '' : getFormatDate(new Date(message.timestamp)); if(message.username != null) { var c = message.username == username ? 'user-me' : 'user-you'; log.append($('<span>').addClass(c) .text(date+' '+message.username+':\xA0')) .append($('<span>').text(message.content)); } else { log.addClass(message.type == 'ERROR' ? 'error' : 'informational') .text(date + ' ' + message.content); } chatLog.append(log); }; var server; try { server = new WebSocket('ws://' + window.location.host + '/chat/${chatSessionId}'); server.binaryType = 'arraybuffer'; } catch(error) { modalErrorBody.text(error); modalError.show(); return; } server.onopen = function(event) { infoMessage('已经连接到聊天终端服务器'); }; server.onclose = function(event) { if(server != null) infoMessage('断开聊天终端服务器'); server = null; if(!event.wasClean || event.code != 1000) { modalErrorBody.text('Code ' + event.code + ': ' + event.reason); modalError.show(); } }; server.onerror = function(event) { modalErrorBody.text(event.data); modalError.show(); }; server.onmessage = function(event) { if(event.data instanceof ArrayBuffer) { var message = JSON.parse(utf8ArrayBufferToStr(new Uint8Array(event.data))); objectMessage(message); if(message.type == 'JOINED') { otherJoined = true; if(username != message.username){ infoMessage('你当前正在与' + message.username + '聊天'); } } } else { modalErrorBody.text('意料之外的数据类型: [' + typeof(event.data) + ']'); modalError.show(); } }; send = function() { if(server == null) { modalErrorBody.text('你没有连接到聊天终端服务器!'); modalError.show(); } else if(!otherJoined) { modalErrorBody.text('客服人员还没有加入聊天!'); modalError.show(); } else if(messageArea.get(0).value.trim().length > 0) { var message = { timestamp: new Date().getTime(), type: 'TEXT', username: username, content: messageArea.get(0).value }; try { var json = JSON.stringify(message); var buffer = utf8StrToArrayBuffer(json); server.send(buffer); messageArea.get(0).value = ''; } catch(error) { modalErrorBody.text(error); modalError.show(); } } }; disconnect = function() { if(server != null) { infoMessage('已断开聊天终端服务器!'); server.close(); server = null; window.location.href = "chat?action"; } }; window.onbeforeunload = disconnect; }); function getFormatDate(date) { var month = date.getMonth() + 1; var strDate = date.getDate(); if(month >= 1 && month <= 9) { month = "0" + month; } if(strDate >= 0 && strDate <= 9) { strDate = "0" + strDate; } var hour = date.getHours(); var minutes = date.getMinutes(); var seconds = date.getSeconds(); if(hour >= 1 && hour <= 9) { hour = "0" + hour; } if(minutes >= 0 && minutes <= 9) { minutes = "0" + minutes; } if(seconds >= 0 && seconds <= 9) { seconds = "0" + seconds; } var currentdate = date.getFullYear() + "-" + month + "-" + strDate + " " + hour + ":" + minutes + ":" + seconds; return currentdate; } //ArrayBuffer转换成utf-8编码格式的字符串,参数为ArrayBuffer对象 function utf8ArrayBufferToStr(array) { var out, i, len, c; var char2, char3; out = ""; len = array.length; i = 0; while(i < len) { c = array[i++]; switch(c >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 4000 6: case 7: // 0xxxxxxx out += String.fromCharCode(c); break; case 12: case 13: // 110x xxxx 10xx xxxx char2 = array[i++]; out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); break; case 14: // 1110 xxxx 10xx xxxx 10xx xxxx char2 = array[i++]; char3 = array[i++]; out += String.fromCharCode(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0)); break; } } return out; } //字符串转为ArrayBuffer对象,参数为字符串 function utf8StrToArrayBuffer(str) { var buf = new ArrayBuffer(str.length*2); // 每个字符占用2个字节 var bufView = new Uint16Array(buf); for (var i=0, strLen=str.length; i<strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } </script>
11、测试
可以开启两个浏览器进行聊天测试,访问http://127.0.0.1:8082/login先进行用户登录,再根据客户和客服角色的不同发送不同的消息,测试结果如下:
- 在Spring Boot框架下使用WebSocket实现聊天功能
- Java中使用websocket实现在线聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 使用websocket实现在线聊天功能
- Android中使用webSocket实现文字及单张图片发送聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 使用JavaWeb webSocket实现简易的点对点聊天功能实例代码
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 在Spring Boot框架下使用WebSocket实现聊天功能
- 使用ExtJS+WebSocket实现的WebQQ聊天
- Netty 实现 WebSocket 聊天功能
- 使用WebSocket实现多人实时聊天
- websocket 实现聊天功能
- 使用socket实现聊天功能
- WebSocket 和 Golang 实现聊天功能
- 使用UDP实现聊天功能
- 基于smack的即时聊天系统之文件接收功能实现