Netty從入門到禿頭: websocket
1. 核心依賴
<dependencies>
<!--netty的依賴集合,都整合在一個依賴裡面了-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
</dependencies>
2. 程式碼
2.1 啟動項
public class NioWebSocketServer {
private final Logger logger=Logger.getLogger(this.getClass());
private void init(){
logger.info("正在啟動websocket伺服器");
NioEventLoopGroup boss=new NioEventLoopGroup();
NioEventLoopGroup work=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap=new ServerBootstrap();
bootstrap.group(boss,work);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new NioWebSocketChannelInitializer());
Channel channel = bootstrap.bind(8081).sync().channel();
logger.info("webSocket伺服器啟動成功:"+channel);
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
logger.info("執行出錯:"+e);
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
logger.info("websocket伺服器已關閉");
}
}
public static void main(String[] args) {
new NioWebSocketServer().init();
}
}
netty搭建的伺服器基本上都是差不多的寫法:
- 繫結主執行緒組和工作執行緒組,這部分對應架構圖中的事件迴圈組
- 只有伺服器才需要繫結埠,客戶端是繫結一個地址
- 配置channel(資料通道)引數,重點就是ChannelInitializer的配置
- 以非同步的方式啟動,最後是結束關閉兩個執行緒組
2.2 ChannelInitializer寫法
public class NioWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//設定log監聽器,並且日誌級別為debug,方便觀察執行流程
ch.pipeline().addLast("http-codec",new HttpServerCodec());//設定解碼器
ch.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));//聚合器,使用websocket會用到
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());//用於大資料的分割槽傳輸
ch.pipeline().addLast("handler",new NioWebSocketHandler());//自定義的業務handler
}
}
2.3 自定義的處理器NioWebSocketHandler
public class NioWebSocketHandler extends SimpleChannelInboundHandler<Object> {
private final Logger logger=Logger.getLogger(this.getClass());
private WebSocketServerHandshaker handshaker;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.debug("收到訊息:"+msg);
if (msg instanceof FullHttpRequest){
//以http請求形式接入,但是走的是websocket
handleHttpRequest(ctx, (FullHttpRequest) msg);
}else if (msg instanceof WebSocketFrame){
//處理websocket客戶端的訊息
handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//新增連線
logger.debug("客戶端加入連線:"+ctx.channel());
ChannelSupervise.addChannel(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//斷開連線
logger.debug("客戶端斷開連線:"+ctx.channel());
ChannelSupervise.removeChannel(ctx.channel());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handlerWebSocketFrame(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)) {
logger.debug("本例程僅支援文字訊息,不支援二進位制訊息");
throw new UnsupportedOperationException(String.format(
"%s frame types not supported", frame.getClass().getName()));
}
// 返回應答訊息
String request = ((TextWebSocketFrame) frame).text();
logger.debug("服務端收到:" + request);
TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
+ ctx.channel().id() + ":" + request);
// 群發
ChannelSupervise.send2All(tws);
// 返回【誰發的發給誰】
// ctx.channel().writeAndFlush(tws);
}
/**
* 唯一的一次http請求,用於建立websocket
* */
private void handleHttpRequest(ChannelHandlerContext ctx,
FullHttpRequest req) {
//要求Upgrade為websocket,過濾掉get/Post
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
//若不是websocket方式,則建立BAD_REQUEST的req,返回給客戶端
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://localhost:8081/websocket", 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, DefaultFullHttpResponse res) {
// 返回應答給客戶端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
ChannelFuture f = ctx.channel().writeAndFlush(res);
// 如果是非Keep-Alive,關閉連線
if (!isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
執行流程是:
- web發起一次類似是http的請求,並在channelRead0方法中進行處理,並通過instanceof去判斷幀物件是FullHttpRequest還是WebSocketFrame,建立連線是時候會是FullHttpRequest
- 在handleHttpRequest方法中去建立websocket,首先是判斷Upgrade是不是websocket協議,若不是則通過sendHttpResponse將錯誤資訊返回給客戶端,緊接著通過WebSocketServerHandshakerFactory建立socket物件並通過handshaker握手建立連線
- 在連線建立好後的所以訊息流動都是以WebSocketFrame來體現
- 在handlerWebSocketFrame去處理訊息,也可能是客戶端發起的關閉指令,ping指令等等
2.4 儲存客戶端的資訊
當有客戶端連線時候會被channelActive監聽到,當斷開時會被channelInactive監聽到,一般在這兩個方法中去儲存/移除客戶端的通道資訊,而通道資訊儲存在ChannelSupervise中:
public class ChannelSupervise {
private static ChannelGroup GlobalGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private static ConcurrentMap<String, ChannelId> ChannelMap=new ConcurrentHashMap();
public static void addChannel(Channel channel){
GlobalGroup.add(channel);
ChannelMap.put(channel.id().asShortText(),channel.id());
}
public static void removeChannel(Channel channel){
GlobalGroup.remove(channel);
ChannelMap.remove(channel.id().asShortText());
}
public static Channel findChannel(String id){
return GlobalGroup.find(ChannelMap.get(id));
}
public static void send2All(TextWebSocketFrame tws){
GlobalGroup.writeAndFlush(tws);
}
}
ChannelGroup是netty提供用於管理web於伺服器建立的通道channel的,其本質是一個高度封裝的set集合,在伺服器廣播訊息時,可以直接通過它的writeAndFlush將訊息傳送給集合中的所有通道中去。但在查詢某一個客戶端的通道時候比較坑爹,必須通過channelId物件去查詢,而channelId不能人為建立,所有必須通過map將channelId的字串和channel儲存起來。
相關文章
- WebSocket:5分鐘從入門到精通Web
- WebSocket 從入門到寫出開源庫Web
- scala 從入門到入門+
- makefile從入門到入門
- WebSocket 入門Web
- kafka從入門到關門Kafka
- Netty入門Netty
- WebSocket 快速入門Web
- Thymeleaf從入門到精通
- LESS從入門到精通
- Git 從入門到精通Git
- Shell從入門到精通
- Promise從入門到精通Promise
- babel從入門到跑路Babel
- Jdbc從入門到入土JDBC
- vim從入門到精通
- Charles 從入門到精通
- RabbitMQ從入門到精通MQ
- SAP從入門到精通
- redis從入門到精通Redis
- SA:從入門到入土
- MyBatis從入門到精通(一):MyBatis入門MyBatis
- Service Worker 從入門到出門
- 慕課網《Netty入門之WebSocket初體驗》學習總結NettyWeb
- 0到1,Celery從入門到出家
- Netty-入門Netty
- Netty入門程式Netty
- ElasticSearch 7.8.1 從入門到精通Elasticsearch
- Vue 從入門到放棄Vue
- Docker 從入門到掉坑Docker
- Git 從入門到放棄Git
- GraphQL從入門到放棄
- GraphQL 從入門到實踐
- Redis從入門到進階Redis
- 幀數,從入門到入土
- Eclipse從入門到精通Eclipse
- RabbitMQ 從入門到精通 (一)MQ
- RxJava 從入門到全解析RxJava