Netty+WebSocket实现简单网页群聊

Netty+WebSocket实现简单网页群聊

这两天看了下WebSocket的RFC文档,对WebSocket协议有了基本的认识,顺便写了篇博客做点笔记 WebSocket 协议。 例子说明:每个网页一个websocket连接,点发送消息后,消息会发送给除了自己之外的其它在线的websocket客户端,简单实现群聊

服务端

采用Netty实现,Netty版本是4.1.2.Final.

服务端共有以下4个类:

WebSocketServer实现IHttpService和IWebSocketService,WebSocketServerHandler持有IHttpService和 IWebSocketService的引用,若收到FullHttpRequest则交给IHttpService其处理,若收到WebSocketFrame则交给IWebSocketService去处理。

IHttpService.java

Java代码

package cc.lixiaohui.demo.netty4.websocket;  
  
import io.netty.channel.ChannelHandlerContext;  
import io.netty.handler.codec.http.FullHttpRequest;  
  
/** 
 * @author lixiaohui 
 * @date 2016年9月24日 下午3:58:31 
 *  
 */  
public interface IHttpService {  
      
    void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request);  
      
}  

IWebSocketService.java

Java代码

package cc.lixiaohui.demo.netty4.websocket;  
  
import io.netty.channel.ChannelHandlerContext;  
import io.netty.handler.codec.http.websocketx.WebSocketFrame;  
  
/** 
 * @author lixiaohui 
 * @date 2016年9月24日 下午3:46:07 
 *  
 */  
public interface IWebSocketService {  
      
    void handleFrame(ChannelHandlerContext ctx, WebSocketFrame frame);  
      
}  
 

WebSocketServerHandler.java

Java代码 
package cc.lixiaohui.demo.netty4.websocket;  
  
import io.netty.channel.ChannelHandlerContext;  
import io.netty.channel.SimpleChannelInboundHandler;  
import io.netty.handler.codec.http.FullHttpRequest;  
import io.netty.handler.codec.http.websocketx.WebSocketFrame;  
  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
/** 
 * @author lixiaohui 
 * @date 2016年9月24日 下午2:22:33 
 *  
 */  
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {  
  
    @SuppressWarnings("unused")  
    private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandler.class);  
      
    private IWebSocketService websocketService;  
      
    private IHttpService httpService;  
  
    public WebSocketServerHandler(IWebSocketService websocketService, IHttpService httpService) {  
        super();  
        this.websocketService = websocketService;  
        this.httpService = httpService;  
    }  
  
    /* 
     * @see 
     * io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel 
     * .ChannelHandlerContext, java.lang.Object) 
     */  
    @Override  
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {  
        if (msg instanceof FullHttpRequest) {  
            httpService.handleHttpRequest(ctx, (FullHttpRequest) msg);  
        } else if (msg instanceof WebSocketFrame) {  
            websocketService.handleFrame(ctx, (WebSocketFrame) msg);  
        }  
    }  
      
    /*  
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelReadComplete(io.netty.channel.ChannelHandlerContext) 
     */  
    @Override  
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {  
        ctx.flush();  
    }  
      
}  

WebSocketServer.java

Java代码

package cc.lixiaohui.demo.netty4.websocket;  
  
import io.netty.bootstrap.ServerBootstrap;  
import io.netty.channel.Channel;  
import io.netty.channel.ChannelFuture;  
import io.netty.channel.ChannelFutureListener;  
import io.netty.channel.ChannelHandlerContext;  
import io.netty.channel.ChannelId;  
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.nio.NioServerSocketChannel;  
import io.netty.handler.codec.http.FullHttpRequest;  
import io.netty.handler.codec.http.HttpHeaderNames;  
import io.netty.handler.codec.http.HttpHeaders;  
import io.netty.handler.codec.http.HttpMethod;  
import io.netty.handler.codec.http.HttpObjectAggregator;  
import io.netty.handler.codec.http.HttpServerCodec;  
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;  
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;  
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;  
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;  
import io.netty.handler.codec.http.websocketx.WebSocketFrame;  
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;  
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;  
import io.netty.handler.stream.ChunkedWriteHandler;  
import io.netty.util.AttributeKey;  
  
import java.util.Map;  
import java.util.concurrent.ConcurrentHashMap;  
  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
/** 
 * @author lixiaohui 
 * @date 2016年9月24日 下午2:08:58 
 *  
 */  
public class WebSocketServer implements IWebSocketService, IHttpService {  
  
    public static void main(String[] args) {  
        new WebSocketServer(9999).start();  
    }  
      
    // ----------------------------static fields -----------------------------  
      
    private static final Logger logger = LoggerFactory.getLogger(WebSocketServer.class);  
  
    private static final String HN_HTTP_CODEC = "HN_HTTP_CODEC";  
    private static final String HN_HTTP_AGGREGATOR = "HN_HTTP_AGGREGATOR";  
    private static final String HN_HTTP_CHUNK = "HN_HTTP_CHUNK";  
    private static final String HN_SERVER = "HN_LOGIC";  
      
    // handshaker attachment key  
    private static final AttributeKey<WebSocketServerHandshaker> ATTR_HANDSHAKER = AttributeKey.newInstance("ATTR_KEY_CHANNELID");  
      
    private static final int MAX_CONTENT_LENGTH = 65536;  
      
    private static final String WEBSOCKET_UPGRADE = "websocket";  
    private static final String WEBSOCKET_CONNECTION = "Upgrade";  
    private static final String WEBSOCKET_URI_ROOT_PATTERN = "ws://%s:%d";  
  
    // ------------------------ member fields -----------------------  
      
    private String host; // 绑定的地址  
    private int port; // 绑定的端口  
      
    /** 
     * 保存所有WebSocket连接 
     */  
    private Map<ChannelId, Channel> channelMap = new ConcurrentHashMap<ChannelId, Channel>();  
      
    private final String WEBSOCKET_URI_ROOT;  
      
    public WebSocketServer(int port) {  
        this("localhost", port);  
    }  
      
    public WebSocketServer(String host, int port) {  
        this.host = host;  
        this.port = port;  
        WEBSOCKET_URI_ROOT = String.format(WEBSOCKET_URI_ROOT_PATTERN, host, port);  
    }  
  
    public void start() {  
        EventLoopGroup bossGroup = new NioEventLoopGroup();  
        EventLoopGroup workerGroup = new NioEventLoopGroup();  
        ServerBootstrap b = new ServerBootstrap();  
        b.group(bossGroup, workerGroup);  
        b.channel(NioServerSocketChannel.class);  
        b.childHandler(new ChannelInitializer<Channel>() {  
  
            @Override  
            protected void initChannel(Channel ch) throws Exception {  
                ChannelPipeline pl = ch.pipeline();  
                // 保存该Channel的引用  
                channelMap.put(ch.id(), ch);  
                logger.info("new channel {}", ch);  
                ch.closeFuture().addListener(new ChannelFutureListener() {  
                      
                    public void operationComplete(ChannelFuture future) throws Exception {  
                        logger.info("channel close {}", future.channel());  
                        // Channel 关闭后不再引用该Channel  
                        channelMap.remove(future.channel().id());  
                    }  
                });  
  
                pl.addLast(HN_HTTP_CODEC, new HttpServerCodec());  
                pl.addLast(HN_HTTP_AGGREGATOR, new HttpObjectAggregator(MAX_CONTENT_LENGTH));  
                pl.addLast(HN_HTTP_CHUNK, new ChunkedWriteHandler());  
                pl.addLast(HN_SERVER, new WebSocketServerHandler(WebSocketServer.this, WebSocketServer.this));  
            }  
  
        });  
  
        try {  
            // 绑定端口  
            ChannelFuture future = b.bind(host, port).addListener(new ChannelFutureListener() {  
  
                public void operationComplete(ChannelFuture future) throws Exception {  
                    if (future.isSuccess()) {  
                        logger.info("websocket started.");  
                    }  
                }  
            }).sync();  
              
            future.channel().closeFuture().addListener(new ChannelFutureListener() {  
  
                public void operationComplete(ChannelFuture future) throws Exception {  
                    logger.info("server channel {} closed.", future.channel());  
                }  
  
            }).sync();  
        } catch (InterruptedException e) {  
            logger.error(e.toString());  
        } finally {  
            bossGroup.shutdownGracefully();  
            workerGroup.shutdownGracefully();  
        }  
        logger.info("websocket server shutdown");  
    }  
  
    /*  
     * @see cc.lixiaohui.demo.netty4.websocket.IHttpService#handleHttpRequest(io.netty.handler.codec.http.FullHttpRequest) 
     */  
    public void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {  
        if (isWebSocketUpgrade(req)) { // 该请求是不是websocket upgrade请求   
            logger.info("upgrade to websocket protocol");  
              
            String subProtocols = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);  
              
            WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(WEBSOCKET_URI_ROOT, subProtocols, false);  
            WebSocketServerHandshaker handshaker = factory.newHandshaker(req);  
              
            if (handshaker == null) {// 请求头不合法, 导致handshaker没创建成功  
                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());  
            } else {  
                // 响应该请求  
                handshaker.handshake(ctx.channel(), req);  
                // 把handshaker 绑定给Channel, 以便后面关闭连接用  
                ctx.channel().attr(ATTR_HANDSHAKER).set(handshaker);// attach handshaker to this channel  
            }  
            return;  
        }  
          
        // TODO 忽略普通http请求  
        logger.info("ignoring normal http request");  
    }  
      
    /* 
     * @see 
     * cc.lixiaohui.demo.netty4.websocket.IWebSocketService#handleFrame(io.netty 
     * .channel.Channel, io.netty.handler.codec.http.websocketx.WebSocketFrame) 
     */  
    public void handleFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {  
        // text frame  
        if (frame instanceof TextWebSocketFrame) {  
            String text = ((TextWebSocketFrame) frame).text();  
            TextWebSocketFrame rspFrame = new TextWebSocketFrame(text);  
            logger.info("recieve TextWebSocketFrame from channel {}", ctx.channel());  
            // 发给其他所有channel  
            for (Channel ch : channelMap.values()) {  
                if (ctx.channel().equals(ch)) {   
                    continue;   
                }  
                ch.writeAndFlush(rspFrame);  
                logger.info("write text[{}] to channel {}", text, ch);  
            }  
            return;  
        }  
          
        // ping frame, 回复pong frame即可  
        if (frame instanceof PingWebSocketFrame) {  
            logger.info("recieve PingWebSocketFrame from channel {}", ctx.channel());  
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));  
            return;  
        }  
          
        if (frame instanceof PongWebSocketFrame) {  
            logger.info("recieve PongWebSocketFrame from channel {}", ctx.channel());  
            return;  
        }  
        // close frame,   
        if (frame instanceof CloseWebSocketFrame) {  
            logger.info("recieve CloseWebSocketFrame from channel {}", ctx.channel());  
            WebSocketServerHandshaker handshaker = ctx.channel().attr(ATTR_HANDSHAKER).get();  
            if (handshaker == null) {  
                logger.error("channel {} have no HandShaker", ctx.channel());  
                return;  
            }  
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());  
            return;  
        }  
        // 剩下的是binary frame, 忽略  
        logger.warn("unhandle binary frame from channel {}", ctx.channel());  
    }  
      
    //三者与:1.GET? 2.Upgrade头 包含websocket字符串?  3.Connection头 包含 Upgrade字符串?  
    private boolean isWebSocketUpgrade(FullHttpRequest req) {  
        HttpHeaders headers = req.headers();  
        return req.method().equals(HttpMethod.GET)   
                && headers.get(HttpHeaderNames.UPGRADE).contains(WEBSOCKET_UPGRADE)  
                && headers.get(HttpHeaderNames.CONNECTION).contains(WEBSOCKET_CONNECTION);  
    }  
  
}  

客户端

客户端采用浏览器,代码:

Js代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html xmlns="http://www.w3.org/1999/xhtml">  
<head>  
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
<title></title>  
</head>  
  </head>  
  <script type="text/javascript">  
  var socket;  
    
  if(!window.WebSocket){  
      window.WebSocket = window.MozWebSocket;  
  }  
   
  if(window.WebSocket){  
      socket = new WebSocket("ws://localhost:9999");  
        
      socket.onmessage = function(event){             
            appendln("接收:" + event.data);  
      };  
   
      socket.onopen = function(event){  
            appendln("WebSocket 连接已建立");  
              
      };  
   
      socket.onclose = function(event){  
            appendln("WebSocket 连接已关闭");  
      };  
  }else{  
        alert("浏览器不支持WebSocket协议");  
  }  
   
  function send(message){  
    if(!window.WebSocket){return;}  
    if(socket.readyState == WebSocket.OPEN){  
        socket.send(message);  
        appendln("发送:" + message);  
    }else{  
        alert("WebSocket连接建立失败");  
    }  
      
  }  
    
  function appendln(text) {  
    var ta = document.getElementById('responseText');  
    ta.value += text + "\r\n";  
  }  
    
  function clear() {  
    var ta = document.getElementById('responseText');  
    ta.value = "";  
  }  
        
  </script>  
  <body>  
    <form onSubmit="return false;">  
        <input type = "text" name="message" value="你好啊"/>  
        <br/><br/>  
        <input type="button" value="发送 WebSocket 请求消息" onClick="send(this.form.message.value)"/>  
        <hr/>  
        <h3>服务端返回的应答消息</h3>  
        <textarea id="responseText" style="width: 800px;height: 300px;"></textarea>  
    </form>  
  </body>  
</html>  

测试

打开客户端,可以看到能接收到其他客户端发的消息
在这里插入图片描述

实现局域网音视频通话可以用Spring Boot作为后端框架,Netty作为网络通信框架,WebSocket作为实现双向通信的协议。以下是一个简单实现过程: 1. 首先需要搭建一个Spring Boot项目,可以使用Spring Initializr来快速生成项目。在pom.xml中添加NettyWebSocket的依赖,例如: ```xml <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 创建一个WebSocket处理器类,用来处理WebSocket的连接、关闭和消息收发等逻辑。例如: ```java @Component @ServerEndpoint("/video-chat") public class VideoChatHandler { private static final Logger LOGGER = LoggerFactory.getLogger(VideoChatHandler.class); @OnOpen public void onOpen(Session session) { LOGGER.info("WebSocket opened: {}", session.getId()); } @OnMessage public void onMessage(String message, Session session) { LOGGER.info("Received message: {}", message); // TODO: 处理收到的消息 } @OnClose public void onClose(Session session) { LOGGER.info("WebSocket closed: {}", session.getId()); } @OnError public void onError(Throwable error) { LOGGER.error("WebSocket error", error); } } ``` 3. 在Spring Boot的配置类中添加WebSocket的配置,例如: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private VideoChatHandler videoChatHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(videoChatHandler, "/video-chat").setAllowedOrigins("*"); } } ``` 4. 使用Netty实现音视频的传输。可以使用Netty提供的UDP协议来实现多人音视频通话,也可以使用TCP协议来实现点对点的音视频通话。需要根据实际情况选择相应的协议,这里以TCP协议为例: ```java @Component public class VideoChatServer { private static final Logger LOGGER = LoggerFactory.getLogger(VideoChatServer.class); @Value("${server.video-chat.port}") private int port; @PostConstruct public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // TODO: 添加音视频相关的编解码器和处理器 } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture future = bootstrap.bind(port).sync(); LOGGER.info("Video chat server started on port {}", port); future.channel().closeFuture().sync(); } catch (InterruptedException e) { LOGGER.error("Video chat server interrupted", e); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } ``` 5. 在WebSocket处理器中实现音视频数据的收发逻辑。当收到音视频数据时,可以将数据转发给所有连接的WebSocket客户端。例如: ```java @Component @ServerEndpoint("/video-chat") public class VideoChatHandler { private static final Logger LOGGER = LoggerFactory.getLogger(VideoChatHandler.class); private List<Session> sessions = new CopyOnWriteArrayList<>(); @OnOpen public void onOpen(Session session) { LOGGER.info("WebSocket opened: {}", session.getId()); sessions.add(session); } @OnMessage public void onMessage(ByteBuffer buffer, Session session) throws IOException { LOGGER.info("Received video data from {}", session.getId()); byte[] data = new byte[buffer.remaining()]; buffer.get(data); for (Session s : sessions) { if (s.isOpen() && !s.getId().equals(session.getId())) { s.getBasicRemote().sendBinary(ByteBuffer.wrap(data)); } } } @OnClose public void onClose(Session session) { LOGGER.info("WebSocket closed: {}", session.getId()); sessions.remove(session); } @OnError public void onError(Throwable error) { LOGGER.error("WebSocket error", error); } } ``` 6. 在前端页面中使用WebSocket实现音视频通话。可以使用WebRTC等技术来实现音视频采集、编解码、传输等功能。这里不再赘述。 以上就是一个简单的局域网音视频通话的实现过程。需要注意的是,音视频通话涉及到的技术较多,需要根据实际情况进行选择和配置。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值