Java+Netty+WebRTC、语音、视频、屏幕共享【聊天室设计实践】

背景

本文使用webtrc实现了一个简单的语音视频聊天室、支持多人音视频聊天、屏幕共享。

环境配置

音视频功能需要在有Https协议的域名下才能获取到设备信息,

测试环境搭建Https服务参考Windows下Nginx配置SSL实现Https访问(包含openssl证书生成)_殷长庆的博客-CSDN博客

正式环境可以申请一个免费的证书 

复杂网络环境下需要自己搭建turnserver,网络上搜索大多是使用coturn来搭建turn服务 

turn默认监听端口3478,可以使用webrtc.github.io 测试服务是否可用

本文在局域网内测试,不必要部署turn,使用的谷歌的stun:stun.l.google.com:19302

webrtc参考文章

WebRTC技术简介 - 知乎 (zhihu.com)

实现 

服务端 

服务端使用netty构建一个websocket服务,用来完成为音视频传递ICE信息等工作。 

maven配置


	4.0.0
	com.luck.cc
	cc-im
	1.0-SNAPSHOT
	cc-im
	http://maven.apache.org

	
		${env.JAVA_HOME}
		UTF-8
		1.8
	

	
		
			io.netty
			netty-all
			4.1.74.Final
		
        
            cn.hutool
            hutool-all
            5.5.7
        
	
	
		
			
				maven-compiler-plugin
				
					1.8
					1.8
				
			
	        
	            maven-assembly-plugin
	            3.0.0
	            
	                
	                    
	                        com.luck.im.ServerStart
	                    
	                
	                
	                    jar-with-dependencies
	                
	            
	            
	                
	                    make-assembly
	                    package
	                    
	                        single
	                    
	                
	            
	        
		
	

 JAVA代码

 聊天室服务

package com.luck.im;

import java.util.List;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

public class ChatSocket {
	private static EventLoopGroup bossGroup = new NioEventLoopGroup();
	private static EventLoopGroup workerGroup = new NioEventLoopGroup();
	private static ChannelFuture channelFuture;

	/**
	 * 启动服务代理
	 * 
	 * @throws Exception
	 */
	public static void startServer() throws Exception {
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer() {
						@Override
						public void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new HttpServerCodec());
							pipeline.addLast(
									new WebSocketServerProtocolHandler("/myim", null, true, Integer.MAX_VALUE, false));
							pipeline.addLast(new MessageToMessageCodec() {
								@Override
								protected void decode(ChannelHandlerContext ctx, TextWebSocketFrame frame,
										List list) throws Exception {
									list.add(frame.text());
								}

								@Override
								protected void encode(ChannelHandlerContext ctx, String msg, List list)
										throws Exception {
									list.add(new TextWebSocketFrame(msg));
								}
							});
							pipeline.addLast(new ChatHandler());
						}
					});
			channelFuture = b.bind(8321).sync();

			channelFuture.channel().closeFuture().sync();
		} finally {
			shutdown();
			// 服务器已关闭
		}
	}

	public static void shutdown() {
		if (channelFuture != null) {
			channelFuture.channel().close().syncUninterruptibly();
		}
		if ((bossGroup != null) && (!bossGroup.isShutdown())) {
			bossGroup.shutdownGracefully();
		}
		if ((workerGroup != null) && (!workerGroup.isShutdown())) {
			workerGroup.shutdownGracefully();
		}
	}

} 
  

聊天室业务 

package com.luck.im;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.AttributeKey;
import io.netty.util.internal.StringUtil;

public class ChatHandler extends SimpleChannelInboundHandler {

	/** 用户集合 */
	private static Map umap = new ConcurrentHashMap<>();

	/** channel绑定自己的用户ID */
	public static final AttributeKey UID = AttributeKey.newInstance("uid");

	@Override
	public void channelRead0(ChannelHandlerContext ctx, String msg) {
		JSONObject parseObj = JSONUtil.parseObj(msg);
		Integer type = parseObj.getInt("t");
		String uid = parseObj.getStr("uid");
		String tid = parseObj.getStr("tid");
		switch (type) {
		case 0:
			// 心跳
			break;
		case 1:
			// 用户加入聊天室
			umap.put(uid, ctx.channel());
			ctx.channel().attr(UID).set(uid);
			umap.forEach((x, y) -> {
				if (!x.equals(uid)) {
					JSONObject json = new JSONObject();
					json.set("t", 2);
					json.set("uid", uid);
					json.set("type", "join");
					y.writeAndFlush(json.toString());
				}
			});
			break;
		case 2:
			Channel uc = umap.get(tid);
			if (null != uc) {
				uc.writeAndFlush(msg);
			}
			break;
		case 9:
			// 用户退出聊天室
			umap.remove(uid);
			leave(ctx, uid);
			ctx.close();
			break;
		default:
			break;
		}
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		String uid = ctx.channel().attr(UID).get();
		if (StringUtil.isNullOrEmpty(uid)) {
			super.channelInactive(ctx);
			return;
		}
		ctx.channel().attr(UID).set(null);
		umap.remove(uid);
		leave(ctx, uid);
		super.channelInactive(ctx);
	}

	/**
	 * 用户退出
	 * 
	 * @param ctx
	 * @param uid
	 */
	private void leave(ChannelHandlerContext ctx, String uid) {
		umap.forEach((x, y) -> {
			if (!x.equals(uid)) {
				// 把数据转到用户服务
				JSONObject json = new JSONObject();
				json.set("t", 9);
				json.set("uid", uid);
				y.writeAndFlush(json.toString());
			}
		});
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
}

启动类

package com.luck.im;

public class ServerStart {
	public static void main(String[] args) throws Exception {
		// 启动聊天室
		ChatSocket.startServer();
	}
}

前端

网页主要使用了adapter-latest.js,下载地址webrtc.github.io

github访问不了可以用webrtc/adapter-latest.js-Javascript文档类资源-CSDN文库 

index.html 




    
    聊天室
	




Nginx配置

上面的index.html文件放到D盘根目录下了,然后配置一下websocket

    server {
        listen       443 ssl;
        server_name    mytest.com;
    
        ssl_certificate      lee/lee.crt;
        ssl_certificate_key  lee/lee.key;
    
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
    
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
    
        location / {
            root   d:/;
            index  index.html index.htm index.php;
        }
    
        location /myim {
            proxy_pass http://127.0.0.1:8321/myim;
        }
    }

运行 

java启动

java -jar cc-im.jar

网页访问

https://127.0.0.1/index.html

你可能感兴趣的:(webrtc,netty)