Springboot 、Netty-SocketIO、Redission 集群实现web消息通讯

说明

最近因需要研究了一下消息推送,也是在网上参考其他大佬的文章以后进行的尝试,特此记录探索的小实践,也是为了后面需要时参考参考, 如果大家发现不妥之处欢迎指正。


目标

  • 需要负载均衡,部署多服务,业务场景触发处理消息提醒,实现业务端出发消息推送
    1. 后台给指定用户发送消息
    2. 后台给指定群体进行发送消息

关于

  • 使用默认namespace,后面有时间再研究自定义namespace
  • 关于room, 目前打算将用户id作为room进行管理,方便做点对点消息推送
    1. 在创建连接时,创建用户id房间并加入
    2. 如果有群发的业务场景,如给某个部门发消息,可以继续加入部门编号命名的房间

实现方案介绍

以2台机器部署服务端为例

一、 服务端

1. 技术框架

  • Springboot
  • Redission(实现集群)
  • Netty-SocketIO

2. 代码实现

2.1 maven配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>websocket-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.25</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.21.3</version>
        </dependency>
   </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
2.2 配置文件
  • application.yml
server:
  port: 8080
websocket:
  socket-port: 8090
  maxFramePayloadLength: 1048576
  maxHttpContentLength: 1048576
  upgradeTimeout: 1000000
  pingTimeout: 6000000
  pingInterval: 25000

redisson:
  address: redis://127.0.0.1:6379
  password:
  database: 0
  • application-s1.yml
server:
  port: 8080
websocket:
  socket-port: 8090
  • application-s2.yml
server:
  port: 9080
websocket:
  socket-port: 9090
2.3 启动调试
  • IDEA 在启动时添加启动参数 --spring.profiles.active=s1--spring.profiles.active=s1
2.4 关键代码
  • springboot 跨域配置 CorsConfig.java,允许跨域,可以自行指定跨域路径,socketIO, 路径前缀为/socket.io
package com.ddw.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")//项目中的所有接口都支持跨域
                .allowedOriginPatterns("*")//所有地址都可以访问,也可以配置具体地址
                .allowCredentials(true)
                .allowedMethods("*")//"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"
                .maxAge(3600);// 跨域允许时间
    }
}
  • Properties配置类 RedissonProperties.javaWebSocketProperties读取socketIO、和redisson配置
package com.ddw.chat.controller.socket.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
    private int timeout = 3000;
    private String address;
    private String password;
    private int connectionPoolSize = 5;
    private int connectionMinimumIdleSize = 2;
    private int slaveConnectionPoolSize = 250;
    private int masterConnectionPoolSize = 250;
    private String[] sentinelAddresses;
    private String masterName;
    private int database = 1;
}
package com.ddw.chat.controller.socket.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "websocket")
public class WebSocketProperties {
    /**
     * socket 地址
     */
    private String host;
    /**
     * socket 端口
     */
    private Integer socketPort;
    /**
     * 最大每帧处理数据的长度
     */
    private String maxFramePayloadLength;
    /**
     * http交互最大内容长度
     */
    private String maxHttpContentLength;
    /**
     * Ping 心跳间隔(毫秒)
     */
    private Integer pingInterval;
    /**
     * Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
     */
    private Integer pingTimeout;
    /**
     * 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
     */
    private Integer upgradeTimeout;
}
  • Configuration配置:RedissonConfig.javaSocketIoConfig.java
package com.ddw.chat.controller.socket.config;

import com.ddw.chat.controller.socket.properties.RedissonProperties;
import jodd.util.StringUtil;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Autowired
    private RedissonProperties conf;

    @Bean(name="redission",destroyMethod="shutdown")
    public RedissonClient redission() {
        Config config = new Config();
        config.setCodec(new org.redisson.client.codec.StringCodec());
        if(conf.getSentinelAddresses()!=null && conf.getSentinelAddresses().length>0){
            config.useSentinelServers()
                    .setMasterName(conf.getMasterName()).addSentinelAddress(conf.getSentinelAddresses())
                    .setPassword(conf.getPassword()).setDatabase(conf.getDatabase());
        }else{
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(conf.getAddress())
                    .setTimeout(conf.getTimeout())
                    .setConnectionPoolSize(conf.getConnectionPoolSize())
                    .setConnectionMinimumIdleSize(conf.getConnectionMinimumIdleSize())
                    .setDatabase(conf.getDatabase());
            if(StringUtil.isNotBlank(conf.getPassword())) {
                serverConfig.setPassword(conf.getPassword());
            }
        }
        return Redisson.create(config);
    }
}
package com.ddw.chat.controller.socket.config;

import com.corundumstudio.socketio.AckMode;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import com.corundumstudio.socketio.store.RedissonStoreFactory;
import com.corundumstudio.socketio.store.pubsub.PubSubStore;
import com.ddw.chat.controller.socket.CustomRedissonStoreFactory;
import com.ddw.chat.controller.socket.properties.WebSocketProperties;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class SocketIoConfig {
    @Autowired
    private RedissonClient redisson;

    @Autowired
    private WebSocketProperties webSocketProperties;

    private RedissonStoreFactory createRedissonStoreFactory(){
        log.info("创建 RedissonStoreFactory 开始");
//        RedissonStoreFactory redissonStoreFactory = new RedissonStoreFactory(redisson);
        RedissonStoreFactory redissonStoreFactory = new CustomRedissonStoreFactory(redisson);
        log.info("创建 RedissonStoreFactory 结束");
        return redissonStoreFactory;
    }

    @Bean
    public SocketIOServer getSocketIOServer(){
        log.info("创建 SocketIOServer 开始");
        //Sokcket配置 参考 jdk
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setTcpNoDelay(true);
        //在默认情况下,当调用close关闭socke的使用,close会立即返回,
        // 但是,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回.
        socketConfig.setSoLinger(0);
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        // 设置监听端口
        config.setPort(webSocketProperties.getSocketPort());
        // 协议升级超时时间(毫秒),默认10000。HTTP握手升级为ws协议超时时间
        config.setUpgradeTimeout(webSocketProperties.getUpgradeTimeout());
        // Ping消息间隔(毫秒),默认25000。客户端向服务器发送一条心跳消息间隔
        config.setPingInterval(webSocketProperties.getPingInterval());
        // Ping消息超时时间(毫秒),默认60000,这个时间间隔内没有接收到心跳消息就会发送超时事件
        config.setPingTimeout(webSocketProperties.getPingTimeout());
        // 推荐使用redisson
        config.setStoreFactory(createRedissonStoreFactory());
        //异常处理
//        config.setExceptionListener(nettyExceptionListener);
        //手动确认
        config.setAckMode(AckMode.MANUAL);
        // 握手协议参数使用JWT的Token认证方案 认证方案
        config.setAuthorizationListener(data -> {
           /* HttpHeaders httpHeaders = data.getHttpHeaders();
            String token = httpHeaders.get("Authorization");*/
            return  true;
        });
        socketConfig.setTcpKeepAlive(true);
        config.setSocketConfig(socketConfig);
        log.info("创建 SocketIOServer 结束");
        return new SocketIOServer(config);
    }

    /**
     * spring
     * @param socketServer
     * @return
     */
    @Bean
    public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
        return new SpringAnnotationScanner(socketServer);
    }

    @Bean
    public PubSubStore pubSubStore(SocketIOServer socketServer) {
        return socketServer.getConfiguration().getStoreFactory().pubSubStore();
    }
}
  • 事件处理器:NettySocketEventHandler.java 处理客户端发送事件
package com.ddw.chat.controller.socket;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.HandshakeData;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class NettySocketEventHandler {

    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        log.info("--------------------客户端已断开连接--------------------");
        client.disconnect();
    }

    @OnConnect
    public void onConnect(SocketIOClient client) {
        HandshakeData handshakeData = client.getHandshakeData();
        String room = handshakeData.getSingleUrlParam("room");
        client.joinRoom(room);
        //存储SocketIOClient,用于向不同客户端发送消息
        log.info("-客户端[{}]连接成功", room);
    }

    @OnEvent(value = "msg_event")
    public void onMessage(SocketIOClient client, AckRequest ackRequest, String data) throws Exception {
        client.sendEvent("msg_event", "收到消息了!,你发送的内容为:" + data);
    }
}
  • 启动类 SocketServerRunner.java
package com.ddw.chat.controller.socket;

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;

@Component
@Order(1)
@Slf4j
public class SocketServerRunner implements CommandLineRunner {
    @Autowired
    private SocketIOServer socketIOServer;

    @Override
    public void run(String... args) throws Exception {
        log.info("socketIOServer 启动");
        socketIOServer.start();
    }
}
  • 自定义StoreFactory:CustomRedissonStoreFactory.java ,此处代码可以根据自己需求自行修改,这里是从 源码里粘贴过来的
package com.ddw.chat.controller.socket;

import com.corundumstudio.socketio.handler.AuthorizeHandler;
import com.corundumstudio.socketio.namespace.Namespace;
import com.corundumstudio.socketio.namespace.NamespacesHub;
import com.corundumstudio.socketio.protocol.JsonSupport;
import com.corundumstudio.socketio.store.RedissonStore;
import com.corundumstudio.socketio.store.RedissonStoreFactory;
import com.corundumstudio.socketio.store.Store;
import com.corundumstudio.socketio.store.pubsub.*;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;

import java.util.Set;
import java.util.UUID;

@Slf4j
public class CustomRedissonStoreFactory extends RedissonStoreFactory {
    private final RedissonClient redisClient;
    private final RedissonClient redisPub;
    private final RedissonClient redisSub;
    private final PubSubStore pubSubStore;

    public CustomRedissonStoreFactory(RedissonClient redisson) {
        this.redisClient = redisson;
        this.redisPub = redisson;
        this.redisSub = redisson;
        this.pubSubStore = new CustomRedissonPubSubStore(this.redisPub, this.redisSub, this.getNodeId());
    }

    public Store createStore(UUID sessionId) {
        return new RedissonStore(sessionId, this.redisClient);
    }

    public PubSubStore pubSubStore() {
        return this.pubSubStore;
    }

    public void shutdown() {
        this.redisClient.shutdown();
        this.redisPub.shutdown();
        this.redisSub.shutdown();
    }

    @Override
    public void init(NamespacesHub namespacesHub, AuthorizeHandler authorizeHandler, JsonSupport jsonSupport) {
        this.pubSubStore().subscribe(PubSubType.DISCONNECT, msg -> log.debug("{} sessionId: {}", PubSubType.DISCONNECT, msg.getSessionId()), DisconnectMessage.class);
        this.pubSubStore().subscribe(PubSubType.CONNECT, msg -> {
            authorizeHandler.connect(msg.getSessionId());
            log.debug("{} sessionId: {}", PubSubType.CONNECT, msg.getSessionId());
        }, ConnectMessage.class);
        this.pubSubStore().subscribe(PubSubType.DISPATCH, msg -> {
			//TODO 重点关注,订阅消息分发逻辑
            String name = msg.getRoom();
            Namespace n = namespacesHub.get(msg.getNamespace());
            if (n != null) {
                n.dispatch(name, msg.getPacket());
            }
            log.debug("{} packet: {}", PubSubType.DISPATCH, msg.getPacket());
        }, DispatchMessage.class);
        this.pubSubStore().subscribe(PubSubType.JOIN, msg -> {
            String name = msg.getRoom();
            Namespace n = namespacesHub.get(msg.getNamespace());
            if (n != null) {
                n.join(name, msg.getSessionId());
            }
            log.debug("{} sessionId: {}", PubSubType.JOIN, msg.getSessionId());
        }, JoinLeaveMessage.class);
        this.pubSubStore().subscribe(PubSubType.BULK_JOIN, msg -> {
            Set<String> rooms = msg.getRooms();

            for (String room : rooms) {
                Namespace n = namespacesHub.get(msg.getNamespace());
                if (n != null) {
                    n.join(room, msg.getSessionId());
                }
            }
            log.debug("{} sessionId: {}", PubSubType.BULK_JOIN, msg.getSessionId());
        }, BulkJoinLeaveMessage.class);
        this.pubSubStore().subscribe(PubSubType.LEAVE, new PubSubListener<JoinLeaveMessage>() {
            public void onMessage(JoinLeaveMessage msg) {
                String name = msg.getRoom();
                Namespace n = namespacesHub.get(msg.getNamespace());
                if (n != null) {
                    n.leave(name, msg.getSessionId());
                }
                log.debug("{} sessionId: {}", PubSubType.LEAVE, msg.getSessionId());
            }
        }, JoinLeaveMessage.class);
        this.pubSubStore().subscribe(PubSubType.BULK_LEAVE, new PubSubListener<BulkJoinLeaveMessage>() {
            public void onMessage(BulkJoinLeaveMessage msg) {
                Set<String> rooms = msg.getRooms();

                for (String room : rooms) {
                    Namespace n = namespacesHub.get(msg.getNamespace());
                    if (n != null) {
                        n.leave(room, msg.getSessionId());
                    }
                }
                log.debug("{} sessionId: {}", PubSubType.BULK_LEAVE, msg.getSessionId());
            }
        }, BulkJoinLeaveMessage.class);

    }
}
  • 自定义 PubSubStore:CustomRedissonPubSubStore.java,这里只改动部分代码,其余代码也是从源码中粘贴的,同样根据自己的需求进行修改
package com.ddw.chat.controller.socket;

import com.corundumstudio.socketio.store.RedissonPubSubStore;
import com.corundumstudio.socketio.store.pubsub.PubSubListener;
import com.corundumstudio.socketio.store.pubsub.PubSubMessage;
import com.corundumstudio.socketio.store.pubsub.PubSubType;
import io.netty.util.internal.PlatformDependent;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.redisson.codec.SerializationCodec;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;

public class CustomRedissonPubSubStore extends RedissonPubSubStore {
    private final RedissonClient redissonPub;
    private final RedissonClient redissonSub;
    private final Long nodeId;
    private final ConcurrentMap<String, Queue<Integer>> map = PlatformDependent.newConcurrentHashMap();

    public CustomRedissonPubSubStore(RedissonClient redissonPub, RedissonClient redissonSub, Long nodeId) {
        super(redissonPub, redissonSub, nodeId);
        this.redissonPub = redissonPub;
        this.redissonSub = redissonSub;
        this.nodeId = nodeId;
    }

    public void publish(PubSubType type, PubSubMessage msg) {
        msg.setNodeId(this.nodeId);
        this.redissonPub.getTopic(type.toString(), new SerializationCodec()).publish(msg);
    }

    public <T extends PubSubMessage> void subscribe(PubSubType type, final PubSubListener<T> listener, Class<T> clazz) {
        String name = type.toString();
        RTopic topic = this.redissonSub.getTopic(name, new SerializationCodec());
        int regId = topic.addListener(clazz, (channel, msg) -> {
        	 //TODO 重点关注,可以根据具体业务场景进行修改
//            if (!CustomRedissonPubSubStore.this.nodeId.equals(msg.getNodeId())) {
                listener.onMessage(msg);
//            }
        });
        Queue<Integer> list = this.map.get(name);
        if (list == null) {
            list = new ConcurrentLinkedQueue<>();
            Queue<Integer> oldList = this.map.putIfAbsent(name, list);
            if (oldList != null) {
                list = oldList;
            }
        }
        list.add(regId);
    }

    public void unsubscribe(PubSubType type) {
        String name = type.toString();
        Queue<Integer> regIds = this.map.remove(name);
        RTopic topic = this.redissonSub.getTopic(name, new SerializationCodec());

        for (Integer id : regIds) {
            topic.removeListener(id);
        }

    }

    public void shutdown() {
    }
}
  • 消息实体类:SocketMessage<T> 根据需要自行修改
package com.ddw.chat.controller.socket;

import lombok.Data;

@Data
public class SocketMessage<T> {
    private String room;
    private String namespace;
    private String fromUserId;
    private String toUserId;
    private String eventName;
    private T message;
}
  • 消息触发controller:SocketPushController.java
package com.ddw.chat.controller;

import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.protocol.EngineIOVersion;
import com.corundumstudio.socketio.protocol.Packet;
import com.corundumstudio.socketio.protocol.PacketType;
import com.corundumstudio.socketio.store.pubsub.DispatchMessage;
import com.corundumstudio.socketio.store.pubsub.PubSubStore;
import com.corundumstudio.socketio.store.pubsub.PubSubType;
import com.ddw.chat.controller.socket.SocketMessage;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;

@RestController
@Slf4j
public class SocketPushController {

    @Autowired
    private PubSubStore pubSubStore;
    @Autowired
    private RedissonClient redisson;
    @Autowired
    private SocketIOServer socketIOServer;

    @PostMapping("/push")
    public String push(@RequestBody SocketMessage<String> socketMessage) {
        Packet packet = new Packet(PacketType.MESSAGE, EngineIOVersion.V3);
        packet.setSubType(PacketType.EVENT);
        packet.setName(socketMessage.getEventName());
        packet.setData(Collections.singletonList(socketMessage.getMessage()));
        DispatchMessage dispatchMessage = new DispatchMessage(socketMessage.getRoom(), packet, socketMessage.getNamespace());
        pubSubStore.publish(PubSubType.DISPATCH,dispatchMessage);
        return "ok";

    }

}

二、前端

1. 技术框架
  • bootstrap v3
  • Socket.IO v2.3.0
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>

<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> 
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
2.前端页面代码
  • index.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta charset="utf-8" />
    <title>webSocket测试</title>
    <link rel="stylesheet" href="lib/bootstrap.min.css" />
    <link rel="stylesheet" href="lib/bootstrap-theme.min.css" />
    <link rel="stylesheet" href="lib/app.css" />
    <script src="lib/socket.io.js"></script>
    <script src="lib/jquery.min.js"></script>
    <script src="lib/bootstrap.min.js"></script>
  </head>

  <body>
    <div class="main-page">
      <div class="col-md-3 part-page">
        <div class="user-box">
          <div class="user-part">
            <img src="img/head-icon.png" class="img-circle head-icon" />
          </div>
          <div class="user-part user-ipart-info">
            <div>用户ID:<span id="userId">86888</span></div>
            <div id="onlineState">
              <span class="label label-default">离线</span>
            </div>
          </div>
        </div>
        <div class="server-config">
          <div class="form-group">
            <label for="ip" class="control-label">IP</label>
            <input class="form-control" id="ip" />
          </div>
          <div class="form-group">
            <label for="port" class="control-label">端口</label>
            <input class="form-control" id="port" />
          </div>
          <div class="form-group">
            <label for="namespace" class="control-label">namespace</label>
            <input class="form-control" id="namespace" value="/websocket" />
          </div>
          <div class="form-group">
            <label for="room" class="control-label">room</label>
            <input class="form-control" id="room" value="def_pub_room" />
          </div>
          <div class="form-group">
            <button type="submit" class="btn btn-danger" id="connect">
              连接
            </button>
          </div>
        </div>
      </div>
      <div class="col-md-9 part-page">
        <div class="panel panel-default" style="height: 100%">
          <div class="panel-heading">消息记录</div>
          <div class="panel-body" style="height: calc(100% - 60px)">
            <div class="well" id="msg-box"></div>
            <div class="col-lg">
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  placeholder="发送信息..."
                  id="message"
                />
                <span class="input-group-btn">
                  <button class="btn btn-default" type="button" id="send">
                    发送
                  </button>
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  <script type="text/javascript">
    $(function () {
      var socket = null
      var userId = ''

      init()

      $('#connect').bind('click', function () {
        var namespace = $('#namespace').val()
        namespace=""
        // var room = $('#room').val()
        var port = $('#port').val()
        var url =
          'http://localhost:' +
          port +
          namespace +
          '?room=' +
          userId +
          '&userId=' +
          userId
        socket = io.connect(url, {
          'reconnection delay': 2000,
          'force new connection': true
        })
        socketInit(socket)
      })

      $('#send').bind('click', function () {
        send()
      })

      function init () {
        userId = 'U' + Math.ceil(Math.random() * 10000)
        $('#userId').html(userId)
        var host = window.location.host
        var ip = host.split(':')[0]
        $('#ip').val(ip)
        $('#port').val('90')
      }

      function socketInit (socket) {
        //监听服务器连接事件
        socket.on('connect', function () {
          $('#msg-box').html(
            $('#msg-box').html() + '<br/>【系统消息】:连接服务器成功!'
          )
          $('#onlineState').html(
            '<span class="label label-success">在线</span>'
          )
        })
        //监听服务器关闭服务事件
        socket.on('disconnect', function () {
          $('#msg-box').html(
            $('#msg-box').html() + '<br/>【系统消息】:与服务器断开了连接!'
          )
          $('#onlineState').html(
            '<span class="label label-default">离线</span>'
          )
        })
        //监听服务器端发送消息事件
        socket.on('msg_event', function (data) {
          $('#msg-box').html(
            $('#msg-box').html() + '<br/>【收到消息】:' + data
          )
        })
      }

      function send () {
        if (socket != null) {
          console.log("发送消息");
          var message = document.getElementById('message').value
          var title = 'message'
          var obj = { message: message, title: title }
          var str = JSON.stringify(obj)
          socket.emit('msg_event', str)
        } else {
          alert('未与服务器链接.')
        }
      }
    })
  </script>
</html>

  • app.css
html,
body,
.main-page,
.part-page {
    height: 100%;
}

.part-page {
    padding: 40px 20px;
}

.part-page:nth-child(1) {
    background-image: linear-gradient(to top, #a8edea 0%, #fed6e3 100%);
}

.part-page:nth-child(2) {
    border-right: 1px solid #eee;
}


.user-box {
    border-bottom: 1px solid #bebebe;
    margin-bottom: 10px;
    padding-bottom: 10px;
}

.user-box::after {
    content: "";
    clear: both;
    display: block;
}

.user-part {
    float: left;
}

.user-part:nth-child(2) {
    padding: 10px;
}

.user-part span:nth-child(1) {
    margin: 0 5px;
}

.user-part div {
    margin-bottom: 10px;
    font-weight: 600;
}

.user-ipart-info {
    margin-top: 15px;
}

.user-ipart-info #userId {
    color: brown;
}

.head-icon {
    height: 100px;
    padding: 5px;
    background-color: #fff;
}

.btn-settings-box .btn {
    margin-right: 5px;
}

.server-config {
    margin-top: 20px;
    padding: 10px;
    border: 1px solid #bebebe;
    border-radius: 5px;
}

#msg-box  {
    height: calc(100% - 50px);
    overflow: auto;
}

三、Nginx配置

  • conf/nginx.cnf 全量配置

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_requests 8192;
	keepalive_timeout 180s 180s;
	
	map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
	}
       
    upstream web-server{
	   server localhost:8080 weight=1;
	   server localhost:9080 weight=1;
	}
	
	upstream ws-server {
		hash $remote_addr consistent;
		server localhost:8090 weight=1; 
		server localhost:9090 weight=1;
	}

    server {
        listen       80;
        server_name  localhost;   
        
        location / {			
            proxy_pass http://web-server/;           
        }
    }
    
    server {
        listen       90;
        server_name  localhost;  
        
		location ~/socket.io/(.*) {
			proxy_pass http://ws-server;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";
			proxy_set_header Host $http_host;
			proxy_set_header X-NginX-Proxy true;
			proxy_redirect off;
		} 
    }  
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Spring Boot是一个非常流行的Java开发框架,而Netty-socketio是一个基于Netty框架的WebSocket实现,提供了方便的实时通信解决方案。将它们结合起来,可以实现高效的WebSocket通信服务。 下面是整合的步骤: 1. 添加依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.16</version> </dependency> ``` 2. 编写Netty-socketio服务 创建一个类,继承自SpringBoot的ApplicationListener接口,用于启动Netty-socketio服务。 ``` import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.SocketIOServer; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class NettySocketIoServer implements ApplicationListener<ApplicationReadyEvent> { @Value("${socketio.host}") private String host; @Value("${socketio.port}") private Integer port; private SocketIOServer server; @Override public void onApplicationEvent(ApplicationReadyEvent event) { Configuration config = new Configuration(); config.setHostname(host); config.setPort(port); server = new SocketIOServer(config); server.start(); } } ``` 其中,@Value注解用于从配置文件中读取host和port的值,SocketIOServer是Netty-socketio提供的服务类,用于启动和管理WebSocket服务。 3. 配置WebSocket处理器 创建一个类,继承自Spring BootWebSocketHandler接口,用于处理WebSocket连接和消息。 ``` import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; @Component public class SocketIoHandler extends TextWebSocketHandler { @Autowired private SocketIOServer server; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); SocketIOClient client = server.getClient(session.getId()); if (client == null) { client = server.addClient(session); } } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); SocketIOClient client = server.getClient(session.getId()); if (client != null) { client.sendEvent("message", message.getPayload()); } } } ``` 其中,@Autowired注解用于从Spring容器中获取SocketIOServer实例,afterConnectionEstablished方法用于处理WebSocket连接建立时的逻辑,handleTextMessage方法用于处理WebSocket消息。 4. 配置WebSocket处理器映射 创建一个WebSocketHandlerRegistry类,用于配置WebSocket处理器的映射关系。 ``` import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private SocketIoHandler socketIoHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(socketIoHandler, "/ws").setAllowedOrigins("*"); } } ``` 其中,@EnableWebSocket注解用于开启WebSocket支持,registerWebSocketHandlers方法用于配置WebSocket处理器映射关系。 5. 配置application.yml 在application.yml文件中添加以下配置: ``` socketio: host: localhost port: 8080 ``` 其中,host和port的值应与Netty-socketio服务的配置一致。 6. 运行程序 现在,可以运行程序,并访问http://localhost:8080/ws,即可建立WebSocket连接。发送消息时,可以使用socket.emit()方法,接收消息时,可以使用socket.on()方法。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值