打造一个简单的视频同步网站2
前面的内容将前端的部分讲完了,这里说一下后端部分。
后端
根据前面的需求,可以看出后端主要需要三个请求,/get、/update、/getupdate,所以只需要这三个controller即可,对于实体而言,需要一个视频状态的实体,以及一个封装返回信息的实体。
public class MovieSyc {
private String roomId;
private String roomPasswd;
private String movieUrl;
private Integer time = 0; //单位秒 当前播放位置
private boolean isPlay;
public class Msg { //符合链式
private Integer code;
private String msg;
private HashMap<String,Object> extend = new HashMap<>();
public static Msg success(){
Msg m = new Msg();
m.setCode(100);
m.setMsg("成功");
return m;
}
public Msg add(String key,Object value){
this.extend.put(key,value);
return this;
}
后端逻辑分析
之前分析过,对于我们的视频状态信息来说,也就是MovieSyc,只需要保存到webcontex即可,我们可以用roomId作为key。但是对于同步而言,我们前端会发送update请求以及get_update,这两个请求其实是应该用同步进行控制,我们的get_update应该一致处于阻塞状态,一致等到有update的时候才会释放阻塞。所以这里可以用一个阻塞队列来实现。但如果只有一个阻塞队列,会出现如下的问题:
用户1发送了update,然后用户1的get_update拿到了这个更新,那么用户2的get_update还会仍然阻塞,并不会更新,所以需要给每个用户都建立一个阻塞队列,更新的时候大家的阻塞队列都会被放入新的内容,并且在get_update里面会取出,如果取不到则阻塞。所以这里需要一个:
HashMap<String,ArrayBlockingQueue<MovieSyc> > blockingQueueHashMap = new HashMap<>();
这样的结构,其实key是roomId+username,因为这两个唯一标识一个用户。
get分析
在get里面,主要是进行一个判断和一些初始化,判断主要在如果房间存在,则需要判断密码是否正确。而初始化主要指的是创建一个新的MovieSyc和对新加入用户分配阻塞队列。
@RequestMapping("/get")
@ResponseBody
public Msg get(@RequestParam("roomId") String roomId, @RequestParam("roomPasswd") String roomPasswd,@RequestParam("userName") String userName, HttpServletRequest req){
if(blockingQueueHashMap.get(roomId+"_"+userName)==null){
blockingQueueHashMap.put(roomId+"_"+userName,new ArrayBlockingQueue<MovieSyc>(1));
}
ServletContext context = req.getSession().getServletContext();
MovieSyc movieSyc = (MovieSyc) context.getAttribute(roomId);
if(movieSyc==null){
movieSyc = new MovieSyc();
movieSyc.setRoomId(roomId);
movieSyc.setRoomPasswd(roomPasswd);
context.setAttribute(roomId,movieSyc);
return Msg.success().add("movieSyc",movieSyc);
}else{
if(movieSyc.getRoomPasswd().equals(roomPasswd)){
return Msg.success().add("movieSyc",movieSyc);
}else{
return Msg.fail("密码错误");
}
}
}
update分析
这个函数里面主要是把前台的数据更新到我们的状态里面,并且放到阻塞队列里面,可以唤醒get_update.
@RequestMapping("update")
@ResponseBody
public Msg update(String roomId,String movieUrl,Integer time,Boolean play,HttpServletRequest req) throws InterruptedException {
if(roomId==null){
return Msg.fail("更新请提供房间号");
}
ServletContext context = req.getSession().getServletContext();
MovieSyc movieSyc = (MovieSyc) context.getAttribute(roomId);
if(movieSyc==null){
return Msg.fail("房间不存在");
}
if(movieUrl!=null && !movieUrl.equals("")){
movieSyc.setMovieUrl(movieUrl);
}
if(time!=null){
movieSyc.setTime(time);
}
if(movieSyc.getMovieUrl()!=null && play!=null){
movieSyc.setPlay(play);
}
for (Iterator<Map.Entry<String, ArrayBlockingQueue<MovieSyc>>> it = blockingQueueHashMap.entrySet().iterator(); it.hasNext();){
Map.Entry<String, ArrayBlockingQueue<MovieSyc>> entry = it.next();
if(entry.getKey().split("_")[0].equals(roomId)){ //一个房间的
if(entry.getValue().offer(movieSyc,200, TimeUnit.MILLISECONDS)){
System.out.println(entry.getKey()+"放入");
}else{
System.out.println("移除用户"+entry.getKey());
blockingQueueHashMap.remove(entry.getKey());
}
}
}
return Msg.success();
}
这里注意的是,更新阻塞队列的时候,是更新所有同房间的,而不是全部都进行了更新,所以加入了是否同房间的判断。另外可能有些用户已经离开了房间,也就是不在发送对应的get_update请求了,里面的数据不会被取出了。而我们阻塞队列的大小是1,所以放入数据会失败,所以如果放入数据失败的时候,也就是这个用户推出了,所以可以讲其对应的阻塞队列删除即可。
get_update分析
@RequestMapping("/getupdate")
@ResponseBody
public Msg getUpdate(String roomId,String userName,HttpServletRequest req) throws InterruptedException {
MovieSyc movieSyc = blockingQueueHashMap.get(roomId+"_"+userName).take();
System.out.println(roomId+"_"+userName+"取出");
return Msg.success().add("movieSyc",movieSyc);
}
可以看出其主要操作就是一个take,而对于阻塞队列而言,如果没有数据,则会阻塞在这里,知道update放入数据,所以update和get_update实现了一个同步的过程,一个放入一个取出。