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

【转载】深入理解Tornado之异步web服务器(源码分析指南)

阅读更多

这篇文章的目的在于对Tornado这个异步服务器软件的底层进行一番探索。我采用自底向上的方式进行介绍,从轮询开始,向上一直到应用层,指出我认为有趣的部分。

所以,如果你有打算要阅读Tornado这个web框架的源码,又或者是你对一个异步web服务器是如何工作的感兴趣,我可以在这成为你的指导。

通过阅读这篇文章,你将可以:

  • 自己写一个 Comet 架构程序的服务器端部分,即使你是从拷贝别人的代码开始。
  • 如果你想在Tornado框架上做开发,通过这篇文章你将更好的理解Tornado web框架。
  • Tornado和Twisted的争论上,你将更有见解。

 

介绍

假设你还不知道Tornado是什么也不知道为什么应该对它感兴趣,那我将用简短的话来介绍Tornado这个项目。如果你已经对它有了兴趣,你可以跳去看下一节内容。

Tornado 是一个用Python编写的异步HTTP服务器,同时也是一个web开发框架。该框架服务于 FriendFeed 网站,最近Facebook也在使用它。FriendFeed网站有用户数多和应用实时性强的特点,所以性能和可扩展性是很受重视的。由于现在它是开源的了(这得归功于Facebook),我们可以彻底的对它是如何工作的一探究竟。

我觉得对非阻塞IO (nonblocking IO)或异步IO (asynchronous IO, AIO)很有必要谈一谈。如果你已经完全知道他们是什么了,可以跳去看下一节。我尽可能的使用一些例子来说明它们是什么。

让我们假设你正在写一个需要请求一些来自其他服务器上的数据(比如数据库服务,再比如新浪微博的open api)的应用程序,然后呢这些请求将花费一个比较长的时间,假设需要花费5秒钟。大多数的web开发框架中处理请求的代码大概长这样:

def handler_request(self, request):
    answ = self.remote_server.query(request) # this takes 5 seconds
    request.write_response(answ)

如果这些代码运行在单个线程中,你的服务器只能每5秒接收一个客户端的请求。在这5秒钟的时间里,服务器不能干其他任何事情,所以,你的服务效率是每秒0.2个请求,哦,这太糟糕了。

当然,没人那么天真,大部分服务器会使用多线程技术来让服务器一次接收多个客户端的请求,我们假设你有20个线程,你将在性能上获得20倍的提高,所以现在你的服务器效率是每秒接受4个请求,但这还是太低了,当然,你可以通过不断地提高线程的数量来解决这个问题,但是,线程在内存和调度方面的开销是昂贵的,我怀疑如果你使用这种提高线程数量的方式将永远不可能达到每秒100个请求的效率。

如果使用AIO,达到每秒上千个请求的效率是非常轻松的事情。服务器请求处理的代码将被改成这样:

def handler_request(self, request):
    self.remote_server.query_async(request, self.response_received)

def response_received(self, request, answ): # this is called 5 seconds later
    request.write(answ)

AIO的思想是当我们在等待结果的时候不阻塞,转而我们给框架一个回调函数作为参数,让框架在有结果的时候通过回调函数通知我们。这样,服务器就可以被解放去接受其他客户端的请求了。

然而这也是AIO不太好的地方:代码有点不直观了。还有,如果你使用像Tornado这样的单线程AIO服务器软件,你需要时刻小心不要去阻塞什么,因为所有本该在当前返回的请求都会像上述处理那样被延迟返回。

关于异步IO,比当前这篇过分简单的介绍更好的学习资料请看 The C10K problem 。

源代码

该项目由 github 托管,你可以通过如下命令获得,虽然通过阅读这篇文章你也可以不需要它是吧。

$ git clone git://github.com/facebook/tornado.git

在 tornado 的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。在每个源代码的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,doc string中给出了一到两个关于如何使用该模块的例子。

IOLoop模块

让我们通过查看 ioloop.py 文件直接进入服务器的核心。这个模块是异步机制的核心。它包含了一系列已经打开的文件描述符和每个描述符的处理器(handlers)。它的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器(一种IO多路复用的实现,其实就是socket众多IO模型中的select模型,在Java中就是NIO,译者注)。

可以通过调用 add_handler() 方法将一个socket加入 IOLoop 中:

def add_handler(self, fd, handler, events):
    """Registers the given handler to receive the given events for fd."""
    self._handlers[fd] = handler
    self._impl.register(fd, events | self.ERROR)

_handlers 这个字典类型的变量保存着文件描述符到当该文件描述符准备好时需要调用的方法的映射(在Tornado中,该方法被称为处理器)。然后,文件描述符被注册到epoll列表中。Tornado关心三种类型的事件: READ , WRITE和 ERROR 。正如你所见, ERROR 是默认为你自动添加的。

self._impl 是 select.epoll() 和 selet.select() 两者中的一个。我们稍后将看到Tornado是如何在它们之间进行选择的。

现在让我们来看看实际的主循环,不知何故,这段代码被放在了 start() 方法中:

def start(self):
    """Starts the I/O loop.
    The loop will run until one of the I/O handlers calls stop(), which
    will make the loop stop after the current event iteration completes.
    """
    self._running = True
    while True:

    [ ... ]

        if not self._running:
            break

        [ ... ]

        try:
            event_pairs = self._impl.poll(poll_timeout)
        except Exception, e:
            if e.args == (4, "Interrupted system call"):
                logging.warning("Interrupted system call", exc_info=1)
                continue
            else:
                raise

        # Pop one fd at a time from the set of pending fds and run
        # its handler. Since that handler may perform actions on
        # other file descriptors, there may be reentrant calls to
        # this IOLoop that update self._events
        self._events.update(event_pairs)
        while self._events:
            fd, events = self._events.popitem()
            try:
                self._handlers[fd](fd, events)
            except KeyboardInterrupt:
                raise
            except OSError, e:
                if e[0] == errno.EPIPE:
                    # Happens when the client closes the connection
                    pass
                else:
                    logging.error("Exception in I/O handler for fd %d",
                                  fd, exc_info=True)
            except:
                logging.error("Exception in I/O handler for fd %d",
                              fd, exc_info=True)

poll() 方法返回一个形如 (fd: events) 的键值对,并赋值给 event_pairs 变量。由于当一个信号在任何一个事件发生前到来时,C函数库中的 poll() 方法会返回 EINTR (实际是一个值为4的数值),所以"Interrupted system call"这个特殊的异常需要被捕获。更详细的请查看 man poll 。

在内部的while循环中, event_pairs 中的内容被一个一个的取出,然后相应的处理器会被调用。pipe异常在这里默认不进行处理。为了让这个类适应更一般的情况,在http处理器中处理这个异常是一个更好的方案,但是选择现在这样处理或许是因为更容易一些。

注释中解释了为什么使用字典的 popitem() 方法,而不是使用更普遍一点的下面这种做法:

for fd, events in self._events.items():

原因很简单,在主循环期间,这个 _events 字典变量可能会被处理器所修改。比如 remove_handler() 处理器。这个方法把 fd 从 _events 字典中取出,所以即使 fd 被选择到了,它的处理器也不会被调用。

def remove_handler(self, fd):
    """Stop listening for events on fd."""
    self._handlers.pop(fd, None)
    self._events.pop(fd, None)
    try:
        self._impl.unregister(fd)
    except OSError:
        logging.debug("Error deleting fd from IOLoop", exc_info=True)

(意义不大的)循环结束技巧

怎么让这个主循环停止是很有技巧性的。 self._running 变量被用来在运行时从主循环中跳出,处理器可以通过调用stop() 方法把它设置为 False 。通常情况下,这就能让主循环停止了,但是 stop() 方法还能被一个信号处理器所调用,所以,如果 1) 主循环正阻塞在 poll() 方法处, 2) 服务端没有接收到任何来自客户端的请求, 3) 信号没有被OS投递到正确的线程中,你将不得不等待 poll() 方法出现超时情况后才会返回。考虑到这些情况并不时常发生,还有 poll() 方法的默认超时时间只不过是0.2秒,所以这种让主循环停止的方式还算过得去。

但不管怎样,Tornado的开发者为了让主循环停止,还是额外的创建了一个没有名字的管道和对应的处理器,并把管道的一端放在了轮询文件描述符列表中。当需要停止时,在管道的另一端随便写点什么,这能高效率的唤醒主循环在poll() 方法处的阻塞(貌似Java NIO的Windows实现就用了这种方法,译者注)。这里节选了一些代码片段:

def __init__(self, impl=None):

    [...]

    # Create a pipe that we send bogus data to when we want to wake
    # the I/O loop when it is idle
    r, w = os.pipe()
    self._set_nonblocking(r)
    self._set_nonblocking(w)
    self._waker_reader = os.fdopen(r, "r", 0)
    self._waker_writer = os.fdopen(w, "w", 0)
    self.add_handler(r, self._read_waker, self.WRITE)

def _wake(self):
    try:
        self._waker_writer.write("x")
    except IOError:
        pass

实际上,上述代码中存在一个bug:那个只读文件描述符 r ,虽然是用来读的,但在注册时却附加上了 WRITE 类型的事件,这将导致该注册实际不会被响应。正如我先前所说的,用不用专门找个方法其实没什么的,所以我对他们没有发现这个方法不起作用的事实并不感到惊讶。我在 mail list 中报告了这个情况,但是尚未收到答复。

定时器

另外一个在 IOLoop 模块中很有特点的设计是对定时器的简单实现。一系列的定时器会被以是否过期的形式来维护和保存,这用到了python的 bisect 模块:

def add_timeout(self, deadline, callback):
    """Calls the given callback at the time deadline from the I/O loop."""
    timeout = _Timeout(deadline, callback)
    bisect.insort(self._timeouts, timeout)
    return timeout

在主循环中,所有过期了的定时器的回调会按照过期的顺序被触发。 poll() 方法中的超时时间会动态的进行调整,调整的结果就是如果没有新的客户端请求,那么下一个定时器就好像没有延迟一样的被触发(意思是如果没有新的客户端的请求, poll() 方法将被阻塞直到超时,这个超时时间的设定会根据下一个定时器与当前时间之间的间隔进行调整,调整后,超时的时间会等同于距离下一个定时器被触发的时间,这样在 poll() 阻塞完后,下一个定时器刚好过期,译者注)。

self._running = True
while True:
    poll_timeout = 0.2

    [ ... ]
    if self._timeouts:
        now = time.time()
        while self._timeouts and self._timeouts[0].deadline <= now:
            timeout = self._timeouts.pop(0)
            self._run_callback(timeout.callback)
        if self._timeouts:
            milliseconds = self._timeouts[0].deadline - now
            poll_timeout = min(milliseconds, poll_timeout)

[ ... poll ]

选择select方案

让我们现在快速的看一下poll和select这两种select方案的实现代码。Python已经在版本2.6的标准库中支持了epoll,你可以通过在 select 模块上使用 hasattr() 方法检测当前Python是否支持epoll。如果python版本小于2.6,Tornado将用它自己的基于C的epoll模块。你可以在 tornado/epoll.c 文件中找到它源代码。如果最后这也不行(因为epoll不是每个Linux都有的),它将回退到select。 _Select 和 _EPoll 类是模拟 select.epoll API的封装。在你做性能测试之前,请确定你能使用 epoll ,因为 select 在有大量文件描述符情况下的效率非常低。

# Choose a poll implementation. Use epoll if it is available, fall back to
# select() for non-Linux platforms
if hasattr(select, "epoll"):
    # Python 2.6+ on Linux
    _poll = select.epoll
else:
    try:
        # Linux systems with our C module installed
        import epoll
        _poll = _EPoll
    except:
        # All other systems
        import sys
        if "linux" in sys.platform:
            logging.warning("epoll module not found; using select()")
        _poll = _Select

通过上述阅读,我们的介绍已经涵盖了大部分 IOLoop 模块。正如广告中介绍的那样,它是一段优雅而又简单的代码。

从sockets到流

让我们来看看 IOStream 模块。它的目的是提供一个对非阻塞sockets的轻量级抽象,它提供了三个方法:

  • read_until() ,从socket中读取直到遇到指定的字符串。这为在读取HTTP头时遇到空行分隔符自动停止提供了方便。
  • read_bytes() ,从socket中读取指定数量的字节。这为读取HTTP消息的body部分提供了方便。
  • write() ,将指定的buffer写入socket并持续监测直到这个buffer被发送。

所有上述的方法都可以通过异步方式在它们完成时触发回调函数。

write() 方法提供了将调用者提供的数据加以缓冲直到 IOLoop 调用了它的处理器的功能,因为到那时候就说明socket已经为写数据做好了准备:

def write(self, data, callback=None):
    """Write the given data to this stream.
    If callback is given, we call it when all of the buffered write
    data has been successfully written to the stream. If there was
    previously buffered write data and an old write callback, that
    callback is simply overwritten with this new callback.
    """
    self._check_closed()
    self._write_buffer += data
    self._add_io_state(self.io_loop.WRITE)
    self._write_callback = callback

该方法只是用 socket.send() 来处理 WRITE 类型的事件,直到 EWOULDBLOCK 异常发生或者buffer被发送完毕:

def _handle_write(self):
    while self._write_buffer:
        try:
            num_bytes = self.socket.send(self._write_buffer)
            self._write_buffer = self._write_buffer[num_bytes:]
        except socket.error, e:
            if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                break
            else:
                logging.warning("Write error on %d: %s",
                                self.socket.fileno(), e)
                self.close()
                return
    if not self._write_buffer and self._write_callback:
        callback = self._write_callback
        self._write_callback = None
        callback()

读数据的方法和上述过程正好相反。读事件的处理器持续读取数据直到缓冲区被填满为止。这就意味着要么读取指定数量的字节(如果调用的是 read_bytes() ),要么读取的内容中包含了指定的分隔符(如果调用的是 read_util()):

def _handle_read(self):
    try:
        chunk = self.socket.recv(self.read_chunk_size)
    except socket.error, e:
        if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
            return
        else:
            logging.warning("Read error on %d: %s",
                            self.socket.fileno(), e)
            self.close()
            return
    if not chunk:
        self.close()
        return
    self._read_buffer += chunk
    if len(self._read_buffer) >= self.max_buffer_size:
        logging.error("Reached maximum read buffer size")
        self.close()
        return
    if self._read_bytes:
        if len(self._read_buffer) >= self._read_bytes:
            num_bytes = self._read_bytes
            callback = self._read_callback
            self._read_callback = None
            self._read_bytes = None
            callback(self._consume(num_bytes))
    elif self._read_delimiter:
        loc = self._read_buffer.find(self._read_delimiter)
        if loc != -1:
            callback = self._read_callback
            delimiter_len = len(self._read_delimiter)
            self._read_callback = None
            self._read_delimiter = None
            callback(self._consume(loc + delimiter_len))

如下所示的 _consume() 方法是为了确保在要求的返回值中不会包含多余的来自流的数据,并且保证后续的读操作会从当前字节的下一个字节开始:

def _consume(self, loc):
    result = self._read_buffer[:loc]
    self._read_buffer = self._read_buffer[loc:]
    return result

还值得注意的是在上述 _handle_read() 方法中read buffer的上限—— self.max_buffer_size 。默认值是100MB,这似乎对我来说是有点大了。举个例子,如果一个攻击者和服务端建立了100个连接,并持续发送不带头结束分隔符的头信息,那么Tornado需要 10GB的内存 来处理这些请求。即使内存ok,这种数量级数据的复制操作(比如像上述_consume() 方法中的代码)很可能使服务器超负荷。我们还注意到在每次迭代中 _handle_read() 方法是如何在这个buffer中搜索分隔符的,所以如果攻击者以小块形式发送大量的数据,服务端不得不做很多次搜索工作。归根结底,你应该想要调整这个参数,除非你真的需要接收这么多的数据并且能满足硬件条件。

HTTP 服务器

有了 IOLoop 模块和 IOStream 模块的帮助,写一个异步的HTTP服务器只差一步之遥,这一步就在 httpserver.py 中完成。

HTTPServer 类它自己只负责处理将接收到的新连接的socket添加到 IOLoop 中。该监听型的socket自己也是 IOLoop 的一部分,正如在 listen() 方法中见到的那样:

def listen(self, port, address=""):
    assert not self._socket
    self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
    flags |= fcntl.FD_CLOEXEC
    fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
    self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self._socket.setblocking(0)
    self._socket.bind((address, port))
    self._socket.listen(128)
    self.io_loop.add_handler(self._socket.fileno(), self._handle_events,
                             self.io_loop.READ)

除了绑定给定的地址和端口外,上述代码还设置了"close on exec"和"reuse address"这两个标志位。前者在应用程序创建子进程的时候特别有用。在这种情况下,我们不想让套接字保持打开的状态。后者用来避免在服务器重启的时候发生“该地址已被使用”这种错误时很有用。

正如你所见到的,后备连接所允许的最大数目是128。这意味着如果有128个连接正在等待被接受,那么直到服务器有时间将前面128个连接中的某几个被接受了,新的连接都将被拒绝。我建议你在做性能测试的时候将该参数调高,因为当新的连接被抛弃的时候将直接影响你做测试的准确性。

在上述代码中注册的 _handle_events() 处理器用来接受新连接,并创建相关的 IOStream 对象和初始化一个HTTPConnection 对象, HTTPConnection 对象负责处理剩下的交互部分:

def _handle_events(self, fd, events):
    while True:
        try:
            connection, address = self._socket.accept()
        except socket.error, e:
            if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                return
            raise
        try:
            stream = iostream.IOStream(connection, io_loop=self.io_loop)
            HTTPConnection(stream, address, self.request_callback,
                           self.no_keep_alive, self.xheaders)
        except:
            logging.error("Error in connection callback", exc_info=True)

可以看到这个方法在一次迭代中接受了所有正在等待处理的连接。也就是说直到 EWOULDBLOCK 异常发生时 while True循环才会退出,这也就意味着当前没有需要接受的连接了。

HTTP头的部分的解析工作开始于 HTTPConnection 类的构造函数 __init__() :

def __init__(self, stream, address, request_callback, no_keep_alive=False,
             xheaders=False):
    self.stream = stream
    self.address = address
    self.request_callback = request_callback
    self.no_keep_alive = no_keep_alive
    self.xheaders = xheaders
    self._request = None
    self._request_finished = False
    self.stream.read_until("rnrn", self._on_headers)

如果你很想知道 xheaders 参数的意义,请看这段注释:

 如果 xheaders 为 True ,我们将支持把所有请求的HTTP头解析成X-Real-Ip和X-Scheme格式,而原先我们将HTTP头解析成remote IP和HTTP scheme格式。这种格式的HTTP头在Tornado运行于反向代理或均衡负载服务器的后端时将非常有用。

_on_headers() 回调函数实际用来解析HTTP头,并在有请求内容的情况下通过使用 read_bytes() 来读取请求的内容部分。 _on_request_body() 回调函数用来解析 POST 的参数并调用应用层提供的回调函数:

def _on_headers(self, data):
    eol = data.find("rn")
    start_line = data[:eol]
    method, uri, version = start_line.split(" ")
    if not version.startswith("HTTP/"):
        raise Exception("Malformed HTTP version in HTTP Request-Line")
    headers = HTTPHeaders.parse(data[eol:])
    self._request = HTTPRequest(
        connection=self, method=method, uri=uri, version=version,
        headers=headers, remote_ip=self.address[0])

    content_length = headers.get("Content-Length")
    if content_length:
        content_length = int(content_length)
        if content_length > self.stream.max_buffer_size:
            raise Exception("Content-Length too long")
        if headers.get("Expect") == "100-continue":
            self.stream.write("HTTP/1.1 100 (Continue)rnrn")
        self.stream.read_bytes(content_length, self._on_request_body)
        return

    self.request_callback(self._request)

def _on_request_body(self, data):
    self._request.body = data
    content_type = self._request.headers.get("Content-Type", "")
    if self._request.method == "POST":
        if content_type.startswith("application/x-www-form-urlencoded"):
            arguments = cgi.parse_qs(self._request.body)
            for name, values in arguments.iteritems():
                values = [v for v in values if v]
                if values:
                    self._request.arguments.setdefault(name, []).extend(
                        values)
        elif content_type.startswith("multipart/form-data"):
            boundary = content_type[30:]
            if boundary: self._parse_mime_body(boundary, data)
    self.request_callback(self._request)

将结果写回客户端的工作在 HTTPRequest 类中处理,你可以在上面的 _on_headers() 方法中看到具体的实现。HTTPRequest 类仅仅将写回的工作代理给了stream对象。

def write(self, chunk):
    assert self._request, "Request closed"
    self.stream.write(chunk, self._on_write_complete)

未完待续?

通过这篇文章,我已经涵盖了从socket到应用层的所有方面。这应该能给你关于Tornado是如何工作的一个清晰的理解。总之,我认为Tornado的代码是非常友好的,我希望你也这样认为。

Tornado框架还有很大一部分我们没有探索,比如 web.py 这个实际与你应用打交道的模块,又或者是 template engine 。如果我有足够兴趣的话,我也会介绍这些部分。可以通过订阅我的 RSS feed 来鼓励我。

 http://golubenco.org/2009/09/19/understanding-the-code-inside-tornado-the-asynchronous-web-server-powering-friendfeed/

 

1
0
分享到:
评论

相关推荐

    利用Simulink实现混合储能系统在直流微网中的下垂控制策略研究:保持直流母线电压稳定的实践与探究,Simulink仿真下的光储直流微网混合储能系统下垂控制策略优化研究(注意版本要求为2021A以上

    利用Simulink实现混合储能系统在直流微网中的下垂控制策略研究:保持直流母线电压稳定的实践与探究,Simulink仿真下的光储直流微网混合储能系统下垂控制策略优化研究(注意版本要求为2021A以上),混合储能系统 光储微网 下垂控制 Simulink仿真 注意版本2021A以上 由光伏发电系统和混合储能系统构成直流微网。 混合储能系统由超级电容器和蓄电池构成,通过控制混合储能系统来维持直流母线电压稳定。 混合储能系统采用下垂控制来实现超级电容和蓄电池的功率分配,蓄电池响应低频量,超级电容响应高频量。 通过改变光照来影响光伏出力,控制混合储能系统保持微网直流母线电压稳定在380V,不受光伏出力变化影响。 ,混合储能系统; 光储微网; 下垂控制; Simulink仿真; 版本2021A; 直流母线电压稳定; 光伏出力变化; 超级电容器; 蓄电池。,2021A+混合储能系统:光储微网下垂控制Simulink仿真研究

    JavaScript入门到精通: 全栈编程语言的基础与进阶学习指南

    内容概要:本文档是针对JavaScript这一跨平台解释型语言的详尽入门手册,首先概述了JavaScript的概念及其重要特性,强调它不仅适用于前端同时也活跃于Node.js的服务器环境之中,从而成为全栈开发的重要技能。紧接着文档阐述了JavaScript的基本语法元素如变量声明、数据类型、运算符及控制结构,让新手理解JavaScript的语法规则,并通过函数与对象操作加深印象。之后介绍了一些常见的实用工具和高级用法,例如模板字符串、解构赋值以及异步编程手段(比如Promise)。对于想要深入探索的应用场景给出了广泛的指引,无论是传统的web开发还是新兴领域的IoT或自动化脚本编写皆有所涉猎。 适合人群:对于那些没有编程背景或有其他编程经验但仍希望了解并擅长运用JavaScript的个人来说非常适合。 使用场景及目标:目的是向初学者提供足够的理论指导和技术实践机会,使他们能够在不同平台上利用JavaScript创造出有意义的作品;不论是想要从事专业软件开发或是业余项目爱好者都能够从中受益。 其他说明:文档还提供了大量权威且有用的外部链接供进一步深造学习,包括但不限于主流的在线课程、权威的技术参考资料及充满活力的支持社区。

    2D3D 中弗里德里希常数和庞加莱常数的计算 附Matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均衡管理,级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均

    级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均衡管理,级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均衡管理,不平衡电网下的svg无功补偿,级联H桥svg无功补偿statcom,采用三层控制策略。 (1)第一层采用电压电流双闭环pi控制,电压电流正负序分离,电压外环通过产生基波正序有功电流三相所有H桥模块直流侧平均电压恒定,电流内环采用前馈解耦控制; (2)第二层相间电压均衡控制,注入零序电压,控制通过注入零序电压维持相间电压平衡; (3)第三层相内电压均衡控制,使其所有子模块吸收的有功功率与其损耗补,从而保证所有H桥子模块直流侧电压值等于给定值。 有参考资料。 639,核心关键词: 1. 不平衡电网下的SVG无功补偿 2. 级联H桥SVG无功补偿STATCOM 3. 三层控制策略 4. 电压电流双闭环PI控制 5. 电压电流正负序分离 6. 直流侧平均电压恒定 7. 前馈解耦控制 8. 相间电压均衡控制 9. 零序电压注入 10. 相内电压均衡控制 以上十个关键词用分号分隔的格式为:不

    基于时空RBF-NN的混沌时间序列预测 附Matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    基于主从博弈的动态定价策略与电动汽车充电管理优化在智能小区的实践(MATLAB+CPLEX gurobi实现),基于主从博弈理论的智能小区电动汽车充电与代理商动态定价策略优化研究,MATLAB代码:基

    基于主从博弈的动态定价策略与电动汽车充电管理优化在智能小区的实践(MATLAB+CPLEX gurobi实现),基于主从博弈理论的智能小区电动汽车充电与代理商动态定价策略优化研究,MATLAB代码:基于主从博弈的智能小区代理商定价策略及电动汽车充电管理 关键词:电动汽车 主从博弈 动态定价 智能小区 充放电优化 参考文档:《基于主从博弈的智能小区代理商定价策略及电动汽车充电管理》基本复现 仿真平台:MATLAB+CPLEX gurobi平台 主要内容:代码主要做的是一个电动汽车充电管理和智能小区代理商动态定价的问题,将代理商和车主各自追求利益最大化建模为主从博弈,上层以代理商的充电电价作为优化变量,下层以电动汽车的充电策略作为优化变量,通过优化得出最优电价策略以及动态充电策略。 ,电动汽车; 主从博弈; 动态定价; 智能小区; 充放电优化; MATLAB; CPLEX; gurobi平台。,基于主从博弈的电动汽车充电管理与定价策略优化MATLAB代码实现

    (程序、GUI、思路)MATLAB打印纸缺陷检测GUI设计.zip

    基于Matlab语言实现的设计项目 2、适用人群:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业或毕业设计中的部分功能,作为“参考资料”使用。 3、解压说明:本资源需要电脑端使用WinRAR、7zip等解压工具进行解压,没有解压工具的自行百度下载即可。 4、免责声明:本资源作为“参考资料”而不是“定制需求”,代码只能作为参考,不能完全复制照搬。不一定能够满足所有人的需求,需要有一定的基础能够看懂代码,能够自行调试代码并解决报错,能够自行添加功能修改代码。由于作者大厂工作较忙,不提供答疑服务,如不存在资源缺失问题概不负责,谢谢理解。

    《基于 Transformer 的恶意软件检测器》(毕业设计,源码,教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计.zip

    资源内项目源码是均来自个人的课程设计、毕业设计或者具体项目,代码都测试ok,都是运行成功后才上传资源,答辩评审绝对信服的,拿来就能用。放心下载使用!源码、说明、论文、数据集一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 4、如有侵权请私信博主,感谢支持

    Labiew噪音与振动检测模块源码揭秘:傅里叶变换与倍频程技术应用于实际项目,LabVIEW平台噪声与振动检测模块源码解析:基于傅里叶变换与倍频程原理的实用功能模块,已成功应用于实际项目,虚拟产品退换

    Labiew噪音与振动检测模块源码揭秘:傅里叶变换与倍频程技术应用于实际项目,LabVIEW平台噪声与振动检测模块源码解析:基于傅里叶变换与倍频程原理的实用功能模块,已成功应用于实际项目,虚拟产品退换政策严谨执行,Labiew噪音与振动检测模块源码,改功能模块已运用到实际项目,原理是利用傅里叶变和倍频程实现的,产品一旦发概不 。 需要的可以联系哟 ,Labiew源码; 噪音与振动检测模块; 傅里叶变换; 倍频程; 实际项目运用,Labiew傅里叶变换倍频程噪音振动检测模块源码

    基于Comsol多物理场仿真的光伏集热器异形体建模技术研究,探索comsol多物理场仿真技术:光伏集热器异形体建模应用,comsol多物理场仿真,光伏集热器,异形体建模 ,comsol多物理场仿真;

    基于Comsol多物理场仿真的光伏集热器异形体建模技术研究,探索comsol多物理场仿真技术:光伏集热器异形体建模应用,comsol多物理场仿真,光伏集热器,异形体建模 ,comsol多物理场仿真; 光伏集热器仿真; 异形体建模,Comsol多物理场仿真在光伏集热器及异形体建模中的应用

    器官3D分割-基于WinForm框架开发的医学影像系统源码+sln+演示视频(毕设基于c#和python开发).zip

    器官3D分割-基于WinForm框架开发的医学影像系统源码+sln+演示视频(毕设基于c#和python开发).zip 【项目简单介绍】 主要功能 肺炎诊断 器官 3D 分割 该系统具备肺炎诊断和器官 3D 分割的功能,并模仿了罗万科技的系统界面风格。 python和c#开发实现

    界面GUI设计MATLAB BP的水果识别.zip

    MATLAB可以用于开发水果识别系统。这种系统通常利用机器学习和图像处理技术,对输入的水果图像进行特征提取和分类识别。以下是开发水果识别系统的一般步骤: 1. 数据收集:收集包含各种水果类别的图像数据集。 2. 数据预处理:对图像进行预处理,包括裁剪、缩放、灰度化等操作。 3. 特征提取:从每个水果图像中提取特征,例如颜色直方图、纹理特征、形状特征等。 4. 数据标记:为每个图像标记水果类别,形成训练集和测试集。 5. 模型训练:使用机器学习算法(如支持向量机、卷积神经网络等)对训练集进行训练,建立水果识别模型。 6. 模型测试:使用测试集对模型进行测试和评估,调整模型超参数以提高准确率。 7. 系统集成:将训练好的模型集成到MATLAB应用程序中,实现水果识别功能。 8. 用户界面设计:设计用户友好的界面,以便用户上传水果图像并查看识别结果。 MATLAB提供了丰富的图像处理工具箱和机器学习工具箱,可以帮助开发者快速构建水果识别系统。通过结合这些工具箱,可以实现水果的快速、准确识别。

    COMSOL声子晶体仿真研究:一维至三维能带与带隙分析及色散曲线弹性波声波分析,声子晶体仿真:COMSOL代做能带图、带隙图及弹性波、声波分析与优化设计,COMSOL代做 声子晶体仿真,一维,二维,三

    COMSOL声子晶体仿真研究:一维至三维能带与带隙分析及色散曲线弹性波声波分析,声子晶体仿真:COMSOL代做能带图、带隙图及弹性波、声波分析与优化设计,COMSOL代做 声子晶体仿真,一维,二维,三维能带图,带隙图,色散曲线,弹性波,声波。 ,COMSOL代做;声子晶体仿真;一维/二维/三维能带图;带隙图;色散曲线;弹性波仿真;声波分析,COMSOL声子晶体仿真专家:一至三维声波模拟及能带图绘制

    Matlab Simulink仿真探究Flyback反激式开关电源性能表现与优化策略,Matlab Simulink仿真探究Flyback反激式开关电源的工作机制,Matlab Simulimk仿真

    Matlab Simulink仿真探究Flyback反激式开关电源性能表现与优化策略,Matlab Simulink仿真探究Flyback反激式开关电源的工作机制,Matlab Simulimk仿真,Flyback反激式开关电源仿真 ,Matlab; Simulink仿真; Flyback反激式; 开关电源仿真,Matlab Simulink在Flyback反激式开关电源仿真中的应用

    陪读租房系统(源码+数据库+论文+ppt)java开发springboot框架javaweb,可做计算机毕业设计或课程设计

    陪读租房系统(源码+数据库+论文+ppt)java开发springboot框架javaweb,可做计算机毕业设计或课程设计 【功能需求】 本系统有三个角色:管理员、租客和房主,要求具备以下功能: (a) 管理员;管理员使用本系统涉到的功能主要有:首页、个人中心、租客管理、房主管理、房源信息管理、房源类型管理、教育书籍管理、文章分类管理、租房信息管理、合同信息管理、在线咨询管理、咨阅回复管理、教育论坛、系统管理等功能。 (b) 租客;进入前台系统可以实现首页、房源信息、教育书籍、教育论坛、公告信息、后台管理等功能进行操作。 (C) 房主;进入系统可以实现首页、个人中心、房源信息管理、租房信息管理、合同信息管理、在线咨询管理、咨询回复管理等功能进行操作。 【环境需要】 1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境:IDEA,Eclipse,Myeclipse都可以。 3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可 4.数据库:MySql 5.7/8.0等版本均可; 【购买须知】 本源码项目经过严格的调试,项目已确保无误,可直接用于课程实训或毕业设计提交。里面都有配套的运行环境软件,讲解视频,部署视频教程,一应俱全,可以自己按照教程导入运行。附有论文参考,使学习者能够快速掌握系统设计和实现的核心技术。

    vue3的一些语法以及知识点

    vue3的一些语法以及知识点

    libicu-doc-50.2-4.el7-7.x64-86.rpm.tar.gz

    1、文件内容:libicu-doc-50.2-4.el7_7.rpm以及相关依赖 2、文件形式:tar.gz压缩包 3、安装指令: #Step1、解压 tar -zxvf /mnt/data/output/libicu-doc-50.2-4.el7_7.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm 4、更多资源/技术支持:公众号禅静编程坊

    水果销售商城(源码+数据库+论文+ppt)java开发springboot框架javaweb,可做计算机毕业设计或课程设计

    水果销售商城(源码+数据库+论文+ppt)java开发springboot框架javaweb,可做计算机毕业设计或课程设计 【功能需求】 水果购物网站用户可以注册登录,在首页开通会员卡,查看水果,购买水果,查看水果信息,以及个人中心修改个人资料,在自己的后台查看自己的购买记录等。 水果购物网站管理员功能:个人中心管理,用户管理,会员管理,会员卡管理,开通会员记录管理,积分管理,水果管理,购买水果订单管理,积分兑换管理,积分兑换记录管理,加积分记录管理,减积分记录管理。 【环境需要】 1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境:IDEA,Eclipse,Myeclipse都可以。 3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可 4.数据库:MySql 5.7/8.0等版本均可; 【购买须知】 本源码项目经过严格的调试,项目已确保无误,可直接用于课程实训或毕业设计提交。里面都有配套的运行环境软件,讲解视频,部署视频教程,一应俱全,可以自己按照教程导入运行。附有论文参考,使学习者能够快速掌握系统设计和实现的核心技术。

    基于Matlab的双输入深度学习模型构建指南:处理序列与图像数据的创新性应用,Matlab双输入深度学习模型搭建指南:如何处理两种输入数据并实现创新与优势,Matlab搭建双输入深度学习模型,双输入网

    基于Matlab的双输入深度学习模型构建指南:处理序列与图像数据的创新性应用,Matlab双输入深度学习模型搭建指南:如何处理两种输入数据并实现创新与优势,Matlab搭建双输入深度学习模型,双输入网络。 相比普通的单输入网络,双输入网络能处理两种输入数据,在科研上也更具有优势和创新性。 如何用Matlab搭建双输入网络也是困扰本人很长时间的一个问题,现已弄明白。 注意,需要Matlab 2022b及以上版本,以下版本估计是都不行。 本程序是两个输入全为一维序列的情况(第二个输入序列是第一个输入序列的特征值,或者变后的序列)。 也可改为两边输入都是图像,或者一边输入图像,一边输入图像的一维特征序列。 本程序工作如下: 1、加载数据,两种输入数据一一对应,第二个数据是第一个数据做FFT之后的序列,属于一个类别。 两种数据样本数相等,序列长度不相等。 2、搭建双输入网络,此网络一边是CNN-LSTM,一边是CNN。 3、训练。 4、测试,输出准确率。 注:程序可直接运行,包教会和调通。 可以有偿修改为两边输入都是图像,或一边输入图像一边输入序列的模型。 可有偿替数据,调通程序。 程序注释详

    十大管理的49个过程组强化记忆

    包含十大管理49个过程组的输入与输出和解释,还有EVA铮值管理的公式汇总和解释

Global site tag (gtag.js) - Google Analytics