Reacto线程模型,基于Netty的弹幕系统案例

Reacto线程模型,基于Netty的弹幕系统案例

1. 开门见山

相信大家都看过多多少少的直播,这里我们只拿出弹幕系统来模拟一下。

1.1 弹幕系统特点

1.实时性:你发大家收,毫秒之差,你这边发了一个弹幕,其他用户会在毫秒间看到你的弹幕飘过
2.并发性:一人直播,万人叭叭,比如主播发了一个“火力全开” ,发弹幕抽奖的活动,突然间上万的人要去各种刷弹幕,也要保证不崩掉,数据也要在。
3.大数据量:那比如有人来晚了,主播的消息被顶到很远,你还想翻翻。

1.2 架构模型

在这里插入图片描述

1.3 实现方案

标题已经说了,我们要使用的是netty,reactor模型。那么为什么要使用reactor?

每个连接都需要独立线程处理,当并发数大时,创建线程数多,占用资源
采用阻塞IO模型,连接建立后,若当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费

针对传统阻塞IO模型的两个问题,可以采用如下的方案

基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理
基于IO复用模型,多个连接共用同一个阻塞对象,不用等待所有的连接。遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理

根据Reactor的数量和处理资源的线程数量的不同,分为三类:

单Reactor单线程模型

单Reactor多线程模型

多Reactor多线程模型

单reactor单线程相当于一个饭店接待员服务员是同一个人,对于用户流量小还可以,接待服务都是自己完成
单reactor多线程就好比一个接待员多个服务员,每个服务员负责几个区域的桌子就行了
多reactor多线程模型就是多个接待员多个服务员,应付客流量很大的问题,一个接待员忙不过来的情况

具体的介绍可以参考百度
我们这里做的demo使用多reactor多线程模型。先上代码。

实现

服务端启动类:

/**
 * Websocket 聊天服务器-服务端
 *
 */
public class WebsocketDanmuServer {
    
    

    private int port;

    public WebsocketDanmuServer(int port) {
    
    
        this.port = port;
    }

    public void run() throws Exception {
    
    
        
        EventLoopGroup bossGroup = new NioEventLoopGroup(2); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup(3);
        try {
    
    
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new WebsocketDanmuServerInitializer())  //(4)
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
            
    		System.out.println("SnakeGameServer 启动了" + port);
    		
            // 绑定端口,开始接收进来的连接
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 等待服务器  socket 关闭 。
            // 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
            f.channel().closeFuture().sync();

        } finally {
    
    
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            
    		System.out.println("SnakeGameServer 关闭了");
        }
    }

    public static void main(String[] args) throws Exception {
    
    
        int port;
        if (args.length > 0) {
    
    
            port = Integer.parseInt(args[0]);
        } else {
    
    
            port = 8080;
        }
        new WebsocketDanmuServer(port).run();

    }
}

注意看run方法里的 bossGroup,workerGroup实际是就是多reactor,多线程

服务端初始器:

**
 * 服务端 ChannelInitializer
 *
 */
public class WebsocketDanmuServerInitializer extends
        ChannelInitializer<SocketChannel> {
    
    	//1

	@Override
    public void initChannel(SocketChannel ch) throws Exception {
    
    //2
		 ChannelPipeline pipeline = ch.pipeline();
		pipeline.addLast("http-decodec",new HttpRequestDecoder());
		pipeline.addLast("http-aggregator",new HttpObjectAggregator(65536));
		pipeline.addLast("http-encodec",new HttpResponseEncoder());
		pipeline.addLast("http-chunked",new ChunkedWriteHandler());
		pipeline.addLast("WebSocket-protocol",new WebSocketServerProtocolHandler("/ws"));
		pipeline.addLast("WebSocket-request",new MyWebSocketFrameHandler());

    }
}```
channel处理器:

```java
/**
 * 处理TextWebSocketFrame
 *
 */
public class MyWebSocketFrameHandler extends
        SimpleChannelInboundHandler<TextWebSocketFrame> {
    
    

	// 用于记录和管理所有客户端的channel
	public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx,
			TextWebSocketFrame msg) throws Exception {
    
     // (1)
		Channel incoming = ctx.channel();
		for (Channel channel : channels) {
    
    
            if (channel != incoming){
    
    
				channel.writeAndFlush(new TextWebSocketFrame(msg.text()));
			} else {
    
    
				channel.writeAndFlush(new TextWebSocketFrame("我发送的"+msg.text() ));
			}
        }
	}
	
	@Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    
      // (2)
        Channel incoming = ctx.channel();
        
        // Broadcast a message to multiple Channels
        channels.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 加入"));
        
        channels.add(incoming);
		System.out.println("Client:"+incoming.remoteAddress() +"加入");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    
      // (3)
        Channel incoming = ctx.channel();
        
        // Broadcast a message to multiple Channels
        channels.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 离开"));
        
		System.err.println("Client:"+incoming.remoteAddress() +"离开");

        // A closed Channel is automatically removed from ChannelGroup,
        // so there is no need to do "channels.remove(ctx.channel());"
    }
	    
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
     // (5)
        Channel incoming = ctx.channel();
		System.out.println("Client:"+incoming.remoteAddress()+"在线");
	}
	
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    
     // (6)
        Channel incoming = ctx.channel();
		System.err.println("Client:"+incoming.remoteAddress()+"掉线");
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)	// (7)
			throws Exception {
    
    
    	Channel incoming = ctx.channel();
		System.err.println("Client:"+incoming.remoteAddress()+"异常");
				// 当出现异常就关闭连接
				cause.printStackTrace();
        ctx.close();
	}

}

HTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
	<meta name="Keywords" content="danmu">
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>弹幕网站</title>
	<style type="text/css">
		body {
     
     
			background: url(http://ot0ak6eri.bkt.clouddn.com/01.jpg); no-repeat:top center;
			font-size: 12px;
			font-family: "微软雅黑";
		}


		* {
     
     
			margin: 0;
			padding: 0;
		}
		/* screen start*/
		.screen {
     
     
			width: 300px;
			height: 100px;
			background: #669900;
		}


		.dm {
     
     
			width: 100%;
			height: 100%;
			position: absolute;
			top: 0;
			left: 0;
			display: none;
		}


		.dm .d_screen .d_del {
     
     
			width: 38px;
			height: 38px;
			background: #600;
			display: block;
			text-align: center;
			line-height: 38px;
			text-decoration: none;
			font-size: 20px;
			color: #fff;
			border-radius: 19px;
			border: 1px solid #fff;
			z-index: 2;
			position: absolute;
			right: 20px;
			top: 20px;
			outline: none;
		}


		.dm .d_screen .d_del:hover {
     
     
			background: #F00;
		}


		.dm .d_screen .d_mask {
     
     
			width: 100%;
			height: 100%;
			background: #000;
			position: absolute;
			top: 0;
			left: 0;
			opacity: 0.6;
			filter: alpha(opacity = 60);
			z-index: 1;
		}


		.dm .d_screen .d_show {
     
     
			position: relative;
			z-index: 2;
		}


		.dm .d_screen .d_show div {
     
     
			font-size: 26px;
			line-height: 36px;
			font-weight: 500;
			position: absolute;
			top: 76px;
			left: 10;
			color: #fff;
		}
		/*end screen*/


		/*send start*/
		.send {
     
     
			width: 100%;
			height: 76px;
			position: absolute;
			bottom: 0;
			left: 0;
			border: 1px solid red;
		}


		.send .s_filter {
     
     
			width: 100%;
			height: 76px;
			background: #000;
			position: absolute;
			bottom: 0;
			left: 0;
			opacity: 0.6;
			filter: alpha(opacity = 60);
		}


		.send  .s_con {
     
     
			width: 100%;
			height: 76px;
			position: absolute;
			top: 0;
			left: 0;
			z-index: 2;
			text-align: center;
			line-height: 76px;
		}


		.send .s_con .s_text {
     
     
			width: 800px;
			height: 36px;
			border: 0;
			border-radius: 6px 0 0 6px;
			outline: none;
		}


		.send .s_con .s_submit {
     
     
			width: 100px;
			height: 36px;
			border-radius: 0 6px 6px 0;
			outline: none;
			font-size: 14px;
			color: #fff;
			background: #65c33d;
			font-family: "微软雅黑";
			cursor: pointer;
			border: 1px solid #5bba32;
		}


		.send .s_con .s_submit:hover {
     
     
			background: #3eaf0e;
		}
		/*end send*/
	</style>


</head>
<body>
<a href="#" id="startDm">开启弹幕</a>
<!-- dm start -->
<div class="dm">
	<!-- d_screen start -->
	<div class="d_screen">
		<a href="#" class="d_del">X</a>
		<div class="d_mask"></div>
		<div class="d_show">
		</div>
	</div>
	<!-- end d_screen -->


	<!-- send start -->
	<div class="send">
		<div class="s_filter"></div>
		<div class="s_con">
			<input type="text" class="s_text" /> <input type="button"
														value="发表评论" class="s_submit" id="btn"/>
		</div>
	</div>
	<!-- end send -->
</div>
<!-- end dm-->
<script type="text/javascript"
		src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.js"></script>

<script type="text/javascript" >

    String.prototype.endWith=function(str){
     
     
        if(str==null||str==""||this.length==0||str.length>this.length)
            return false;
        if(this.substring(this.length-str.length)==str)
            return true;
        else
            return false;
        return true;
    }

    String.prototype.startWith=function(str){
     
     
        if(str==null||str==""||this.length==0||str.length>this.length)
            return false;
        if(this.substr(0,str.length)==str)
            return true;
        else
            return false;
        return true;
    }
</script>
<!--<script type="text/javascript" src="websocket.js"></script>-->
<script type="text/javascript">
    $(function() {
     
     

        $("#startDm,.d_del").click(function() {
     
     
            $("#startDm,.dm").toggle(1000);
//init_screen();
        });
        $("#btn").click(function(){
     
     
            send();
        });
        $(".s_text").keydown(function() {
     
     
            var code = window.event.keyCode;





            if (code == 13)//回车键按下时,输出到弹幕


            {
     
     
                send();
            }
        });


    });


    function launch()
    {
     
     

        var _height = $(window).height();
        var _left = $(window).width() - $("#"+index).width();
        var time=10000;
        if(index%2==0)
            time=20000;
        _top+=80;
        if(_top>_height-100)
            _top=80;
        $("#"+index).css({
     
     
            left:_left,
            top:_top,
            color:getRandomColor()

        });
        $("#"+index).animate({
     
     
                left:"-"+_left+"px"},
            time,
            function(){
     
     });
        index++;
    }



	/* //初始化弹幕
	 function init_screen() {
	 var _top = 0;
	 var _height = $(window).height();
	 $(".d_show").find("div").show().each(function() {
	 var _left = $(window).width() - $(this).width();
	 var time=10000;
	 if($(this).index()%2==0)
	 time=20000;
	 _top+=80;
	 if(_top>_height-100)
	 _top=80;
	 $(this).css({
	 left:_left,
	 top:_top,
	 color:getRandomColor()

	 });
	 $(this).animate({
	 left:"-"+_left+"px"},
	 time,
	 function(){});


	 });
	 } */

    //随机获取颜色值
    function getRandomColor() {
     
     
        return '#' + (function(h) {
     
     
                return new Array(7 - h.length).join("0") + h
            })((Math.random() * 0x1000000 << 0).toString(16))
    }
</script>

<script type="text/javascript">

    var websocket=null;
    var _top=80;
    var index=0;

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
     
     
        websocket=new WebSocket("ws://127.0.0.1:8080/ws");
    }
    else{
     
     
        alert("Not Support WebSocket!");
    }


    //连接发生错误的回调方法
    websocket.onerror = function(){
     
     
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(event){
     
     
        setMessageInnerHTML("open");
    }

    //接收到消息的回调方法
	// 收到服务器发送的消息
    websocket.onmessage = function(){
     
     
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
     
     
        setMessageInnerHTML("close");
    }


    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
     
     
        websocket.close();
    }


    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
     
     
        //修改背景图
        var imgurl;
        if (innerHTML.startWith("~background,")) {
     
     
            var cmd = innerHTML;
            imgurl = cmd.split(",")[1];
            document.body.style.background = "url("+imgurl+")";
        }else{
     
     
            $(".d_show").append("<div id='"+index+"'>"+ innerHTML + "</div>");
		}

        launch();
    }


    //发送消息
    function send(){
     
     
        //var message = document.getElementById('text').value;
        var message = $(".s_text").val();
        $(".s_text").val("");
        websocket.send(message);
    }
</script>

</body>
</html>

运行之后,就可以开启两个网页就行测试了,可以看到弹幕飞过!

猜你喜欢

转载自blog.csdn.net/qq_44772660/article/details/113649227