这是一个基本的实现。 (我不能保证它的生产质量!)将它保存到一个文件并执行类似这样的操作,每个都在不同的终端窗口中:
> python myscript.py 10001 10002 10003
> python myscript.py 10002 10003 10001
> python myscript.py 10003 10001 10002
第一个参数是监听端口,其余参数是其他服务器的端口。
import argparse
import logging
import os
import random
import socket
import struct
from tornado import gen
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, StreamClosedError
from tornado.tcpclient import TCPClient
from tornado.tcpserver import TCPServer
from tornado.options import options as tornado_options
parser = argparse.ArgumentParser()
parser.add_argument("port", type=int, help="port to listen on")
parser.add_argument("peers", type=int, nargs="+", help="peers' ports")
opts = parser.parse_args()
# This is just to configure Tornado logging.
tornado_options.parse_command_line()
logger = logging.getLogger(os.path.basename(__file__))
logger.setLevel(logging.INFO)
# Cache this struct definition; important optimization.
int_struct = struct.Struct("<i")
_UNPACK_INT = int_struct.unpack
_PACK_INT = int_struct.pack
tcp_client = TCPClient()
@gen.coroutine
def client(port):
while True:
try:
stream = yield tcp_client.connect('localhost', port)
logging.info("Connected to %d", port)
# Set TCP_NODELAY / disable Nagle's Algorithm.
stream.set_nodelay(True)
while True:
msg = ("Hello from port %d" % opts.port).encode()
length = _PACK_INT(len(msg))
yield stream.write(length + msg)
yield gen.sleep(random.random() * 10)
except StreamClosedError as exc:
logger.error("Error connecting to %d: %s", port, exc)
yield gen.sleep(5)
loop = IOLoop.current()
for peer in opts.peers:
loop.spawn_callback(client, peer)
class MyServer(TCPServer):
@gen.coroutine
def handle_stream(self, stream, address):
logging.info("Connection from peer")
try:
while True:
# Read 4 bytes.
header = yield stream.read_bytes(4)
# Convert from network order to int.
length = _UNPACK_INT(header)[0]
msg = yield stream.read_bytes(length)
logger.info('"%s"' % msg.decode())
del msg # Dereference msg in case it's big.
except StreamClosedError:
logger.error("%s disconnected", address)
server = MyServer()
server.listen(opts.port)
loop.start()
请注意,我们不调用 read_until_close,所以我们需要一些方法来知道消息何时被完全接收。我在每条消息的开头使用一个 32 位整数来执行此操作,该整数对消息其余部分的长度进行编码。
您问,“当客户端写入服务器时,它会以某种方式中断 Tornado IOLoop 以调用 read()?”这就是 Tornado 的 IOLoop 的用途,也就是我们所说的“异步”:许多 Tornado 协程或回调可以等待网络事件,当它们等待的事件发生时,IOLoop 会唤醒它们。这就是您在上面的代码中看到“yield”时发生的情况。