多文件自平衡云传输(二)资源接收端篇

  • 资源接收
    • Consumer
    • ReceiveServer
    • ReceiveDeal
    • ReceiveDetail
  • 断点续传
    • ContinueTransfer
    • FileIFComplete


资源接收端从请求到接收的过程:

1.资源接收端启动时先与拥有资源的服务器(可以是APP服务器,也可以是拥有资源的服务器)进行短连接进行资源资本信息的获取。

2.根据获取的资源的信息中的资源的名称进行短连接资源注册中心,请求拥有对应资源的节点列表。

3.根据资源基本信息中的资源的多个文件的基本信息进行本地资源的查询,若本地拥有该资源(之前可能进行过请求保存到本地)则进行缺省部分资源文件的请求,若本地无该资源,则进行全部资源的全部文件的请求。

4.在请求的过程中,资源接收端根据资源分配策略进行分配请求的资源的多个文件,将每个文件分成长度相等的多个片段。

5.接收端进行采用负载均衡策略进行可发送资源节点的选取,选取合适发送的节点,根据节点的个数将多个文件片段分成与节点个数相同的片段组,将每一组分派给节点。

6.将分好的资源片段组通过短连接的方式进行链接发送给该组对应的节点。

7.接收端开启临时的长连接服务器,进行接收发送端的链接与资源片段的发送
8.直到接收完整,当发送端出现异常导致接收端无法接收完整的时候,接收端启用断点续传的功能,再次进行缺省部分资源的请求。直到接收完整接收端拥有该资源。可以选择接收端此时作为一个发送端进行资源的注册,随意。

资源接收

Consumer

此类功能也即消费者的功能
1.进行资源发送节点的获取
2.选择负载均衡的策略,进行节点的选择。

public class Consumer {private static int PORT = 54200;private static String IP = "127.0.0.1";private rmiCilent rc;private CilentProxy cp;private int RegisterPort = PORT;private String RegisterIp = IP;private NetNode self;//节点选择策略private INodeSelect nodeSelect;//资源分配策略private IResourceSelect resourceSelect;private String resourceName;public Consumer() {this.cp = new CilentProxy();rc = new rmiCilent();rc.setPort(RegisterPort);rc.setId(RegisterIp);cp.setRmicilent(rc);defaultInit();}public List<NetNode> getServerNode(String resorceName){//通过名称得到拥有资源的节点列表//通过链接注册中心得到节点列表  this.resourceName = resorceName;IRegister ir = this.cp.getProxy(IRegister.class);return ir.getServerList(resorceName);}   private void defaultInit() {//采用默认节点选择和分配策略nodeSelect = new NodeSelect();resourceSelect = new ResourceSelect();//这里可以采用配置文件的方式进行节点策略的设定}public INodeSelect getNodeSelect() {return nodeSelect;}public IResourceSelect getResourceSelect() {return resourceSelect;}/**与注册中心进行短连接时的异常处理*/class RequstNodeException implements ICilentException{@Overridepublic void peerAbnormalDrop() {getServerNode(resourceName);}}}

ReceiveServer

接收端的入口类,与Consumer不同的是,Consumer不同之处在于,Consumer的主要功能是获取节点和节点的负载均衡的选择。
ReceiveServer给外部一个可以请求资源的方法,对外部而言不用关心内部的请求过程,只需要知道调用了该方法可以请求资源。
真正的资源请求过程由内部去完成,在这里ReceiveServer这个类中关键的作用请求一个资源的基本信息,剩下的工作交给了ReceiveDeal 和Consumer来做,ReceiveDeal 类中拥有Consumer作为成员。


public class ReceiveServer{ private rmiCilent serverRc;private CilentProxy cp;private boolean processBar;private IRequstAction Ire;private ICilentException Ice;public ReceiveServer(int connectServerPort, String connectServerIp) {serverRc = new rmiCilent();serverRc.setPort(connectServerPort);serverRc.setId(connectServerIp);cp = new CilentProxy();cp.setRmicilent(serverRc); }/**通过链接资源服务器进行资源基本信息的获取*并创建ReceiveDeal对象进行真正的节点列表的获取和资源的分配*/public void requstResource(String ResourceName) {//先链接服务器,然后得到对应的资源的基本的信息//先通过服务器得到资源的一个基本的信息。Iserver server = cp.getProxy(Iserver.class);//根据服务器得到资源的基本的信息ResourceInfo ri = server.getResourceInfo(ResourceName);ResourceInfo temp = ri.getResoureFromXml();if(temp == null) {//表明没有该资源,用户第一次请求资源的时候需要设置本地的存储路径//可以给外边进行路径的设置,String localRoot = Ire.notfoundLocalResource(); ri.setRoot(localRoot);ri.saveObject();ri.getAllFiles(ri.getFileInfoMap());//根据请求的资源的左右构造请求的条件}else {//TODO              //根据本地的已经存储的路径,判断该路径下的文件是否完整//如果完整则不用请求//此时应该根据本地的记录,判断是否进行获取。return;}//创建一个线程立即返回,这样对于客户端来说请求资源可以快速的响应new ThreadPool().Execute(new ReceiveDeal(ri, processBar));}/** 是否设置使用接收文件进度条* 同样也可以进行设置进度条的格式,采用set方法进行设置,* 我这里给出的是默认的进度条,进度条不是必选项。*/public void IFuseProcessBar(boolean choose) {this.processBar = choose;   }/**设置请求资源过程中的异常和问题*例如 本地无对应资源表明第一次申请,则需要设置存储路径*/public void setRequstServerException(IRequstAction Ire) {this.Ire = Ire;}/**设置连接服务器的异常*/public void setConnectServerException(ICilentException Ice){this.Ice = Ice;}}

ReceiveDeal

此类的作用就是进行需要请求资源的分配和合适节点的选择(节点列表的获取,资源的分片和节点的选择由Consumer来完成),ReceiveDeal主要进行将分片资源根据节点的个数分组,然后分配给相应个数的节点,与其节点进行短连接告知节点应该发送的片段,并开启线程等待发送节点进行链接发送资源,进行资源的接收。

public class ReceiveDeal implements Runnable{private Consumer consumer;private ResourceInfo ri;private CilentProxy cp;private rmiCilent rc;private static NetNode self = new NetNode();private progressView view;private boolean showProcess;//是否设置进度条private List<NetNode> nodelist; private List<List<FileHead>> headList;static {      self.setIp(getSelfIp());self.setPort(PortPool.getPort());}public ReceiveDeal(ResourceInfo ri, boolean choose) {this.ri = ri;   cp = new CilentProxy();rc = new rmiCilent();this.showProcess = choose;consumer = new Consumer();List<NetNode> templist = consumer.getServerNode(ri.getResourceName());nodelist =  consumer.getNodeSelect().getProperNodeList(templist);headList =  consumer.getResourceSelect().getSendcountList(ri.getFileHeadList(), nodelist.size());}@Overridepublic void run() {     NetNode node = new NetNode();node.setIp(self.getIp());node.setPort(self.getPort());int sendCount = nodelist.size();new Receive(sendCount).start();//开启线程进行accept发送端的链接并且进行资源的接收//进行短连接可发送资源的节点,告知节点其需要发送的资源片段。for(int index = 0; index < nodelist.size(); index++) {//NetNode sender = nodelist.get(index);ResourceInfo requst = new ResourceInfo(ri);requst.setFileHeadList(headList.get(index));         rc.setPort(sender.getPort());rc.setId(sender.getIp());cp.setRmicilent(rc);Isender peer = cp.getProxy(Isender.class);peer.send(node, requst);   }   }public ReceiveDeal getReceiveDeal() {return this;}/**进行资源响应响应链接,并且为每一个接收端开启线程进行资源接收**/class Receive implements Runnable{private int sendCount;private ServerSocket server;private int count = 0;private ContinueTransfer ct;private boolean goon;public Receive(int count) {this.sendCount = count;ct = new ContinueTransfer();ct.initMap(ri.getFileHeadList(), ri.getFileInfoMap());ct.setRd(getReceiveDeal()); initProcessBar();}public void start() {if(goon == true) {return;}goon = true;try {server = new ServerSocket(self.getPort());} catch (IOException e) {e.printStackTrace();}new Thread(new barView()).start();new Thread(this).start();}/**当用户设置显示进度条,则在开始进行接收线程的时候,开启另一个线程进行展示进度条*/class barView implements Runnable{@Overridepublic void run() {if(showProcess) {view.showView();}           }}@Overridepublic void run() {if(showProcess) {            view.showView();}while(goon) {try {Socket socket = server.accept();    //开启处理接收资源的线程,由ReceiveDetail进行处理     new ThreadPool().Execute(new ReceiveDetail(socket, ri, ct, view)); } catch (IOException e) {e.printStackTrace();}++count;if(count >= sendCount) {while(goon) {if(ct.IfRecived(sendCount)) {if(ct.Ifcomplete()) {goon = false;}count = 0;break;                      }                   }                   }               }close();   }private void close() {if(server != null) {try {server.close();} catch (IOException e) {e.printStackTrace();}finally {server = null;}}            }}/**初始化进度条对象*/public void initProcessBar() {Map<String, Object> nameList = new HashMap<String, Object>();List<FileHead> headList =  ri.getFileHeadList();Map<Integer, FileInfo> fileInfoMap = ri.getFileInfoMap();for(int i = 0; i < headList.size(); i++) {int handle = headList.get(i).getFileHandle();String name = fileInfoMap.get(handle).getFileRoot();nameList.put(name, null);}List<String> temp = new ArrayList<String>();temp.addAll(nameList.keySet());view = new progressView();view.showProgressView(temp, ri.getResourceName());}public static String getSelfIp() {try {InetAddress address = InetAddress.getLocalHost();String ip = address.getHostAddress();return ip;} catch (UnknownHostException e) {e.printStackTrace();}return null;}
}

ReceiveDetail

此类为真正进行侦听发送端的链接,并且接收资源写入本地。此处有一个需要注意的点就是,对于不同的发送端来说,开启了不同的接受线程,但对于不同的发送端来说,可能发送同一个文件的不同片段,我们知道在进行本地文件写操作时,需要打开和关闭文件。
问题就在于打开和关闭文件的操作,频繁的进行IO操作,消耗系统,CPU,还有内存的资源。
若不进行频繁的打开关闭,当开始写某一文件片段的时候,打开该文件,等资源接收完全之后再进行个文件的关闭操作,明显有一个大问题就是,多线程带来的不安全性,当某一线程本来写到某一个文件片段的一半时,时间片段到了,刚好有另外一个线程同样是写相同文件的另外一个片段,但是由于公用的一个文件读写指针,那么第二个线程开始写的时候,会在上次读写指针停留的地方接着写,这样就会造成片段写的位置错误,整个文件看似完整,但是里边的内容早已错乱。
所以我们采用RandAccessFilePool 类来进行文件指针的保存,每一个接收线程产生一个RandAccessFilePool ,只保存该线程在写的时候对应文件的读写指针,不同线程对同一文件进行写操作的时候,用的是各自的文件指针,所以保证了文件写入的准确性,而对于同一线程在不同时刻进行写同一文件的不同片段,同样是准确的,因为同一线程执行事件的本来就是顺序执行。

public class ReceiveDetail implements Runnable{private Socket socket;private DataInputStream dis;private ResourceInfo ri;private RandAccessFilePool rfp;private boolean goon;private String root;private ContinueTransfer ct;//进度条的设置private progressView view;public ReceiveDetail(Socket socket, ResourceInfo ri, ContinueTransfer ct, progressView view) {this.socket = socket;  rfp = new RandAccessFilePool();this.ri = ri;this.view = view;this.ct = ct;root = ri.getRoot();goon = true;try {dis = new DataInputStream(socket.getInputStream());} catch (IOException e) {e.printStackTrace();}}@Overridepublic void run() {while(goon) {TransferDetail fs = new TransferDetail();        FileHead fh;try {fh = new FileHead(fs.readSignBytes(dis));              } catch (IOException e) {goon = false;break;}
//当通过读取标志信息字节流,将字节流转化为fileHead对象,需要读取的sectionLength为0是表明某个发送端已经结束发送或者是对方掉线
//对应接收端该线程线程停止接收。if(fh.getSectionLength() == 0) {//表示接收完全             goon = false;close();closeFile();
//设置已经该线程已经接收完成,为后期断点续传做准备,因为可能是因为发送端发送完毕
//或者是发送端半途中断,ct.setOk()设置接收端是否接收完毕,
//在Ct中会进行判断如果接收完毕,是否所有的片段都已接收,通过所有片段是否接收完整
//来进行判断需不需要进行断点续传ct.setOk();break;              }byte[] content = null;int handle = fh.getFileHandle();FileInfo fi =  ri.getFileInfoMap().get(handle);String absolut=fi.getFileRoot();String completeRoot = root + absolut;fileCreat.judgeFile(completeRoot);RandomAccessFile raf = rfp.getWriteRaf(completeRoot);try {content = fs.readContentBytes(dis, fh.getSectionLength());raf.seek(fh.getOffset());raf.write(content);   //将接受到的片段描述信息对象放入到对应文件所对应的判断文件是否完整的类FileIFComplete类中的list中,//每个文件对应一个FileIFComplete类,类中拥有一个list存储一个文件的多个片段//最后通过文件的完整性决顶是否需要断点续传ct.getMap().get(handle).afterInsertion(fh);//当用户设置了显示进度条的操作才会进行初始化一个进度条//view == null 表明用户没有进行响应的设置if(view != null) {new ThreadPool().Execute(new paint(absolut, fh.getSectionLength(), (int)fi.getFileLength()));}} catch (IOException e) {e.printStackTrace();}}}class Paint implements Runnable{private String handle;private int per;private int perAll;public Paint(String handle, int per, int perAll) {this.handle = handle;this.per = per;this.perAll = perAll;}@Overridepublic void run() {view.changeProgress(handle, per, perAll);  }}private void closeFile() {for (RandomAccessFile raf : rfp.getRandomFiles()) {try {raf.close();} catch (IOException e) {}}}private void close() {try {if (dis != null) {dis.close();}} catch (IOException e) {} finally {dis = null;}try {if (socket != null && !socket.isClosed()) {socket.close();}} catch (IOException e) {} finally {socket = null;}}
}

断点续传

ContinueTransfer

断点续传的管理类

public class ContinueTransfer {//map的键为文件的句柄,值为FileIFComplete对象,FileIFComplete中拥有一个list成员,用来存储一个文件的多个fileHead信息,相当于存储文件的片段信息private static final Map<Integer,FileIFComplete> completeMap = new ConcurrentHashMap<Integer, FileIFComplete>();private int senderCount;//用来记录接收端接收完毕的个数//因为多个线程进行接收,一个线程接受完需要给SenderCount + 1//牵扯到多线程安全的问题,所以需要进行同步增加的操作。//采用lock锁保证线程安全private ReentrantLock lock;private ReceiveDeal rd;public ContinueTransfer() {lock = new ReentrantLock();}/**初始化话completeMap,先根据headList中的文件片段信息,对照map的内容*初始化每个FileIFComplete,在一开始的时候FileIFComplete只存在一个fileHead,偏移量为0,长度为文件的总长度。*/public void initMap(List<FileHead> headList, Map<Integer, FileInfo> fileInfo) {Set<Integer> keys = fileInfo.keySet();for(FileHead one : headList) {int handle = one.getFileHandle();FileInfo fi = fileInfo.get(handle);FileIFComplete tempffc = new FileIFComplete((int) fi.getFileLength(), fi.getFileHandle());completeMap.put(handle, tempffc);}}public void setRd(ReceiveDeal rd) {this.rd = rd;}public void setOk() {lock.lock();try {++senderCount;  }finally {lock.unlock();    }   }public Map<Integer,FileIFComplete> getMap(){return this.completeMap;}public boolean IfRecived(int count) {lock.lock();try {return count == senderCount;    }finally {lock.unlock();    }   }public boolean Ifcomplete() {//判断是否每个文件都完整Collection<FileIFComplete> value = completeMap.values();Iterator<FileIFComplete> itr = value.iterator();List<FileHead> hlist = new ArrayList<FileHead>();while(itr.hasNext()) {FileIFComplete one = itr.next();if(!one.ifComplete()) {//如果不完全,将剩余片段记录下来hlist.addAll(one.getHeadList());               }}if(hlist.isEmpty()) {//当每个文件都完整的话,就不会往hlist进行添加片段,则hlist为空//说明文件接受完整return true;}else {new Thread(rd).start();//表明没有接受完全,用该重新将这些个文件分片,进行重新的请求return false;}}}

FileIFComplete

public class FileIFComplete {//通过检查每一个文件的完整性
//因为对于每个文件来说,可能存在被分片的概念,所以,当前这个类的作用就是
//将每个文件的片段组装进行判断,每一个文件是否完整private int handle;private List<FileHead> fileHeadList;private ReentrantLock lock;public FileIFComplete(int size, int handle) {lock = new ReentrantLock();fileHeadList = new CopyOnWriteArrayList<FileHead>();this.handle = handle;FileHead fh = new FileHead();fh.setFileHandle(this.handle);fh.setOffset(0L);fh.setSectionLength(size);  fileHeadList.add(fh);}public List<FileHead> getHeadList(){return fileHeadList;}public boolean ifComplete() {return fileHeadList.isEmpty();}public FileHead getRightHead(FileHead fh) throws Exception {for(int i = 0; i < fileHeadList.size(); i++) {FileHead listOfHead  = fileHeadList.get(i);if(listOfHead.ifRightHead(fh)) {return fileHeadList.get(i);}}throw new Exception("片段错误" + fh);}/**当添加一个fileHead对象,表明添加一个已经接受的文件片段*进行加入片段信息之后的计算,*/public void afterInsertion(FileHead fh) {//先判断是否是合格的片段if(fh == null) {return;}FileHead replaceHead;try {lock.lock();replaceHead = getRightHead(fh);long orgOffset = replaceHead.getOffset();int orgSize = replaceHead.getSectionLength();long currentOffset = fh.getOffset();int currentSize = fh.getSectionLength();long leftOffset = orgOffset;int leftSize = (int) (currentOffset - orgOffset);long rightOffset = currentOffset + currentSize;int rightSize = (int) (orgOffset + orgSize - rightOffset);fileHeadList.remove(replaceHead);if(leftSize > 0) {fileHeadList.add(new FileHead(fh.getFileHandle(), leftOffset, leftSize));}if(rightSize > 0) {fileHeadList.add(new FileHead(fh.getFileHandle(), rightOffset, rightSize));}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}//得到替换的head,将一个head根据传入的偏移量和长度//进行合理的拆分}}

下面所说的给FileIFComplete添加filehead ,事实上FileIFComplete中有一个list成员,给list进行添加。
这里阐述一下上面的检查文件完整性的一个算法技巧。
在我们初始化一个文件对应的FileIFComplete 对象的时候,我们会在初始化的时候会给FileIFComplete对象中添加一个filehead来表示该文件的信息。
例如,一个文件的总长度为811字节。
那我们初始化一个FileIFComplete 的时候会添加一个fileHead对象,对象的三个成员值分别为 handle = 该文件handle, offset = 0, length = 811.

当我们接收端接受收一个片段时,因为是先接接收标记信息同样也是fileHead对象的字节流,我们将提取fileHead中的handle,通过handle找到此片段对应的文件的FileIFComplete对象,然后将接收到的fileHead添加进FileIFComplete中。
假设我们接收到的fileHead的偏移量为8, 片段长度为64.
那么FileIFComplete会进行添加后的计算,同样是该类中afterInsertion(FileHead fh)方法代码所描述,下面简单说说计算过程。

因为我们初始化的时候该文件对应的FileIFComplete中已经有了一个filhead
offset = 0 length = 811;
现在添加进来一个filehead offset = 8 length = 64;

那么首先判断新添加的filehead是否是正确的文件片段,判断方法就是在FileIFComplete中已有的filehead中寻找,是否有片段信息包含添加的片段信息,像本例来说,因为初始化中已有开始片段信息,表明文件的0位置到810位置为空,那么此时加进来filehead 写的位置为文件的8位置开始往后64个字节,而此时FileIFComplete中原有的filehead满足新添加片段写的要求,因为整个文件都是空的,当然新添加的这个写哪个位置都是可以的,
如果FileIFComplete此时之后一个filehead为 offset = 10 length = 32;那此时如果新添加的filehead offset = 8 length = 64;此时的片段就是错误的,因为FileIFComplete中拥有的filehead表明文件还有哪些片段没有写,此时表明文件从10位置开始往后32字节的地方还没有,而新添加的明显想写的部分不符合没有写的片段内容。

所以FileIFComplete的afterInsertion(FileHead fh)通过进行传入的fileHead正确新的判断,并且根据加入新的filehead计算出文件中还没有被写的片段信息。当所有片段都写完,则FileIFComplete为空,同时通过空来判断文件的完整性。

多文件自平衡云传输(二)资源接收 断点续传篇 —————— 开开开山怪相关推荐

  1. 在线web免登陆云传输工具推荐【青蛙快传】

    一个比较好用的文件或者文本云传输工具,在线web版的,重要的是不需要登录,不需要登录,不需要登录! 优点1:无需登录,急速快传 优点2:手机电脑互传文件 优点3:不用安装软件,仅一个网址 青蛙快传 h ...

  2. [Java]分布式自平衡多文件云传输

    [Java]分布式自平衡多文件云传输 概述 基本思想 节点 Receiver接收方 资源分配及节点选择策略类 接收服务器端口池 资源请求类 短连接资源请求接口 资源信息类 资源节点关系表(资源管理中心 ...

  3. java 上传文件到服务器_java上传文件到OSS云服务器(二)

    上篇文章中已经把接口端和service业务层写了,这次就把OSS上传文件的工具类补上. 一.首先配置好OSS服务器各项节点,这是在springboot中appliaction.yml配置文件中的写法. ...

  4. 电脑与云服务器传输文件,电脑与云服务器传输文件

    电脑与云服务器传输文件 内容精选 换一换 当创建文件系统后,您需要使用云服务器来挂载该文件系统,以实现多个云服务器共享使用文件系统的目的.本章节以Windows 2012版本操作系统为例进行CIFS类 ...

  5. 以“国土资源云”统领国土资源信息化建设

    以"国土资源云"统领国土资源信息化建设       2015-02-05 | 作者: 程秀娟 | 来源: 中国国土资源报 | [大中 小][打印][关闭]   1月26日,国土资源 ...

  6. 手机 服务器文件,手机查看云服务器文件

    手机查看云服务器文件 内容精选 换一换 华为云帮助中心,为用户提供产品简介.价格说明.购买指南.用户指南.API参考.最佳实践.常见问题.视频帮助等技术文档,帮助您快速上手使用华为云服务. 密钥对鉴权 ...

  7. Hhadoop-2.7.0中HDFS写文件源码分析(二):客户端实现(1)

    一.综述 HDFS写文件是整个Hadoop中最为复杂的流程之一,它涉及到HDFS中NameNode.DataNode.DFSClient等众多角色的分工与合作. 首先上一段代码,客户端是如何写文件的: ...

  8. http服务器 如何传输文件,http服务器 如何传输文件

    http服务器 如何传输文件 内容精选 换一换 华为云主机迁移服务帮助中心,为用户提供产品简介.快速入门.用户指南.API参考.最佳实践.常见问题.视频帮助等技术文档,帮助您快速上手使用主机迁移服务. ...

  9. 弹性服务器怎么上传文件,上传哪个文件夹弹性云服务器

    上传哪个文件夹弹性云服务器 内容精选 换一换 华为云帮助中心,为用户提供产品简介.价格说明.购买指南.用户指南.API参考.最佳实践.常见问题.视频帮助等技术文档,帮助您快速上手使用华为云服务. 本节 ...

  10. 如果访问云服务器上的文件,如果访问云服务器上的文件

    如果访问云服务器上的文件 内容精选 换一换 WinSCP工具可以实现在本地与远程计算机之间安全地复制文件.与使用FTP上传代码相比,通过 WinSCP 可以直接使用服务器账户密码访问服务器,无需在服务 ...

最新文章

  1. pandas读取csv文件的前几行数据(nrows参数)、pandas读取csv文件的中间几行数据(skiprows=range(a,b))
  2. 用SVD和字典学习方法重建图像(cifar-10图片集)
  3. 人脸识别(3)---静态人脸识别和动态人脸识别的区别
  4. I.MX6 make menuconfig OTG to slave only mode
  5. python web 文件管理器_利用Python 1分钟搭建测试Web服务器,可实现linux目录文件共享...
  6. Android IntentService的使用与源码解析
  7. 炫酷报表制作工具推荐:RDP报表工具
  8. androidbenchmark和iphonebenchmark这两页面中设备信息爬虫
  9. Unity中Text中首行缩进两个字符和换行的代码
  10. Android手机里的垃圾文件和文件夹清理
  11. 手把手教你搭建SpringCloud项目
  12. java软件制作教程_Minecraft Java版材质包制作教程
  13. Java8 Stream,过分丝滑!
  14. nginx-http重定向到https配置
  15. python开发工程师是干嘛的-python开发工程师是做什么的
  16. java导出excel 边框不全_POI 导出Excel合并单元格后部分边框不显示
  17. Neo4j 新手入门指南
  18. 【贪玩巴斯】带你一起攻克英语语法长难句—— 第五章——尾声的凯旋:状语和状语从句 ——2022年2月25日-3月17日
  19. 使用jieba对新闻标题进行切词,然后使用word2vec训练词向量及相似词计算的一个小例子
  20. 模仿墨迹天气-demo

热门文章

  1. Metron基础概念
  2. 积化和差、和差化积公式
  3. 点点动画~画出懂你的3D魔方
  4. html制作小短片,教你制作微视频 一分钟成为电影大师!
  5. 小鸟云服务器linux版本的登陆
  6. 安卓系统Remix_OS 的vmware虚拟机安装
  7. Matlab 线性拟合 非线性拟合
  8. VirtulBox安装虚拟机(鼠标点击时)0x00000000指令引用的0x00000000内存该内存不能为written错误解决方案
  9. 实现页面的图文混排布局(Web作业)
  10. 微信表情符号写入案件判决