今天初步完成了websocket视频接口的编写。websocket入门请看这位大神的文章SpringBoot2.0集成WebSocket,实现后台向前端推送信息
和前后端配合完成websocket传输 根据上面两篇文章可以完胜基本的需求
具体流程如下:
另一家公司会在客户端通过websocket传输视频,需要调用我的接口,我这里通过websocket接受这个视频。
代码如下(示例):
org.springframework.boot
spring-boot-starter-websocket
org.java-websocket
Java-WebSocket
1.3.5
首先需要进行配置(示例):
package dzftxt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/8 14:08
* @description:websocket的bean
* @modified By:
* @version: $
*/
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
然后进行serve的开发(示例):
package dzftxt.web.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import dzftxt.data.dataobject.Videos;
import dzftxt.data.form.VideoForm;
import dzftxt.service.SaveFileI;
import dzftxt.service.VideoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/12 10:24
* @description:用于接受处理客户端向服务端传递的信息
* @modified By:
* @version: $
*/
@Component
@ServerEndpoint("/upload/{sid}")
public class WebSocketUploadServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketUploadServer.class);
//用来记录当前在线连接数,
private static int onlineCount = 0;
//线程安全的set,用来存放每个客户端对应的websocket对象
private static CopyOnWriteArraySet<WebSocketUploadServer> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static SaveFileI saveFileI;
@Autowired
public void setSaveFileI(SaveFileI saveFileI){
WebSocketUploadServer.saveFileI=saveFileI;
}
//用hashmap存储文件对象和文件路径
private HashMap docUrl;
//结束标志
private String endupload = "over";
private static VideoService videoService;
@Autowired
public void setVideoService(VideoService videoService){
WebSocketUploadServer.videoService=videoService;
}
//连接成功时,服务端会自动调用这个方法
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid){
//这一句是注入session,否则this无法发送消息
this.session=session;
webSocketSet.add(this);
addOnlineCount();
LOG.info(sid + "------连接成功---当前在线人数为"+onlineCount);
}
/**
* 连接关闭时调用的方法
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
//在线人数减1
subOnlineCount();
//从set中删除
webSocketSet.remove(this);
LOG.info(sid + "已关闭连接" + "----剩余在线人数为:" + onlineCount);
}
/*
下面这个onmessage只负责处理客户端传来的字符串类型的文件,主要用于和前端的配合,
*/
/*@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
//把前端传来的json转为对象‘
JSONObject jsonObject = JSON.parseObject(message);
//消息类型 比如类型有文件名称filename,传的文件的第几份filecount,是否结束over
String type = jsonObject.getString("type");
//得到消息内容
String data = jsonObject.getString("data");
//一开始文件名传进来,服务端接受后生成有具体地址的文件对象,并向客户端传递ok进行下一步的传输
if ("fileName".equals(type)) {
LOG.info("传输的文件名为:" + data);
try {
Map map = saveFileI.docPath(data);
docUrl = (HashMap) map;
this.sendMessage("ok");
} catch (IOException e) {
e.printStackTrace();
}
}
//如果是发来的文件的第几份,那么打印出是第几份
else if("fileCount".equals(type)){
LOG.info("传输第"+data+"份");
}
//如果传递的是结束,则给客户端返回一个文件存储地址
else if (endupload.equals(type)){
LOG.info("===============>传输成功");
String path = (String) docUrl.get("nginxPath");
try {
this.sendMessage(path);
} catch (IOException e) {
e.printStackTrace();
}
}
}*/
/*
下面这个onmessage只负责处理客户端传来的byte[]的文件,主要用于和前端的配合,进行分段传输
*/
/*@OnMessage
public void onMessage(byte[] message,Session session){
//
try{
//把传来的字节流数组写入到上面创建的文件对象里去
saveFileI.saveFileFromBytes(message,docUrl);
// saveFileI.saveFileFromBytes(message);
//只有客户端接受到ok才会传输下一段文件
this.sendMessage("服务端已成功接受视频");
}catch (IOException e){
e.printStackTrace();
}
}*/
/*
* 下面的onmessage用于接受经过json转换的map
* */
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
//把前端传来的json转为对象‘
JSONObject jsonObject = JSON.parseObject(message);
//消息类型 比如类型有文件名称filename,传的文件的第几份filecount,是否结束over
String fileName= jsonObject.getString("fileName");
VideoForm videos=JSON.toJavaObject(jsonObject,VideoForm.class);
//得到消息内容
if (videos.getContent()==null) System.out.println("传输的内容为空!");
Map<String, Object> map = saveFileI.docPath(fileName);
//
try {
saveFileI.saveFileFromBytes(videos.getContent(),map);
this.sendMessage("服务端已成功接受视频:"+videos.toString());
//保存视频信息到数据库
Videos videos1=new Videos();
BeanUtils.copyProperties(videos,videos1);
videos1.setPath((String) map.get("path"));
videoService.saveVideo(videos1);
this.sendMessage("服务端已成功保存该视频");
LOG.info(sid + "------视频传输成功---");
}catch (IOException e){
e.printStackTrace();
}
}
//这个方法用于接受处理客户端传来的字节流数组类型的文件
@OnMessage
public void onMessage(byte[] message,Session session){
//
try{
//把传来的字节流数组写入到上面创建的文件对象里去
// saveFileI.saveFileFromBytes(message,docUrl);
saveFileI.saveFileFromBytes(message);
//只有客户端接受到ok才会传输下一段文件
this.sendMessage("服务端已成功接受视频");
}catch (IOException e){
e.printStackTrace();
}
}
//服务端向客户端发送消息
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
}
/**
* 群发消息功能
*
* @param message 消息内容
* @param sid 房间号
*/
public static void sendInfo(String message, @PathParam("sid") String sid) {
LOG.info("推送消息到所有客户端" + sid + ",推送内容:" + message);
for (WebSocketUploadServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
item.sendMessage(message);
} catch (IOException e) {
LOG.error("消息发送失败" + e.getMessage(), e);
return;
}
}
}
/**
* 原子性的++操作
*/
public static synchronized void addOnlineCount() {
WebSocketUploadServer.onlineCount++;
}
/**
* 原子性的--操作
*/
public static synchronized void subOnlineCount() {
WebSocketUploadServer.onlineCount--;
}
}
savefileI.java:
package dzftxt.service;
import java.util.Map;
public interface SaveFileI {
/**
* 生成文件路径
* @param fileName 接收文件名
* @return 返回一个map,里面包换文件路径,文件,相对路径
*/
Map docPath(String fileName);
/**
* 将字节流写入文件
* @param b 字节流数组,是前端传来的。把这个字节流数组添加到map中的file中去(先通过file生成文件流对象outputstream)
* @param map 文件路径
* @return 返回是否成功
*/
boolean saveFileFromBytes(byte[] b, Map map);
boolean saveFileFromBytes(byte[] b);
}
saveFileImpl.java:
package dzftxt.service;
import org.springframework.stereotype.Service;
import javax.websocket.server.ServerEndpoint;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/12 10:17
* @description:
* @modified By:
* @version: $
*/
@Service
public class SaveFileImpl implements SaveFileI{
public Map docPath(String fileName) {
HashMap map = new HashMap<>();
//根据时间生成文件夹路径
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd");
String docUrl = simpleDateFormat.format(date);
//文件保存地址
String path = "/data/images/" + docUrl;
//创建文件
File dest = new File(path+"/" + fileName);
//如果文件已经存在就先删除掉
if (dest.getParentFile().exists()) {
dest.delete();
}
map.put("dest", dest);
map.put("path", path+"/" + fileName);
map.put("nginxPath","/"+docUrl+"/"+fileName);
return map;
}
public boolean saveFileFromBytes(byte[] b, Map map) {
//创建文件流对象
FileOutputStream fstream = null;
//从map中获取file对象
File file = (File) map.get("dest");
//判断路径是否存在,不存在就创建
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try {
fstream = new FileOutputStream(file, true);
fstream.write(b);
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
if (fstream != null) {
try {
fstream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return true;
}
public boolean saveFileFromBytes(byte[] b) {
//创建文件流对象
FileOutputStream fstream = null;
//从map中获取file对象
File file = new File("E:\\Users\\Administrator.DESKTOP-K38H7QV\\Desktop\\videos\\test.mp4");
//判断路径是否存在,不存在就创建
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try {
fstream = new FileOutputStream(file, true);
fstream.write(b);
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
if (fstream != null) {
try {
fstream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return true;
}
}
客户端的WebsocketConfig,java
package com.nju.config;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.java_websocket.client.WebSocketClient;
import java.net.URI;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/8 14:08
* @description:websocket的bean
* @modified By:
* @version: $
*/
@Component
public class WebSocketConfig {
@Bean
public WebSocketClient webSocketClient(){
try{
WebSocketClient webSocketClient=new WebSocketClient(new URI("ws://localhost:8090/dzftxt/upload/25477fes1"),new Draft_6455()) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("客户端建立连接");
}
@Override
public void onMessage(String s) {
System.out.println("客户端收到消息----"+s);
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("客户端关闭连接");
}
@Override
public void onError(Exception e) {
}
};
webSocketClient.connect();
return webSocketClient;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
package com.nju.service;
import org.java_websocket.client.WebSocketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/8 11:19
* @description:用于websocket传输视频的接口
* @modified By:
* @version: $
*/
@Component
public class WebSocketService {
@Autowired
private WebSocketClient webSocketClient;
public void sendMessageToServe(String s){
webSocketClient.send(s);
}
public void sendByteMessage(byte[] bytes){
webSocketClient.send(bytes);
}
}
package com.nju.controller;
import com.alibaba.fastjson.JSON;
import com.nju.Utils.FileUtil;
import com.nju.service.WebSocketService;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.websocket.server.ServerEndpoint;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/9 10:39
* @description:测试文件上传
* @modified By:
* @version: $
*/
@Controller
@ServerEndpoint("/clientWs")
public class UploadFileController {
private static WebSocketService webSocketService;
@Autowired
public void setWebSocketService(WebSocketService webSocketService){
UploadFileController.webSocketService=webSocketService;
}
@RequestMapping("/upload")
public String uploadFile(){
return "fileUpload";
}
/*
* 下面这个wsupload用于直接传输byte类型
* */
@RequestMapping("/wsUpload")
public String wsUpload() throws FileNotFoundException {
// File file=new File("E:\\Users\\Administrator.DESKTOP-K38H7QV\\Desktop\\test.mp4");
File file=new File("D:\\BaiduYunDownload\\14test.mp4");
String name=file.getName();
byte[] bytes = FileUtil.fileToBinArray(file);
//如果直接传入会不会出问题呢测试一下
webSocketService.sendByteMessage(bytes);
/* int start_size=0;
int end_size=0;
//当服务端处理完了
while(end_size map=new HashMap();
map.put("fileName",file.getName());
map.put("content",bytes);
map.put("ah","案号test");
map.put("dzftId","锁id");
map.put("fydm","法院代码test");
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
map.put("createTime",(df.format(new Date())).toString());
webSocketService.sendMessageToServe(JSON.toJSONString(map));
/* int start_size=0;
int end_size=0;
//当服务端处理完了
while(end_size
可以编写多个这样的客户端进行测试 测试方式,在浏览器输入localhost:xxxx/wsUpload1
经过了测试,发现几个问题,
一.连接断开,而且不报任何错
解决方式:是因为缓冲区过小,而传输的视频过大,websocket的默认缓冲区应该是8k,我们传输的视频有10几兆了,所以我们应该去改动缓冲区,查询资料,这里的缓冲区是独立的,有几个websocket连接就有几个缓冲区。
package dzftxt.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.util.WebAppRootListener;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/11 9:23
* @description:设置服务端接受websocket缓冲区大小
* @modified By:
* @version: $
*/
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class WebAppRootContext implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addListener(WebAppRootListener.class);
//这里设置了30兆的缓冲区
//Tomcat每次请求过来时在创建session时都会把这个webSocketContainer作为参数传进去所以对所有的session都生效了
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","30000000");
servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","30000000");
}
}
二,堆溢出
如果设置缓冲区过大,那么jvm可能会出现堆溢出,我的jvm最大缓冲区是512M,多连一些就会出现堆溢出,解决方式:1设置恰当的缓冲区大小 2.使用jetty而不是tomcat,参考这位大神的文章Tomcat与jetty对比3.改动jvm的默认堆内存,治标不治本
三.在 WebSocketUploadServer类中 ,@Autowired无法注入,这个问题很多大神都有解答,总结一下
A:使用websocket @Autowired注解可能会出现问题:
(因为这个解答参考了太多文章,不知道是哪位的原创了)WebSocketClient自动注入会包空指针吧,SpringBoot+WebSocket 对每个客户端连接都会创建一个 WebSocketServer(@ServerEndpoint 注解对应的) 对象,Bean 注入操作会被直接略过。
B.解决思路:WebSocket是线程安全的,有用户连接时就会创建一个新的端点实例,一个端点只能保证一个线程调用。总结就是,WebSocket是多对象的。不管单独使用也好,结合spring也好,或者结合SpringBoot也罢,他都是多对象的。
C.问题原因:WebSocket是多对象的,使用的spring却是单例模式。这两者刚好冲突。@Autowired注解注入对象是在启动的时候就把对象注入,而不是在使用A对象时才把A需要的B对象注入到A中。而WebSocket在刚刚有说到,有连接时才实例化对象,而且有多个连接就有多个对象。由此得知,RedisUtil根本就没有注入到WebSocket当中。
默认:
@Autowired
Private SaveFileI saveFileI;
静态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,普通方法(实例方法)才属于对象。而Spring容器管理的都是实例对象,包括它的@Autowired依赖注入的均是容器内的对象实例,所以对于static成员是不能直接使用@Autowired注入的。但是对static使用@Autowired也不行,因为
扫描Class类需要注入的元数据的时候,直接选择忽略掉了static成员(包括属性和方法)
//而使用过static之后,成为了类对象,就需要自己写出set方法
private static SaveFileI saveFileI;
@Autowired
public void setSaveFileI(SaveFileI saveFileI){
WebSocketUploadServer.saveFileI=saveFileI;
}
思想只是一个:延迟为static成员属性赋值。
四,通过直接的jsoObject无法把byte[]转化为正常的byte[],会提示无法从string,转为byte[]。这种情况下,要写一个对象,比如videos,把string转为jsonobject后,再转为videos对象即可。这时候,就可以通过video.getBytes得到该比特数组了。
package dzftxt.data.dataobject;
import lombok.Data;
import javax.persistence.*;
/**
* @author :Siyuan Gao
* @date :Created in 2020/9/14 10:30
* @description:用于接受websocket传来的视频
* @modified By:
* @version: $
*/
@Data
@Entity
@Table( name ="videos" , schema = "")
public class Videos {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name="vid")
private int vid;
/**
*文件路径
*/
@Column(name="path")
private String path;
/**
*电子锁id
*/
@Column(name="dzftId")
private String dzftId;
/**
*案号
*/
@Column(name="ah")
private String ah;
/**
*触发时间
*/
@Column(name="createTime")
private String createTime;
/**
*法院代码
*/
@Column(name="fydm")
private String fydm;
/**
*逻辑删除位
*/
@Column(name="deleted")
private Boolean deleted=false;
}
多思考,多读大神博客,多看源码
https://www.iteye.com/blog/haoningabc-2345038