完成环境搭建之后,正式开始搭建服务器端websocket程序
WebSocket简介
WebSocket用于在Web浏览器和服务器之间进行任意的双向数据传输的一种技术。WebSocket协议基于TCP协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。其目的是在WebSocket应用和WebSocket服务器进行频繁双向通信时,可以使服务器避免打开多个HTTP连接进行工作来节约资源,提高了工作效率和资源利用率。传送门
搭建WebSocket服务
- 新建普通的java类,这里我们起名叫P2PChatRoom
- 使用注解配置url映射
@ServerEndpoint(value =”/chatRoom/{username}”)
花括号里面代表的是在使用ws协议连接服务器时要传入的参数,因为我们要实现的是聊天室功能,因此我们需要对在线的某个特定用户进行双向连接,也就是说我们要使用到这个username参数。 - 实现必需的四个自定义方法,但是返回类型必须为void
OnOpen:建立ws连接时调用的方法,在这里可以使用@PathParam获取参数值
OnMessage(Session session, String msg):服务器接收消息时调用的方法
OnError:连接出现异常时调用的方法,这里我们一般打印错误日志及错误原因
OnClose:连接关闭时调用此方法。
所以这个时候代码应该是这个样子的:
package cn.zipple.ws;
import cn.zipple.util.Log;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/**
* Created by zipple on 2017/11/20.
* demo聊天室
*/
@ServerEndpoint(value ="/chatRoom/{username}")
public class P2PChatRoom {
public P2PChatRoom(){
Log.info("---------------启动webSocket P2PChatRoom聊天室-----------------------");
}
@OnOpen
public void onOpen(@PathParam("username")String username, Session session){
}
@OnMessage
public void onMessage(Session session, String msg){
}
@OnError
public void onError(Session session, Throwable throwable){
}
@OnClose
public void onClose(Session session, CloseReason reason){
}
}
使用Log4j打印日志
在上面的构造方法中,我们使用了Log类的info方法,实际上这是我们在util包中自定义的Log4j类:
package cn.zipple.util;
import org.apache.log4j.Logger;
/**
* Created by zipple on 2017/11/13.
* log4j日志
*/
public class Log {
private static Logger log = Logger.getLogger(Log.class);
public static void info(String content){
log.info(content);
}
}
为了使用Log4j为我们提供的打印日志的方法,我们还需要在src根目录下创建log4j.properties文件,文件内容如下:
log4j.rootCategory = INFO, stdout, file
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n
log4j.appender.file = org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File = ./demo_log4j.log
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %n%-d{yyyy-MM-dd HH:mm:ss}%n[%p]-[Thread: %t]-[%C.%M()]: %m%n
log4j.appender.file.Threshold = INFO
log4j.appender.file.DatePattern = '.'yyyy-MM-dd
添加在线列表
用户在进入聊天室的时候,应该要明确的知道当前聊天室应该有哪些人在线。为此,我们应该要在每一个用户请求ws连接的时候在全局静态变量中保存当前用户对象(也就是本文 P2PChatRoom实例)。为了更加方便的发送消息,我们一并将对象的session(WebSocket Session 用于发送消息),httpSession保存下来。
至于为什么要保存httpSession,我们后文再提。
与此同时,我们应该还要保存一些更加重要的对象。
因为毕竟这是一个点对点的即时聊天程序。
也就是说系统应该在当前用户请求连接ws连接的时候判断,是否携带有与目标用户通信的请求。如果有,则建立双方的连接:给当前P2PChatRoom实例保存待通信对象的username和session。
为此,我们为P2PChatRoom添加如下成员变量,注意构造set,get方法:
private Session session;
private HttpSession httpSession;
private static CopyOnWriteArraySet<P2PChatRoom> P2PChatRoomSet = new CopyOnWriteArraySet<>();//保证线程安全
private Session targetSession;//目标session
private String targetName;//目标name
于是,我们可以在onOpen方法中实现上面的需求,在代码中我们可以这样写:
(关于获取HttpSession的步骤以及可能会遇到的异常,点此获取更多详细信息)
@OnOpen
public void onOpen(@PathParam("username")String username, Session session, EndpointConfig config){
this.setSession(session);
HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.setHttpSession(httpSession);
P2PChatRoomSet.add(this);
//给所有的用户发送上线消息
try {
send2All("***上线");
} catch (Exception e) {
e.printStackTrace();
}
/**
* 将消息发送给所有人
* @param msg 消息
*/
public void send2All(String msg) throws IOException, EncodeException {
//发送在线列表
for (P2PChatRoom p2pChatRoom: P2PChatRoomSet){
p2pChatRoom.getSession().getBasicRemote().sendText(msg);
}
}
但是在上面的代码中,我们又发现了一个问题,我们并不能获取到用户的姓名信息,也就是说我们在给所有在线用户广播上线信息时并不知道到底是谁上线了。
解决这个办法的方案就是使用struts2登录之后保存在httpSession中的用户对象实例。
好的,我们暂时跳过这部分内容。
那么我们如何发送在线列表呢—这里为了方便起见,我们暂时把所有的session发送到前端。
我们在onOpen方法中调用send2All方法的后面继续添加以下内容:
//当有新用户连接服务器时,将在线列表发送给所有在线用户
for (P2PChatRoom p2pChatRoom: P2PChatRoomSet){
p2pChatRoom.getSession().getBasicRemote().sendObject(P2PChatRoomSet);
}
添加上述代码以后,idea提示我们需要加载编码器(因为我们使用了sendObject方法)
在encoder包中,我们添加ServerEncoder类:
package cn.zipple.encoder;
import cn.zipple.util.Log;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import java.util.Collection;
/**
* Created by zipple on 2017/11/14.
* websocket 对象实体解码器
*/
public class ServerEncoder implements Encoder.Text<Object> {
@Override
public String encode(Object chatRecord) throws EncodeException {
//将chatRecord转化成json格式
String result;
if (chatRecord instanceof Collection){//如果是集合类型按照数组转化json
result = JSONArray.fromObject(chatRecord).toString();
}else{
result = JSONObject.fromObject(chatRecord).toString();
}
Log.info("对象转化json:"+result);
return result;
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
为了使这个编码器起作用,我们还需要修改P2PChatRoom中的第一行注解 @ServerEndpoint(value =”/chatRoom/{username}”,configurator=HttpSessionWSHelper.class,encoders = {ServerEncoder.class})
如有bug,欢迎留言讨论