【Websocket】介绍和通讯

一、WebSocket概况

WebSocket 是一种非常重要的网络协议,特别适用于需要实时更新和双向通信的应用场景。下面是一些关于 WebSocket 的详细信息,进一步阐明它的特性和应用:

1.1WebSocket 的关键特性:


全双工通信:WebSocket 允许客户端和服务器在单个连接上同时进行双向通信。这意味着一旦连接建立,服务器可以主动推送数据到客户端,而客户端也可以向服务器发送数据。

持久连接:WebSocket 连接在建立之后会保持打开状态,直到被显式关闭。这与传统的 HTTP 请求-响应模型不同,HTTP 每次通信都需要建立新的连接。

低延迟:由于 WebSocket 连接是持久的,并且省去了传统 HTTP 协议中的开销(如头部重传和握手),因此它通常比 HTTP 更适合需要低延迟的应用场景。

减少带宽消耗:WebSocket 协议在握手阶段使用的是较小的开销,之后的数据帧也较轻便。这样可以显著减少由于重复的 HTTP 头部信息所带来的带宽消耗。

1.2 WebSocket 的工作流程:


握手:客户端发起一个 WebSocket 握手请求,通过 HTTP 协议发送给服务器。这个请求中包含了 WebSocket 协议的特定头部信息,表明客户端希望升级到 WebSocket 连接。

升级:服务器响应客户端的握手请求,并确认升级到 WebSocket 协议。一旦握手成功,WebSocket 连接建立完成。

数据传输:一旦连接建立,客户端和服务器就可以在连接上进行任意的消息交换。消息可以是文本、二进制数据等。

关闭连接:当客户端或服务器希望关闭 WebSocket 连接时,可以发送一个关闭帧,另一方接收到关闭帧后也会关闭连接。

1.3 工作原理

客户端依靠发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端。

请求头Request Headers

GET /test HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: sehfiowqweuq1psd==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp
Origin: http://hello.com
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13

首先客户端(如浏览器)发出带有特殊消息头(Upgrade、Connection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。其中关键的字段就是Upgrade,Connection,告诉 Apache 、 Nginx 等服务器:注意啦,发起的是Websocket协议,不再 使用原先的HTTP。其中,Sec-WebSocket-Key当成是请求id就好了。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Sec-WebSocket-Protocol: v12.stomp

Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。 

1.3 应用场景


实时聊天:聊天应用需要实时的消息传递,WebSocket 提供了高效的解决方案。

在线游戏:游戏中的实时交互(例如玩家动作和状态更新)可以通过 WebSocket 高效地处理。

股票市场:股票和金融市场应用需要实时更新数据,WebSocket 能够提供实时行情和交易信息。

实时通知:例如社交网络应用中的即时通知和更新。

协作应用:如实时文档编辑和在线协作工具,可以使用 WebSocket 实现多用户之间的同步更新。

WebSocket 协议在许多现代网络应用中扮演了重要角色,特别是在需要高频率数据交换和低延迟响应的场景中。

二、Java实现相关客户端与服务器

2.1 服务器端代码

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
/**
 * @author 赵海洋
 * @date 2021-9-15
 * @des 简单的WebSocketServer服务器实例
 * */
public class WebSocketServerExample {
    private int port = 1780;
    private EventLoopGroup bossGroup = null;
    private EventLoopGroup workerGroup = null;
    private ServerBootstrap bootstrap = null;
    private ChannelFuture future= null;

    //端口号
    public WebSocketServerExample(int port){
        this.port = port;
    }
    public void start() throws Exception {
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();

        bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                 .channel(NioServerSocketChannel.class) // 指定使用NIO传输
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                   @Override
                   public void initChannel(SocketChannel ch) throws Exception {
                            // 添加HTTP编解码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            // 添加HTTP对象聚合器,将HTTP消息的多个部分合成一条完整的HTTP消息
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            // 添加WebSocket协议处理器,将HTTP协议升级为WebSocket协议
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket"));
                            // 添加自定义的WebSocket处理器
                            ch.pipeline().addLast(new WebSocketServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置TCP参数
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // 设置TCP参数

            // 绑定端口,开始接收进来的连接
            future= bootstrap.bind(port).sync();

        System.out.println("成功启动!");
    }

    //关闭链接
    public void close() throws InterruptedException {
        // 等待服务器套接字关闭
        future.channel().closeFuture().sync();
    }

    //销毁
    public void destory(){
        // 优雅地关闭EventLoopGroup,释放所有的资源
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }


    public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
            @Override
            protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg){
                System.out.println("服务器接受消息中...");
                System.out.println("Received WebSocket message: " + msg.text());
            }

            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
                // 发生异常时关闭连接
                cause.printStackTrace();
                ctx.close();
            }

            @Override
            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                super.channelActive(ctx);
            }
    }

    public static void main(String[] args) throws Exception {
        WebSocketServerExample server = new WebSocketServerExample(8088);
        server.start();
    }
}

2.2 客户端实现代码
 

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.freeswitch.esl.client.conf.SetingConf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author 赵海洋
 * @date 2024-9-15
 * @des 通过Netty实现的PaddleSpech流式的交互
 * */
public class PSASRWebClient {
    private Channel socketChannel = null;//上下文信息
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    //使用原子性布尔变量实现等待
    private final AtomicBoolean authenticatorResponded = new AtomicBoolean(false);

    private final ReentrantLock syncLock = new ReentrantLock();
    private WebSocketListener webSocketListener = null;

    //添加监听器
    public PSASRWebClient addWebSocketListenter(WebSocketListener webSocketListener){
        this.webSocketListener = webSocketListener;
        return this;
    }

    //建立链接
    public void connect(String url) throws URISyntaxException, IOException {
        // 创建事件循环组
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        URI webSocketURL = new URI(url);
        // 创建引导程序
        Bootstrap bootstrap = new Bootstrap()
                .group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO)) // 添加日志处理器,用于打印日志信息
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel sc) throws Exception {
                        ChannelPipeline pipeline = sc.pipeline();
                        pipeline.addLast(new HttpClientCodec()); // HTTP 客户端编解码器,用于处理 HTTP 请求和响应
                        pipeline.addLast(new ChunkedWriteHandler()); // 支持大数据流写入
                        pipeline.addLast(new HttpObjectAggregator(64 * 1024)); // 聚合 HTTP 消息,将多个消息合并成一个完整的 FullHttpRequest 或 FullHttpResponse
                        // WebSocket 客户端协议处理器,用于处理 WebSocket 握手和帧的编解码
                        pipeline.addLast(new WebSocketClientProtocolHandler(
                                WebSocketClientHandshakerFactory.newHandshaker(webSocketURL, WebSocketVersion.V13, null, false, new DefaultHttpHeaders())));
                        pipeline.addLast(new WebSocketClientHandler(webSocketListener));

                    }
                });

        // Attempt connection
        // 连接到目标 WebSocket 服务器
        ChannelFuture future = bootstrap.connect(webSocketURL.getHost(), webSocketURL.getPort());

        // Wait till attempt succeeds, fails or timeouts
        if (!future.awaitUninterruptibly(30*1000, TimeUnit.SECONDS)) {
            throw new IOException("Timeout connecting to " + url);
        }
        // Did not timeout
        socketChannel = future.channel();
        // But may have failed anyway
        if (!future.isSuccess()) {
            if(SetingConf.LOG_ON) {
                log.warn("Failed to connect to [{}]", url, future.cause());
            }
            workerGroup.shutdownGracefully();
            throw new IOException("Could not connect to " + url, future.cause());
        }

        //  Wait for the authentication handshake to call back
        while (!authenticatorResponded.get()) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    //发送开始命令
    public void start() throws IOException {
        String startStr = "{\"name\": \"test.wav\",\"signal\": \"start\", \"nbest\": 1}";
        sendMessage(startStr);
    }

    //发送开始命令
    public void stop() throws IOException {
        String startStr = "{\"name\": \"test.wav\",\"signal\": \"end\",\"nbest\": 1}";
        sendMessage(startStr);
    }

    //发送数据流
    public void sendStreamData(byte[] datas) throws IOException, InterruptedException {
        ByteBuf binaryData = Unpooled.wrappedBuffer(datas);
        BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(binaryData);
        sendStreamData(binaryFrame);
    }
    //发送数据流信息
    public void sendStreamData(BinaryWebSocketFrame datas) throws IOException, InterruptedException {
        if(socketChannel==null || !socketChannel.isActive()){
            throw new IOException("socketChannel is unActive");
        }
        syncLock.lock();
        try {
            socketChannel.writeAndFlush(datas); // 发送消息
        } finally {
            syncLock.unlock();
        }
        System.out.println("数据信息流发送完成-");
    }

    //发送字符串信息
    public void sendMessage(String str) throws IOException {
        //Channel dest = dest(url); // 获取目标通道
        if(socketChannel==null || !socketChannel.isActive()){
            throw new IOException("socketChannel is unActive");
        }
        syncLock.lock();
        try {
            socketChannel.writeAndFlush(new TextWebSocketFrame(str)); // 发送消息
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            syncLock.unlock();
        }
    }
    public void send(Channel channel) {
        final String textMsg = "hello"; // 要发送的消息内容
        if (channel != null && channel.isActive()) {
            TextWebSocketFrame frame = new TextWebSocketFrame(textMsg); // 创建 WebSocket 文本帧
            channel.writeAndFlush(frame).addListener((ChannelFutureListener) channelFuture -> {
                if (channelFuture.isDone() && channelFuture.isSuccess()) {
                    System.out.println("     ================= 发送成功.");
                } else {
                    channelFuture.channel().close();
                    System.out.println("     ================= 发送失败. cause = " + channelFuture.cause());
                    channelFuture.cause().printStackTrace();
                }
            });
        } else {
            System.out.println("消息发送失败! textMsg = " + textMsg);
        }
    }

    // WebSocket客户端处理器
    public class WebSocketClientHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

        private  WebSocketListener webSocketMsg = null;

        public WebSocketClientHandler(WebSocketListener webSocketListener){
            this.webSocketMsg = webSocketListener;
        }

        // 当从服务器接收到消息时调用
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
            //String resultOk = "{\"status\":\"ok\",\"signal\":\"server_ready\"}";
            String result = msg.text();
            System.out.println(" 客户端收到消息======== " + result);
            if(result!=null && result.contains("server_ready")){
                authenticatorResponded.set(true);
                webSocketMsg.onConnect(ctx,true);
            }else{
                webSocketMsg.onReciveMsg(ctx,result);
               /* if(countDownLatch!=null){
                    countDownLatch.countDown();
                }*/
            }
        }

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            super.userEventTriggered(ctx, evt);
            if (WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE.equals(evt)) {
                System.out.println(ctx.channel().id().asShortText() + " 握手完成!");
                //发送解析指令
                sendStartCommand(ctx);
                //authenticatorResponded.set(true);
                //webSocketMsg.onConnect(ctx,true);
                //latch.countDown(); // 计数减一,握手完成
               // send(ctx.channel()); // 发送消息
            }
        }

        // 当通道不活动时调用
        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            System.out.println("channelInactive");
        }
        //发送开始命令信息
        private void sendStartCommand(ChannelHandlerContext ctx) throws IOException {
            String str = "{\"name\": \"test.wav\",\"signal\": \"start\", \"nbest\": 1}";
            if(ctx.channel()==null || !ctx.channel().isActive()){
                throw new IOException("socketChannel is unActive");
            }
            syncLock.lock();
            try {
                socketChannel.writeAndFlush(new TextWebSocketFrame(str)); // 发送消息
            } finally {
                syncLock.unlock();
            }
        }
    }

    public interface WebSocketListener{
        //链接成功
        public abstract void onConnect(ChannelHandlerContext ctx, boolean isSuccess);

        //收到消息收到
        public abstract void onReciveMsg(ChannelHandlerContext ctx, String msg);
    }
}

 具体调用实例如下:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;

/**
 * @author 赵海洋
 * @date 2024-9-15
 * @des 通过Netty实现的PaddleSpech流式的交互案例
 * */
public class PSASRWebClientExample {
    public PSASRWebClientExample(){}

    private class PSASRWebSocketListener implements PSASRWebClient.WebSocketListener{
        @Override
        public void onConnect(ChannelHandlerContext ctx, boolean isSuccess) {
            System.out.println("客户端已成功连接!");
        }

        @Override
        public void onReciveMsg(ChannelHandlerContext ctx, String msg) {
            System.out.println("识别结果:"+msg);
        }
    }

    public void connect() throws IOException, URISyntaxException {
        // 目标 WebSocket 地址
        String url = "ws://127.0.0.1:8090/paddlespeech/asr/streaming";
        PSASRWebSocketListener webSocketListener = new PSASRWebSocketListener();
        PSASRWebClient client = new PSASRWebClient().addWebSocketListenter(webSocketListener);
        client.connect(url);

        try{
            final String FILE_PATH = "E:\\57.wav";
            RandomAccessFile reader = new RandomAccessFile(FILE_PATH, "r");
            byte[] buffer = new byte[2048*10];
            int bytesRead =-1;
            //ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            while ((bytesRead = reader.read(buffer)) != -1) {
                ByteBuf binaryData = Unpooled.wrappedBuffer(buffer,0, bytesRead);
                BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(binaryData);
                client.sendStreamData(binaryFrame);
            }
        }catch (IOException | InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        new PSASRWebClientExample().connect();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

常生果

喜欢我,请支持我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值