目录
2、2 : onOpen():websocket连接成功后的回调方法
2、4 : sendEof() : 发送结束表示给ws服务器
一、简介:
WebSocket是一种在Web浏览器和服务器之间进行全双工通信的协议。它允许服务器主动向客户端推送数据,而不需要客户端发起请求。相比传统的HTTP请求-响应模式,WebSocket提供了更低的延迟和更高的实时性。
WebSocket协议基于TCP协议,通过在HTTP握手阶段升级到WebSocket连接来实现。在握手完成后,客户端和服务器之间的连接将保持打开状态,双方可以随时发送消息。
二、WebSocket的特点:
- 实时性:WebSocket提供了低延迟的双向通信,使得服务器可以实时向客户端推送数据。
- 高效性:WebSocket使用二进制帧传输数据,相比文本协议如HTTP,可以减少数据传输的大小和带宽消耗。
- 跨域支持:WebSocket支持跨域通信,可以在不同域名下的客户端和服务器之间建立连接。
- 可扩展性:WebSocket协议可以通过扩展来支持更多的功能和协议。
三、WebSocket基本流程:
- 客户端发起WebSocket连接请求。
- 服务器接受连接请求,并进行握手。
- 握手成功后,客户端和服务器之间建立起WebSocket连接。
- 双方可以通过发送消息进行实时通信。
- 连接关闭时,客户端和服务器都可以发送关闭消息来关闭连接。
四、 WebSocket基本使用
1、webScoket实例对象常用属性和方法的介绍
属性 | 说明 |
url | 当前连接的websocket接口地址 |
readyState | 当前连接的状态: 0:正在链接中 1:已经链接并且可以通讯 2:连接正在关闭 3;连接已关闭或者没有链接成功 |
onopen | 连接成功的回调函数 |
onerror | 连接失败的回调函数 |
onmessage | 从服务端接受到信息的回调函数 |
onclose | 连接关闭的回调函数 |
binaryType | 使用二进制的数据类型连接 |
protocol | 服务器选择的下属协议 |
bufferedAmount | 未发送至服务器的二进制字节数 |
close() | 关闭当前连接 |
send(data) | 发送消息到服务器 |
2、代码解析:
2、1 : connectWs() : 进行webSocket连接,并获取服务器返回的结果( onMessage()方法中设置一个全局变量,在此方法中不断去检测这个全局变量是否有值,有值则返回,没值则继续等待 )
public static String connectWs(String host, String port, String url) throws URISyntaxException, InterruptedException {
try {
if (StringUtils.isBlank(host) || StringUtils.isBlank(port) || StringUtils.isBlank(url)) {
logger.info("传入字段存在空值,当前host为【" + host + "】,port为【" + port + "】,url为【" + url + "】");
return "";
}
String str = "--host " + host + " --port " + port + " --mode offline --audio_in " + url;
String[] args = str.split(" ");
// 设置命令行的相关参数
ArgumentParser parser = ArgumentParsers.newArgumentParser("ws client").defaultHelp(true);
// 设置端口号
parser
.addArgument("--port")
.help("Port on which to listen.")
.setDefault("8889")
.type(String.class)
.required(false);
// 设置服务器IP
parser
.addArgument("--host")
.help("the IP address of server.")
.setDefault("127.0.0.1")
.type(String.class)
.required(false);
// 设置音频数据地址
parser
.addArgument("--audio_in")
.help("wav path for decoding.")
.setDefault("asr_example.wav")
.type(String.class)
.required(false);
// 设置线程数
parser
.addArgument("--num_threads")
.help("num of threads for test.")
.setDefault(1)
.type(Integer.class)
.required(false);
// 设置分块大小
parser
.addArgument("--chunk_size")
.help("chunk size for asr.")
.setDefault("5, 10, 5")
.type(String.class)
.required(false);
// 设置分块间隔
parser
.addArgument("--chunk_interval")
.help("chunk for asr.")
.setDefault(10)
.type(Integer.class)
.required(false);
/** 音频识别系统(ASR)的状态参数
* online:在线模式,音频数据实时传输到服务器进行处理,然后服务器实时返回识别结果
* offline:离线模式,音频数据先存储在本地,然后一次性传输到服务器进行处理,服务器处理完成后一次性返回识别结果
* hybrid:部分处理可以在本地进行,而另一部分可以再服务器端进行,以实现更好的性能和用户体验
*/
parser
.addArgument("--mode")
.help("mode for asr.")
.setDefault("offline")
.type(String.class)
.required(false);
// 热词
parser
.addArgument("--hotwords")
.help("hotwords, splited by space, hello 30 nihao 40")
.setDefault("")
.type(String.class)
.required(false);
// ws的ip
String srvIp = "";
// ws的端口
String srvPort = "";
// 音频路径
String wavPath = "";
// 线程数
int numThreads = 1;
// 分块大小
String chunk_size = "";
// 分块间隔
int chunk_interval = 10;
// 状态:默认为离线状态
String strmode = "offline";
// 热词
String hot = "";
try {
Namespace ns = parser.parseArgs(args);
srvIp = ns.get("host");
srvPort = ns.get("port");
wavPath = ns.get("audio_in");
numThreads = ns.get("num_threads");
chunk_size = ns.get("chunk_size");
chunk_interval = ns.get("chunk_interval");
strmode = ns.get("mode");
hot = ns.get("hotwords");
System.out.println(srvPort);
} catch (ArgumentParserException ex) {
ex.getParser().handleError(ex);
return "";
}
WsSendDataUtils.strChunkSize = chunk_size;
WsSendDataUtils.chunkInterval = chunk_interval;
WsSendDataUtils.wavPath = wavPath;
WsSendDataUtils.mode = strmode;
WsSendDataUtils.hotwords = hot;
System.out.println(
"serIp="
+ srvIp
+ ",srvPort="
+ srvPort
+ ",wavPath="
+ wavPath
+ ",strChunkSize"
+ strChunkSize);
// 设置音频采样率为16000Hz
int RATE = 16000;
//
String[] chunkList = strChunkSize.split(",");
// 音频分块大小
int int_chunk_size = 60 * Integer.valueOf(chunkList[1].trim()) / chunkInterval;
// 计算每个音频分块的大小,以采样点为单位
int CHUNK = Integer.valueOf(RATE / 1000 * int_chunk_size);
// 计算音频分块的步长,以采样点为单位
int stride =
Integer.valueOf(
60 * Integer.valueOf(chunkList[1].trim()) / chunkInterval / 1000 * 16000 * 2);
System.out.println("chunk_size:" + String.valueOf(int_chunk_size));
System.out.println("CHUNK:" + CHUNK);
System.out.println("stride:" + String.valueOf(stride));
// 设置WebSocket客户端发送数据的分块大小
WsSendDataUtils.sendChunkSize = CHUNK * 2;
// ws地址
String wsAddress = "ws://" + srvIp + ":" + srvPort;
WsSendDataUtils c = new WsSendDataUtils(new URI(wsAddress));
// 连接WebSocket
c.connect();
// 等待ws返回结果
String wsData = WsSendDataUtils.messageMap.get("receivedWsData");
while (StringUtils.isEmpty(wsData)) {
Thread.sleep(3000);
wsData = WsSendDataUtils.messageMap.get("receivedWsData");
logger.info("正在获取wsData");
}
if (StringUtils.isNotBlank(wsData)) {
WsSendDataUtils.messageMap.remove("receivedWsData");
return wsData;
}
System.out.println("wsAddress:" + wsAddress);
return "";
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
2、2 : onOpen():websocket连接成功后的回调方法
/**
* 连接成功后的回调方法
*
* @param serverHandshake
*/
@Override
public void onOpen(ServerHandshake serverHandshake) {
// 连接成功后,发送音频数据
sendAudio();
}
2、3 : sendAudio() : 发送音频数据
/**
* 发送音频数据
*/
private void sendAudio() {
String fileName = WsSendDataUtils.wavPath;
String suffix = fileName.split("\\.")[fileName.split("\\.").length - 1];
sendJson(mode, strChunkSize, chunkInterval, wavName, true, suffix);
File file = new File(WsSendDataUtils.wavPath);
int chunkSize = sendChunkSize;
byte[] bytes = new byte[chunkSize];
int readSize = 0;
try (FileInputStream fis = new FileInputStream(file)) {
if (WsSendDataUtils.wavPath.endsWith(".wav")) {
fis.read(bytes, 0, 44); //skip first 44 wav header
}
readSize = fis.read(bytes, 0, chunkSize);
while (readSize > 0) {
// send when it is chunk size
if (readSize == chunkSize) {
send(bytes); // send buf to server
} else {
// send when at last or not is chunk size
byte[] tmpBytes = new byte[readSize];
for (int i = 0; i < readSize; i++) {
tmpBytes[i] = bytes[i];
}
send(tmpBytes);
}
// if not in offline mode, we simulate online stream by sleep
if (!mode.equals("offline")) {
Thread.sleep(Integer.valueOf(chunkSize / 32));
}
readSize = fis.read(bytes, 0, chunkSize);
}
if (!mode.equals("offline")) {
// if not offline, we send eof and wait for 3 seconds to close
Thread.sleep(2000);
sendEof();
Thread.sleep(3000);
close();
} else {
// if offline, just send eof
sendEof();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendJson(String mode, String strChunkSize, int chunkInterval, String wavName, boolean isSpeaking, String suffix) {
try {
JSONObject obj = new JSONObject();
obj.put("mode", mode);
JSONArray array = new JSONArray();
String[] chunkList = strChunkSize.split(",");
for (int i = 0; i < chunkList.length; i++) {
array.add(Integer.valueOf(chunkList[i].trim()));
}
obj.put("chunk_size", array);
obj.put("chunk_interval", Integer.valueOf(chunkInterval));
obj.put("wav_name", wavName);
if (WsSendDataUtils.hotwords.trim().length() > 0) {
String regex = "\\d+";
JSONObject jsonitems = new JSONObject();
String[] items = WsSendDataUtils.hotwords.trim().split(" ");
Pattern pattern = Pattern.compile(regex);
String tmpWords = "";
for (int i = 0; i < items.length; i++) {
Matcher matcher = pattern.matcher(items[i]);
if (matcher.matches()) {
jsonitems.put(tmpWords.trim(), items[i].trim());
tmpWords = "";
continue;
}
tmpWords = tmpWords + items[i] + " ";
}
obj.put("hotwords", jsonitems.toString());
}
if (suffix.equals("wav")) {
suffix = "pcm";
}
obj.put("wav_format", suffix);
if (isSpeaking) {
obj.put("is_speaking", Boolean.valueOf(true));
} else {
obj.put("is_speaking", Boolean.valueOf(false));
}
logger.info("sendJson: " + obj);
// return;
send(obj.toString());
return;
} catch (Exception e) {
e.printStackTrace();
}
}
2、4 : sendEof() : 发送结束表示给ws服务器
/**
* 发送结束表示给ws服务器
*/
public void sendEof() {
try {
JSONObject obj = new JSONObject();
obj.put("is_speaking", Boolean.valueOf(false));
logger.info("sendEof: " + obj);
// return;
send(obj.toString());
iseof = true;
return;
} catch (Exception e) {
e.printStackTrace();
}
}
2、5 : onClose() : 连接关闭时的回调方法
/**
* @param statusCode:连接关闭的状态码
* @param reason:连接关闭的附加信息,例如异常关闭时,这个参数可能包含异常的详细描述信息
* @param flag:连接关闭时是否为远程主机(服务器)发起关闭,如果为true,则表示由远程主机发起的关闭, 如果为false,则表示由本地客户端发起的关闭。
*/
@Override
public void onClose(int statusCode, String reason, boolean flag) {
logger.info("WebSocket返回的状态码为【" + statusCode + "】,连接关闭的附加信息为:"
+ reason + ",是否为远程主机(服务器)发起的关闭:" + flag);
}
2、6 : onError(): 异常时的回调方法
@Override
public void onError(Exception e) {
e.printStackTrace();
logger.info("WebSocket发生异常,异常原因为:" + e);
}
3、整体代码
package com.xinlian.common.utils;
import com.alibaba.excel.util.StringUtils;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.handshake.ServerHandshake;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author:yaojunjie
* @Package:com.common.utils
* @Date:2024/4/27 12:57
*/
public class WsSendDataUtils extends WebSocketClient {
private boolean iseof = false;
public static String wavPath;
static String mode = "online";
static String strChunkSize = "5,10,5";
static int chunkInterval = 10;
static int sendChunkSize = 1920;
static String hotwords = "";
String wavName = "javatest";
static HashMap<String, String> messageMap = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(WsSendDataUtils.class);
public WsSendDataUtils(URI serverURI) {
super(serverURI);
}
public WsSendDataUtils(URI serverUri, Draft draft) {
super(serverUri, draft);
}
public WsSendDataUtils(URI serverUri, Draft draft, Map<String, String> headers, int connecttimeout) {
super(serverUri, draft, headers, connecttimeout);
}
/**
* 连接成功后的回调方法
*
* @param serverHandshake
*/
@Override
public void onOpen(ServerHandshake serverHandshake) {
// 连接成功后,发送音频数据
sendAudio();
}
/**
* ws返回数据的回调方法
* @param message:ws返回的信息
*/
@Override
public void onMessage(String message) {
JSONObject jsonObject = new JSONObject();
JSONParser jsonParser = new JSONParser();
logger.info("ws服务器返回的数据为: " + message);
try {
jsonObject = (JSONObject) jsonParser.parse(message);
Object text = jsonObject.get("text");
messageMap.put("receivedWsData", "" + text);
} catch (org.json.simple.parser.ParseException e) {
e.printStackTrace();
}
if (iseof && mode.equals("offline") && !jsonObject.containsKey("is_final")) {
close();
}
if (iseof && mode.equals("offline") && jsonObject.containsKey("is_final") && jsonObject.get("is_final") != null && jsonObject.get("is_final").toString().equals("false")) {
close();
}
}
/**
* @param statusCode:连接关闭的状态码
* @param reason:连接关闭的附加信息,例如异常关闭时,这个参数可能包含异常的详细描述信息
* @param flag:连接关闭时是否为远程主机(服务器)发起关闭,如果为true,则表示由远程主机发起的关闭, 如果为false,则表示由本地客户端发起的关闭。
*/
@Override
public void onClose(int statusCode, String reason, boolean flag) {
logger.info("WebSocket返回的状态码为【" + statusCode + "】,连接关闭的附加信息为:"
+ reason + ",是否为远程主机(服务器)发起的关闭:" + flag);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
logger.info("WebSocket发生异常,异常原因为:" + e);
}
/**
* 发送音频数据
*/
private void sendAudio() {
String fileName = WsSendDataUtils.wavPath;
String suffix = fileName.split("\\.")[fileName.split("\\.").length - 1];
sendJson(mode, strChunkSize, chunkInterval, wavName, true, suffix);
File file = new File(WsSendDataUtils.wavPath);
int chunkSize = sendChunkSize;
byte[] bytes = new byte[chunkSize];
int readSize = 0;
try (FileInputStream fis = new FileInputStream(file)) {
if (WsSendDataUtils.wavPath.endsWith(".wav")) {
fis.read(bytes, 0, 44); //skip first 44 wav header
}
readSize = fis.read(bytes, 0, chunkSize);
while (readSize > 0) {
// send when it is chunk size
if (readSize == chunkSize) {
send(bytes); // send buf to server
} else {
// send when at last or not is chunk size
byte[] tmpBytes = new byte[readSize];
for (int i = 0; i < readSize; i++) {
tmpBytes[i] = bytes[i];
}
send(tmpBytes);
}
// if not in offline mode, we simulate online stream by sleep
if (!mode.equals("offline")) {
Thread.sleep(Integer.valueOf(chunkSize / 32));
}
readSize = fis.read(bytes, 0, chunkSize);
}
if (!mode.equals("offline")) {
// if not offline, we send eof and wait for 3 seconds to close
Thread.sleep(2000);
sendEof();
Thread.sleep(3000);
close();
} else {
// if offline, just send eof
sendEof();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送结束表示给ws服务器
*/
public void sendEof() {
try {
JSONObject obj = new JSONObject();
obj.put("is_speaking", Boolean.valueOf(false));
logger.info("sendEof: " + obj);
// return;
send(obj.toString());
iseof = true;
return;
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendJson(String mode, String strChunkSize, int chunkInterval, String wavName, boolean isSpeaking, String suffix) {
try {
JSONObject obj = new JSONObject();
obj.put("mode", mode);
JSONArray array = new JSONArray();
String[] chunkList = strChunkSize.split(",");
for (int i = 0; i < chunkList.length; i++) {
array.add(Integer.valueOf(chunkList[i].trim()));
}
obj.put("chunk_size", array);
obj.put("chunk_interval", Integer.valueOf(chunkInterval));
obj.put("wav_name", wavName);
if (WsSendDataUtils.hotwords.trim().length() > 0) {
String regex = "\\d+";
JSONObject jsonitems = new JSONObject();
String[] items = WsSendDataUtils.hotwords.trim().split(" ");
Pattern pattern = Pattern.compile(regex);
String tmpWords = "";
for (int i = 0; i < items.length; i++) {
Matcher matcher = pattern.matcher(items[i]);
if (matcher.matches()) {
jsonitems.put(tmpWords.trim(), items[i].trim());
tmpWords = "";
continue;
}
tmpWords = tmpWords + items[i] + " ";
}
obj.put("hotwords", jsonitems.toString());
}
if (suffix.equals("wav")) {
suffix = "pcm";
}
obj.put("wav_format", suffix);
if (isSpeaking) {
obj.put("is_speaking", Boolean.valueOf(true));
} else {
obj.put("is_speaking", Boolean.valueOf(false));
}
logger.info("sendJson: " + obj);
// return;
send(obj.toString());
return;
} catch (Exception e) {
e.printStackTrace();
}
}
public static String connectWs(String host, String port, String url) throws URISyntaxException, InterruptedException {
try {
if (StringUtils.isBlank(host) || StringUtils.isBlank(port) || StringUtils.isBlank(url)) {
logger.info("传入字段存在空值,当前host为【" + host + "】,port为【" + port + "】,url为【" + url + "】");
return "";
}
String str = "--host " + host + " --port " + port + " --mode offline --audio_in " + url;
String[] args = str.split(" ");
// 设置命令行的相关参数
ArgumentParser parser = ArgumentParsers.newArgumentParser("ws client").defaultHelp(true);
// 设置端口号
parser
.addArgument("--port")
.help("Port on which to listen.")
.setDefault("8889")
.type(String.class)
.required(false);
// 设置服务器IP
parser
.addArgument("--host")
.help("the IP address of server.")
.setDefault("127.0.0.1")
.type(String.class)
.required(false);
// 设置音频数据地址
parser
.addArgument("--audio_in")
.help("wav path for decoding.")
.setDefault("asr_example.wav")
.type(String.class)
.required(false);
// 设置线程数
parser
.addArgument("--num_threads")
.help("num of threads for test.")
.setDefault(1)
.type(Integer.class)
.required(false);
// 设置分块大小
parser
.addArgument("--chunk_size")
.help("chunk size for asr.")
.setDefault("5, 10, 5")
.type(String.class)
.required(false);
// 设置分块间隔
parser
.addArgument("--chunk_interval")
.help("chunk for asr.")
.setDefault(10)
.type(Integer.class)
.required(false);
/** 音频识别系统(ASR)的状态参数
* online:在线模式,音频数据实时传输到服务器进行处理,然后服务器实时返回识别结果
* offline:离线模式,音频数据先存储在本地,然后一次性传输到服务器进行处理,服务器处理完成后一次性返回识别结果
* hybrid:部分处理可以在本地进行,而另一部分可以再服务器端进行,以实现更好的性能和用户体验
*/
parser
.addArgument("--mode")
.help("mode for asr.")
.setDefault("offline")
.type(String.class)
.required(false);
// 热词
parser
.addArgument("--hotwords")
.help("hotwords, splited by space, hello 30 nihao 40")
.setDefault("")
.type(String.class)
.required(false);
// ws的ip
String srvIp = "";
// ws的端口
String srvPort = "";
// 音频路径
String wavPath = "";
// 线程数
int numThreads = 1;
// 分块大小
String chunk_size = "";
// 分块间隔
int chunk_interval = 10;
// 状态:默认为离线状态
String strmode = "offline";
// 热词
String hot = "";
try {
Namespace ns = parser.parseArgs(args);
srvIp = ns.get("host");
srvPort = ns.get("port");
wavPath = ns.get("audio_in");
numThreads = ns.get("num_threads");
chunk_size = ns.get("chunk_size");
chunk_interval = ns.get("chunk_interval");
strmode = ns.get("mode");
hot = ns.get("hotwords");
System.out.println(srvPort);
} catch (ArgumentParserException ex) {
ex.getParser().handleError(ex);
return "";
}
WsSendDataUtils.strChunkSize = chunk_size;
WsSendDataUtils.chunkInterval = chunk_interval;
WsSendDataUtils.wavPath = wavPath;
WsSendDataUtils.mode = strmode;
WsSendDataUtils.hotwords = hot;
System.out.println(
"serIp="
+ srvIp
+ ",srvPort="
+ srvPort
+ ",wavPath="
+ wavPath
+ ",strChunkSize"
+ strChunkSize);
// 设置音频采样率为16000Hz
int RATE = 16000;
//
String[] chunkList = strChunkSize.split(",");
// 音频分块大小
int int_chunk_size = 60 * Integer.valueOf(chunkList[1].trim()) / chunkInterval;
// 计算每个音频分块的大小,以采样点为单位
int CHUNK = Integer.valueOf(RATE / 1000 * int_chunk_size);
// 计算音频分块的步长,以采样点为单位
int stride =
Integer.valueOf(
60 * Integer.valueOf(chunkList[1].trim()) / chunkInterval / 1000 * 16000 * 2);
System.out.println("chunk_size:" + String.valueOf(int_chunk_size));
System.out.println("CHUNK:" + CHUNK);
System.out.println("stride:" + String.valueOf(stride));
// 设置WebSocket客户端发送数据的分块大小
WsSendDataUtils.sendChunkSize = CHUNK * 2;
// ws地址
String wsAddress = "ws://" + srvIp + ":" + srvPort;
WsSendDataUtils c = new WsSendDataUtils(new URI(wsAddress));
// 连接WebSocket
c.connect();
// 等待ws返回结果
String wsData = WsSendDataUtils.messageMap.get("receivedWsData");
while (StringUtils.isEmpty(wsData)) {
Thread.sleep(3000);
wsData = WsSendDataUtils.messageMap.get("receivedWsData");
logger.info("正在获取wsData");
}
if (StringUtils.isNotBlank(wsData)) {
WsSendDataUtils.messageMap.remove("receivedWsData");
return wsData;
}
System.out.println("wsAddress:" + wsAddress);
return "";
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
}