结构:
WebSocketConfig:
@Configuration
public class WebSocketConfig extends ServerEndpointConfig.Configurator {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
SocketController:
@RestController
@RequestMapping(value = "/socket")
public class SocketController {
private static final Logger logger = LoggerFactory.getLogger(SocketController.class);
@Autowired
private ClientSocket clientSocket;
@ApiOperation(value = "获取bilibili弹幕")
@PostMapping(value = "/open")
public Result open(@ApiParam(value = "房间号") @RequestParam(value = "roomId", required = false) Integer roomId) {
try {
clientSocket.start(roomId);
} catch (Exception e) {
logger.error(" ===> 获取bilibili弹幕错误:{}", e.getMessage());
}
return ResultUtil.success();
}
}
ResultCodeEnum:
public enum ResultCodeEnum {
ERROR_CODE(1),
SUCCESS_CODE(0),
;
private Integer code;
ResultCodeEnum(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
}
ResultMsgEnum:
public enum ResultMsgEnum {
ERROR_MSG_DEFAULT("error"),
SUCCESS_MSG_DEFAULT("success"),
;
private String msg;
ResultMsgEnum(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
}
UintEnum:
public enum UintEnum {
UINT32(8),
UINT16(4),
UINT8(2),
;
private Integer byteNum;
public Integer getByteNum() {
return byteNum;
}
public void setByteNum(Integer byteNum) {
this.byteNum = byteNum;
}
UintEnum(Integer byteNum) {
this.byteNum = byteNum;
}
}
LiveRespDanMuVO:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ApiModel
public class LiveRespDanMuVO {
@ApiParam(value = "用户ID")
private Integer uid;
@ApiParam(value = "用户名")
private String uName;
@ApiParam(value = "弹幕内容")
private String content;
}
BinaryHandleInstance:
@Component
@Data
public class BinaryHandleInstance {
private static final char ZERO = '0';
private StringBuffer reqParam;
public StringBuffer handleByteData(Integer offset, UintEnum unit, String valueStr) {
valueStr = fillValueStr(unit, valueStr);
try {
if (Objects.isNull(this.reqParam)) {
this.reqParam = new StringBuffer();
}
this.reqParam.insert(finalOffset(offset), valueStr);
} catch (Exception e) {
throw new RuntimeException(" ===> valueStr or offset is error");
}
return reqParam;
}
private String fillValueStr(UintEnum unit, String valueStr) {
while (valueStr.length() < lengthFromUnit(unit)) {
valueStr = ZERO + valueStr;
}
return valueStr;
}
private Integer lengthFromUnit(UintEnum unit) {
return unit.getByteNum();
}
private Integer finalOffset(Integer offset) {
return 2 * offset;
}
}
LiveResponseMsgServiceImpl:
@Service
public class LiveResponseMsgServiceImpl implements LiveResponseMsgService {
private static final String DANMU_INFO = "info";
@Override
public LiveRespDanMuVO ToVOFromJson(JSONObject jsonObject) {
if (Objects.isNull(jsonObject)) {
return null;
}
JSONArray info = jsonObject.getJSONArray(DANMU_INFO);
if (CollectionUtils.isEmpty(info)) {
return null;
}
JSONArray userInfo = info.getJSONArray(2);
if (CollectionUtils.isEmpty(userInfo)){
return null;
}
Integer uid = userInfo.getInteger(0);
String uName = userInfo.getString(1);
String content = info.getString(1);
return LiveRespDanMuVO.builder()
.uid(uid)
.uName(uName)
.content(content)
.build();
}
}
LiveResponseMsgService:
public interface LiveResponseMsgService {
LiveRespDanMuVO ToVOFromJson(JSONObject jsonObject);
}
Regex:
public class Regex {
/**
* 匹配b站返回的弹幕前缀
*/
public static final String LIVE_RESPONSE_DANMU_MSG = "\\{\"cmd\":\"DANMU_MSG\",[^\\}].*";
/**
* 匹配b站响应通用前缀
*/
public static final String LIVE_RESPONSE_MSG = "\\{\"cmd\"[^\\}].*";
/**
* 匹配json字符串
*/
@Deprecated
public static final String KEY_VALUE_MSG = "\\{\"([a-zA-Z_]+)\\\":(.+)}";
/**
* 匹配双字节的字符
*/
@Deprecated
public static final String BYTE2_MSG = "[\\x00-\\xff].?";
}
Result:
public class Result<T> {
private Integer code;
private String msg;
private T data;
public Result() {
}
public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
BinaryHandleUtil:
@Component
public class BinaryHandleUtil {
private static final String COMMA = ",";
@Autowired
private BinaryHandleInstance instance;
public BinaryHandleUtil setUnit(Integer offset, UintEnum unit, Integer value) {
if (Objects.isNull(value)) {
throw new RuntimeException(" ===> value is null");
}
String valueStr = Integer.toHexString(value);
instance.handleByteData(offset, unit, valueStr);
return this;
}
public String getHexBytesStr() {
return this.instance.getReqParam().toString();
}
public byte[] HexStrToByteArray() {
String hexStr = this.getHexBytesStr();
if (Objects.isNull(hexStr)) {
return null;
}
if (Objects.equals(hexStr, 0)) {
return new byte[0];
}
byte[] byteArray = new byte[hexStr.length() / 2];
for (int i = 0; i < byteArray.length; i++) {
String subStr = hexStr.substring(2 * i, 2 * i + 2);
byteArray[i] = ((byte) Integer.parseInt(subStr, 16));
}
return byteArray;
}
public byte[] HexByteArray() {
return this.HexStrToByteArray();
}
public void clearInstanceBuffer() {
instance.setReqParam(new StringBuffer());
}
public int[] toUintArrayFromByteArray(byte[] bytes) {
int[] uintArray = new int[bytes.length];
for (int i = 0; i < bytes.length; i++) {
uintArray[i] = Byte.toUnsignedInt(bytes[i]);
}
return uintArray;
}
public StringBuffer toBinaryStrFromUintArray(int value, StringBuffer builder) {
if (Objects.isNull(value)){
throw new RuntimeException(" value is null");
}
String param1 = new String(String.valueOf(value));
String param2 = new String(COMMA);
builder.append(param1);
builder.append(param2);
return builder;
}
public String getStrByDecompress(StringBuffer buffer){
buffer.deleteCharAt(buffer.length() - 1);
String binaryStr = buffer.toString();//binaryStr: 0, 0, 0, 26, 0, 16, 0, 1, 0, 0, 0, 8
if (Objects.isNull(binaryStr)){
throw new RuntimeException("binaryStr is null");
}
byte[] clientBytes = PakoUtil.receive(binaryStr);
byte[] bytes = ZlibUtil.decompress(clientBytes);
return new String(bytes, StandardCharsets.UTF_8);
}
public byte[] getByteArrayFromInt(int value){
byte[] bytes = new byte[1];
bytes[0] = (byte) (value & 0xFF);
// bytes[1] = (byte) ((value>>8) & 0xFF);
// bytes[2] = (byte) ((value>>16) & 0xFF);
// bytes[3] = (byte) ((value>>24) & 0xFF);
return bytes;
}
}
PakoUtil:
public class PakoUtil {
public static byte[] receive(String arrInt){
/**
* 将数字字符串 -> byte[]
*/
String[] a = arrInt.split(",");//arrInt: 0, 0, 0, 26, 0, 16, 0, 1, 0, 0, 0, 8,
byte[] clientBytes = new byte[a.length];
int i = 0;
for (String e : a) {
clientBytes[i] = Integer.valueOf(e).byteValue();
i++;
}
return clientBytes;
}
/**
* 发送给 Pako 的数据格式
* @param bytes 服务端生成的字节数组
* @return String 发送给 pako.js 的数据格式
*/
public static String send(byte[] bytes) {
String[] ints = new String[bytes.length];
int j=0;
for(byte e:bytes) {
int t = e;
if(t<0) {
t = 256+t;
}
ints[j++] = String.valueOf(t);
}
return String.join(",", ints);
}
}
ResultUtil:
public class ResultUtil {
/**
* 成功对象
* 返回携带data、msg的
*
* @param msg
* @param data
* @return
*/
public static Result success(String msg, Object data) {
return successResult(msg, data);
}
/**
* 成功对象
* 返回携带data的
*
* @param data
* @return
*/
public static Result success(Object data) {
return successResult(null, data);
}
/**
* 成功对象
* 返回携带msg的
*
* @param msg
* @return
*/
public static Result success(String msg) {
return successResult(msg, null);
}
/**
* 成功对象
* 返回默认的
*
* @return
*/
public static Result success() {
return successResult(null, null);
}
/**
* 失败对象
* 返回携带data、msg的
*
* @param msg
* @param data
* @return
*/
public static Result error(String msg, Object data) {
return errorResult(msg, data);
}
/**
* 失败对象
* 返回携带data的
*
* @param data
* @return
*/
public static Result error(Object data) {
return errorResult(null, data);
}
/**
* 失败对象
* 返回携带msg的
*
* @param msg
* @return
*/
public static Result error(String msg) {
return errorResult(msg, null);
}
/**
* 失败对象
* 返回默认的
*
* @return
*/
public static Result error() {
return errorResult(null, null);
}
private static Result errorResult(String msg, Object data) {
return StringUtils.isNotEmpty(msg) ? new Result(ResultCodeEnum.ERROR_CODE.getCode(), msg, data) : new Result(ResultCodeEnum.ERROR_CODE.getCode(), ResultMsgEnum.ERROR_MSG_DEFAULT.getMsg(), data);
}
private static Result successResult(String msg, Object data) {
return StringUtils.isNotEmpty(msg) ? new Result(ResultCodeEnum.SUCCESS_CODE.getCode(), msg, data) : new Result(ResultCodeEnum.SUCCESS_CODE.getCode(), ResultMsgEnum.SUCCESS_MSG_DEFAULT.getMsg(), data);
}
}
ZlibUtil:
/**
* zlib 压缩算法
*/
public class ZlibUtil {
/**
* 压缩
*
* @param data 待压缩数据
* @return byte[] 压缩后的数据
*/
public static byte[] compress(byte[] data) {
byte[] output = new byte[0];
Deflater compresser = new Deflater();
compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
return output;
}
/**
* 压缩
* @param data 待压缩数据
* @param os 输出流
*/
public static void compress(byte[] data, OutputStream os) {
DeflaterOutputStream dos = new DeflaterOutputStream(os);
try {
dos.write(data, 0, data.length);
dos.finish();
dos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 解压缩
* @param data 待解压的数据
* @return byte[] 解压缩后的数据
*/
public static byte[] decompress(byte[] data) {
byte[] output = new byte[0];
Inflater decompresser = new Inflater();
decompresser.reset();
decompresser.setInput(data);
ByteArrayOutputStream o = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!decompresser.finished()) {
int i = decompresser.inflate(buf);
o.write(buf, 0, i);
}
output = o.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
o.close();
} catch (IOException e) {
e.printStackTrace();
}
}
decompresser.end();
return output;
}
/**
* 解压缩
* @param is 输入流
* @return byte[] 解压缩后的数据
*/
public static byte[] decompress(InputStream is) {
InflaterInputStream iis = new InflaterInputStream(is);
ByteArrayOutputStream o = new ByteArrayOutputStream(1024);
try {
int i = 1024;
byte[] buf = new byte[i];
while ((i = iis.read(buf, 0, i)) > 0) {
o.write(buf, 0, i);
}
} catch (IOException e) {
e.printStackTrace();
}
return o.toByteArray();
}
}
ClientSocket:
@ClientEndpoint
@Component
public class ClientSocket {
private static final Logger logger = LoggerFactory.getLogger(ClientSocket.class);
private static final Pattern PATTERN = Pattern.compile(Regex.LIVE_RESPONSE_DANMU_MSG);
private static final String BUILD_JSON_PARAM_WEB = "web";
private static final String BUILD_JSON_PARAM_CLIENTVER = "1.4.0";
private static final Integer BUILD_JSON_PARAM_UID = 0;
private static final Integer BUILD_JSON_PARAM_PROTOVER = 1;
private JSONObject json;
private Session session;
private Integer roomId;
String url = "wss://dsa-cn-live-comet-01.chat.bilibili.com:2245/sub";
@Autowired
private BinaryHandleUtil binaryHandleUtil;
@Autowired
private LiveResponseMsgService liveResponseMsgService;
private JSONObject init(Integer roomId) {
this.json = new JSONObject();
json.put("roomid", roomId);
json.put("uid", BUILD_JSON_PARAM_UID);
json.put("protover", BUILD_JSON_PARAM_PROTOVER);
json.put("platform", BUILD_JSON_PARAM_WEB);
json.put("clientver", BUILD_JSON_PARAM_CLIENTVER);
// json.put("type", 2);
// json.put("key", "u91Sk476h2FV7KCv59U35sEAuVNmQUq0yBfeqJVHIKZqkxVPe_hJYD1GKS-cS43jNMVpD_TEih7K-ybwqpGvotO7luheMjhEi0w7wjILrm8WJ-dL4xid3D0rnJiP7QruH3xTVYNw41xffdF5-UM=");
return json;
}
public void start(Integer roomId) {
try {
this.roomId = roomId;
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
URI uri = URI.create(url);
container.connectToServer(ClientSocket.this, uri);
} catch (DeploymentException | IOException e) {
logger.error(e.getMessage(), e);
}
}
@OnOpen
public void onOpen(Session session) {
this.session = session;
logger.info(" ===> 连接 billibili 成功");
ByteArrayOutputStream dataBytes = buildCertifyByte(init(this.roomId));
if (Objects.isNull(dataBytes)) {
throw new RuntimeException(" ===> 认证数据结果为空");
}
int size = dataBytes.size();
byte[] bytes = dataBytes.toByteArray();
binaryHandleUtil.setUnit(0, UintEnum.UINT32, size + 16)
.setUnit(4, UintEnum.UINT16, 16)
.setUnit(6, UintEnum.UINT16, 1)
.setUnit(8, UintEnum.UINT32, 7)
.setUnit(12, UintEnum.UINT32, 1);
for (int i = 0; i < size; i++) {
binaryHandleUtil.setUnit(16 + i, UintEnum.UINT8, Integer.parseInt(bytes[i] + ""));
}
ByteBuffer byteBuffer = ByteBuffer.wrap(binaryHandleUtil.HexByteArray());
try {
session.getBasicRemote().sendBinary(byteBuffer);
binaryHandleUtil.clearInstanceBuffer();//清除缓存
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
heartbeat();//心跳请求 保持连接
}
},1,30, TimeUnit.SECONDS);
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
@OnClose
public void onClose() {
logger.info(" ===> 关闭 billibili 成功");
}
@OnError
public void onError(Throwable e) {
logger.info(" ===> 连接bilibili 发生异常");
logger.error(e.getMessage(), e);
}
@OnMessage
public void onMessage(Session session, byte[] message) {
// logger.info(" ===> message:{}", message);
int[] uintArray = binaryHandleUtil.toUintArrayFromByteArray(message);//转成无符号整型 uint: 0, 0, 1, -12, 0, 16, 0, 1, 0, 0, 0, 8
Integer packageLength = readIntFromByteArray(uintArray, 0, 4);
Integer headLength = readIntFromByteArray(uintArray, 4, 2);
Integer ver = readIntFromByteArray(uintArray, 6, 2);
Integer op = readIntFromByteArray(uintArray, 8, 4);
Integer seq = readIntFromByteArray(uintArray, 12, 4);
if (Objects.equals(op, 5)) {
String bodyStr;
int offset = 0;
while (offset < packageLength) {
int countPackageLength = readIntFromByteArray(uintArray, offset + 0, 4);
int countHeadLength = 16;
int[] data = Arrays.copyOfRange(uintArray, offset + countHeadLength, offset + countPackageLength);
StringBuffer buffer = new StringBuffer();
if (Objects.equals(ver, 2)) {
//协议版本为 2 时 数据有进行压缩
for (int value : data) {//data: 0, 0, 0, 26, 0, 16, 0, 1, 0, 0, 0, 8
buffer = binaryHandleUtil.toBinaryStrFromUintArray(value, buffer);
}
bodyStr = binaryHandleUtil.getStrByDecompress(buffer);
logger.info(" ===> strByDecompress:{}", bodyStr);
} else {
//协议版本为 0 时 数据没有进行压缩
for (int value : data) {
//没不需要解压直接把无符号整型转成字节 在重新构造字符串
byte[] array = binaryHandleUtil.getByteArrayFromInt(value);
String strByByteArray = new String(array, StandardCharsets.UTF_8);
buffer.append(strByByteArray);
}
bodyStr = buffer.toString();
logger.info(" ===> str:{}", bodyStr);
}
// 同一条弹幕消息中可能存在多条信息,用正则筛出来
Matcher matcher = PATTERN.matcher(bodyStr);
StringBuffer stringBuffer = new StringBuffer();
String group = "";
JSONObject jsonObject = null;
if (matcher.find()) {
group = matcher.group();//group: "{\"cmd\":\"DANMU_MSG\",\"info\":[[0,1,25,16777215,1673881110416,1673881101,0,\"748aa4fc\",0,0,0,\"\",0,\"{}\",\"{}\",{\"mode\":0,\"show_player_type\":0,\"extra\":\"{\\\"send_from_me\\\":false,\\\"mode\\\":0,\\\"color\\\":16777215,\\\"dm_type\\\":0,\\\"font_size\\\":25,\\\"player_mode\\\":1,\\\"show_player_type\\\":0,\\\"content\\\":\\\"kana;\\\",\\\"user_hash\\\":\\\"1955243260\\\",\\\"emoticon_unique\\\":\\\"\\\",\\\"bulge_display\\\":0,\\\"recommend_score\\\":0,\\\"main_state_dm_color\\\":\\\"\\\",\\\"objective_state_dm_color\\\":\\\"\\\",\\\"direction\\\":0,\\\"pk_direction\\\":0,\\\"quartet_direction\\\":0,\\\"anniversary_crowd\\\":0,\\\"yeah_space_type\\\":\\\"\\\",\\\"yeah_space_url\\\":\\\"\\\",\\\"jump_to_url\\\":\\\"\\\",\\\"space_type\\\":\\\"\\\",\\\"space_url\\\":\\\"\\\",\\\"animation\\\":{},\\\"emots\\\":null}\"},{\"activity_identity\":\"\",\"activity_source\":0,\"not_show\":0}],\"kana;\",[49515343,\"nenpenAIagi\",0,0,0,10000,1,\"\"],[],[0,0,9868950,\"\\u003e50000\",0],[\"\",\"\"],0,0,null,{\"ts\":1673881110,\"ct\":\"762E818\"},0,0,null,null,0,7]}\u0000\u0000\u0000{\u0000\u0010\u0000\u0000\u0000\u0000\u0000\u0005\u0000\u0000\u0000\u0000{\"cmd\":\"WATCHED_CHANGE\",\"data\":{\"num\":35975380,\"text_small\":\"3597.5万\",\"text_large\":\"3597.5万人看过\"}}";
char[] chars = group.toCharArray();
//遇到\u0000 后面就可以不需要遍历了 直接终止
for (char aChar : chars) {
if (Objects.equals(0, aChar - 0)) {
break;
}
stringBuffer.append(String.valueOf(aChar));
}
try {
jsonObject = JSON.parseObject(stringBuffer.toString());
LiveRespDanMuVO liveRespDanMuVO = liveResponseMsgService.ToVOFromJson(jsonObject);
logger.info(" ===> 弹幕内容:{}", liveRespDanMuVO.toString());
} catch (Exception e) {
logger.error(" ===> error:{}", stringBuffer.toString());
}
}
offset += countPackageLength;
}
}
}
private ByteArrayOutputStream buildCertifyByte(JSONObject jsonObject) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
String jsonStr = jsonObject.toJSONString();
int length = jsonStr.length();
char c;
for (int i = 0; i < length; i++) {
c = jsonStr.charAt(i);
if (c >= 0x010000 && c <= 0x10FFFF) {
bytes.write(((c >> 18) & 0x07) | 0xF0);
bytes.write(((c >> 12) & 0x3F) | 0x80);
bytes.write(((c >> 6) & 0x3F) | 0x80);
bytes.write((c & 0x3F) | 0x80);
} else if (c >= 0x000800 && c <= 0x00FFFF) {
bytes.write(((c >> 12) & 0x0F) | 0xE0);
bytes.write(((c >> 6) & 0x3F) | 0x80);
bytes.write((c & 0x3F) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007FF) {
bytes.write(((c >> 6) & 0x1F) | 0xC0);
bytes.write((c & 0x3F) | 0x80);
} else {
bytes.write(c & 0xFF);
}
}
return bytes;
}
/* private void certifyRequest(Integer roomId, String url) {
logger.info("===" + "发送认证请求");
JsScriptUtil.certifyRequest(roomId, url);
}*/
// @Scheduled(fixedRate = 30 * 1000)
private void heartbeat() {
binaryHandleUtil.setUnit(0, UintEnum.UINT32, 0)
.setUnit(4, UintEnum.UINT16, 16)
.setUnit(6, UintEnum.UINT16, 1)
.setUnit(8, UintEnum.UINT32, 2)
.setUnit(12, UintEnum.UINT32, 1);
try {
ByteBuffer byteBuffer = ByteBuffer.wrap(binaryHandleUtil.HexByteArray());
this.session.getBasicRemote().sendBinary(byteBuffer);
logger.info(" ===> heartbeat ing");
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
private Integer readIntFromByteArray(int[] byteBuffer, Integer start, Integer len) {
Double result = 0.0;
for (int i = len - 1; i >= 0; i--) {
result += Math.pow(256, len - i - 1) * byteBuffer[start + i];
}
return result.intValue();
}
private void clearBuffer(StringBuffer buffer) {
buffer.delete(0, buffer.length());
}
}
LiveBackendApplication:
@SpringBootApplication(scanBasePackages = "com.liveQIQI")
@EnableScheduling
public class LiveBackendApplication {
public static void main(String[] args) {
SpringApplication.run(LiveBackendApplication.class, args);
}
}
yaml文件:
spring:
application:
name: livebackend
mvc:
pathmatch:
matching-strategy: ant_path_matcher
server:
port: 9090
# mysql
datasource:
url: jdbc:mysql://localhost:3306/powerlive?useUnicode=true&characterEncoding=utf-8&useOldAliasMetadataBehavior=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 677saber
driver-class-name: com.mysql.cj.jdbc.Driver
pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<!-- knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
结果:
完整代码地址:https://gitee.com/power-live/live-backend
(仓库分支选择test-live分支)