demo下载地址:多种websocket实现方式,其中有基于spring-websocekt,也有基于netty框架,即下即用。
之前的博客使用了spring-websocket实现了websocket服务端,现在我们利用netty框架实现,更灵活,更性能。在一些复杂场景下,可以通过调整参数提高效率。
之前的实现可以参考:
springboot实现webocket(一)
springboot实现webocket(二)
首先定义一个netty的server端,用于启动端口。
说明一下,netty需要占用一个端口,如果你的项目也提供了web服务,两者端口不能一样。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 网状服务器
*
* @author lukou
* @date 2023/05/17
*/
public class NettyServer {
private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
private int port;
private Channel channel;
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
private ChannelInitializer<SocketChannel> channelInitializer;
public NettyServer(int port, ChannelInitializer<SocketChannel> channelInitializer) {
this.port = port;
this.channelInitializer = channelInitializer;
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
}
/**
* 开始
*
* @throws Exception 异常
*/
public void start() throws Exception {
try {
ServerBootstrap sb = new ServerBootstrap();
//绑定线程池
sb.group(bossGroup, workGroup)
//指定使用的channel
.channel(NioServerSocketChannel.class)
//临时存放已完成三次握手的请求的队列的最大长度
.option(ChannelOption.SO_BACKLOG, 1024)
//禁用nagle算法,不等待,立即发送
.childOption(ChannelOption.TCP_NODELAY, true)
//当没有数据包过来时超过一定时间主动发送一个ack探测包
.childOption(ChannelOption.SO_KEEPALIVE, true)
//允许共用端口
.childOption(ChannelOption.SO_REUSEADDR, true)
//绑定监听端口
.localAddress(this.port)
//添加自定义处理器
.childHandler(this.channelInitializer);
//服务器异步创建绑定
ChannelFuture cf = sb.bind().sync();
channel = cf.channel();
log.info("netty服务启动。。正在监听:[{}]", channel.localAddress());
//关闭服务器通道
channel.closeFuture().sync();
} catch (Exception e) {
throw new Exception("启动netty服务发生异常,端口号:" + this.port, e);
}
}
/**
* 摧毁
*
* @throws Exception 异常
*/
public void destroy() throws Exception {
try {
channel.close().sync();
workGroup.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();
} catch (Exception e) {
throw new Exception("停止netty服务发生异常,端口号:" + this.port, e);
}
}
}
接下来,需要实现业务处理逻辑的类,首先定义一个抽象类,将一些公共逻辑放到里面
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import static io.netty.handler.codec.http.HttpMethod.GET;
/**
* 基本套接字服务器
* 抽象了一层.
*
* @author lukou
* @date 2023/05/17
*/
public abstract class BaseSocketServer extends SimpleChannelInboundHandler<Object> {
private static final Logger log = LoggerFactory.getLogger(BaseSocketServer.class);
/**websocket协议内容*/
public static final String WEBSOCKET = "websocket";
public static final String UPGRADE = "Upgrade";
/**
* 客户端连接地址
*/
public static final String ENDPOINT = "/example4/ws";
/**
* 连接唯一id,方便链路追踪
*/
protected String taskId;
/**
* 上下文
*/
protected ChannelHandlerContext context;
/**
* websocket握手处理器
*/
private WebSocketServerHandshaker webSocketServerHandshaker;
/**
* 通道活性
* 客户端与服务端创建链接的时候调用.
*
* @param context 上下文
*/
@Override
public abstract void channelActive(ChannelHandlerContext context);
/**
* 频道不活跃
* 客户端与服务端断开连接的时候调用.
*
* @param context 上下文
*/
@Override
public abstract void channelInactive(ChannelHandlerContext context);
/**
* 通道读完整
* 服务端接收客户端发送过来的数据结束之后调用.
*
* @param context 上下文
*/
@Override
public void channelReadComplete(ChannelHandlerContext context) {
context.flush();
}
/**
* 例外了
* 工程出现异常的时候调用.
*
* @param context 上下文
* @param throwable throwable
*/
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable throwable) {
context.close();
log.info("taskId:[{}]中发生错误,原因:[{}]", this.taskId, throwable.toString(), throwable);
}
/**
* 通道read0
* 连接和帧信息.
*
* @param ctx ctx
* @param msg 味精
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof WebSocketFrame) {
this.handWebSocketFrame(ctx, (WebSocketFrame) msg);
return;
}
if (msg instanceof FullHttpRequest) {
log.info("taskId:[{}]开始处理websocket握手请求。。", taskId);
this.httpRequestHandler(ctx, (FullHttpRequest) msg);
log.info("taskId:[{}]处理websocket握手请求结束。。", taskId);
}
}
/**
* 用户事件触发
* 这里设置了一个读超时事件,可以参考{@link Example4WebSocketChannelHandler}中设置
*
* @param ctx ctx
* @param evt evt
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (IdleStateEvent.class.isAssignableFrom(evt.getClass())) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
ctx.close();
log.info("taskId:[{}]读操作超时。。断开连接。。", this.taskId);
}
}
}
/**
* 处理客户端与服务端之间的websocket业务.
*
* @param context 上下文
* @param webSocketFrame 网络套接字框架
*/
public void handWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame) {
//判断是否是关闭websocket的指令
if (webSocketFrame instanceof CloseWebSocketFrame) {
webSocketServerHandshaker.close(context.channel(), (CloseWebSocketFrame) webSocketFrame.retain());
log.info("taskId:[{}]接收到关闭帧。。断开连接。。", this.taskId);
return;
}
//判断是否是ping消息
if (webSocketFrame instanceof PingWebSocketFrame) {
context.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
log.info("taskId:[{}]接收到心跳帧。。", this.taskId);
return;
}
//判断是否是二进制消息
if (webSocketFrame instanceof TextWebSocketFrame) {
this.handTextWebSocketFrame(context, webSocketFrame);
}
}
/**
* http请求处理程序
* http握手请求校验.
*
* @param context 上下文
* @param fullHttpRequest 完整http请求
*/
private void httpRequestHandler(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {
//判断是否http握手请求
if (!fullHttpRequest.decoderResult().isSuccess() || !(WEBSOCKET.equals(fullHttpRequest.headers().get(UPGRADE)))
|| !GET.equals(fullHttpRequest.method())) {
sendHttpResponse(context, new DefaultFullHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.BAD_REQUEST));
log.error("taskId:{{}}websocket握手内容不正确。。响应并关闭。。", taskId);
return;
}
String uri = fullHttpRequest.uri();
log.info("taskId:{{}}websocket握手uri[{}]", taskId, uri);
if (!ENDPOINT.equals(getBasePath(uri))) {
sendHttpResponse(context, new DefaultFullHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.NOT_FOUND));
log.info("taskId:[{}]websocket握手协议不正确。。响应并关闭。。", taskId);
return;
}
WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory("", null, false);
webSocketServerHandshaker = webSocketServerHandshakerFactory.newHandshaker(fullHttpRequest);
if (webSocketServerHandshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(context.channel());
log.info("taskId:[{}]websocket握手协议版本不正确。。响应并关闭。。", taskId);
return;
}
webSocketServerHandshaker.handshake(context.channel(), fullHttpRequest);
this.checkOpenInfo(context, fullHttpRequest);
}
/**
* 得到基本路径
*
* @param url url
* @return {@link String}
*/
public static String getBasePath(String url) {
if (StringUtils.isEmpty(url)) {
return null;
}
int idx = url.indexOf("?");
if (idx == -1) {
return url;
}
return url.substring(0, idx);
}
/**
* 发送http响应
* 服务端发送响应消息.
*
* @param context 上下文
* @param defaultFullHttpResponse 默认完整http响应
*/
private void sendHttpResponse(ChannelHandlerContext context, DefaultFullHttpResponse defaultFullHttpResponse) {
if (defaultFullHttpResponse.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(defaultFullHttpResponse.status().toString(), CharsetUtil.UTF_8);
defaultFullHttpResponse.content().writeBytes(buf);
buf.release();
}
//服务端向客户端发送数据
ChannelFuture future = context.channel().writeAndFlush(defaultFullHttpResponse);
if (defaultFullHttpResponse.status().code() != 200) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 回复消息给客户端.
*
* @param message 消息
* @return {@link ChannelFuture}
*/
protected ChannelFuture reply( String message) {
ChannelFuture channelFuture = context.writeAndFlush(new TextWebSocketFrame(message));
log.info("taskId:[{}]回复给客户端消息完成:[{}]", this.taskId, message);
return channelFuture;
}
/**
* 检查打开信息
* 检验连接打开时的信息.
*
* @param context 上下文
* @param fullHttpRequest 完整http请求
*/
protected abstract void checkOpenInfo(ChannelHandlerContext context, FullHttpRequest fullHttpRequest);
/**
* 手文本框架网络套接字
* 文本帧处理.
*
* @param context 上下文
* @param webSocketFrame 网络套接字框架
*/
protected abstract void handTextWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame);
}
实例化
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* 服务段实例化
*
* @author lukou
* @date 2023/05/17
*/
@Component
public class MyWebSocketServer extends BaseSocketServer {
private static final Logger log = LoggerFactory.getLogger(MyWebSocketServer.class);
@Override
public void channelActive(ChannelHandlerContext context) {
this.taskId = UUID.randomUUID().toString().replaceAll("-", "");
this.context = context;
log.info("taskId:[{}]有一个新请求进来了。。开始初始化上下文。。。", this.taskId);
}
@Override
public void channelInactive(ChannelHandlerContext context) {
log.info("taskId:[{}]识别服务触发关闭事件.", this.taskId);
// 这边可以收尾处理
}
@Override
protected void checkOpenInfo(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {
log.info("taskId:[{}]识别服务中websocket握手协议正确。。开始校验其它。。", this.taskId);
}
@Override
protected void handTextWebSocketFrame(ChannelHandlerContext context, WebSocketFrame webSocketFrame) {
String text = ((TextWebSocketFrame) webSocketFrame).text();
this.reply(this.taskId + " : " + text + System.currentTimeMillis());
}
}
之后,将业务处理层绑定到netty的channel上
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
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.IdleStateHandler;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import java.util.concurrent.TimeUnit;
/**
* example4网络套接字通道处理程序
*
* @author lukou
* @date 2023/05/17
*/
public class Example4WebSocketChannelHandler extends ChannelInitializer<SocketChannel> {
private static final EventExecutorGroup EVENT_EXECUTOR_GROUP = new DefaultEventExecutorGroup(100);
@Override
protected void initChannel(SocketChannel ch) {
// 设置30秒没有读到数据,则触发一个READER_IDLE事件。
ch.pipeline().addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
// websocket协议本身就是基于http协议的,所以这边也要使用http编解码器
ch.pipeline().addLast(new HttpServerCodec());
// 以块的方式来写处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
// netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
ch.pipeline().addLast(new HttpObjectAggregator(8192));
// 在管道中添加我们自己的接收数据实现方法
ch.pipeline().addLast(EVENT_EXECUTOR_GROUP, new MyWebSocketServer());
ch.pipeline().addLast(new WebSocketServerProtocolHandler(BaseSocketServer.ENDPOINT, null, true, 65536 * 10));
}
}
之后,就是真正的使用了,这里是选择项目一启动就执行netty服务端,并注入到容器中(这里看个人选择,不一定非要注入到spring中,直接new也一样)。
import com.example.wsdemo.websocketserver.example4.netty.Example4WebSocketChannelHandler;
import com.example.wsdemo.websocketserver.example4.netty.NettyServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServerConfig {
@Value("${netty.websocket.port:8081}")
private int port;
@Bean("example4WebSocketChannelHandler")
public Example4WebSocketChannelHandler example4WebSocketChannelHandler() {
return new Example4WebSocketChannelHandler();
}
@Bean("nettyServer")
public NettyServer nettyServer(Example4WebSocketChannelHandler example4WebSocketChannelHandler) {
return new NettyServer(this.port, example4WebSocketChannelHandler);
}
}
import com.example.wsdemo.websocketserver.example4.netty.NettyServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
/**
* example4网络套接字服务端启动初始化
*
* @author lukou
* @date 2023/05/17
*/
@Component
public class Example4WebSocketStartInit implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(Example4WebSocketStartInit.class);
@Resource
private NettyServer nettyServer;
/**
* 需要异步启动,不然会阻塞主线程
* 这里自定义一个线程启动,也可以在方法上加上注解@Async,一样的效果
*
* @param args arg游戏
*/
@Override
public void run(String... args) {
new Thread(() -> {
try {
nettyServer.start();
} catch (Exception e) {
log.error("识别服务中netty服务启动报错!", e);
}
}).start();
}
@PreDestroy
public void destroy() {
if (nettyServer != null) {
try {
nettyServer.destroy();
} catch (Exception e) {
log.error("停止netty服务发生异常!", e);
}
}
log.info("netty识别服务已经销毁。。");
}
}
启动测试,出现如下就代表服务启动成功了。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.5.RELEASE)
2023-05-18 14:36:06.423 INFO 3624 --- [ main] c.e.w.w.WebsocketServerApplication : Starting WebsocketServerApplication on qianpeng with PID 3624 (D:\projects\websocket-max\websocket-server\target\classes started by 钱鹏 in D:\projects\websocket-max)
2023-05-18 14:36:06.425 INFO 3624 --- [ main] c.e.w.w.WebsocketServerApplication : No active profile set, falling back to default profiles: default
2023-05-18 14:36:06.957 INFO 3624 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9000 (http)
2023-05-18 14:36:06.963 INFO 3624 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-05-18 14:36:06.963 INFO 3624 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.39]
2023-05-18 14:36:07.012 INFO 3624 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-05-18 14:36:07.012 INFO 3624 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 560 ms
2023-05-18 14:36:07.078 INFO 3624 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'clientInboundChannelExecutor'
2023-05-18 14:36:07.079 INFO 3624 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'clientOutboundChannelExecutor'
2023-05-18 14:36:07.164 INFO 3624 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'defaultSockJsTaskScheduler'
2023-05-18 14:36:07.181 INFO 3624 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'messageBrokerTaskScheduler'
2023-05-18 14:36:07.186 INFO 3624 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'brokerChannelExecutor'
2023-05-18 14:36:07.319 INFO 3624 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path ''
2023-05-18 14:36:07.320 INFO 3624 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting...
2023-05-18 14:36:07.320 INFO 3624 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]]
2023-05-18 14:36:07.320 INFO 3624 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Started.
2023-05-18 14:36:07.325 INFO 3624 --- [ main] c.e.w.w.WebsocketServerApplication : Started WebsocketServerApplication in 1.11 seconds (JVM running for 1.619)
2023-05-18 14:36:07.679 INFO 3624 --- [ Thread-4] c.e.w.w.example4.netty.NettyServer : netty服务启动。。正在监听:[/0:0:0:0:0:0:0:0:8081]
websocket的访问地址为:ws://localhost:8081/example4/ws
端口不再是项目的端口了。
另外,启动nettyserver的时候需要另起一个线程,不能直接在主线程中,不然会阻塞在那边的。
欢迎指正!