乐趣区

基于WebSocket的web端IM即时通讯应用的开发

基于 WebSocket 的 web 端 IM 即时通讯应用的开发


功能列表:
1、Web 端的 IM 应用
2、支持上线、下线、实时在线提醒
3、单聊、群聊的建立
4、普通文字、表情、图片的传输(子定义富文本)
5、单人的顶级提醒,多对话的窗口的提醒
6、调用图灵机器人的自动回复演示
核心技术列表
1、websocket、sockjs、stomp
2、前端展示涉及的 jquery、vue、elementUI、jquerybase64js
3、后端 springboot、jsoup、spring-security、spring-websocket
成果展示:




技术实现说明:
Websocket 部分
web 端的 IM 应用,要想实现两个客户端的通信,必然要通过服务器进行信息的转发。例如 A 要和 B 通信,则应该是 A 先把信息发送给 IM 应用服务器,服务器根据 A 信息中携带的接收者将它再转发给 B,同样 B 到 A 也是这种模式。
而要实现 web 端的实时通讯,websocket 也是其中最好的方式,其他的协议如长轮询、短轮询、iframe 数据、htmlfile 等。
在实际开发中,我们通常使用的是一些别人写好的实时通讯的库,比如 socket.io、sockjs(我们本次使用了他,类似 jquery,对其他即时通讯技术做了封装),他们的原理就是将上面(还有一些其他的如基于 Flash 的 push)的一些技术进行了在客户端和服务端的封装,然后给开发者一个统一调用的接口。这个接口在支持 websocket 的环境下使用 websocket,在不支持它的时候启用上面所讲的一些 hack 技术。
WebSocket 是 HTML5 的一种新通信协议(ws 协议),是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义;与处在应用层的 HTTP 不同,WebSocket 处在 TCP 上非常薄的一层,会将字节流转换为文本 / 二进制消息,因此,对于实际应用来说,WebSocket 的通信形式层级过低,因此,可以在 WebSocket 之上使用 STOMP 协议,来为浏览器 和 server 间的 通信增加适当的消息语义。
STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议。同 HTTP 在 TCP 套接字上添加请求 - 响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;

STOMP 源码 http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js,有兴趣的可以看一下能大致了解其原理和用法。

本例程序核心代码:

<!--TO 创建 socket 连接 并订阅相关频道 -->
var socket = new SockJS('/im-websocket');
stompClient = Stomp.over(socket);
// 设置 stomp 控制台日志为不输出
stompClient.debug=null;
stompClient.connect({}, function (frame) {
       // 相当于连接 ws://localhost:8080/gs-guide-websocket/041/hk5tax0r/websocket hk5tax0r 就是 sessionid
    console.log("正在连接",socket._transport.url);
    // 订阅通用私聊频道 群组也通过这里实现
    stompClient.subscribe('/user/topic/private', function (greeting) {});
    // 订阅用户上线下线的公共频道
    stompClient.subscribe('/topic/userlist', function (greeting) {});
},function errorCallBack (error) {// 连接失败时(服务器响应 ERROR 帧)的回调方法});

数据发送如下:
// 第一个参数对应 controller 的 @MessageMapping 注解 /app 为后台定义的通用前缀
// 第三个参数为内容字符串
stompClient.send(“/app/private”, {}, JSON.stringify(message));// 发送服务器

对应服务端部分

#WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {public List<User> onlineUser=new ArrayList<User>();
  @Autowired
  private SimpMessagingTemplate template;
  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/topic");
    config.setUserDestinationPrefix("/user");
    config.setApplicationDestinationPrefixes("/app");
    config.setCacheLimit(1048576);// 大小 1M
  }
  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    // 注册的 websocket 接入点,前端链接的就是它
    registry.addEndpoint("/im-websocket").withSockJS();}

  @Override
  public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
    // 设置 文件缓冲 大小 1M
// 如不设置文件稍微大一点就报错了
    registration.setMessageSizeLimit(1048576);
    registration.setSendBufferSizeLimit(1048576);
    registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
      @Override
      public WebSocketHandler decorate(final WebSocketHandler handler) {return new WebSocketHandlerDecorator(handler) {
          @Override
          public void afterConnectionEstablished(final WebSocketSession session) throws Exception {******}

          @Override
          public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
            throws Exception {*****}
        };
      }
    });
    super.configureWebSocketTransport(registration);
  }
}


#contoller
@MessageMapping("/private")
public void privatechat(ImMessage message) throws Exception {
*****
template.convertAndSendToUser(message.getReceiver(),"/topic/private",message);
//// 发送其订阅的频道
/// 浏览器客户端,订阅了’/user/topic/private 这条路径,}

其中 @MessageMapping(“bar”) //@MessageMapping 接收客户端消息
另外 @SendTo(“/topic/brocast”) //@SendTo 广播消息出去
@SendToUser(“/topic/greetings”)// 发送对应人。
这两个注解可以用 template.convertAndSendTo、template.convertAndSendToUser 在代码实现。


Spring-security 部分
做了简单的登录验证,登录成功即在系统存有 sessionid。结合上面的订阅,实际链接的后台地址会附加上 sessionid,也就是 httprequest 中的登录授权信息即 javax.security.Principal 会被绑定到 websocket 的 session 中。即可以实现与指定登录人的双向链接。


页面展示部分
页面展示核心应用 vue,vue 不好实现的地方使用的是 jquery。样式采用的 elmentUI。
在开发这个 im 应用测试案例的时候。实现了一些前端效果。核心有用的列到下面,各位也许在后面的学习中能够用到。

1.vue 兼容性的问题
因为本来不是 webpack 开发模式,属于直接引入 js 的普通 HTML 开发,如需要解决 vue 兼容性问题,可以引入
https://cdn.bootcss.com/babel… 以解决。

2、vue 用法(v-model、@click、v-html、v-forv-if,v-bind)的应用,指令、过滤器、全局方法、watch 的使用。其中指令用来实现 div 的默认焦点。全局方法用来代替过滤器,实现实时的消息内容 base64 解码。
3、利用 vue 核心数据的双向绑定,不刷新显示更新。但数据设计为多层的 json 数组数据,当底层数据变化,vue 不能自动检测到变化。需要进行手动检测。
代码 -this.$forceUpdate();

4、关于图片消息的用法
用图片上传按钮上,存在透明的 fileinput 文件,每次上传完,通过 onchange 方法,先检测文件大小、类型后,通过 fileReader 预览。这其中涉及富文本框焦点的问题、和 base64 转码的问题。另外当上传失败通过.val(”)清空 file,这样才能重新选择文件上传并成功触发 onchange 事件(值得了解,试验了半天才行)

var filec=$("#file"+index);
if (filec&&filec.length>0) {var fileList = filec[0].files;
    if(!/image\/\w+/.test(fileList[0].type))            // 判断获取的是否为图片文件
    {this.$message.error('请确保文件为图像文件');
        // 清空 input 可以再次上传并触发 onchange
        filec.val('')
        return false;
    }
    if(fileList[0].size>1048576){this.$message.error('请确保图片不大于 1M');
        // 清空 input 可以再次上传并触发 onchange
        filec.val('')
        return false;
    }
    fileReader.onload = function (e) {
        // 获取文件的 base64 编码
        var base64 = e.target.result
        var image = new Image();
        image.src = base64;
        image.onload = function() {
            // 文件像素过大,调整为稍小的
            var newW="";var newH="";
            if(this.width>this.height&&this.width>200){
                newW=200;
                newH=200/this.width*this.height;
            }
            if(this.width<=this.height&&this.height>200){
                newH=200;
                newW=200/this.height*this.width;
            }
            var h = '<img src=' + base64 + 'width='+newW+'height='+newH+'>';
            _insertimg(h, index)// 插入到富文本对应的位置
        };
    }
    fileReader.readAsDataURL(fileList[0]);

5、关于富文本在指定焦点位置插入数据的问题,后续可以考虑 baidueditor 等成熟产品。
当前富文本主要利用了 html5 的属性 contenteditable 解决的
具体可以查看_insertimg 方法
6、在实现上述富文本的时候,类似插入表情、选择图片的时候,只要点击屏幕,则当前页面焦点即转移,影响实际插入的位置。所以需要设置这些按钮点击的时候屏蔽默认效果。
一个是按钮的 @click.prvent。另外可以通过下面的方法解决

document.addEventListener("mousedown", function(e){if(e.target.id=="emoijT"){e.preventDefault()
    }
}, false);

7、因为当前发送的消息是带 html 标签的富文本信息,为避免传输的问题,将内容进行 base64 转码,消息被接收后再转码回来。
var stompClient = null;
// 防止乱码
$.base64.utf8encode = true;
$.base64.btoa(thisMessage);// 使用插件 base64 编码
// 解码 $.base64.atob(c, true);

8、当前案例不仅实现了多对话窗口,隐藏的对话提醒。也实现了当前人的浏览器标题提醒。

var pageMessage = {
    time: 0,
    title: document.title,
    timer: null,
    // 显示新消息提示
    show: function () {var title = pageMessage.title.replace("【】", "").replace("【新消息】","");
        // 定时器,设置消息切换频率闪烁效果就此产生
        pageMessage.timer = setTimeout(function () {
            pageMessage.time++;
            pageMessage.show();
            if (pageMessage.time % 2 == 0) {document.title = "【新消息】" + title}
            else {document.title = "【】" + title}
            ;
        }, 600);
        return [pageMessage.timer, pageMessage.title];
    },
    // 取消新消息提示 v
    clear: function () {clearTimeout(pageMessage.timer);
        document.title = pageMessage.title;
    }
};

9、关于机器人自动对话,目前使用 jsoup 调用的远程接口,由其返回答案。虽然是免费接口,但是一天不能调用多次。

String url = "http://www.tuling123.com/openapi/api";
// 请填写自己的 key
String userid="454995";
String post = "{\"key\": \"646d321c227045a69253fd07d8703840\",\"info\": \""+message.getContent()+"\",\"userid\":\""+userid+"\"}";
String body = Jsoup.connect(url).method(Connection.Method.POST)
        .requestBody(post)
        .header("Content-Type", "application/json; charset=utf-8")
        .ignoreContentType(true).execute().body();
退出移动版