前段时间碰到个项目,需求是用户在第三方会议系统签到后需要把用户头像实时发送显示到大屏上展示,因为签到时间持续时间比较长,前端ajax轮询的方式不是很理想,所以考虑使用websocket,就拿公司其他的项目来研究了一下,在此记录下初识springboot + netty + websocket的过程,主要是Server端的实现过程。
在pom.xml中添加以下依赖
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.31.Final</version> </dependency>
添加启动类实现CommandLineRunner接口并重写run方法,启动自执行,即启动springboot时就启动netty
import com.stt.experiment.trynetty.server.NettyServer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; /** * 启动服务 */ @Slf4j @Component public class ServerRunner implements CommandLineRunner { @Autowired private NettyServer nettyServer; @Override public void run(String... strings) throws Exception { Thread thread = new Thread(nettyServer); thread.start(); } }
netty服务
import com.stt.experiment.trynetty.server.config.ServerConfig; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Slf4j @Component public class NettyServer implements Runnable{ private final ServerConfig serverConfig; private final ServerChannelInitializer serverChannelInitializer; @Autowired public NettyServer(ServerConfig serverConfig, ServerChannelInitializer serverChannelInitializer) { this.serverConfig = serverConfig; this.serverChannelInitializer = serverChannelInitializer; } @Override public void run() { String host = serverConfig.getHost(); int port = serverConfig.getPort(); // 服务端需要2个线程组,boss处理客户端链接,work进行客户端连接之后的处理 // boss这个EventLoopGroup作为一个acceptor负责接收来自客户端的请求,然后分发给worker这个EventLoopGroup来处理所有的事件event和channel的IO EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); // 服务器配置 // childHandler(serverChannelInitializer) // 该函数的主要作用是设置channelHandler来处理客户端的请求的channel的IO。 bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(serverChannelInitializer); //绑定端口,开始接收进来的连接 ChannelFuture f = bootstrap.bind(host, port).sync(); log.info("===========启动成功==========="); f.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }
该类实现Runnable接口,实现run方法,在ServerRunner中用new Thread(Runnable target).start()方法来启动。
@Autowired的使用:推荐对构造函数进行注释 感谢博友的分享,这里是这篇文章的原文链接
这是我之前不明白的一个点,为什么要使用@Autowired对构造函数进行注释,而不是直接注入bean。看了这篇分享,做了试验之后发现:
直接使用@Autowired注入,发现IDE报了warning:Spring Team recommends “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.
这段代码原来的写法是:
@Autowired
private ServerConfig serverConfig;
@Autowired
private ServerChannelInitializer serverChannelInitializer;
根据IDE的建议修改后的代码写法是:
private final ServerConfig serverConfig;
private final ServerChannelInitializer serverChannelInitializer;
@Autowired
public NettyServer(ServerConfig serverConfig, ServerChannelInitializer serverChannelInitializer) {
this.serverConfig = serverConfig;
this.serverChannelInitializer = serverChannelInitializer;
}
@Autowired 可以对成员变量、方法以及构造函数进行注释,@Autowired注入bean,相当于在配置文件中配置bean,并且使用setter注入。而对构造函数进行注释,就相当于是使用构造函数进行依赖注入。
那为什么IDE会这么建议处理呢,主要原因就是使用构造器注入的方法,可以明确成员变量的加载顺序(PS:Java变量的初始化顺序为:静态变量或静态语句块–>实例变量或初始化语句块–>构造方法–>@Autowired)。
来看一个例子:
@Autowired
private User user;
private String school;
public UserAccountServiceImpl(){
this.school = user.getSchool();
}
比如以上这段代码,能运行成功吗?答案是不能,因为Java类会先执行构造方法,然后再给注解了@Autowired 的user注入值,所以在执行构造方法的时候,就会报错。
4.netty 相关配置文件
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* netty 相关配置文件
*/
@Getter
@Setter
@Component
@Configuration
// 引用自定义配置文件
@PropertySource("classpath:netty_config.properties")
public class ServerConfig {
@Value("${netty.server.host}")
private String host; // 服务启动地址
@Value("${netty.server.port}")
private int port; // 服务启动端口
@Value("${netty.server.workerThreads}")
private int workerThreads; // 服务线程池中的线程数
@Value("${netty.server.maxContentLength}")
private int maxContentLength; // 可接受的最大的消息长度
@Value("${netty.server.readTimeout}")
private int readTimeout; // 连接读取超时时间
@Value("${netty.server.writeTimeout}")
private int writeTimeout; // 写操作超时时间
@Value("${netty.server.webSocketPath}")
private String webSocketPath; // websocket 接口地址
}
5.构建Handler处理流程
import com.stt.experiment.trynetty.server.config.ServerConfig;
import com.stt.experiment.trynetty.server.handler.WebSocketServerHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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 io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import io.netty.channel.socket.SocketChannel;
/**
* 构建Handler处理流程
*/
@Component
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel>{
@Autowired
private ServerConfig serverConfig;
@Autowired
private WebSocketServerHandler webSocketServerHandler;
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 添加各种功能handler,依次执行
ChannelPipeline pipeline = socketChannel.pipeline();
// HttpServerCodec: 将请求和应答的消息解码为HTTP消息
pipeline.addLast(new HttpServerCodec());
// HttpObjectAggregator: 将HTTP消息的多个部分合成一条完整的HTTP消息
pipeline.addLast(new HttpObjectAggregator(serverConfig.getMaxContentLength()));
// ChunckedWriteHandler: 处理大数据传输,支持异步写大数据流,不引起高内存消耗。
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new ReadTimeoutHandler(serverConfig.getReadTimeout()));
pipeline.addLast(new WriteTimeoutHandler(serverConfig.getWriteTimeout()));
pipeline.addLast(new WebSocketServerProtocolHandler(serverConfig.getWebSocketPath()));
// 添加自定义默认处理器
pipeline.addLast(webSocketServerHandler);
}
}
可将ChannelPipeline视为ChannelHandler实例链,可拦截流经通道的入站和出站事件,当创建一个新的Channel时,都会分配一个新的ChannelPipeline,该关联是永久的,该通道既不能附加另一个ChannelPipeline也不能分离当前的ChannelPipeline。 文章出处
6.默认消息处理器
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 默认消息处理器
*/
@Slf4j
@ChannelHandler.Sharable // 可以被添加至多个ChannelPipiline中
@Component
public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame>{
// 心跳信息,可移到constants文件
private static final String HEART_BEAT = "heart_beat";
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, WebSocketFrame frame) throws Exception {
String res = null;
if (frame instanceof TextWebSocketFrame) {
// 获取请求消息内容
String requestContent = ((TextWebSocketFrame) frame).text();
// if 心跳消息
if (HEART_BEAT.equalsIgnoreCase(requestContent)) {
res = requestContent;
} else {
// else 其他请求则请求分发
}
} else {
log.warn("unsupport dataType: {}", frame.getClass().getName());
}
if (null != res) {
channelHandlerContext.writeAndFlush(new TextWebSocketFrame(res));
}
}
}
消息类型:
BinaryWebSocketFrame 二进制数据
TextWebSocketFrame 文本数据
ContinuationWebSocketFrame 超大文本或者二进制数据
这里只对心跳进行了处理,简单的业务(即请求比较少的情况下),可以直接在这里进行数据处理,如果业务比较复杂,可自行做请求分发,到各自的Action中处理相关的业务逻辑。