SpringBoot2+Netty 实现websocket,支持URL参数,支持Channel和用户绑定关系

1.pom依赖

<!-- WebSocket -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.36.Final</version>
        </dependency>

2.SpringBootApplication

@SpringBootApplication
@EnableFeignClients
@EnableScheduling
@EnableAsync//开启异步调用
public class ServiceWSApplication {

	public static void main(String[] args) {
		SpringApplication.run(ServiceWSApplication.class, args);
		try {
			new NettyServer(12345).start();
		}catch(Exception e) {
			System.out.println("NettyServerError:"+e.getMessage());
		}
	}
}

3.NettyServer

启动的NettyServer,这里进行配置

/**
 * NettyServer Netty服务器配置
 */
public class NettyServer {
    private final int port;

    public NettyServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap sb = new ServerBootstrap();
            sb.option(ChannelOption.SO_BACKLOG, 1024);
            sb.group(group, bossGroup) // 绑定线程池
                    .channel(NioServerSocketChannel.class) // 指定使用的channel
                    .localAddress(this.port)// 绑定监听端口
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("收到新连接");
                            //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            //以块的方式来写的处理器
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new HttpObjectAggregator(8192));
                            ch.pipeline().addLast(new MyWebSocketHandler());
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
                        }
                    });
            ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
            System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
            cf.channel().closeFuture().sync(); // 关闭服务器通道
        } finally {
            group.shutdownGracefully().sync(); // 释放线程池资源
            bossGroup.shutdownGracefully().sync();
        }
    }
}

4.MyChannelHandlerPool

通道组池,管理所有websocket连接

/**
 * MyChannelHandlerPool
 * 通道组池,管理所有websocket连接
 */
public class MyChannelHandlerPool {

    public MyChannelHandlerPool(){}

    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    //用户id=>channel
    public static ConcurrentHashMap<String, Channel> channelMap = new ConcurrentHashMap<>();

}

5.MyWebSocketHandler

处理ws一下几种情况:

channelActive与客户端建立连接
channelInactive与客户端断开连接
channelRead0客户端发送消息处理

/**
 * MyWebSocketHandler
 * WebSocket处理器,处理websocket连接相关
 */
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端建立连接,通道开启!当前通道数量----"+MyChannelHandlerPool.channelGroup.size());
        //添加到channelGroup通道组
        MyChannelHandlerPool.channelGroup.add(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端断开连接,通道关闭!当前通道数量----"+MyChannelHandlerPool.channelGroup.size());
        //添加到channelGroup 通道组
        MyChannelHandlerPool.channelGroup.remove(ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //首次连接是FullHttpRequest,处理参数 by zhengkai.blog.csdn.net
        if (null != msg && msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.uri();
            System.out.println("uri-----"+uri);
            Map paramMap=getUrlParams(uri);
            System.out.println("接收到的参数是:"+ JSON.toJSONString(paramMap));
            System.out.println("uid------"+paramMap.get("uid"));
            online(ctx.channel(),paramMap.get("uid")+"");
            //如果url包含参数,需要处理
            if(uri.contains("?")){
                String newUri=uri.substring(0,uri.indexOf("?"));
                System.out.println("newUri----------"+newUri);
                request.setUri(newUri);
            }

        }else if(msg instanceof TextWebSocketFrame){
            //正常的TEXT消息类型
            TextWebSocketFrame frame=(TextWebSocketFrame)msg;
            System.out.println("客户端收到服务器数据:" +frame.text()+"------当前通道数量----"+MyChannelHandlerPool.channelGroup.size());
        }
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {

    }

    private void sendAllMessage(String message){
        //收到信息后,群发给所有channel
        MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame(message));
    }


    /**
     * 上线一个用户
     *
     * @param channel
     * @param userId
     */
    public void online(Channel channel, String userId) {
        //先判断用户是否在web系统中登录?
        //这部分代码个人实现,参考上面redis中的验证
        MyChannelHandlerPool.channelMap.put(userId, channel);
        AttributeKey<String> key = AttributeKey.valueOf("user");
        channel.attr(key).set(userId);
    }






    private static Map getUrlParams(String url){
        Map<String,String> map = new HashMap<>();
        url = url.replace("?",";");
        if (!url.contains(";")){
            return map;
        }
        if (url.split(";").length > 0){
            String[] arr = url.split(";")[1].split("&");
            for (String s : arr){
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key,value);
            }
            return  map;

        }else{
            return map;
        }
    }

}

6.socket.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Netty-Websocket</title>
</head>
<script src="jquery.js"></script>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
    	// uid字段要和后台WebSocketHandler类中的paramMap.get("uid")字段对应
        socket = new WebSocket("ws://127.0.0.1:12345/ws?uid=" + 55); // 55代表用户id
        socket.onmessage = function (event) {
            $("#ws").html(event.data)
            console.log(event.data)
        };
        socket.onopen = function (event) {
            console.log("Netty-WebSocket服务器。。。。。。连接")
        };
        socket.onclose = function (event) {
            console.log("Netty-WebSocket服务器。。。。。。关闭")
        };
    } else {
        alert("您的浏览器不支持WebSocket协议!");
    }
</script>
<body>
    <div id="ws" class="ws">

    </div>
</body>
</html>

7.Controller

@Api(tags = "实时推送相关接口")
@RestController
public class IndexController {

	@ApiOperation("给某一个用户发送实时消息")
	@PostMapping("/push/toUserId")
	@Async
	@ApiImplicitParams({ @ApiImplicitParam(name = "message", value = "发送消息", defaultValue = "", required = true),
			@ApiImplicitParam(name = "toUserId", value = "用户Id", defaultValue = "", required = true) })
	public void pushToUser(@RequestParam("message") String message, @RequestParam("toUserId") String toUserId) {
		Channel channel = getChannelByUserId(toUserId);
		// 收到信息后,发给当前用户的channel
		if (channel != null) {
			if (online(toUserId)) {
				channel.writeAndFlush(new TextWebSocketFrame(message));
			} else {// 不在前剔除当前用户
				MyChannelHandlerPool.channelMap.remove(toUserId);
			}
		}
	}

	@ApiOperation("给多个用户发送实时消息")
	@ApiImplicitParams({ @ApiImplicitParam(name = "message", value = "发送消息", defaultValue = "", required = true),
			@ApiImplicitParam(name = "userIdList", value = "用户id-List<String>", defaultValue = "", required = true) })
	@PostMapping("/push/users")
	@Async
	public void pushToUsers(@RequestParam("message") String message,
			@RequestParam("userIdList") List<String> userIdList) {
		System.out.println("用户id集合" + userIdList.size());
		userIdList.forEach(id -> {
			Channel channel = getChannelByUserId(id);
			System.out.println("channel----" + id + "-----" + channel);
			if ("null".equals(channel)) {
				System.out.println("字符串null");
			}

			if (channel != null) {
				if (online(id)) {
					System.out.println("-------发送的通道" + id);
					channel.writeAndFlush(new TextWebSocketFrame(message));
				} else {
					System.out.println(id + "-----不在线剔除");
					MyChannelHandlerPool.channelMap.remove(id);
				}

			}
		});
	}

	@ApiOperation("给所有在线用户发送实时消息")
	@ApiImplicitParams({ @ApiImplicitParam(name = "message", value = "发送消息", defaultValue = "", required = true), })
	@PostMapping("/push/all")
	@Async
	public void pushAll(String message) {
		MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame(message));
	}

	/**
	 * 根据用户id获取该用户的通道
	 *
	 * @param userId
	 * @return
	 */
	public Channel getChannelByUserId(String userId) {
		System.out.println("getChannelByUserId当前通道数量----" + MyChannelHandlerPool.channelGroup.size());
		return MyChannelHandlerPool.channelMap.get(userId);
	}

	/**
	 * 判断一个用户是否在线
	 *
	 * @param userId
	 * @return
	 */
	public Boolean online(String userId) {
		return MyChannelHandlerPool.channelMap.containsKey(userId)
				&& MyChannelHandlerPool.channelMap.get(userId) != null;
	}

	/**
	 * 判断一个通道是否有用户在使用 可做信息转发时判断该通道是否合法
	 * 
	 * @param channel
	 * @return
	 */
	public boolean hasUser(Channel channel) {
		AttributeKey<String> key = AttributeKey.valueOf("user");
		return (channel.hasAttr(key) || channel.attr(key).get() != null);// netty移除了这个map的remove方法,这里的判断谨慎一点
	}

	/**
	 * Map通配推送
	 * 
	 * @param keyLike 通配key
	 * @param message 消息
	 */
	@Async
	@PostMapping("/push/keyLike/toUser")
	public void getLikeByMap(@RequestParam("keyLike") String keyLike, @RequestParam("message") String message) {
		ConcurrentHashMap<String, Channel> map = MyChannelHandlerPool.channelMap;
		for (Map.Entry<String, Channel> entity : map.entrySet()) {
			if (entity.getKey().contains(keyLike)) {
				Channel channel = entity.getValue();
				if (channel != null) {
					channel.writeAndFlush(new TextWebSocketFrame(message));
				}
			}
		}
	}
}

8.效果演示

这里就做演示了 按着步骤操作木有问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值