一、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();
}
}