原博客地址http://blog.csdn.net/chlaws/article/details/37742597
前言
MapReduce的源码分析是基于Hadoop1.2.1基础上进行的代码分析。
该章节会分析在MapTask端的详细处理流程以及MapOutputCollector是如何处理map之后的collect输出的数据。
map端的主要处理流程
图1 MapTask处理流程
图1所示为MapTask的主要代码执行流程,在MapTask启动后会进入入口run函数,根据是否使用新的api来决定选择运行新的mapper还是旧的mapper,最后完成执行向外汇报。
在这,我们选择分析旧的api,也就是runOldMapper。在runOldMapper内部主要分为MapperRunner.run执行用户端编写的map函数,在所有都执行完毕后,会调用MapOutputCollector的flush,讲最后一部分内存中的数据刷入到磁盘中。
根据上述的流程我们对代码依次进行分析,先看入口代码:
- public void run(finalJobConf job, finalTaskUmbilicalProtocol umbilical)
- throwsIOException, ClassNotFoundException, InterruptedException {
- this.umbilical = umbilical;
- // start thread that will handlecommunication with parent
- TaskReporter reporter = new TaskReporter(getProgress(), umbilical,
- jvmContext);
- reporter.startCommunicationThread();
- booleanuseNewApi = job.getUseNewMapper();
- initialize(job, getJobID(), reporter, useNewApi);
- ....
- if(useNewApi) {
- runNewMapper(job, splitMetaInfo, umbilical, reporter);
- } else{
- runOldMapper(job, splitMetaInfo, umbilical, reporter); //运行旧的mapper
- }
- done(umbilical, reporter);
- }
入口代码很简单,我们只需要关心是否使用新旧api来判断选择运行哪种mapper,在这里,分析runOldMapper,runOldMapper是封装了一个mapper是如何被执行,代码如下:
- private<INKEY,INVALUE,OUTKEY,OUTVALUE>
- void runOldMapper(finalJobConf job,
- final TaskSplitIndex splitIndex,
- final TaskUmbilicalProtocol umbilical,
- TaskReporter reporter
- ) throws IOException,InterruptedException,
- ClassNotFoundException {
- InputSplit inputSplit = getSplitDetails(new Path(splitIndex.getSplitLocation()),
- splitIndex.getStartOffset()); //流程1
- updateJobWithSplit(job, inputSplit);
- reporter.setInputSplit(inputSplit);
- RecordReader<INKEY,INVALUE> in = isSkipping() ?
- new SkippingRecordReader<INKEY,INVALUE>(inputSplit,umbilical, reporter) :
- newTrackedRecordReader<INKEY,INVALUE>(inputSplit, job, reporter);
- job.setBoolean("mapred.skip.on", isSkipping()); //流程2
- intnumReduceTasks = conf.getNumReduceTasks();
- LOG.info("numReduceTasks: "+ numReduceTasks);
- MapOutputCollector collector = null;
- if(numReduceTasks > 0) { //流程3
- collector = new MapOutputBuffer(umbilical, job, reporter);
- } else{
- collector = new DirectMapOutputCollector(umbilical, job, reporter);
- }
- MapRunnable<INKEY,INVALUE,OUTKEY,OUTVALUE> runner =
- ReflectionUtils.newInstance(job.getMapRunnerClass(), job);
- try{
- runner.run(in, new OldOutputCollector(collector, conf), reporter); //流程4
- collector.flush(); //流程5
- in.close();
- in = null;
- collector.close();
- collector = null;
- } finally{
- ...
- }
- }
- public void collect(K key, V value) throws IOException {
- try {
- collector.collect(key, value,
- partitioner.getPartition(key, value, numPartitions));
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- throw new IOException("interrupt exception", ie);
- }
- }
缓冲区分析
MapOutputBuffer定义了三个缓冲区,分别是:
int [] kvoffsets, int[] kvindices, byte[] kvbuffer
kvoffsets是索引缓冲区,它的作用是用来记录kv键值对在kvindices中的偏移位置信息。
kvindices也是一个索引缓冲区,索引区的每个单元包含了分区号,k,v在kvbuffer中的偏移位置信息。
kvbuffer是数据缓冲区,保存了实际的k,v。
图1索引区关系
缓冲区之间的关系,从图1即可一目了然, kvoffsets作为一级索引,一个用途是用来表示每个k,v在kvindices中的位置,另一个是用来统计当前索引的缓存的占用比,当超过设定的阀值,就会触发spill动作,将已写入的数据区间spill出去,新写入的时候持续向后写入,当写到尾部后,回过头继续写入。
kvindices为什么要如此用这样结构表示是为了在指定了多个reducetask的时候,maptask的输出需要进行分区,比如有2个reducetask,那么需要将maptask的输出数据均衡的分布到2个reducetask上,因此在索引里引入了分区信息,另外一个是为了每个分区的key有序,避免直接在比较后直接拷贝key,而只要相互交换一下整形变量即可。
kvbuffer存储了实际的k,v,为了保证k,v的键值成对的出现,引入了mark标记上一个完成的k,v的位置。同时类似kvoffset一样也加入了表示缓冲区是否满足溢出的一些标志。还有一点就是,k,v的大小不向索引区一样明确的是一对占一个int,可能会出现尾部的一个key被拆分两部分,一步存在尾部,一部分存在头部,但是key为保证有序会交给RawComparator进行比较,而comparator对传入的key是需要有连续的,那么由此可以引出key在尾部剩余空间存不下时,如何处理。处理方法是,当尾部存不下,先存尾部,剩余的存头部,同时在copy key存到接下来的位置,但是当头部开始,存不下一个完整的key,会付出溢出flush到磁盘。当碰到整个buffer都存储不下key,那么会抛出异常MapBufferTooSmallException表示buffer太小容纳不小.
核心成员变量
先看看MapOutputBuffer的主要的一些成员变量
- kvoffset相关的成员变量如下:
- private volatile int kvstart = 0; // marks beginning of spill
- private volatile int kvend = 0; // marks beginning of collectable
- private int kvindex = 0; // marks end of collected
- private final int[] kvoffsets; // indices into kvindices
- 在默认情况下kvstart,kvend是相等等,kvindex是表示在kvoffsets中下一个可以写入的位置,当缓冲区达到阀值的时候,kvend=kvindex。在完成溢出写入过程之后,kvend=kvstart。
- 注意,这里所的阀值是索引区满足一定使用量,在采用默认配置的时候是达到缓冲区的80%, 也就是kvoffsets.length * 0.8
- kvindices相关的成员变量如下:
- private final int[] kvindices; // partition, k/v offsets into kvbuffer
- private static final int PARTITION = 0; // partition offset in acct
- private static final int KEYSTART = 1; // key offset in acct
- private static final int VALSTART = 2; // val offset in acct
- //RECSIZE表示一条索引记录占用16字节,即keoffsets中占用1个int,kvindices中占用3个int
- private static final int ACCTSIZE = 3; // total #fields in acct
- private static final int RECSIZE =
- (ACCTSIZE + 1) * 4; // acct bytes per record
- 在前面我们说过kvindices中的是按三个int作为一个单元(partition,keyoffset,valoffset)来表示k,v在keybuffer中的位置信息以及属于哪个分区。因此每次操作的时候都是
- //ind是kvoffsets中存储的值
- kvindices[ind + PARTITION] = partition;
- kvindices[ind + KEYSTART] = keystart;
- kvindices[ind + VALSTART] = valstart;
kvbuffer相关的成员变量如下:
- private volatile int bufstart = 0; // marks beginning of spill
- private volatile int bufend = 0; // marks beginning of collectable
- private volatile int bufvoid = 0; // marks the point where we should stop
- // reading at the end of the buffer
- private int bufindex = 0; // marks end of collected
- private int bufmark = 0; // marks end of record
- private byte[] kvbuffer; // main output buffer
bufstart,bufend,bufindex的作用和kvoffsets中的kvstart,kvend,kvindex一样。
bufmark用来记录一个完整的k,v记录结束的位置,bufvoid用来表示kvbuffer中有效内存结束位置。kvbuffer也有一个阀值,在采用默认配置的时候是达到缓冲区的80%,是kvbuffer.length * 0.8。
还有一部分是和处理spill相关的成员变量
- // spill accounting
- privatevolatileintnumSpills= 0;//记录当前spill的次数,还会用于组成spill输出的临时文件名
- //key,value的序列化类
- privatefinalSerializer<K> keySerializer;
- privatefinalSerializer<V> valSerializer;
- //BlockingBuffer是DataOutputStream类型,k,v的写入会通过流的形式写入到bb中,最后满足溢出条件才从kvbuffer写入到磁盘
- privatefinalBlockingBuffer bb= newBlockingBuffer();
- //满足溢出条件,干脏活累活的线程
- privatefinalSpillThread spillThread= newSpillThread();
初始化分析
- final float spillper = job.getFloat("io.sort.spill.percent",(float)0.8);
- final float recper = job.getFloat("io.sort.record.percent",(float)0.05);
- final int sortmb = job.getInt("io.sort.mb", 100);
- intmaxMemUsage = sortmb << 20;
- intrecordCapacity = (int)(maxMemUsage * recper);
- recordCapacity -= recordCapacity % RECSIZE;
- kvbuffer= newbyte[maxMemUsage- recordCapacity];
- bufvoid= kvbuffer.length;
- recordCapacity /= RECSIZE;
- kvoffsets= newint[recordCapacity];
- kvindices= newint[recordCapacity* ACCTSIZE];
- softBufferLimit = (int)(kvbuffer.length* spillper);
- softRecordLimit= (int)(kvoffsets.length * spillper);
在MR的配置选项里有两个参数比较常见到的,一个是io.sort.spill.percent,另一个是io.sort.mb。前者表示在缓冲区使用到多少的时候开始触发spill,后者表示一个MapTask能使用多少的内存大小,将其用作输出的缓存。
从上面我们能够看到kvbuffer,kvoffsets,kvindices的在整个sortmb大小的内存中占用的比例,按默认值算分别是kvbuffer占95M,kvoffsets占1.25M,kvindices占3.75M。
另外,还有kvbuffer,kvoffsets使用到多少会触发spill的一个上限值,这里默认是其长度的80%。
- // k/v serialization
- comparator= job.getOutputKeyComparator();
- keyClass= (Class<K>)job.getMapOutputKeyClass();
- valClass= (Class<V>)job.getMapOutputValueClass();
- serializationFactory = newSerializationFactory(job);
- keySerializer= serializationFactory.getSerializer(keyClass);
- keySerializer.open(bb);
- valSerializer= serializationFactory.getSerializer(valClass);
- valSerializer.open(bb);
comparator是key之间用于比较的类,在没有设置的情况下,默认是key所属类里面的一个子类,这个子类继承自WritableComparator。以Text作为key为例,就是class Comparator extends WritableComparator。
keyClass和valClass一般情况下用户都没有去设置的,也可以不用去设置,这种情况是指map的key,value的输出和reduce的key,value输出是一样的类型。因为在没有设置map阶段的key,value的输出类型的时候,会调用getOutputKeyClass/getOutputValueClass进行获取。
keySerializer和valSerializer这两个序列化对象,通过序列化工厂类中获取到的,实际上就是WritableSerialization类内的静态类:static classWritableSerializer implements Serializer<Writable>的一个实例。
关于WritableSerialization需要简单的说明下,这个类有包含了两个静态类,分别是WritableDeserializer和WritableSerializer,序列化和反序列化的操作基本类似,都是打开一个流,将输出写入流中或者从流中读取数据。对于序列化是对输入类型调用write接口得到序列化后的内容输出到流中:
- public void serialize(Writable w) throws IOException {
- w.write(dataOut);
- }
对于反序列化从流中读取输出,这个要读取解析的对象可以是构造时传入的,也可以是调用deserialize接口传入的类型。
- public Writable deserialize(Writable w) throws IOException {
- Writable writable;
- if(w == null){
- writable
- = (Writable) ReflectionUtils.newInstance(writableClass, getConf());
- } else{
- writable = w;
- }
- writable.readFields(dataIn);
- returnwritable;
- }
最终调用的都是大家熟悉的hadoop在common包中org.apache.hadoop.io这个包内的各种writable类型的write/readFields接口。
keySerializer.open(bb)和valSerializer.open(bb)打开的是流,但不是文件流,而是BlockingBuffer,也就是说后续调用serialize输出key/value的时候,都是先写入到Buffer中,这个后续还会在提到。
collect分析
这里分析的collect是MapOutputBuffer中的collect方法,在用户层的map方法内调用collector.collect最终会一层层调用到MapOutputBuffer.collect,这个在前面的"什么是MapOutputBuffer"这一小节中有提到。
collect的代码我们分为两部分来看,一部分是根据索引区来检查是否需要触发spill,
另外一部分是操作buffer并更新索引区的记录。
第一部分代码如下:
- public synchronized void collect(K key,V value, int partition
- ) throws IOException {
- ... //无关紧要的代码
- finalintkvnext = (kvindex+ 1) % kvoffsets.length; //获取下一个的索引位置
- spillLock.lock();
- try{
- boolean kvfull;
- do {
- if (sortSpillException != null){
- throw (IOException)new IOException("Spill failed"
- ).initCause(sortSpillException);
- }
- //步骤1,判断是否需要触发
- // sufficient acct space
- kvfull = kvnext == kvstart; //判断是否索引区满了
- final boolean kvsoftlimit = ((kvnext > kvend) //判断索引区使用达到上限
- ? kvnext - kvend > softRecordLimit
- : kvend - kvnext <= kvoffsets.length - softRecordLimit);
- if (kvstart == kvend&& kvsoftlimit) { //判断是否触发spill
- LOG.info("Spilling map output: record full = "+ kvsoftlimit);
- startSpill(); //发起通知,通知SpillThread开始做溢出动作
- }
- //步骤2,缓冲区满的时候,是否需要等待
- if (kvfull) {
- try {
- //spill动作还未完成,持续等待
- while (kvstart != kvend){
- reporter.progress();
- spillDone.await();
- }
- } catch (InterruptedException e) {
- throw (IOException)new IOException(
- "Collector interrupted while waiting for the writer"
- ).initCause(e);
- }
- }
- } while (kvfull);
- } finally{
- spillLock.unlock();
- }
步骤1解析:
1.判断缓冲区是否满了(指kvoffsets),缓冲区满的判断标准是kvnext==kvstart,因为是循环缓存区,因此kvnext追上了kvstart所指示的起始位置,就是缓冲区满了
2. 在kvstart==kvend,并且kvoffsets的使用是否达到了上限,触发激活SpillThread开始执行spill动作。为什么会有kvstart==kvend这个判断呢,这是因为在缓冲区没有满足spill时,kvend都是指向kvstart,当触发spill时,kvend会指向kvindex位置,也就是说kvstart到kvindex这段区间会被标识出来,是需要spill这段区间,在spill动作完成之后,会将kvstart指向kvend。因此为了避免已经触发过的了动作再次触发,需要加入kvstart==kvend这个条件。
3.startSpill的动作,会执行这3条语句:
- kvend= kvindex;//将kvend指向kvindex,表示spill的区域
- bufend= bufmark;//将bufend指向bufmark,bufmark表示最后一个完整的kv记录结束的位置
- spillReady.signal();//发起信号,唤醒SpillThread
步骤2解析:
1.如果缓冲区已经满了,说明SpillThread还在执行spill动作的过程中,那么需要等待到spill动作的完成,在完成之后,SpillThread会将kvstart指向kvend,并且发送spillDone信号。
第二部分代码如下:
- try {
- //步骤1:序列化key,判断是否需要对buffer进行调整
- // serialize key bytes into buffer
- int keystart = bufindex;
- keySerializer.serialize(key);
- if (bufindex < keystart) {
- // wrapped the key; reset required
- bb.reset();
- keystart = 0;
- }
- //步骤2:序列化value,并标记一个完整k,v的结束的位置
- // serialize value bytes into buffer
- final int valstart = bufindex;
- valSerializer.serialize(value);
- int valend = bb.markRecord();
- if (partition < 0 || partition >= partitions) {
- throw new IOException("Illegal partition for " + key + " (" +
- partition + ")");
- }
- mapOutputRecordCounter.increment(1);
- mapOutputByteCounter.increment(valend >= keystart
- ? valend - keystart
- : (bufvoid - keystart) + valend);
- //步骤3:更新一级索引,二级索引。
- // update accounting info
- int ind = kvindex * ACCTSIZE;
- kvoffsets[kvindex] = ind;
- kvindices[ind + PARTITION] = partition;
- kvindices[ind + KEYSTART] = keystart;
- kvindices[ind + VALSTART] = valstart;
- kvindex = kvnext;
- } catch (MapBufferTooSmallException e) {
- LOG.info("Record too large for in-memory buffer: " + e.getMessage());
- spillSingleRecord(key, value, partition);
- mapOutputRecordCounter.increment(1);
- return;
- }
- }
步骤1解析:
1.根据key的序列化类,序列化输出key到kvbuffer。
1)key是如何输出到kvbuffer的呢,带着这个问题,我们一步步分析。根据前面说过,keySerializer.serialize(key);将会调用的是WritableSerialization.WritableSerializer.serialize(Writable w)方法,为便于分析,现假设key为Text类型。那么serialize方法内执行的将会是Text中的write方法,也就是如下所示:
- publicvoid write(DataOutput out) throws IOException {
- WritableUtils.writeVInt(out, length);
- out.write(bytes,0, length);
- }
这里会写入Text的长度和数据内容。
这里的这个out又是什么呢,keySerializer在构造完成的时候,调用过一个open函数,传入了一个BlockBuffer的对象,BlockBuffer对象就是这里的out。
再来看看BlockingBuffer的构造:
- public BlockingBuffer() {
- this(new Buffer());
- }
- privateBlockingBuffer(OutputStream out) {
- super(out);
- }
它new了一个Buffer传递给DataOutputStream,Buffer是BlockBuffer内部实现的一个继承自OutputStream的类,它实现了write接口。因此在调用out.write的时候,最终调用的是Buffer.write。
2)Buffer.write,对于输入的数据,会判断当前kvbuffer缓冲区是否满,如果满了或者是使用达到上限了,但是kvoffsets索引缓冲区还没有达到使用上限(也就是没有kvoffsets的使用没有触发spill),那么会调用startSpill去激活SpillThread执行spill。
2.当bufindex出现从kvbuffer尾部的位置重新循环到头部是,说明有key存在尾部存了一部分,头部存了一部分。由于key的比较函数需要的是一个连续的key,因此需要对key进行特殊处理。
重新写入一个完整的key。看具体处理代码:
- protected synchronized void reset() throwsIOException {
- // key被拆分为两部分,第一部分是在尾部
- int headbytelen = bufvoid - bufmark;
- //缩短bufvoid为最后一个kv记录结束的位置,也就是第一部分的key在后续不处理
- bufvoid = bufmark;
- //因为bufindex已经循环了,索引bufindex肯定是在bufstart前面
- //这里需要判断bufindex开始到bufstart这一段区间是否能容纳的下第一部分的key
- if (bufindex + headbytelen < bufstart) {
- //容纳的下,触发两次copy,先将第二部分key往后copy
- //再将第一部分的key copy到kvbuffer起始位置
- System.arraycopy(kvbuffer, 0, kvbuffer, headbytelen, bufindex);
- System.arraycopy(kvbuffer, bufvoid, kvbuffer, 0, headbytelen);
- bufindex += headbytelen;
- } else {
- /*
- 当容纳不下的时候,先copy第二部分的key
- 然后将bufindex重置,重新写入第一部分的key,当缓存不足够写入第一部分的key
- 会触发spill;当可以写入则写入第一部分的key,在写入keytmp所存放的第二部分的key的时候,会触发spill,当spill完成之后该第二部分key仍不能完整的写入,则会throw一个异
相关推荐
4000套Excel表格模板资料包,涵盖财务、人事、行政、销售、库房等多个领域,提供丰富多样的模板选择。
Zhang 等 - 2022 - Stability-Oriented STAR-RIS Aided MISO-NOMA Commun(1)(1)
非支配排序的蜣螂优化算法:多目标优化的新策略与全局探索-局部开发的有效结合,非支配排序的蜣螂优化算法:多目标优化问题的进化计算新方法,非支配排序的蜣螂优化算法(Non-dominated Sorting Dung Beetle Optimization, NSDBO)是一种结合了非支配排序机制和蜣螂优化算法(Dung Beetle Optimization, DBO)的进化计算方法,专门用于解决多目标优化问题。 在多目标优化中,目标之间通常存在竞争关系,算法的目标是找到一组解,这些解在多个目标之间达到一种平衡,即Pareto最优解集。 蜣螂优化算法(DBO)简介 蜣螂优化算法(dung beetle optimizer,DBO)是东华大学Shen团队推出的第二个算法,其灵感来自于蜣螂的滚球、跳舞、觅食、偷窃和繁殖行为。 该算法同时考虑了全局探索和局部开发,从而具有收敛速度快和准确率高的特点,可以有效地解决复杂的寻优问题。 非支配排序的蜣螂优化算法(NSDBO) NSDBO算法结合了DBO算法和非支配排序的概念,用于解决多目标优化问题。 以下是NSDBO的关键步骤: 初始化:生成初始种群
【毕业设计】前端html模板参考_pgj
基于matlab平台的车牌识别GUI实现.zip
DeepSeek 使用技巧,强烈建议收藏!.pdf
维吾尔文维文数码电子产品店管理系统vb.net 源码
1993A
项目已获导师指导并通过的高分毕业设计项目,可作为课程设计和期末大作业,下载即用无需修改,项目完整确保可以运行。 包含:项目源码、数据库脚本、软件工具等,该项目可以作为毕设、课程设计使用,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。 项目都经过严格调试,确保可以运行!可以放心下载 技术组成 语言:java 开发环境:idea 数据库:MySql5.7以上 部署环境:maven 数据库工具:navicat
基于matlab平台的 ORL的人脸考勤系统.zip
**基于二阶自抗扰控制器的双惯量伺服系统机械谐振抑制的Matlab Simulink仿真模型研究**,基于二阶自抗扰控制器的双惯量伺服系统机械谐振抑制与速度控制的Matlab Simulink仿真模型研究,伺服系统基于二阶自抗扰控制器的双惯量伺服系统机械谐振抑制matlab Simulink仿真(加入了电流环 PI 、自抗扰控制器和观测器参数整定) 如果需要一阶自抗扰控制器进行速度控制,请联系我,也有速度控制 1.模型简介 模型为基于二阶自抗扰控制器的双惯量伺服系统机械谐振抑制仿真,采用Matlab simulink 搭建,支持各个版本。 仿真模型由simscape 库模型搭建,模型内主要包含DC直流电压源、三相逆变器、永磁同步电机、采样模块、SVPWM、Clark、Park、Ipark、三角波发生器、速度环、电流环等模块,其中,SVPWM、Clark、Park、Ipark、三角波发生器适用模块搭建。 位置环和转速环合并成一环,采用二阶自抗扰控制器,控制器参数以及观测器参数已经进行整定, 扩展状态观测器采用Matlab funtion编写,其与C语言编程较为接近,容易进行实物移植
【毕业设计】Python基于图神经网络与多任务学习的图像分类器
"永磁同步电机无感控制技术:基于反电势观测器与锁相环的全速域解决方案",**永磁同步电机无感控制技术——反电势观测器+锁相环PLL方法的全速域应用**,永磁同步电机无感控制--基于反电势观测器+锁相环 在全速域范围内,一般的永磁同步电机无感控制要分为低速区域和高速区域两个部分。 原因在于常规的方法是利用模型建立反电动势观测器来求解转子位置信息,但其只适合在中高速区域。 本介绍一种back-EMF+PLL的方法。 反电势观测器+锁相环PLL的永磁电机无感控制只适合于中、高速区域(一般额定转速的10%以上的速度范围)。 因为在低速区域的信噪比低、反电势与转速成正比,加上采样精度等问题,反电势的估计误差大导致无法正确地计算出转速和位置信息。 基于反电动势的无感控制技术显示出明显的优势,主要体现在如下方面:首先是算法复杂度低,容易理解和实现;其次是具有较高的动态响应速率,能够在短时间内做出响应;最后就是成熟度较高,适用性强,应用场景较多。 3、 ,永磁同步电机; 无感控制; 反电势观测器; 锁相环PLL; 高速区域; 信噪比低; 算法复杂度
重点:所有项目均附赠详尽的SQL文件,这一细节的处理,让我们的项目相比其他博主的作品,严谨性提升了不止一个量级!更重要的是,所有项目源码均经过我亲自的严格测试与验证,确保能够无障碍地正常运行。 1.项目适用场景:本项目特别适用于计算机领域的毕业设计课题、课程作业等场合。对于计算机科学与技术等相关专业的学生而言,这些项目无疑是一个绝佳的选择,既能满足学术要求,又能锻炼实际操作能力。 2.超值福利:所有定价为9.9元的项目,均包含完整的SQL文件。如需远程部署可随时联系我,我将竭诚为您提供满意的服务。在此,也想对一直以来支持我的朋友们表示由衷的感谢,你们的支持是我不断前行的动力! 3.求关注:如果觉得我的项目对你有帮助,请别忘了点个关注哦!你的支持对我意义重大,也是我持续分享优质资源的动力源泉。再次感谢大家的支持与厚爱! 4.资源详情:https://blog.csdn.net/2301_78888169/article/details/141762088 更多关于项目的详细信息与精彩内容,请访问我的CSDN博客!
本项目是自己做的设计,有GUI界面,完美运行,适合小白及有能力的同学进阶学习,大家可以下载使用,整体有非常高的借鉴价值,大家一起交流学习。该资源主要针对计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者下载使用,亦可作为期末课程设计、课程大作业、毕业设计等。 项目整体具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现不同的功能。
Airwave升级指导文档翻译中文版本,工具翻译所以有一些可能不通,但大致意思是一样的
"威纶通三菱PLC系列机械手示教器模板:多功能程序流程自定义系统","威纶通三菱PLC系列机械手示教器模板开发:智能编程系统,助力程序流程自由定制",示教器模板(威纶通+三菱plc系列) 本系统开发类似机械手示教器,可以替代多轴机械手示教器功能。 项目应用场景:最终客户需要自由的修改程序流程,如检测什么信号,多少时间,在进入下一步,还是轴定位轨迹可以自由修改,方便最终可以在不修改PLC程序的情况下,自由示教程序。 ,示教器模板; 威纶通; 三菱plc系列; 机械手示教器; 程序流程修改; 信号检测; 时间控制; 轴定位轨迹修改; PLC程序替代。,威纶通三菱PLC机械手示教器模板:自由编程,轻松控制多轴机械手
【上传下载】实现一个简易的FTP服务器,支持文件的上传、下载,以及断点续传 #采用多线程模型完成 #有port和pasv两种工作模式_pgj
2024年度制造业数字化转型典型案例集
(springboot+mysql)旅客行程智能推荐系统 包含数据库mysql+前端页面vue 毕业论文以及开题报告+答辩PPT