比websocket更强性能的Netty
首先创建一个netty的config
配置类可以不改,
netty不像websocket,可以一直拿到请求头里面的数据,netty只有第一次连接可以拿到,
我们将第一次拿到的数据存入map集合,一个Channel对应一个业务id,再弄一个新的
Multimap,这个map是谷歌的一个集合,一个key对应多个value,value是一个集合,这样一个业务id可以对应多个客户端
package com.sdk.controller.controller.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* NettyServer Netty服务器配置
* @author weikun
* @date 2020-06-20
*/
@Component
@Slf4j
@Data
public class NettyServer {
@Value("${netty.websocket.port}")
private Integer port;
public String baseUrl(){
return "ws://127.0.0.1:" + port;
}
/**
* 根据自己业务配url
*/
public static String MESSAGE_PATH = "/ws";
public String getMessageUrl(){
return this.baseUrl() + MESSAGE_PATH;
}
/**
* BOSS 用于接收客户端传过来的请求
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
/**
* Worker 接收到请求后将后续操作交由 workerGroup 处理
*/
EventLoopGroup workGroup = new NioEventLoopGroup();
@PostConstruct
public void start() throws Exception {
//ServerBootstrap 用来为 Netty 程序的启动组装配置一些必须要组件
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
// 绑定线程池
sb.group(workGroup, bossGroup)
// 指定使用的channel
.channel(NioServerSocketChannel.class)
// 绑定监听端口
.localAddress(port)
// 绑定客户端连接时候触发操作
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
// 用于消息的分发
ch.pipeline().addLast(new NettyHandler());
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
}
});
// 服务器异步创建绑定
ChannelFuture cf = sb.bind().sync();
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
if (cf.isSuccess()){
System.out.println("启动成功");
}
}
}
使用类
这是我这个netty请求的地址,也可以是userId,会通过判断最后传的值来进行不同的业务,如传的是userId可以建立新的Map集合存储,根据对应的map进行对应的操作,我这里只演示了id,map集合只有id对应的channel和channerId对应的id,这个id可以是业务id,有的业务需要根据对应的id查询数据
ws://127.0.0.1:1024/websocket/ws?id=5555
netty进行消息处理都在channelRead里面,在这下面进行对应的消息处理,netty会自行监听客户端心跳,不跳动时会调用删除方法,我们自己将删除逻辑写好
package com.sdk.controller.controller.netty;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import io.netty.channel.*;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 前面对应对应的id,用来存储客户端和唯一标识
*/
static Multimap<String, Channel> map = HashMultimap.create();
/**
* 用来存储会话与id的关系
*/
static ConcurrentHashMap<ChannelId, String> currentHashMap = new ConcurrentHashMap();
static NettyServer nettyServer;
static {
nettyServer = SpringUtil.getBean(NettyServer.class);
}
private WebSocketServerHandshaker webSocketServerHandshaker;
/**
* 连接websocekt
* 这个方法没有任何处理逻辑,只会在第一次连接上时触发,并且获取不到任何参数
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().id());
System.out.println("连接的客户端地址:" + ctx.channel().remoteAddress());
super.channelActive(ctx);
}
/**
* 解析数据。第一次url参数也是走这个方法
* 注意:一定要回调父类该方法。不然内存会泄露。不要以为重写该方法后,在最后面添加清除ByteBuf告诉你这是无用的,如果谁成功了请呼唤我。
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,处理参数以及真实ip、虚拟ip,虚拟ip、channel id 映射关系
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
//真实ip
String clientIp = request.headers().get("X-Real-IP");
//虚拟ip
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
String proxyIp = inSocket.getAddress().getHostAddress();
if(clientIp==null){
clientIp=proxyIp;
}
Map<String,String> paramMap=getUrlParams(uri);
//传的参数是userId就走这个
if(paramMap.get("userId")!=null){
WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory(nettyServer.getMessageUrl(),null,false);
webSocketServerHandshaker = webSocketServerHandshakerFactory.newHandshaker(request);
if (webSocketServerHandshaker == null){
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
}else{
webSocketServerHandshaker.handshake(ctx.channel(),request);
}
//传的是id走这个
} else if (paramMap.get("id")!=null) {
WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory(nettyServer.getMessageUrl(),null,false);
webSocketServerHandshaker = webSocketServerHandshakerFactory.newHandshaker(request);
if (webSocketServerHandshaker == null){
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
}else{
webSocketServerHandshaker.handshake(ctx.channel(),request);
}
currentHashMap.put(ctx.channel().id(),paramMap.get("id"));
map.put(paramMap.get("id"),ctx.channel());
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr("连接成功")));
} else {
ctx.channel().writeAndFlush(new TextWebSocketFrame("参数输入错误"));
}
//接收的消息都在这里处理
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame =(TextWebSocketFrame) msg;
String text = frame.text();
//在这里处理对应的逻辑
log.info("服务器数据收到客户端数据:" + text);
}
}
/**
* 关闭连接(服务端自动调用)
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//获取id
String s = currentHashMap.get(ctx.channel().id());
//根据id清除会话
map.remove(s,ctx.channel());
currentHashMap.remove(ctx.channel().id());
ctx.close(); // 关闭连接
// 清理相关资源和状态
super.channelInactive(ctx);
}
/**
* 关闭连接(当服务端检测不到心跳时自动调用)
* @param ctx
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
//从服务端的channelGroup中移除当前离开的客户端
log.info("与客户端断开连接,通道关闭!");
System.out.println("ctx.channel().id() = " + ctx.channel().id());
//添加到channelGroup 通道组
//MyChannelHandlerPool.channelGroup.remove(channel);
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
String proxyIp = inSocket.getAddress().getHostAddress();
//获取id
String s = currentHashMap.get(ctx.channel().id());
//根据id清除会话
map.remove(s,ctx.channel());
currentHashMap.remove(ctx.channel().id());
System.out.println("删除成功");
}
private static Map<String,String> 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;
}
}
application.yml
pom依赖,都需要
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
当后端要发送实时数据时,后端可以写一个job
具体的逻辑自行进行更换,遍历那个业务map,也就是一个业务id对应多个客户端的,同一个业务id 的channel发送的数据一样
package com.sdk.controller.controller.netty;
import cn.hutool.json.JSONUtil;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
@EnableScheduling
@Component
public class NettyJob {
/**
* 一秒一次
* @param
*/
@Scheduled(fixedRate = 1000)
public void start( ) {
Collection<Map.Entry<String, Channel>> entries =NettyHandler.map.entries();
Iterator<Map.Entry<String, Channel>> iterator = entries.stream().iterator();
while (iterator.hasNext()){
Map.Entry<String, Channel> next = iterator.next();
next.getValue().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr("传递参数"+new Date())));
}
}
}