您的位置:首页 > 其它

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

2018-12-31 03:06 309 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Alexshi5/article/details/85345598

本项目的源代码地址: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先进行用户登录,再根据客户和客服角色的不同发送不同的消息,测试结果如下:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: