接上一节的内容。上一节,使用简单JavaEE的注解方法,创建了一个基于Websocket的简易聊天室。在实际开发中,可以直接使用上节的方法。当然,有童鞋会问了,如果我是基于框架开发的,那怎么办?
下面呢,我们一步一步来,构造一个整合了SpringMVC和AngularJS的另一个聊天室吧。
这里,笔者使用的是Spring4.2.3以及AngularJS 1.5.8。
环境的搭建这里不详细描述了,可以在文章的最后下载源码,导入到Eclipse仔细研究。
【服务器端】
这里研究的是SpringMVC框架下的WebSocket服务器的构建方法。从Spring4开始,提供了WebSocket模块的支持。如此,我们可以使用SpringMVC加入WebSocket的支持。
本例使用Spring的注解配置方法,代码如下:
@Configuration
@EnableWebMvc
@EnableWebSocket
@ComponentScan(
basePackages = "lvhb.test.controller",
useDefaultFilters = false,
includeFilters = @ComponentScan.Filter(Controller.class)
)
public class WebServletContextConfiguration extends WebMvcConfigurerAdapter implements WebSocketConfigurer
这里,新加入了一个注解——@EnableWebSocket(没加也没啥大碍),并且实现了WebSocketConfigurer。这里需实现一个注册方法如下:
返回类型 | 方法和描述 |
void | registerWebSocketHandlers(WebSocketHandlerRegistry registry) 注册一个WebSocket处理器 |
本例中,我们注册一个WebSocketHandler,并且加入一个拦截器。(为什么要加拦截器?在WebSocket握手完成前,处理一些必要的准备工作,如:参数的获取,session处理等)
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
{
registry.addHandler(wsHandler(), "/chat").addInterceptors(new HandInterceptor());
}
registerWebSocketHandlers方法中,使用addHandler加入了WebSocket处理器。这里wsHandler()方法实际上是new了一个处理器对象。之后,添加了拦截器,拦截器的代码如下:
/**
* WebSocket的一个拦截器
* 此处,在握手前,获取请求中的nickname参数,并将其写入session的Attribute中,方便WsHandler处理session
* @author lvhb
*
*/
public class HandInterceptor implements HandshakeInterceptor
{
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Exception e)
{}
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler,
Map<String, Object> map) throws Exception
{
if (request instanceof ServletServerHttpRequest) {
HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();
String userName = req.getParameter("nickname");
map.put("WEBSOCKET_USERNAME", userName);
req.getSession().setAttribute("WEBSOCKET_USERNAME", userName);
}
return true;
}
}
拦截器要实现HandshakeInterceptor接口,这里提供了两个方法,一个是在握手前的处理,另一个是在握手后的处理。本例中,我们使用握手前的处理,将request请求转为HttpServletRequest,并从中获取到客户端发送过来的用户昵称。之后,我们将用户名加入session的Attribute内。
之后,我们看重头戏,处理器类的实现,先上代码:
/**
* WebSocket 处理器
* @author lvhb
*
*/
public class WsHandler extends TextWebSocketHandler
{
private static final Logger logger = LogManager.getLogger("WsHandler");
/**
* 用于记录当前连接的客户端Session
*/
private static Map<String, WebSocketSession> clients;
/**
* 用于将JSON映射成对象
*/
private static ObjectMapper mapper = new ObjectMapper();
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception
{
super.handleTextMessage(session, message);
logger.info("onmessage:"+message.getPayload());
//使用ObjectMapper将获取到的json数据转换成对象
ChatMsgModel msgModel = WsHandler.mapper.readValue(message.getPayload(), ChatMsgModel.class);
UserMessage uMsg = new UserMessage(msgModel.getNickname(), msgModel.getText(), LocalDateTime.now());
broadcastMsg(uMsg);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception
{
super.afterConnectionEstablished(session);
if(clients == null)
clients = new HashMap<String, WebSocketSession>();
Map<String, Object> attrs = session.getAttributes();
if(attrs != null)
{
String nickname = attrs.get("WEBSOCKET_USERNAME").toString();
clients.put(nickname, session);
SystemMessage sysMsg = new SystemMessage((nickname.equals("") ? "有人":nickname) + "进入了聊天室", LocalDateTime.now());
broadcastMsg(sysMsg);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception
{
super.afterConnectionClosed(session, status);
if(clients != null && !clients.isEmpty())
{
Map<String, Object> attrs = session.getAttributes();
if(attrs != null)
{
String nickname = attrs.get("WEBSOCKET_USERNAME").toString();
if(clients.containsKey(nickname))
{
removeSession(nickname);
logger.info("ws removed");
SystemMessage sysMsg = new SystemMessage((nickname.equals("") ? "有人":nickname) + "离开了聊天室", LocalDateTime.now());
broadcastMsg(sysMsg);
}
}
}
}
private synchronized void removeSession(String nickname)
{
clients.remove(nickname);
}
/**
* 广播消息
* @param msg
*/
private void broadcastMsg(Message msg)
{
if(clients == null || clients.isEmpty())
return;
try{
for(Map.Entry<String, WebSocketSession> entry:clients.entrySet())
{
WebSocketSession session = entry.getValue();
if(session.isOpen())
session.sendMessage(msg.parseToTextMessage());
}
}catch(IOException ioe)
{
logger.error(ioe.getMessage());
}
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception
{
super.handlePongMessage(session, message);
logger.info("pongmsg");
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception
{
super.handleTransportError(session, exception);
logger.info("errormsg:"+exception.getMessage());
}
}
这里WsHandler类继承自TextWebSocketHandler(Spring推荐的方法,用于处理文本类型的数据处理。当然,您也可以实现
WebSocketHandler
接口)。
我们重写handleTextMessage、afterConnectionEstablished及afterConnectionClosed方法。对应于WebSocket的OnMessage、OnOpen、OnClose。这里具体聊天室业务逻辑处理不做过多介绍。
在每个重写的方法中,都有WebSocketSession对象传入,这个是客户端与服务器端的会话实例。当我们想给客户端发送内容时,可以使用sendMessage方法,将一个实现了WebSocketMessage的对象发送给客户端。当然,这里推荐使用TextMessage的对象。
服务器的内容就介绍到这里。下面简单说一下Angular部分的内容。
【客户端】
Angular部分的内容和上一节的JavaScript操作WebSocket类似。核心代码如下:
/*
* =============== WebSocket处理 ===================
*/
try {
$scope.server = new WebSocket(WSURL+"?nickname="+$scope.nickname);
} catch(error) {
console.log("ws connection error");
}
$scope.server.onopen = function(event){
console.log("ws onopen");
}
$scope.server.onmessage = function(event){
console.log("ws onmessage");
var message = event.data;
appendLine(message);
}
$scope.server.onclose = function(event){
console.log("ws onclose");
}
$scope.server.onerror = function(event){
console.log("ws onerror");
}
/*
* ================= WebSocket =====================
*/
这里注意WebSocket在创建的时候如HTTP连接一样,可以带参数的。所以这里传入了nickname表示聊天人的昵称。之后,在处理onmessage时,appendLine方法为将接受到的数据加入到聊天界面中。
function appendLine(text){
if(angular.isDefined(text)){
$scope.$apply($scope.chatData.push(text));
}
}
这里注意,笔者使用了$scope.$apply()。让数据修改后立即刷新到页面上。
这个例子实现的截图如下。
好,这一节就到这结束了。大家可以下载源码,运行一下,就会明白SpringMVC的WebSocket如何实现。
点我下载源码