基于websocket协议的即时通讯webapp(摘自本人毕业论文)
即时通讯实现
功能结构图
Spring WebSocket配置
本软件的即时通讯技术采用了WebSocket协议,因为从spring4.0的版本才开始支持WebSocket,所以本软件服务端的spring版本是4.2。第一步,先配置配置WebSocket入口,允许访问的域、注册Handler和拦截器,WebSocket的访问权限是允许所有,访问路径是/wsim.do,根据这个入口来与服务端建立tcp连接通道。核心代码如下:
<bean id="websocket" class="com.wzc.im.websocket.IMWebSocketHander" />
<websocket:handlers allowed-origins="*">
<websocket:mapping path="/wsim.do" handler="websocket" />
<websocket:handshake-interceptors>
<bean class="com.wzc.im.websocket.IMHandshakeInterceptor" />
</websocket:handshake-interceptors>
</websocket:handlers>
WebSocket接口的实现
根据WebSocket的API,我们要实现两个接口类HandshakeInterceptor和WebSocketHandler。
HandshakeInterceptor的实现类,需实现了他的两个方法beforeHandshake和afterHandshake,这个两个方法是客户端与服务器初次进行握手连接的方法,初次握手连接前,连接用户会传过来其用户id,当握手连接成功会用此id与这个用户的连接进行绑定,作为这个链接的唯一标识。
public class IMHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse,
WebSocketHandler webSocketHandler, Map<String, Object> map){
if (request instanceof ServletServerHttpRequest) {
String imkey=request.getURI().getQuery().split("=")[1];
if(imkey==null){
System.out.println("imkey为空");
return false;
}
map.put("imkey", imkey);
}
return true;
}
// 初次握手访问后
public void afterHandshake(ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse,WebSocketHandler webSocketHandler, Exception e) {
}
}
WebSocketHandler的实现类要分别实现他的afterConnectionEstablished(初次链接成功执行的函数)、handleTransportError(连接出错是执行的函数)、afterConnectionClosed(连接关闭时执行的函数)、supportsPartialMessages、handleMessage(接受到消息的处理函数)。
public class IMWebSocketHander implements WebSocketHandler {
private Logger logger = Logger.getLogger(IMWebSocketHander.class);
private Map<String, WebSocketSession> socketsessions = new HashMap<String, WebSocketSession>();
private Gson gson =new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
@Autowired
private IGroupService groupService;
@Autowired
private IUserService userService;
@Autowired
private IMessageService messageService;
// 初次链接成功执行
public void afterConnectionEstablished(WebSocketSession session ) throws Exception {
String imkey = (String) session.getAttributes().get("imkey");
if(socketsessions.containsKey(imkey)){
socketsessions.get(imkey).close();
}
socketsessions.put(imkey,session);
logger.info("链接成功,当前在线人数:"+socketsessions.size());
}
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
if (webSocketSession.isOpen()) {
webSocketSession.close();
}
socketsessions.remove((String) webSocketSession.getAttributes().get("imkey"));
logger.info("链接出错,当前在线人数:"+socketsessions.size());
}
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
socketsessions.remove((String) webSocketSession.getAttributes().get("imkey"));
logger.info("链接关闭,当前在线人数:"+socketsessions.size());
}
public boolean supportsPartialMessages() { return false;}
// 接受消息处理消息
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage)throws Exception {
ImMessage message = gson.fromJson((String) webSocketMessage.getPayload(), ImMessage.class);
message.setMid(Imt.getUUID());
if(messageService.insert(message)){
if(message.getTargettype().equals("1")){//单聊
sendMessageToUser(message,message.getTargetid()+"");
}else if(message.getTargettype().equals("2")){//群聊
sendMessageToGroup(message);
}else if(message.getTargettype().equals("3")){//所有在线人
sendMessageToAll(message,webSocketSession);
}
}
}
/**
* 给所有在线人员发消息(通知)
* @param message
* @param activeSession
*/
public void sendMessageToAll(ImMessage msg,WebSocketSession activeSession) {
for (WebSocketSession user : socketsessions.values()) {
try {
if (user!=activeSession&&user.isOpen()) {
user.sendMessage(new TextMessage(gson.toJson(msg)));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 给指定用户发消息(单聊)
*/
public void sendMessageToUser(ImMessage msg,String tid) {
if(socketsessions.containsKey(tid)&&socketsessions.get(tid).isOpen()){
try {
socketsessions.get(tid).sendMessage(new TextMessage(gson.toJson(msg)));
} catch (IOException e) {
System.out.println("消息发送失败"+e);
}
}else{
int uid = Integer.parseInt(tid);
ImUser user =userService.selectById(uid);
String idstr =user.getOfflinelogs();
if(idstr!=null&&!idstr.equals("")){
user.setOfflinelogs(idstr+","+msg.getMid());
}else{
user.setOfflinelogs(msg.getMid());
}
userService.update(user);
}
}
/**
* 给指定群组用户发消息(群聊)
*/
public void sendMessageToGroup(ImMessage msg){
String str =groupService.selectById(msg.getTargetid()).getMembers();
str = Imt.removeArraychild(str, msg.getFromid()+"");
if(str.indexOf(",")>-1){
String[] tids =str.split(",");
for(String tid:tids){
System.out.println(tid+"=="+msg.getFromid());
sendMessageToUser(msg,tid);
}
}
}
}
通讯数据交互流程
根据本软件需求分析,即时通讯需支持单聊和群聊,因此需自己定义相关的处理函数,其原理是,服务端判断根据客户端传输过来的消息的目标类型如果目标类型为单聊,那么则根据消息的目标id,判断当前连接中是否含有此标识的连接,如果有,服务端将通过这条连接将消息转发给用户,如果不存在该连接,则说明该用户当前为在线,则系统会自动将此条消息存入到目标用户的离线信息数据中,等待用户的下次登陆,然后获取到此条消息,自此完成单聊的实现。然后如果目标类型为群组类型,那么此条消息的目标id即为群组的id,系统会根据此id从数据库的群组表中查找到相应的群组信息,群组信息中包含有该群组的所有成员,然后拿出这些成员的id,分别对每个进行如单聊一般的处理,这样即完成的群聊的实现。通讯流程如图:
用户登陆成功后,软件会将当前登录用户的账号(即id)写在请求的路径上去请求开启websock连接。WebSocket连接有四个处理函数分别是onopen(服务器连接成功是执行一次)、onmessage(当接收到服务端发来的消息是执行的函数)、onclose(服务器连接断开时执行的函数)和onerror(与服务器的连接出错时执行的函数),这些函数完成了整个即时通讯模块的成功实现。核心代码如下:
var wsurl='ws://'+sip.substring(7,sip.length)+'/wsim.do?id='+user.id;
function openWebSocket(){
ws = new WebSocket(wsurl);
ws.onmessage = function(evnt) {receivemsg(evnt.data);};
ws.onopen = function(evnt){ mui.toast("服务器连接成功");};
ws.onclose = function(evnt){ mui.toast("服务器连接断开,正在尝试重新连接...");ws=null;reconnection();}
ws.onerror = function(evnt){ mui.toast("服务器连接出错,正在尝试重新连接...");
ws=null;reconnection();}
}
重要的一点,因为WebSocket的连接是依托当前的HTTML页面的,所以当本页面关闭或刷新,那么连接将会断开,因此需要保证本页面一直存在且不被刷新,因为本软件采用的是mui框架,其打开的页面是一个webview,因此只要不关掉此webview,即便其被隐藏该连接也不会被断开,保证了通道的持续稳定。当接受到从服务端传来的消息时会使用跨页面传值的方式将数据传输到相应的显示页面。
该软件其他模块的使用截图