springboot websocket

本文使用stomp
STOMP(面向简单文本的消息传递协议)最初是为脚本语言(例如Ruby,Python和Perl)创建的,以连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP可以在任何可靠的双向流网络协议上使用,例如TCP和WebSocket。尽管STOMP是面向文本的协议,但是消息有效负载可以是文本或二进制。
客户端可以使用SEND或SUBSCRIBE命令来发送或订阅消息,以及destination描述消息的内容和应由谁接收的标头。这启用了一种简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的客户端,或者将消息发送到服务器以请求执行某些工作。
当您使用Spring的STOMP支持时,Spring WebSocket应用程序将充当客户端的STOMP代理。消息被路由到@Controller消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅的用户广播消息。您还可以将Spring配置为与专用的STOMP代理(例如RabbitMQ,ActiveMQ等)一起使用,以实际广播消息。在那种情况下,Spring维护与代理的TCP连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的WebSocket客户端。因此,Spring Web应用程序可以依靠基于HTTP的统一安全性,通用验证以及用于消息处理的熟悉的编程模型。
--摘自官网

  1. 依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
  1. 定义一个连接校验,用于记录用户的信息
/**
 * 连接时校验用户信息,并返回重写的Principal
 */
@Component
public class MyHandshakeHandler extends DefaultHandshakeHandler {

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
        if (!(request instanceof ServletServerHttpRequest)) {
            return null;
        }
        ServletServerHttpRequest req = (ServletServerHttpRequest) request;
        //获取请求参数中携带的uid
        String uid = req.getServletRequest().getParameter("uid");
        if(uid == null){
            throw new RuntimeException("未登录");
        }
        return new MyPrincipal(uid);
    }
}
  1. 定义一个记录用户登陆退出,方便观察
/**
 * 用户登录退出操作
 */
@Component
public class MyWebSocketHandler implements WebSocketHandlerDecoratorFactory {

    @Override
    public WebSocketHandler decorate(WebSocketHandler handler) {
        return new WebSocketHandlerDecorator(handler) {
            //用户登录
            @Override
            public void afterConnectionEstablished(WebSocketSession session) throws Exception {
                String uid = session.getPrincipal().getName();
                System.out.println(uid + "登陆");
                super.afterConnectionEstablished(session);
            }

            //用户退出
            @Override
            public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
                String uid = session.getPrincipal().getName();
                System.out.println(uid + "退出");

                super.afterConnectionClosed(session, closeStatus);
            }
        };
    }
}
  1. websocket配置
@Configuration
//开启消息代理,默认使用内置消息代理,也可以选择配置RabbitMQ等
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Autowired
    private MyWebSocketHandler myWebSocketHandler;
    @Autowired
    private MyHandshakeHandler myHandshakeHandler;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //启用/user /topic两个消息前缀,消息发送的前缀,也是前端订阅的前缀
        registry.enableSimpleBroker("/user", "/topic");
        //当使用convertAndSendToUser发送消息时,前端订阅用/user开头。即一对一发送消息,使用/user为前缀订阅
        registry.setUserDestinationPrefix("/user");
        //前端向服务端发送消息的前缀
        registry.setApplicationDestinationPrefixes("/im/");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        //客户端和服务端进行连接的endpoint
        //如果使用移动端开发app,需要/im/conn/websocket连接
        stompEndpointRegistry.addEndpoint("/im/conn")
                .setHandshakeHandler(myHandshakeHandler)//设置连接校验
                .setAllowedOrigins("*")//跨域
                .withSockJS();//开启sockjs
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
        //注册登陆退出
        registry.addDecoratorFactory(myWebSocketHandler);
    }
}
  1. 创建一个消息的实体类
/**
 * 一对一发送的消息
 */
@Data
public class SendMsg implements Serializable {
    //发送消息的用户id
    private String uid;

    //接收消息的用户id
    @NotNull(message = "未选择用户")
    private String toUid;

    //发送的文本消息
    @NotNull(message = "消息不能为空")
    private String content;
}
  1. 创建controller
@RestController
public class ImController {
    //发送消息的模板
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    /**
     * 发送消息,一对一
     * Principal为连接websocket校验时返回的,可以直接在参数中使用
     * 也可以使用@Validated校验参数的合法性
     * 
     * @param msg
     * @param principal
     * @return
     */
    @MessageMapping("/send2user")
    public String send2user(@Validated SendMsg msg, Principal principal) {
        //获取用户的uid
        String uid = principal.getName();
        //设置发送信息的uid
        msg.setUid(uid);
        System.out.println(uid + ":" + msg);
        //发送给订阅/user/{toUid}/msg的用户
        //这里的toUid是接收消息用户的uid
        simpMessagingTemplate.convertAndSendToUser(msg.getToUid(), "msg", msg);
        return "success";
    }

    /**
     * 发送消息,发送给所有订阅/topic/sys的用户
     * 也可以使用@SendTo注解,返回值为发送的消息即可
     *
     * @param msg
     * @return
     */
    @GetMapping("/sendAll")
//    @SendTo("/topic/sys")
    public String sendAll(String msg){
        System.out.println("广播消息:" + msg);
        //如果是群聊,根据传递参数的群聊房间号,动态拼接/topic/{房间号},前端订阅/topic/{房间号}即可
        simpMessagingTemplate.convertAndSend("/topic/sys", msg);
        return "success";
//        return msg;
    }
    
}
  1. html页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <!-- sockjs stomp -->
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script>
        let stompClient = null;
        //连接websocket
        function conn() {
            let uid = $("#uidInput").val();
            //可以在后面直接拼接参数,在MyHandshakeHandler中校验用户uid
            let socket = new SockJS('http://localhost:8080/im/conn?uid=' + uid);
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function () {
                //订阅/user/{uid}/msg这个地址,接收往这个地址发送的消息
                stompClient.subscribe('/user/' + uid + '/msg', function (msg) {
                    //后台返回的信息是实体类,将其转换成json,添加到ul中
                    let msgBody = JSON.parse(msg.body);
                    $("#userMsg").append("<li>" + msgBody.uid +":"+ msgBody.content + "</li>")
                });
                //订阅/topic/sys这个地址,接收往这个地址发送的消息
                stompClient.subscribe('/topic/sys', function (msg) {
                    $("#sysMsg").append("<li>" + msg.body + "</li>")
                });
                //stompClient.subscribe()....多个订阅地址,也可以在外面定义。前提是stompClient已经连接

                //隐藏连接div
                $("#connDiv").hide();
                //显示消息div
                $("#msgDiv").show();
            }, function (err) {
                console.log(err);
            });
        }

        //发送一对一消息
        function send() {
            let content = $("#content").val();
            if (!content) {
                alert("请输入消息");
            }
            let toUid = $("#toUid").val();
            if (!toUid) {
                alert("请输入发送给用户的uid");
            }
            let msg = {"content": content, "toUid": toUid};
            if (!stompClient) {
                alert("未连接");
            }
            //前端发送消息以/im开头,往send2user中发送消息,消息为JSON.stringify(msg)
            stompClient.send("/im/send2user", {}, JSON.stringify(msg));
        }
    </script>
</head>
<body>
<div id="connDiv">
    <input type="text" id="uidInput" placeholder="请输入uid">
    <button onclick="conn()">连接</button>
</div>
<div id="msgDiv" style="display: none">
    <input id="content" type="text" placeholder="消息内容"/>
    <input id="toUid" type="text" placeholder="发送给用户的uid">
    <button onclick="send()">发送</button>
    <br/>
    <label>用户消息:</label>
    <ul id="userMsg"></ul>
    <label>系统消息:</label>
    <ul id="sysMsg"></ul>
</div>
</body>
</html>
  1. 测试

浏览器访问http://localhost:8080/index.html
开启两个页面,分别输入两个不同的uid,这里使用111,222
控制台:

111登陆
222登陆

从111的页面发送消息给222,并从222的页面发送消息给111
控制台:

111:SendMsg(uid=111, toUid=222, content=在吗?)
222:SendMsg(uid=222, toUid=111, content=在的)

从浏览器访问sendAll接口,发送消息给所有订阅/topic/sys的用户
http://localhost:8080/sendAll?msg=大家晚上好
控制台:

广播消息:大家晚上好

示例图:


在这里插入图片描述

项目地址


作者博客

作者公众号


在这里插入图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336