您现在的位置是:亿华云 > 域名

老板怒吼:今晚整一个B站弹幕交互功能

亿华云2025-10-03 02:15:10【域名】9人已围观

简介图片来自 包图网今天笔者就抽空做了一个实时视频弹幕交互的小功能,不得不说这样的形式为看视频看直播,讲义 PPT,抽奖等形式增加了许多乐趣。技术选型①Netty官方对于 Netty 的描述:https:

 

图片来自 包图网

今天笔者就抽空做了一个实时视频弹幕交互的老板小功能,不得不说这样的怒吼形式为看视频看直播,讲义 PPT,今晚交互抽奖等形式增加了许多乐趣。整个B站

技术选型

①Netty

官方对于 Netty 的弹幕描述:

https://netty.io/ 

主要关键词描述:Netty 是异步事件驱动网络框架,可做各种协议服务端,老板并且支持了 FTP,怒吼SMTP,今晚交互HTTP 等很多协议,整个B站并且性能,弹幕稳定性,老板灵活性都很棒。怒吼

可以看到 Netty 整体架构上分了三个部分:

以零拷贝,今晚交互一致性接口,整个B站扩展事件模型的弹幕底层核心。 Socket,Datagram,Pipe,Http Tunnel 作为传输媒介。 传输支持的各种协议,HTTP&WebSocket,SSL,大文件,zlib/gzip 压缩,文本,二进制,Google Protobuf 等各种各种的传输形式。源码下载

②WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 通信协议于 2011 年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范。

WebSocket API 也被 W3C 定为标准。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

为什么做这样的技术选型:

由上述可知,实时直播交互作为互动式是一个双向数据传输过程。所以使用 WebSocket。 Netty 本身支持了 WebSocket 协议的实现,让实现更加简单方便。

实现思路

①服务架构

整体架构是所有客户端都和我的服务端开启一个双向通道的架构。

②传输流程

如下图:

实现效果

先看看效果吧,是不是 perfect,接下来就来看具体代码是怎么实现的吧。站群服务器

视频直播弹幕示例

代码实现

①项目结构

一个 maven 项目,将代码放一个包下就行。

②Java 服务端

Java 服务端代码,总共三个类,Server,Initailizer 和 Handler。

先做一个 netty nio 的服务端:一个 nio 的服务,开启一个 tcp 端口。

import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; /**  * Copyright(c)lbhbinhao@163.com  * @author liubinhao  * @date 2021/1/14  * ++++ ______                           ______             ______  * +++/     /|                         /     /|           /     /|  * +/_____/  |                       /_____/  |         /_____/  |  * |     |   |                      |     |   |        |     |   |  * |     |   |                      |     |   |________|     |   |  * |     |   |                      |     |  /         |     |   |  * |     |   |                      |     |/___________|     |   |  * |     |   |___________________   |     |____________|     |   |  * |     |  /                  / |  |     |   |        |     |   |  * |     |/ _________________/  /   |     |  /         |     |  /  * |_________________________|/b    |_____|/           |_____|/  */ public enum BulletChatServer {      /**      * Server instance      */     SERVER;     private BulletChatServer(){          EventLoopGroup mainGroup = new NioEventLoopGroup();         EventLoopGroup subGroup  = new NioEventLoopGroup();         ServerBootstrap server = new ServerBootstrap();         server.group(mainGroup,subGroup)                 .channel(NioServerSocketChannel.class)                 .childHandler(new BulletChatInitializer());         ChannelFuture future = server.bind(9123);     }     public static void main(String[] args) {      } } 

服务端的具体处理逻辑:

import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; /**  * Copyright(c)lbhbinhao@163.com  *  * @author liubinhao  * @date 2021/1/14  * ++++ ______                           ______             ______  * +++/     /|                         /     /|           /     /|  * +/_____/  |                       /_____/  |         /_____/  |  * |     |   |                      |     |   |        |     |   |  * |     |   |                      |     |   |________|     |   |  * |     |   |                      |     |  /         |     |   |  * |     |   |                      |     |/___________|     |   |  * |     |   |___________________   |     |____________|     |   |  * |     |  /                  / |  |     |   |        |     |   |  * |     |/ _________________/  /   |     |  /         |     |  /  * |_________________________|/b    |_____|/           |_____|/  */ public class BulletChatInitializer extends ChannelInitializer<SocketChannel> {      @Override     protected void initChannel(SocketChannel ch) throws Exception {          ChannelPipeline pipeline = ch.pipeline();         pipeline.addLast(new HttpServerCodec());         pipeline.addLast(new ChunkedWriteHandler());         pipeline.addLast(new HttpObjectAggregator(1024*64));         pipeline.addLast(new IdleStateHandler(8, 10, 12));         pipeline.addLast(new WebSocketServerProtocolHandler("/lbh"));         pipeline.addLast(new BulletChatHandler());     } } 

后台处理逻辑,接受到消息,写出到所有的客户端:

import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.concurrent.GlobalEventExecutor; /**  * Copyright(c)lbhbinhao@163.com  *  * @author liubinhao  * @date 2021/1/14  * ++++ ______                           ______             ______  * +++/     /|                         /     /|           /     /|  * +/_____/  |                       /_____/  |         /_____/  |  * |     |   |                      |     |   |        |     |   |  * |     |   |                      |     |   |________|     |   |  * |     |   |                      |     |  /         |     |   |  * |     |   |                      |     |/___________|     |   |  * |     |   |___________________   |     |____________|     |   |  * |     |  /                  / |  |     |   |        |     |   |  * |     |/ _________________/  /   |     |  /         |     |  /  * |_________________________|/b    |_____|/           |_____|/  */ public class BulletChatHandler  extends SimpleChannelInboundHandler<TextWebSocketFrame> {      // 用于记录和管理所有客户端的channel     public static ChannelGroup channels =             new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);     @Override     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {          // 获取客户端传输过来的消息         String content = msg.text();         System.err.println("收到消息:"+ content);         channels.writeAndFlush(new TextWebSocketFrame(content));         System.err.println("写出消息完成:"+content);     }     @Override     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {          channels.add(ctx.channel());     }     @Override     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {          String channelId = ctx.channel().id().asShortText();         System.out.println("客户端被移除,channelId为:" + channelId);         channels.remove(ctx.channel());     }     @Override     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {          cause.printStackTrace();         // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除         ctx.channel().close();         channels.remove(ctx.channel());     } } 

③网页客户端实现

代码如下:

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="utf-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <title>Netty视频弹幕实现 Author:Binhao Liu</title>     <link rel="stylesheet" href="">     <style type="text/css" media="screen">         * {              margin: 0px;             padding: 0px         }         html, body {              height: 100%         }         body {              overflow: hidden;             background-color: #FFF;             text-align: center;         }         .flex-column {              display: flex;             flex-direction: column;             justify-content: space-between;, align-items: center;         }         .flex-row {              display: flex;             flex-direction: row;             justify-content: center;             align-items: center;         }         .wrap {              overflow: hidden;             width: 70%;             height: 600px;             margin: 100px auto;             padding: 20px;             background-color: transparent;             box-shadow: 0 0 9px #222;             border-radius: 20px;         }         .wrap .box {              position: relative;             width: 100%;             height: 90%;             background-color: #000000;             border-radius: 10px         }         .wrap .box span {              position: absolute;             top: 10px;             left: 20px;             display: block;             padding: 10px;             color: #336688         }         .wrap .send {              display: flex;             width: 100%;             height: 10%;             background-color: #000000;             border-radius: 8px         }         .wrap .send input {              width: 40%;             height: 60%;             border: 0;             outline: 0;             border-radius: 5px 0px 0px 5px;             box-shadow: 0px 0px 5px #d9d9d9;             text-indent: 1em         }         .wrap .send .send-btn {              width: 100px;             height: 60%;             background-color: #fe943b;             color: #FFF;             text-align: center;             border-radius: 0px 5px 5px 0px;             line-height: 30px;             cursor: pointer;         }         .wrap .send .send-btn:hover {              background-color: #4cacdc         }     </style> </head> <script>     var ws = new WebSocket("ws://localhost:9123/lbh");     ws.onopen = function () {          // Web Socket 已连接上,使用 send() 方法发送数据         alert("数据发送中...");     };     ws.onmessage = function (e) {          console.log("接受到消息:"+e.data);         createEle(e.data);     };     ws.onclose = function () {          // 关闭 websocket         alert("连接已关闭...");     };     function sendMsg(msg) {          ws.send(msg)     } </script> <body> <div class="wrap flex-column">     <div class="box">         <video src="shape.mp4" width="100%" height="100%" controls autoplay></video>     </div>     <div class="send flex-row">         <input type="text" class="con" placeholder="弹幕发送[]~(^v^)~*"/>         <div class="send-btn" onclick="javascript:sendMsg(document.querySelector(.con).value)">发送</div>     </div> </div> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js" type="text/javascript"></script> <script>     //1.获取元素     var oBox = document.querySelector(.box);   //获取.box元素     var cW = oBox.offsetWidth;   //获取box的宽度     var cH = oBox.offsetHeight;   //获取box的高度     function createEle(txt) {          //动态生成span标签         var oMessage = document.createElement(span);   //创建标签         oMessage.innerHTML = txt;   //接收参数txt并且生成替换内容         oMessage.style.left = cW + px;  //初始化生成位置x         oBox.appendChild(oMessage);   //把标签塞到oBox里面         roll.call(oMessage, {              //call改变函数内部this的指向             timing: [linear, ease-out][~~(Math.random() * 2)],             color: # + (~~(Math.random() * (1 << 24))).toString(16),             top: random(0, cH),             fontSize: random(16, 32)         });     }     function roll(opt) {          //弹幕滚动         //如果对象中不存在timing 初始化         opt.timing = opt.timing || linear;         opt.color = opt.color || #fff;         opt.top = opt.top || 0;         opt.fontSize = opt.fontSize || 16;         this._left = parseInt(this.offsetLeft);   //获取当前left的值         this.style.color = opt.color;   //初始化颜色         this.style.top = opt.top + px;         this.style.fontSize = opt.fontSize + px;         this.timer = setInterval(function () {              if (this._left <= 100) {                  clearInterval(this.timer);   //终止定时器                 this.parentNode.removeChild(this);                 return;   //终止函数             }             switch (opt.timing) {                  case linear:   //如果匀速                     this._left += -2;                     break;                 case ease-out:   //                     this._left += (0 - this._left) * .01;                     break;             }             this.style.left = this._left + px;         }.bind(this), 1000 / 60);     }     function random(start, end) {          //随机数封装         return start + ~~(Math.random() * (end - start));     }     var aLi = document.querySelectorAll(li);   //10     function forEach(ele, cb) {          for (var i = 0, len = aLi.length; i < len; i++) {              cb && cb(ele[i], i);         }     }     forEach(aLi, function (ele, i) {          ele.style.left = i * 100 + px;     });     //产生闭包     var obj = {          num: 1,         add: function () {              this.num++;   //obj.num = 2;             (function () {                  console.log(this.num);             })         }     };     obj.add();//window </script> </body> </html> 

这样一个实时的视频弹幕功能就完成啦,是不是很简单,各位小伙伴快来试试吧。

小结

这个还是很简单,笔者写这个的时候一会儿就写完了。亿华云不过这也得益于笔者很久以前就写过 Netty 的服务,对于 HTTP,TCP 之类协议也比较熟悉。

只有前端会有些难度,问下度娘,也很快能做完,在此分享出来与诸君分享,有问题可找笔者交流。

作者:兴趣使然的程序猿

编辑:陶家龙

出处:http://adkx.net/w71wf

很赞哦!(62393)