其他
【138期】手撸 websocket + netty 实时视频弹幕交互功能(Java版附源码)
点击上方“Java精选”,选择“设为星标”
别问别人为什么,多问自己凭什么!
下方留言必回,有问必答!
每天 08:00 更新文章,每天进步一点点...
1、 技术选型
1.1 netty
1.2 WebSocket
1.3 为什么做这样的技术选型。
2 、实现思路
2.1 服务架构
2.2 传输流程
3 、实现效果
3.1 视频展示
4、 代码实现
4.1 项目结构
4.2 Java服务端
4.2.1 先做一个netty nio的服务端:
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) {
}
}
4.2.2 服务端的具体处理逻辑
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());
}
}
4.3 网页客户端实现
<!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>
5 、小结
作者:兴趣使然的程序猿binhao.blog.csdn.net/article/details/112631642
精品资料,超赞福利!
- 小程序,3000+ 道面试题在线刷,最新、最全 Java 面试题!
【130期】面试官问:HashMap 中变量 modCount 的真实作用是什么?
【131期】面试官问:Comparable 和 Comparator 接口有啥区别?
【132期】面试官:Java 8 数据过滤,removeIf 和 filter 有什么区别?
【133期】面试官:TCP 连接大量 TIME_WAIT 状态 ,对业务有什么影响?如何处理?
【135期】面试官问:为什么要合并 HTTP 请求,实现batch call?