1. 客户端服务器结构
客户端服务器结构是最常见也是最基础的网络通讯结构,无论是http网站,网游,基本都是使用这个结构,它的特点是一个服务器主机可以同时和多个客户端主机交互,为客户端提供统一的功能.
TCP协议需要区分客户端和服务器,所谓服务器是要先监听一个端口等待客户端访问,如果有客户端访问了就会根据请求做出相应的响应;而客户端则是指定一个服务器的地址和端口然后向其发送请求等待响应.从这个交互模式上来说就有个先后顺序.这也是TCP的弊端之一.但总体来说TCP是利大于弊的.
TCP协议的一般分为长连接和短链接,一般来说短链接就是:
建立连接->请求->响应->关闭连接
而长连接就是:
建立连接-> 请求->响应->请求->响应....->关闭连接
比如http协议就是短连接协议,而像mongodb,mysql这样的数据库协议一般都提供长连接协议和短链接协议两种.
短链接比较好控制,但每次都要建立连接就比较消耗性能;而长连接不用频繁建立连接可以更加高效,但管理维护起来也更加麻烦.
1.1. 短链接
我们定义一个这样的简单应用层协议:
- 使用短链接,且由客户端主动断开连接
- 传输的数据第一位为a,b,c,而后面则是一个数必须为一个数.
- 响应为第一位d,后面为传入数的平方,立方,4次方,对应a,b,c3个第一位的请求
1.1.1. 服务端协议实现
短链接的协议相对是比较简单的,无非是编码解码,根据匹配执行操作而已.短链接适合使用asyncio.Protocol
,因为没有对流的操作.
%%writefile short_server.py import asyncio class MyServerProtocol(asyncio.Protocol): HANDLERS = { "a":lambda x:x**2, "b":lambda x:x*x*x, "c":lambda x:x*x*x*x, "d":lambda x:x, } def connection_made(self, transport): peername = transport.get_extra_info('peername') print('Connection from {}'.format(peername)) self.transport = transport def _decoder(self,data): message = data.decode() result = self.HANDLERS.get(message[0])(float(message[1:])) return result def _encoder(self,query,num): message = query+str(num) return message.encode() def data_received(self, data): result =self._decoder(data) self.transport.write(self._encoder("d",result)) def connection_lost(self, exc): print('The client closed the connection') if __name__=="__main__": loop = asyncio.get_event_loop() # Each client connection will create a new protocol instance coro = loop.create_server(MyServerProtocol, '127.0.0.1', 5000) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed print('Serving on {}'.format(server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass # Close the server server.close()
Overwriting short_server.py
1.1.2. 客户端实现
短链接的客户端相对也比较好写,只是需要在初始化的时候先注册一个Future
对象用于等待,当获取到结果时再为这个Future
设置结果.
import asyncio from functools import partial class MyClientProtocol(asyncio.Protocol): HANDLERS = { "a":lambda x:x**2, "b":lambda x:x*x*x, "c":lambda x:x*x*x*x, "d":lambda x:x, } def __init__(self,loop=None): self.loop = loop or asyncio.get_event_loop() self.result = loop.create_future() def connection_made(self, transport): peername = transport.get_extra_info('peername') print('Connection to {}'.format(peername)) self.transport = transport def _decoder(self,data): message = data.decode() result = self.HANDLERS.get(message[0])(float(message[1:])) return result def _encoder(self,query,num): message = query+str(num) return message.encode() def data_received(self, data): result =self._decoder(data) self.result.set_result(result) self.transport.close() class Client: @staticmethod async def square(number,*,host= '127.0.0.1',port=5000,loop=None): prot_fac = partial(MyClientProtocol,loop=loop) tra, pro = await loop.create_connection(prot_fac,host=host,port=port) tra.write(pro._encoder("a",number)) return await pro.result @staticmethod async def cube(number,*,host= '127.0.0.1',port=5000,loop=None): prot_fac = partial(MyClientProtocol,loop=loop) tra, pro = await loop.create_connection(prot_fac,host=host,port=port) tra.write(pro._encoder("b",number)) return await pro.result @staticmethod async def four_square(number,*,host= '127.0.0.1',port=5000,loop=None): prot_fac = partial(MyClientProtocol,loop=loop) tra, pro = await loop.create_connection(prot_fac,host=host,port=port) tra.write(pro._encoder("c",number)) return await pro.result async def main(loop): print(await Client.square(2,loop=loop)) print(await Client.cube(2,loop=loop)) loop = asyncio.get_event_loop() loop.run_until_complete(main(loop))
Connection from ('127.0.0.1', 5000) 4.0 Connection from ('127.0.0.1', 5000) 8.0
上面的例子是最简单的一个原型实现,没有做任何异常处理的工作.
通常来说协议类比较适合写这种短链接的服务器和客户端,因为压根没用到tcp协议面向流的特性.
1.2. 长连接
长连接和适合用于需要维护用户状态的服务,也适合那种同一客户端频繁访问的服务.通常网络游戏就都是使用的长连接.
因为是处理流所以交互就需要知道流中哪一段是请求哪一段是响应,通常有两种方式:
使用帧
规定一段最小的字节流长度为一帧,比如说10个字符算一帧,而所有的操作都必须编码到这10个字符序列中.这种方式高效稳定但可读性较差.正规协议往往使用这种方式
使用标识符字节串
另一种方式是规定每条指令以某个特定的字节串结尾,比如都用
##END##
每次读取数据就读到这个标识符就把它作为一个指令.这种方式是相对可读性强些,但这样一来这种字节串本身就补能用于传输了.
接下来我们试着写一个最简单的网络游戏---猜拳.
游戏规则:
- 石头>剪刀,剪刀>布,布>石头,以此作为标准判断30s内都有动作的话的结果
协议内容:
- 使用长连接
规定一帧为1个字节,而所有操作按照如下编码方式来:
指令:
- 1代表剪刀
- 2代表石头
- 3代表布
结果:
- x代表平局
- y代表获胜
- z代表失败
1.2.1. 服务器实现
长连接就比较适合使用读写流来实现了.这边使用StreamReaderProtocol
作为父类做个示范
%%writefile long_server.py from functools import partial import asyncio import random class LongServer(asyncio.StreamReaderProtocol): def _calcul(self,data,remote): if data> remote: self._stream_writer.write(b"y") elif data==remote: self._stream_writer.write(b"x") else: self._stream_writer.write(b"z") async def hander(self): while True: print("wait") data = await self._stream_reader.read(1) data = int(data) print('recv: {}'.format(data)) remote = random.choice([1,2,3]) print("remot: {}".format(remote)) if remote == 3 and data == 1: self._stream_writer.write(b"y") else: self._calcul(data,remote) print('send done') def __init__(self,stream_reader, client_connected_cb=None,loop=None): self.task = None super().__init__(stream_reader, client_connected_cb=client_connected_cb,loop=loop) def connection_made(self, transport): super().connection_made(transport) self._stream_writer = asyncio.StreamWriter(transport, self, self._stream_reader, self._loop) self.task = asyncio.ensure_future(self.hander()) print("set task") def connection_lost(self, exc): self.task.cancle() self.task = None super().connection_lost(exc) if __name__=="__main__": loop = asyncio.get_event_loop() stream_reader = asyncio.StreamReader(loop=loop) # Each client connection will create a new protocol instance long_server = partial(LongServer,stream_reader =stream_reader,loop=loop) coro = loop.create_server(long_server, '127.0.0.1', 5000) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed print('Serving on {}'.format(server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass # Close the server server.close()
Overwriting long_server.py
1.2.2. 客户端协议实现
通常长连接的话我们就比较适合使用异步上下文管理器,这样就可以简单是实现退出功能了.
import asyncio class LongClient: HANDLERS = { b"x":"平局", b"y":"胜利", b"z":"失败" } def __init__(self,loop): self.loop=loop async def __aenter__(self): print('entering context') await self.connect() return self async def __aexit__(self, exc_type, exc, tb): print('exit context') self.close() async def connect(self): self.reader,self.writer = await asyncio.open_connection(host="127.0.0.1", port=5000,loop=self.loop) def close(self): self.writer.close() async def draw(self,msg): MSGS = {"剪刀":b"1", "石头":b"2", "布":b"3"} if msg not in MSGS.keys(): print("未知的指令") return False self.writer.write(MSGS.get(msg)) #await self.writer.drain() print("write done") result = await self.reader.read(1) print("read done") return self.HANDLERS.get(result)
async def main(loop): async with LongClient(loop) as client: print(await client.draw("剪刀")) print(await client.draw("剪刀")) print(await client.draw("剪刀")) print(await client.draw("剪刀")) print(await client.draw("剪刀")) loop = asyncio.get_event_loop() loop.run_until_complete(main(loop))
entering context write done read done 失败 write done read done 平局 write done read done 平局 write done read done 平局 write done read done 失败 exit context
和之前一样,这个例子只是一个原型,没有考虑任何异常情况,不过从这个之中也可以看出长连接和短连接的区别了.
上面两个简单的例子介绍了应答模式的异步编程方法.总的来说工具还是比较齐全的.但相比同步写法来说确实要麻烦许多.
还没有评论,来说两句吧...