`
Tyrion
  • 浏览: 257543 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Tomcat7中NIO处理分析(一)

阅读更多

Tomcat的Connector有三种运行模式bio、nio、apr,先了解一下这三种的区别。

  1. bio(blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,就是以bio模式运行的。一般而言,bio模式是三种运行模式中性能最低的一种。
  2. 2.nio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio模式来运行只需要在Tomcat安装目录/conf/server.xml文件中将Connector节点的protocol配置成org.apache.coyote.http11.Http11NioProtocol即可。
  3. apr(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。可以简单地理解为Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能。 Tomcat apr也是在Tomcat上运行高并发应用的首选模式。

写个BIO的Socket服务器还是比较容易的,无非是没accept一个socket之后就扔到一个线程中处理请求生成响应,这种方式可以改进的点就是增加线程池的支持。本文主要分析一下Tomcat中NIO处理方式的相关代码逻辑。

 

关键代码都是在org.apache.tomcat.util.net.NioEndpoint这个类里面,它是Http11NioProtocol中负责接收处理socket的主要组件,别看代码很长,仔细阅读会发现有很多共通的地方,如:

  1. 都会对JDK中原有的API做一下扩展或者包装,如ThreadPoolExecutor是对java.util.concurrent.ThreadPoolExecutor的扩展,NioChannel是对ByteChannel的扩展,KeyAttachment则是对NioChannel的包装
  2. 很多类设计成非GC的,方便缓存和重复使用,实现方式都是通过ConcurrentLinkedQueue类构造一个队列。比如NioEndpoint类里面的ConcurrentLinkedQueue<SocketProcessor> processorCache、ConcurrentLinkedQueue<KeyAttachment> keyCache、ConcurrentLinkedQueue<PollerEvent> eventCache、ConcurrentLinkedQueue<NioChannel> nioChannels。Poller类里面的ConcurrentLinkedQueue<Runnable> events

先看下整个Connector组件结构图:

 

看过之前Tomcat启动文章的应该都知道,Connector的启动会调用Connector类的startInternal方法,里面调用了protocolHandler的start(),该方法中将调用抽象的endpoint的start()方法,这个方法会调用到具体Endpoint类的startInternal(),所以代码分析先从NioEndpoint类的startInternal看起。

 

  • 1.NioEndpoint类核心组件的初始化
/** 
 * Start the NIO endpoint, creating acceptor, poller threads. 
 */  
@Override  
public void startInternal() throws Exception {  
  
    if (!running) {  
        running = true;  
        paused = false;  
  
        // Create worker collection  
        if ( getExecutor() == null ) {  
            // 构造线程池,用于后续执行SocketProcessor线程,这就是上图中的Worker。  
            createExecutor();  
        }  
  
        initializeConnectionLatch();  
  
        // Start poller threads  
        // 根据处理器数量构造一定数目的轮询器,即上图中的Poller  
        pollers = new Poller[getPollerThreadCount()];  
        for (int i=0; i<pollers.length; i++) {  
            pollers[i] = new Poller();  
            Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);  
            pollerThread.setPriority(threadPriority);  
            pollerThread.setDaemon(true);  
            pollerThread.start();  
        }  
  
        // 创建接收者线程,即上图中的Acceptor  
        startAcceptorThreads();  
    }  
}  

startAcceptorThreads调用的是父类org.apache.tomcat.util.net.AbstractEndpoint中的实现:

protected final void startAcceptorThreads() {  
    int count = getAcceptorThreadCount();  
    acceptors = new Acceptor[count];  
  
    for (int i = 0; i < count; i++) {  
        // 调用子类的createAcceptor方法,本例中即NioEndpoint类的createAcceptor方法  
        acceptors[i] = createAcceptor();  
        String threadName = getName() + "-Acceptor-" + i;  
        acceptors[i].setThreadName(threadName);  
        Thread t = new Thread(acceptors[i], threadName);  
        t.setPriority(getAcceptorThreadPriority());  
        t.setDaemon(getDaemon());  
        t.start();  
    }  
}  

以上就是Acceptor、Poller、Worker等核心组件的初始化过程。

 

 

  • 2.请求接收

核心组件初始化之后接着就是Acceptor线程接收socket连接,看下Acceptor的源码:

// --------------------------------------------------- Acceptor Inner Class  
/** 
 * 后台线程,用于监听TCP/IP连接以及将它们分发给相应的调度器处理。 
 * The background thread that listens for incoming TCP/IP connections and 
 * hands them off to an appropriate processor. 
 */  
protected class Acceptor extends AbstractEndpoint.Acceptor {  
  
    @Override  
    public void run() {  
  
        int errorDelay = 0;  
  
        // 循环遍历直到接收到关闭命令  
        // Loop until we receive a shutdown command  
        while (running) {  
  
            // Loop if endpoint is paused  
            while (paused && running) {  
                state = AcceptorState.PAUSED;  
                try {  
                    Thread.sleep(50);  
                } catch (InterruptedException e) {  
                    // Ignore  
                }  
            }  
  
            if (!running) {  
                break;  
            }  
            state = AcceptorState.RUNNING;  
  
            try {  
                // 如果已经达到最大连接数则让线程等待  
                //if we have reached max connections, wait  
                countUpOrAwaitConnection();  
  
                SocketChannel socket = null;  
                try {  
                    // 接收连接,这里用的阻塞模式。  
                    // Accept the next incoming connection from the server  
                    // socket  
                    socket = serverSock.accept();  
                } catch (IOException ioe) {  
                    //we didn't get a socket  
                    countDownConnection();  
                    // Introduce delay if necessary  
                    errorDelay = handleExceptionWithDelay(errorDelay);  
                    // re-throw  
                    throw ioe;  
                }  
                // Successful accept, reset the error delay  
                errorDelay = 0;  
  
                // 注意这个setSocketOptions方法  
                // 它将把上面接收到的socket添加到轮询器Poller中  
                // setSocketOptions() will add channel to the poller  
                // if successful  
                if (running && !paused) {  
                    if (!setSocketOptions(socket)) {  
                        countDownConnection();  
                        closeSocket(socket);  
                    }  
                } else {  
                    countDownConnection();  
                    closeSocket(socket);  
                }  
            } catch (SocketTimeoutException sx) {  
                // Ignore: Normal condition  
            } catch (IOException x) {  
                if (running) {  
                    log.error(sm.getString("endpoint.accept.fail"), x);  
                }  
            } catch (OutOfMemoryError oom) {  
                try {  
                    oomParachuteData = null;  
                    releaseCaches();  
                    log.error("", oom);  
                }catch ( Throwable oomt ) {  
                    try {  
                        try {  
                            System.err.println(oomParachuteMsg);  
                            oomt.printStackTrace();  
                        }catch (Throwable letsHopeWeDontGetHere){  
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);  
                        }  
                    }catch (Throwable letsHopeWeDontGetHere){  
                        ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);  
                    }  
                }  
            } catch (Throwable t) {  
                ExceptionUtils.handleThrowable(t);  
                log.error(sm.getString("endpoint.accept.fail"), t);  
            }  
        }  
        state = AcceptorState.ENDED;  
    }  
}  

 

 

  • 3.Socket参数设置

在Acceptor里接收到一个连接之后调用setSocketOptions方法设置SocketChannel的一些参数,然后将SocketChannel注册到Poller中。看下setSocketOptions的实现:

/** 
 * Process the specified connection. 
 */  
protected boolean setSocketOptions(SocketChannel socket) {  
    // Process the connection  
    try {  
        // 将SocketChannel配置为非阻塞模式  
        //disable blocking, APR style, we are gonna be polling it  
        socket.configureBlocking(false);  
        Socket sock = socket.socket();  
        // 设置Socket参数值(从server.xml的Connector节点上获取参数值)  
        // 比如Socket发送、接收的缓存大小、心跳检测等  
        socketProperties.setProperties(sock);  
  
        // 从NioChannel的缓存队列中取出一个NioChannel  
        // NioChannel是SocketChannel的一个的包装类  
        // 这里对上层屏蔽SSL和一般TCP连接的差异  
        NioChannel channel = nioChannels.poll();  
  
        // 缓存队列中没有则新建一个NioChannel  
        if ( channel == null ) {  
            // SSL setup  
            if (sslContext != null) {  
                SSLEngine engine = createSSLEngine();  
                int appbufsize = engine.getSession().getApplicationBufferSize();  
                NioBufferHandler bufhandler = new NioBufferHandler(Math.max(appbufsize,socketProperties.getAppReadBufSize()),  
                                                                   Math.max(appbufsize,socketProperties.getAppWriteBufSize()),  
                                                                   socketProperties.getDirectBuffer());  
                channel = new SecureNioChannel(socket, engine, bufhandler, selectorPool);  
            } else {  
                // normal tcp setup  
                NioBufferHandler bufhandler = new NioBufferHandler(socketProperties.getAppReadBufSize(),  
                                                                   socketProperties.getAppWriteBufSize(),  
                                                                   socketProperties.getDirectBuffer());  
  
                channel = new NioChannel(socket, bufhandler);  
            }  
        } else {  
            // 将SocketChannel关联到从缓存队列中获取的NioChannel上来  
            channel.setIOChannel(socket);  
            if ( channel instanceof SecureNioChannel ) {  
                SSLEngine engine = createSSLEngine();  
                ((SecureNioChannel)channel).reset(engine);  
            } else {  
                channel.reset();  
            }  
        }  
        // 将新接收到的SocketChannel注册到Poller中  
        getPoller0().register(channel);  
    } catch (Throwable t) {  
        ExceptionUtils.handleThrowable(t);  
        try {  
            log.error("",t);  
        } catch (Throwable tt) {  
            ExceptionUtils.handleThrowable(t);  
        }  
        // Tell to close the socket  
        return false;  
    }  
    return true;  
}  

核心调用是最后的getPoller0().register(channel);它将配置好的SocketChannel包装成一个PollerEvent,然后加入到Poller的events缓存队列中。

 

 

  • 4.读取事件注册

getPoller0方法将轮询当前的Poller数组,从中取出一个Poller返回。(Poller的初始化参见上述第1步:NioEndpoint类核心组件的初始化)

/** 
 * Return an available poller in true round robin fashion 
 */  
public Poller getPoller0() {  
    // 最简单的轮询调度算法,poller的计数器不断加1再对poller数组取余数  
    int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;  
    return pollers[idx];  
}  

之后调用Poller对象的register方法:

        public void register(final NioChannel socket) {
            // 设置socket的Poller引用,便于后续处理
            socket.setPoller(this);
            // 从NioEndpoint的keyCache缓存队列中取出一个KeyAttachment
            KeyAttachment key = keyCache.poll();
            // KeyAttachment实际是NioChannel的包装类
            final KeyAttachment ka = key!=null?key:new KeyAttachment(socket);
            // 重置KeyAttachment对象中Poller、NioChannel等成员变量的引用
            ka.reset(this,socket,getSocketProperties().getSoTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());

            // 从Poller的事件对象缓存中取出一个PollerEvent,并用socket初始化事件对象
            PollerEvent r = eventCache.poll();
            // 设置读操作为感兴趣的操作
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);

            // 加入到Poller对象里的事件队列
            addEvent(r);
        }

看下Poller类里addEvent的代码:

        /**
         * Only used in this class. Will be made private in Tomcat 8.0.x
         * @deprecated
         */
        @Deprecated
        public void addEvent(Runnable event) {
            events.offer(event);
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }

就两行,第一行从event对象添加到缓存队列中,第二行如果当前事件队列中没有事件,则唤醒处于阻塞状态的selector 。

 

 

  • 5.Poller处理流程

上面讲的是从Acceptor中接收到的Socket以PollerEvent的形式包装并添加到Poller的事件缓存队列中,接下来看看另外一个核心组件Poller的处理过程:

    /**
     * Poller class.
     */
    public class Poller implements Runnable {

        // 这就是NIO中用到的选择器,可以看出每一个Poller都会关联一个Selector
        protected Selector selector;
        // 待处理的事件队列
        protected ConcurrentLinkedQueue<Runnable> events = new ConcurrentLinkedQueue<Runnable>();

        // 唤醒多路复用器的条件阈值
        protected AtomicLong wakeupCounter = new AtomicLong(0l);

        public Poller() throws IOException {
            // 对Selector的同步访问,通过调用Selector.open()方法创建一个Selector
            synchronized (Selector.class) {
                // Selector.open() isn't thread safe
                // http://bugs.sun.com/view_bug.do?bug_id=6427854
                // Affects 1.6.0_29, fixed in 1.7.0_01
                this.selector = Selector.open();
            }
        }

        // 通过addEvent方法将事件添加到Poller的事件队列中
        /**
         * Only used in this class. Will be made private in Tomcat 8.0.x
         * @deprecated
         */
        @Deprecated
        public void addEvent(Runnable event) {
            events.offer(event);
            // 如果队列中没有待处理的事件则唤醒处于阻塞状态的selector
            if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
        }

        // 处理事件队列中的所有事件,如果事件队列是空的则返回false
        /**
         * Processes events in the event queue of the Poller.
         *
         * @return <code>true</code> if some events were processed,
         *   <code>false</code> if queue was empty
         */
        public boolean events() {
            boolean result = false;

            Runnable r = null;
            // 将Poller的事件队列中的事件逐个取出并执行相应的事件线程
            while ( (r = events.poll()) != null ) {
                result = true;
                try {
                    // 执行事件处理逻辑
                    // 这里将事件设计成线程是将具体的事件处理逻辑和事件框架分开 
                    r.run();
                    if ( r instanceof PollerEvent ) {
                        ((PollerEvent)r).reset();
                        // 事件处理完之后,将事件对象返回NIOEndpoint的事件对象缓存中
                        eventCache.offer((PollerEvent)r);
                    }
                } catch ( Throwable x ) {
                    log.error("",x);
                }
            }

            return result;
        }

        // 将socket包装成统一的事件对象PollerEvent,加入到待处理事件队列中
        public void register(final NioChannel socket) {
            socket.setPoller(this);
            KeyAttachment key = keyCache.poll();
            final KeyAttachment ka = key!=null?key:new KeyAttachment(socket);
            ka.reset(this,socket,getSocketProperties().getSoTimeout());
            ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
            ka.setSecure(isSSLEnabled());
            // 从NIOEndpoint的事件对象缓存中取出一个事件对象
            PollerEvent r = eventCache.poll();
            ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
            if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);
            // 将事件添加打Poller的事件队列中
            addEvent(r);
        }
        
        // Poller是一个线程,该线程同Acceptor一样会监听TCP/IP连接并将它们交给合适的处理器处理
        /**
         * The background thread that listens for incoming TCP/IP connections and
         * hands them off to an appropriate processor.
         */
        @Override
        public void run() {
            // Loop until destroy() is called
            while (true) {
                try {
                    // Loop if endpoint is paused
                    while (paused && (!close) ) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }

                    boolean hasEvents = false;

                    // Time to terminate?
                    if (close) {
                        events();
                        timeout(0, false);
                        try {
                            selector.close();
                        } catch (IOException ioe) {
                            log.error(sm.getString(
                                    "endpoint.nio.selectorCloseFail"), ioe);
                        }
                        break;
                    } else {
                        // 执行事件队列中的事件线程
                        hasEvents = events();
                    }
                    try {
                        if ( !close ) {
                            if (wakeupCounter.getAndSet(-1) > 0) {
                                // 把wakeupCounter设成-1,这是与addEvent里的代码呼应,这样会唤醒selector
                                //if we are here, means we have other stuff to do
                                //do a non blocking select
                                // 以非阻塞方式查看selector是否有事件发生
                                keyCount = selector.selectNow();
                            } else {
                                // 查看selector是否有事件发生,超过指定时间则立即返回
                                keyCount = selector.select(selectorTimeout);
                            }
                            wakeupCounter.set(0);
                        }
                        if (close) {
                            // 执行事件队列中的事件线程
                            events();
                            timeout(0, false);
                            try {
                                selector.close();
                            } catch (IOException ioe) {
                                log.error(sm.getString(
                                        "endpoint.nio.selectorCloseFail"), ioe);
                            }
                            break;
                        }
                    } catch ( NullPointerException x ) {
                        //sun bug 5076772 on windows JDK 1.5
                        if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                        if ( wakeupCounter == null || selector == null ) throw x;
                        continue;
                    } catch ( CancelledKeyException x ) {
                        //sun bug 5076772 on windows JDK 1.5
                        if ( log.isDebugEnabled() ) log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5",x);
                        if ( wakeupCounter == null || selector == null ) throw x;
                        continue;
                    } catch (Throwable x) {
                        ExceptionUtils.handleThrowable(x);
                        log.error("",x);
                        continue;
                    }
                    //either we timed out or we woke up, process events first
                    if ( keyCount == 0 ) hasEvents = (hasEvents | events());

                    Iterator<SelectionKey> iterator =
                        keyCount > 0 ? selector.selectedKeys().iterator() : null;
                    // 根据向selector中注册的key遍历channel中已经就绪的keys,并处理这些key
                    // Walk through the collection of ready keys and dispatch
                    // any active event.
                    while (iterator != null && iterator.hasNext()) {
                        SelectionKey sk = iterator.next();
                        // 这里的attachment方法返回的就是在register()方法中注册的
                        // 而KeyAttachment对象是对socket的包装
                        KeyAttachment attachment = (KeyAttachment)sk.attachment();
                        // Attachment may be null if another thread has called
                        // cancelledKey()
                        if (attachment == null) {
                            iterator.remove();
                        } else {
                            // 更新通道最近一次发生事件的时间
                            // 防止因超时没有事件发生而被剔除出selector
                            attachment.access();
                            iterator.remove();
                            // 具体处理通道的逻辑
                            processKey(sk, attachment);
                        }
                    }//while

                    //process timeouts
                    // 多路复用器每执行一遍完整的轮询便查看所有通道是否超时
                    // 对超时的通道将会被剔除出多路复用器
                    timeout(keyCount,hasEvents);
                    if ( oomParachute > 0 && oomParachuteData == null ) checkParachute();
                } catch (OutOfMemoryError oom) {
                    try {
                        oomParachuteData = null;
                        releaseCaches();
                        log.error("", oom);
                    }catch ( Throwable oomt ) {
                        try {
                            System.err.println(oomParachuteMsg);
                            oomt.printStackTrace();
                        }catch (Throwable letsHopeWeDontGetHere){
                            ExceptionUtils.handleThrowable(letsHopeWeDontGetHere);
                        }
                    }
                }
            }//while
            synchronized (this) {
                this.notifyAll();
            }
            stopLatch.countDown();

        }

        // 处理selector检测到的通道事件 
        protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
            boolean result = true;
            try {
                if ( close ) {
                    cancelledKey(sk, SocketStatus.STOP, attachment.comet);
                } else if ( sk.isValid() && attachment != null ) {
                    // 确保通道不会因超时而被剔除
                    attachment.access();//make sure we don't time out valid sockets
                    sk.attach(attachment);//cant remember why this is here
                    NioChannel channel = attachment.getChannel();
                    // 处理通道发生的读写事件
                    if (sk.isReadable() || sk.isWritable() ) {
                        if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment, false);
                        } else {
                            if ( isWorkerAvailable() ) {
                                // 在通道上注销对已经发生事件的关注
                                unreg(sk, attachment, sk.readyOps());
                                boolean closeSocket = false;
                                // Read goes before write
                                if (sk.isReadable()) {
                                    // 具体的通道处理逻辑
                                    if (!processSocket(channel, SocketStatus.OPEN_READ, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (!closeSocket && sk.isWritable()) {
                                    if (!processSocket(channel, SocketStatus.OPEN_WRITE, true)) {
                                        closeSocket = true;
                                    }
                                }
                                if (closeSocket) {
                                    // 解除无效通道
                                    cancelledKey(sk,SocketStatus.DISCONNECT,false);
                                }
                            } else {
                                result = false;
                            }
                        }
                    }
                } else {
                    //invalid key
                    cancelledKey(sk, SocketStatus.ERROR,false);
                }
            } catch ( CancelledKeyException ckx ) {
                cancelledKey(sk, SocketStatus.ERROR,false);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error("",t);
            }
            return result;
        }

        // 这个unreg()很巧妙,防止了通道对同一个事件不断select的问题
        protected void unreg(SelectionKey sk, KeyAttachment attachment, int readyOps) {
            //this is a must, so that we don't have multiple threads messing with the socket
            reg(sk,attachment,sk.interestOps()& (~readyOps));
        }

        // 向NioChannel注册感兴趣的事件,具体代码看下面的PollerEvent类的说明
        protected void reg(SelectionKey sk, KeyAttachment attachment, int intops) {
            sk.interestOps(intops);
            attachment.interestOps(intops);
            attachment.setCometOps(intops);
        }

    }

 

 

4
2
分享到:
评论
2 楼 zpkbtzmbw 2016-02-26  
学习了
1 楼 cywhoyi 2015-11-15  
剖析的这么深刻

相关推荐

    基于pytorch+ResNet50的眼部疾病图片分类源码+文档说明.zip

    详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;详情请查看资源内容中使用说明;

    基于yolov5的鸟窝目标检测源码+模型.zip

    YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

    中南财经政法大学-答辩PPT模板我给母校送模板作品.pptx

    PPT模板,答辩PPT模板,毕业答辩,学术汇报,母校模板,我给母校送模板作品,周会汇报,开题答辩,教育主题模板下载。PPT素材下载。

    node-v8.7.0-sunos-x64.tar.gz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    2024-2030全球与中国混合光纤同轴网络市场现状及未来发展趋势.docx

    2024-2030全球与中国混合光纤同轴网络市场现状及未来发展趋势

    中南民族大学-答辩通用PPT模板我给母校送模板作品.pptx

    PPT模板,答辩PPT模板,毕业答辩,学术汇报,母校模板,我给母校送模板作品,周会汇报,开题答辩,教育主题模板下载。PPT素材下载。

    使用Spring Boot开发框架和Spark MLlib机器学习框架,通过FP-Growth算法,分析用户的购物车商品数据

    商品关联关系挖掘,使用Spring Boot开发框架和Spark MLlib机器学习框架,通过FP-Growth算法,分析用户的购物车商品数据,挖掘商品之间的关联关系。项目对外提供RESTFul接口。.zip

    OpenCv 使用fffffffff

    OpenCv 使用fffffffff

    node-v10.14.0-linux-ppc64le.tar.gz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    课设&大作业-高校社团管理系统

    【资源说明】【毕业设计】 1、该资源内项目代码都是经过测试运行成功,功能正常的情况下才上传的,请放心下载使用。 2、适用人群:主要针对计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、数学、电子信息等)的同学或企业员工下载使用,具有较高的学习借鉴价值。 3、不仅适合小白学习实战练习,也可作为大作业、课程设计、毕设项目、初期项目立项演示等,欢迎下载,互相学习,共同进步!

    基于java的学校教务管理系统 (jsp+servlet+sqlserver)(含程序运行详细说明文档).zip

    基于java的学校教务管理系统 (jsp+servlet+sqlserver)(含程序运行详细说明文档).zip

    iOS APP提审checklist

    根据App Store的审核条款总结出的checklist

    基于YOLOV5的移动物体检测分类系统源码.zip

    YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

    088ssm-vue教学视频点播系统.zip(可运行源码+数据库文件+文档)

    对于本视频点播系统的设计来说,系统开发主要是采用java语言技术,在整个系统的设计中应用MySQL数据库来完成数据存储,具体根据视频点播系统的现状来进行开发的,具体根据现实的需求来实现视频点播系统网络化的管理,各类信息有序地进行存储,进入视频点播系统页面之后,方可开始操作主控界面,主要功能包括管理员:首页、个人中心、学生管理、教师管理、视频分类管理、视频信息管理、系统管理,学生前台:首页、视频信息、系统公告、个人中心、后台管理,教师:首页、个人中心、视频信息管理等功能。 本文主要讲述了视频点播系统开发背景,该系统它主要是对需求分析和功能需求做了介绍,并且对系统做了详细的测试和总结。具体从业务流程、数据库设计和系统结构等多方面的问题。望能利用先进的计算机技术和网络技术来改变目前的视频点播系统状况,提高管理效率。 关键词:视频点播系统;SSM框架,mysql数据库,B/S模式

    ky10升级内核版本包

    ky10升级内核版本包

    C语言课程设计计费管理系统源码.zip

    高分设计源码,详情请查看资源内容中使用说明 高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明高分设计源码,详情请查看资源内容中使用说明

    基于单片机的含计数功能的智能电子天平的研发

    针对小质量器件的称重和计数包装特点,本文设计了 个基千单片机的可以实现 高精度称重和智能计数的智能仪表。仪表主要由电源、 称重传感器、 单片机、 键盘/开关、LCD显示器等部分构成。仪表输入采用电阻应变式称重传感器, 模数转换利用 具有24位精度的AID转换器把传感器的模拟信号转换为数字信号送给单片机处理。通过软件的设计可以完成高精度的称重和针对小质量器件的准确计数。仪表还设计了按键、液晶显示器及翁鸣器等输入输出器件实现了良好的人机交互功能, 能够完成对 称重和计数的标定以及相关参数的设置。另外仪表还预留了通讯接口, 为以后的升级 以及联网数据传送奠定了基础。

    基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip

    基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip个人经导师指导并认可通过的98分课程设计项目,主要针对计算机相关专业的正在做课程设计、期末大作业的学生和需要项目实战练习的学习者。 基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip个人经导师指导并认可通过的98分课程设计项目,主要针对计算机相关专业的正在做课程设计、期末大作业的学生和需要项目实战练习的学习者。 基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip个人经导师指导并认可通过的98分课程设计项目,主要针对计算机相关专业的正在做课程设计、期末大作业的学生和需要项目实战练习的学习者。 基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip个人经导师指导并认可通过的98分课程设计项目,主要针对计算机相关专业的正在做课程设计、期末大作业的学生和需要项目实战练习的学习者。 基于Python的PCA人脸识别算法的原理及实现代码详解(优质项目).zip个人经导师指导并认可通过的98分课程设计项目,主要针对计算机相关专业的正在做课程设

    python180数学函数绘图软件cs.zip

    python180数学函数绘图软件cs.zip

    2013年教育学311专业基础综合真题.pdf

    教育学考研,考研真题,全国硕士研究生统一考试教育学专业基础综合真题及解析,311历年真题,参考答案,答案解析。教育学统考。

Global site tag (gtag.js) - Google Analytics