1.Maven需要的依賴
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
初始化netty設置解碼器,編碼器(可以自定義)
public class WebScoketServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//http解碼器(websocket是基於http協議的,所以可以直接用現成的
http解碼器)
pipeline.addLast(new HttpServerCodec());
//對寫大數據流的支持
pipeline.addLast(new ChunkedWriteHandler());
//設置單次請求的文件的大小
pipeline.addLast(new HttpObjectAggregator(1024*1024*10));
//webscoket 服務器處理的協議,用於指定給客戶端連接訪問的路由 :/ws
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//自定義handler(作用類似controller,客戶端和服務器端之間發消息都在這個自定義handler裏面)
pipeline.addLast("handler",new WebSocketHandler());
}
}
websocket啓動類的實現
@Component
public class WebSocketServer {
private static EventExecutorGroup group = new DefaultEventExecutorGroup(1024);
public void run(String... args) throws Exception {
//創建兩個線程
EventLoopGroup bossGroup = null;
EventLoopGroup workerGroup = null;
try {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
//服務對象
ServerBootstrap serverBootstrap = new ServerBootstrap();
//構建工程線程池
serverBootstrap.group(bossGroup, workerGroup)
//Nio長連接
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))
//初始化 過濾 解碼
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//配置http解碼組件
pipeline.addLast("http-codec", new HttpServerCodec());
pipeline.addLast("ping", new IdleStateHandler(2, 1, 3, TimeUnit.MINUTES));
//把多個消息轉換爲一個單一 Http請求或響應
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
//文件傳輸
pipeline.addLast("http-chunked", new ChunkedWriteHandler());
//邏輯 連接 服務ing 異常 斷開
pipeline.addLast(group, "handler", new WebSocketHandler());
}
});
//綁定監聽端口號
ChannelFuture channelFuture = serverBootstrap.bind(8082).sync();
//關閉
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
}
websocket處理類
@Slf4j
public class WebSocketHandler extends ChannelInboundHandlerAdapter {
private WebSocketServerHandshaker handShaker;
Connections connections = Connections.getInstance();
/**
* 打開連接
*
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("------------頁面建立連接---------------");
Connections connections = Connections.getInstance();
connections.saveConnection("1",ctx);
super.channelActive(ctx);
}
/**
* 斷開連接時 移除管道 刪除map中管道對應的用戶
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
log.info("[" + ctx.channel().remoteAddress().toString() + "] connect lost.");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("-----------連接出錯------------");
cause.printStackTrace();
ctx.close();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 傳統的HTTP接入
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
//websocket建立連接是基於websocket
log.info("這是http接入了");
}
// WebSocket接入
else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
/**
* 判斷請求內容
*
* @param ctx
* @param frame
*/
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
// 判斷是否是關閉鏈路的指令
if (frame instanceof CloseWebSocketFrame) {
handShaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判斷是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程僅支持文本消息,不支持二進制消息
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
}
try {
log.info("收到的客戶端消息是:"+ ((TextWebSocketFrame) frame).text());
ctx.channel().write(new TextWebSocketFrame("我收到消息啦"));
ctx.flush();
} catch (Exception e2) {
e2.printStackTrace();
log.info("處理Socket請求異常");
}
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
// 如果HTTP解碼失敗,返回HTTP異常
if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
String uri = req.uri();
System.out.println("請求的鏈接地址" + uri);
// 構造握手響應返回
WebSocketServerHandshakerFactory wsFactory =
new WebSocketServerHandshakerFactory("ws://" + req.headers().get(HttpHeaderNames.HOST) + uri, null, false);
handShaker = wsFactory.newHandshaker(req);
if (handShaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handShaker.handshake(ctx.channel(), req);
}
}
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
// 返回應答給客戶端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 如果是非Keep-Alive,關閉連接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
配置Application啓動類
@SpringBootApplication
@EnableSwagger2
public class LoginApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(LoginApplication.class, args);
}
WorkThreadPool workThreadPool = new WorkThreadPool();
public void run(String... args) throws Exception {
workThreadPool.getPool().execute(() -> {
try {
new WebSocketServer().run();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
客戶端在線測試工具:websocket在線測試工具
1.啓動SpringBoot項目
2.輸入連接地址連接
可以測試發消息給客戶端
在代碼裏面服務端也可以打印客戶端的消息
這樣就可以通信了,獲取到客戶端消息。
如果需要羣發消息,需要自己手寫前端代碼建立連接的時候傳一個唯一標識,後端用HashMap或者自定義一個類用來存儲來自不同地方的連接,發消息時候遍歷取到每一個連接,使用ChannelHandlerContext.channel.write(new TextWebsocketFrame(“消息內容”));就可以實現羣發消息
還有就是,如果業務需要返回前端的數據來自數據庫(要調用service層代碼情況),實例化的時候有可能使用Autowire註解實例化失敗會報空指針(先檢查Handler類有沒有用@Component),試着換用setter方式注入,就可以解決了.
源碼地址