SpringBoot+Netty-socketio实现websocket

socket.io是js实现的,websocket框架,为了解决浏览器不兼容问题而设计
socket.io.js下载地址:https://cdnjs.com/libraries/socket.io
常用的方式是,前端使用socket.io.js,后端使用node.js实现socket.io的接口,可是我们的架构后端使用的是java,所以我使用的是netty-socketio,基于spring-boot实现;

一.pom.xml中添加依赖

        <!-- https://mvnrepository.com/artifact/com.corundumstudio.socketio/netty-socketio -->
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.11</version>
        </dependency>

二.修改SpringBoot启动类

package domain;

import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import domain.util.YmlConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;


/**
 * Spring Boot 应用启动类
 *
 * @author : liangxifeng
 * @date : 2018-1-19
 */
// Spring Boot 应用的标识
@SpringBootApplication
//如果mybatis中service实现类中加入事务注解,需要此处添加该注解
@EnableTransactionManagement
// mapper 接口类扫描包配置
@MapperScan("domain.dao")
public class AdverCenterApplication extends SpringBootServletInitializer {
    //读取配置文件
    @Autowired
    private YmlConfig ymlConfig;
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(AdverCenterApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(AdverCenterApplication.class, args);
    }

    /**
     * 注册netty-socketio服务端
     * @author liangxifeng 2018-07-07
     * @return
     */
    @Bean
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();

        String os = System.getProperty("os.name");
        if(os.toLowerCase().startsWith("win")){   //在本地window环境测试时用localhost
            System.out.println("this is  windows");
            config.setHostname("localhost");
        } else {
            config.setHostname("192.168.9.209");
        }
        config.setPort("9092");

        /*config.setAuthorizationListener(new AuthorizationListener() {//类似过滤器
            @Override
            public boolean isAuthorized(HandshakeData data) {
                //http://localhost:8081?username=test&password=test
                //例如果使用上面的链接进行connect,可以使用如下代码获取用户密码信息,本文不做身份验证
                // String username = data.getSingleUrlParam("username");
                // String password = data.getSingleUrlParam("password");
                return true;
            }
        });*/

        final SocketIOServer server = new SocketIOServer(config);
        return server;
    }

    /**
     * tomcat启动时候,扫码socket服务器并注册
     * @param socketServer
     * @return
     */
    @Bean
    public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
        return new SpringAnnotationScanner(socketServer);
    }
}

三.在项目服务启动的时候启动socket.io服务, 新增ServerRunner.java

package domain.websocketio;

import com.corundumstudio.socketio.SocketIOServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 在项目服务启动的时候启动socket.io服务
 * @author liangxifeng 2018-07-07
 */
@Component
@Order(value=1)
@Slf4j
public class ServerRunner implements CommandLineRunner {

    private final SocketIOServer server;


    @Autowired
    public ServerRunner(SocketIOServer server) {
        this.server = server;
    }

    @Override
    public void run(String... args) throws Exception {
        server.start();
        log.info("socket.io启动成功!");
    }

}

四.接收前台用户信息类 MessageInfo.java

package domain.websocketio;

import lombok.ToString;
import org.springframework.stereotype.Component;

/**
 * 接收前台用户信息类
 * @author liangxifeng 2018-07-07
 */
@Component
@ToString
public class MessageInfo {

    String msgContent;

    public String getMsgContent() {
        return this.msgContent;
    }

    public void setMsgContent(String msgContent) {
        this.msgContent = msgContent;

    }
}

消息事件,作为后端与前台交互MessageEventHandler.java

package domain.websocketio;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import domain.websocket.MyWebSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 消息事件,作为后端与前台交互
 * @authoer liangxifeng 2018-07-07
 */
@Component
public class MessageEventHandler {
    public static SocketIOServer socketIoServer;
    static ArrayList<UUID> listClient = new ArrayList<UUID>();
    static final int limitSeconds = 60;
    //线程安全的map
    public static ConcurrentHashMap<String,SocketIOClient> webSocketMap = new ConcurrentHashMap<String, SocketIOClient>();

    @Autowired
    public MessageEventHandler(SocketIOServer server) {
        this.socketIoServer = server;
    }

    /**
     * 客户端连接的时候触发,前端js触发:socket = io.connect("http://192.168.9.209:9092");
     * @param client
     */
    @OnConnect
    public void onConnect(SocketIOClient client) {
        String mac = client.getHandshakeData().getSingleUrlParam("mac");
        listClient.add(client.getSessionId());
        //以mac地址为key,SocketIOClient 为value存入map,后续可以指定mac地址向客户端发送消息
        webSocketMap.put(mac,client);
        //socketIoServer.getClient(client.getSessionId()).sendEvent("message", "back data");
        System.out.println("客户端:" + client.getSessionId() + "已连接,mac="+mac);
    }

    /**
     * 客户端关闭连接时触发:前端js触发:socket.disconnect();
     * @param client
     */
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        System.out.println("客户端:" + client.getSessionId() + "断开连接");
    }

    /**
     * 自定义消息事件,客户端js触发:socket.emit('messageevent', {msgContent: msg}); 时触发
     * 前端js的 socket.emit("事件名","参数数据")方法,是触发后端自定义消息事件的时候使用的,
     * 前端js的 socket.on("事件名",匿名函数(服务器向客户端发送的数据))为监听服务器端的事件
     * @param client 客户端信息
     * @param request 请求信息
     * @param data 客户端发送数据{msgContent: msg}
     */
    @OnEvent(value = "messageevent")
    public void onEvent(SocketIOClient client, AckRequest request, MessageInfo data) {
        System.out.println("发来消息:" + data);
        //服务器端向该客户端发送消息
        //socketIoServer.getClient(client.getSessionId()).sendEvent("messageevent", "你好 data");
        client.sendEvent("messageevent","我是服务器都安发送的信息");
    }

    public static void sendBuyLogEvent() {   //这里就是向客户端推消息了
        //String dateTime = new DateTime().toString("hh:mm:ss");

        for (UUID clientId : listClient) {
            if (socketIoServer.getClient(clientId) == null) continue;
            socketIoServer.getClient(clientId).sendEvent("enewbuy", "当前时间", 1);
        }

    }
}

五.目录结构

image.png

六.html内容

<!doctype html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>websocket-java-socketio</title>
  <script src="/static/js/socket.io.js"></script></script>
 </head>
<body>
<h1>Socket.io Test</h1>
<div><p id="status">Waiting for input</p></div>
<div><p id="message">hello world!</p></div>
<button id="connect" onClick='connect()'/>Connect</button>
<button id="disconnect" onClick='disconnect()'>Disconnect</button>
<button id="send" onClick='send()'/>Send Message</button>
</body>

<script type="text/javascript">

/**
* 前端js的 socket.emit("事件名","参数数据")方法,是触发后端自定义消息事件的时候使用的,
* 前端js的 socket.on("事件名",匿名函数(服务器向客户端发送的数据))为监听服务器端的事件
**/
    var socket = io.connect("http://192.168.9.209:9092?mac=2"); 
    var firstconnect = true;
 
function connect() {
    if(firstconnect) {

        //socket.on('reconnect', function(){ status_update("Reconnected to Server"); });
        //socket.on('reconnecting', function( nextRetry ){ status_update("Reconnecting in "
        //+ nextRetry + " seconds"); });
        //socket.on('reconnect_failed', function(){ message("Reconnect Failed"); });
        //firstconnect = false;
    } else {
        socket.socket.reconnect();
    }
}

//监听服务器连接事件
socket.on('connect', function(){ status_update("Connected to Server"); });
//监听服务器关闭服务事件
socket.on('disconnect', function(){ status_update("Disconnected from Server"); });
//监听服务器端发送消息事件
    socket.on('messageevent', function(data) { 
      message(data)
      //console.log("服务器发送的消息是:"+data); 
    }); 

//断开连接
function disconnect() {
    socket.disconnect();
}
 
function message(data) {
    document.getElementById('message').innerHTML = "Server says: " + data;
}
 
function status_update(txt){
    document.getElementById('status').innerHTML = txt;
}
 
function esc(msg){
    return msg.replace(/</g, '<').replace(/>/g, '>');
}
//点击发送消息触发 
function send() {   
    //console.log("点击了发送消息,开始向服务器发送消息")
            var msg = "我很好的,是的."; 
            socket.emit('messageevent', {msgContent: msg});  
}; 
</script>
</html>

七.测试

  • 访问浏览器


    socketio-1.jpg
  • 服务器端日志内容:


    socketio-2.jpg

服务器指定mac地址用户向客户端发送消息

package domain.controller;

import com.corundumstudio.socketio.SocketIOClient;
import domain.domain.DomainResponse;
import domain.domain.Exhibition;
import domain.service.interfaces.exhibition.InsertExhibitionService;
import domain.websocket.PayWebSocket;
import domain.websocketio.MessageEventHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/exhibiton")
public class ExhibitonController {
    @Autowired
    private InsertExhibitionService insertExhibitionService;
    @GetMapping(value = "/sendMsg/{mac}")
    /**
     * 指定某个websocket发送从服务器发送到浏览器,服务器主动推送
     */
    private DomainResponse testSendMsg(@PathVariable("mac") String mac ) throws  Exception{
        DomainResponse msg = new DomainResponse(Integer.parseInt(mac),mac,"指定给"+mac+"用户发消息");
        PayWebSocket payWebSocket = PayWebSocket.payWebSocketMap.get(mac);
        if(payWebSocket == null)
        {
            return new DomainResponse(0,"没有该用户",0);
        }
        payWebSocket.sendMessage(msg);
        return msg;
    }

    /**
     * web socketio方式指定用户发送消息
     * @param mac
     * @return
     */
    @GetMapping(value = "/socketIo/{mac}")
    private DomainResponse socketIoTest(@PathVariable("mac") String mac){
        SocketIOClient client = MessageEventHandler.webSocketMap.get(mac);
        DomainResponse msg = new DomainResponse(Integer.parseInt(mac),mac,"指定给"+mac+"用户发消息");
        client.sendEvent("messageevent","haha======++");
        return msg;
    }
}

测试

  • 指定mac地址为2的用户发送消息


    image.png
  • 客户端会收到消息


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

推荐阅读更多精彩内容