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>
运行之后,就可以开启两个网页就行测试了,可以看到弹幕飞过!