Netty框架实战篇 - 基于WebSocket实现网页版的聊天室服务器

本文详细介绍了如何使用Netty实现WebSocket协议的聊天室服务器,包括WebSocket简介、通信握手过程,以及Netty对WebSocket数据帧的支持。实战部分阐述了如何配置WebSocket服务器、处理HTTP请求和WebSocket数据帧,实现群聊和单聊功能。测试环节展示了如何启动多个窗口进行群聊和单聊。总结中提到,虽然WebSocket协议有局限性,但在实时通信场景中具有优势。
摘要由CSDN通过智能技术生成

WebSocket简介

WebSocket协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执

WebSocket特点:

  1. HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
  2. 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户端与服务端频繁交互的情况下,如实时共享、多人协作等平台
  3. 采用新的协议,后端需要单独实现
  4. 客户端并不是所有浏览器都支持

WebSocket通信握手

在从标准的 HTTP 或者 HTTPS协议切换到WebSocket时,将会使用一种称为握手的机制 ,因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后

下面是WebSocket请求和响应的标识信息:
在这里插入图片描述

客户端的请求:

  1. Connection属性中标识Upgrade,表示客户端希望连接升级
  2. Upgrade属性中标识为Websocket,表示希望升级成 Websocket 协议
  3. Sec-WebSocket-Key属性,表示随机字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
  4. Sec-WebSocket-Version属性,表示支持的 Websocket 版本,RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用

服务器端响应:

  1. Upgrade属性中标识为websocket
  2. Connection告诉客户端即将升级的是 Websocket 协议
  3. Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key

Netty为WebSocket数据帧提供的支持

由 IETF 发布的WebSocket RFC,定义了6种帧,Netty为它们每种都提供了一个POJO实现

帧类型 描述
BinaryWebSocketFrame 包含了二进制数据
TextWebSocketFrame 包含了文本数据
ContinuationWebSocketFrame 包含属于上一个BinaryWebSocketFrame 或TextWebSocketFrame 的文本或者二进制数据
CloseWebSocketFrame 标识一个CLOSE请求,包含一个关闭的状态码
PingWebSocketFrame 请求传输一个PongWebSocketFrame
PongWebSocketFrame 作为一个对于PingWebSocketFrame的响应被发送

实战

首先,定义WebSocket服务端,其中创建了一个Netty提供ChannelGroup变量用来记录所有已经连接的客户端channel,而这个ChannelGroup就是用来完成群发和单聊功能的

//定义websocket服务端
public class WebSocketServer {
   

	private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
	private static EventLoopGroup workerGroup = new NioEventLoopGroup();
    private static ServerBootstrap bootstrap = new ServerBootstrap();
	
	private static final int PORT =8761;

	//创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
	private final static ChannelGroup channelGroup =
            new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
	
	public static void startServer(){
   
		try {
   
			bootstrap.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new WebSocketServerInitializer(channelGroup));
            Channel ch = bootstrap.bind(PORT).sync().channel();
            System.out.println("打开浏览器访问: http://127.0.0.1:" + PORT + '/');
            ch.closeFuture().sync();
		} catch (Exception e) {
   
			e.printStackTrace();
		}finally{
   
			bossGroup.shutdownGracefully();
	        workerGroup.shutdownGracefully();
		}
	}
	public static void main(String[] args) {
   
		startServer();
	}
}

接下来,初始化Pipeline,向当前Pipeline中注册所有必需的ChannelHandler,主要包括:用于处理HTTP请求编解码的HttpServerCodec、自定义的处理HTTP请求的HttpRequestHandler、用于处理WebSocket帧数据以及升级握手的WebSocketServerProtocolHandler以及自定义的处理TextWebSocketFrame数据帧和握手完成事件的WebSocketServerHanlder

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{
   

	/*websocket访问路径*/
    private static final String WEBSOCKET_PATH = "/ws";
	
    private ChannelGroup channelGroup;
	
	public WebSocketServerInitializer(ChannelGroup channelGroup){
   
		this.channelGroup=channelGroup;
	} 
    
	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
   
		//用于HTTP请求的编解码
		ch.pipeline().addLast(new HttpServerCodec());
		//用于写入一个文件的内容
		ch.pipeline().addLast(new ChunkedWriteHandler());
		//用于http请求的聚合
		ch.pipeline().addLast(new HttpObjectAggregator(64*1024));
		//用于WebSocket应答数据压缩传输
		ch.pipeline().addLast(new WebSocketServerCompressionHandler());
		//处理http请求,对非websocket请求的处理
		ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));
		//根据websocket规范,处理升级握手以及各种websocket数据帧
		ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, "", true));
		//对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件
		ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));
	}
}

HttpRequestHandler用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了WebSocket的URI,如果是那么HttpRequestHandler将调用FullHttpRequest对象上的retain方法,并通过调用fireChannelRead(msg)方法将它转发给下一个ChannelInboundHandler(之所以调用retain方法,是因为调用channelRead0方法完成之后,会进行资源释放)

接下来,读取磁盘上指定路径的index.html文件内容,将内容封装成ByteBuf对象,之后,构造一个FullHttpResponse响应对象,将ByteBuf添加进去,并设置请求头信息。最后,调用writeAndFlush方法冲刷所有写入的消息

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
   

	private static final File INDEX = new File("D:/学习/index.html");
	
	private String websocketUrl;
	
	public HttpRequestHandler(String websocketUrl)
	{
   
		this.websocketUrl = websocketUrl;
	}
	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
   
		if(websocketUrl.equalsIgnoreCase(msg.getUri())){
   
			//如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理
			ctx.fireChannelRead(msg.retain());
		}else{
   
			//生成index页面的具体内容,并送往浏览器
			ByteBuf content = loadIndexHtml(); 
			FullHttpResponse res = new DefaultFullHttpResponse(
		                    HTTP_1_1, OK, content);
            
		    res.headers().set(HttpHeaderNames.CONTENT_TYPE,
		                    "text/html; charset=UTF-8");
		    HttpUtil.setContentLength(res, content.readableBytes());
		    sendHttpResponse(ctx, msg, res);
		}
	}
	
	public static ByteBuf loadIndexHtml(){
   
		FileInputStream fis = null;
		InputStreamReader isr = null;
		BufferedReader  raf = null;
		StringBuffer content = new StringBuffer()
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值