服务器与客户端之间通信模式:
我觉得这个举例是很恰当的。将服务器->客服总线,客户端->客户,新的客户端->客服代表。
客服总线比如说400-xxxxxx这类的电话,一直处于等待状态,当有新的客户来电之后,总线接线员接到电话后,将客户的电话切换给客服代表进行处理。这样空出主线,以便总线接线员可以继续等待新的客户电话。而此时之前接入的客户及对应的客服代表,能够进行他们自己独立的谈话。当有新的客户B进来之后,总线接线员会创建一个新的客服代表进行处理,总线接线员继续进行等待。
接下来我们直接开始撸
1.TCP连接
TCP服务器端:
from socket import * from time import ctime HOST = '' # 对bind方法的标识,标识可以使用任何可用的地址 PORT = 21567 # 随机设置的端口号 BUFSIZE = 1024 # 缓冲区大小1KB ADDR = (HOST, PORT) tcpSerSock = socket(AF_INET, SOCK_STREAM) # 分配TCP服务器套接字 tcpSerSock.bind(ADDR) # 将套接字绑定到服务器地址 tcpSerSock.listen(5) # 开启TCP监听器 while True: print 'waiting fo connection...' # 无限循环中,等待客户端的连接 tcpCliSock, addr = tcpSerSock.accept() print '...connected from:', addr while True: data = tcpCliSock.recv(BUFSIZE) # 接收客户端数据 if not data: # 如果接收的消息是空白数据,这以为着客户端已经退出,跳出循环,关闭当前的客户端连接,继续等待另一个客户端连接 break tcpCliSock.send('[%s] %s' % (ctime(), data)) # 接受到客户端消息不为空,则将其格式化并返回相同的数据,加上当前的时间戳前缀。 tcpCliSock.close() tcpSerSock.close() # 永远不会执行,只是提醒大家,可以用这种方式,关闭服务器套接字,退出服务
TCP客户端:
# coding:utf-8 from socket import * HOST = 'localhost' # 本地连接通信,如果要其余机器连接,可改成服务器端IP PORT = 21567 # 随机设置的端口号 BUFSIZE = 1024 # 缓冲区大小1KB ADDR = (HOST, PORT) tcpCliSock = socket(AF_INET, SOCK_STREAM) # 分配TCP客户端套接字,主动调用并连接到服务器 tcpCliSock.connect(ADDR) while True: data = raw_input('> ') # 用户输入发送数据 if not data: break tcpCliSock.send(data) # 发送数据 data = tcpCliSock.recv(BUFSIZE) # 接收服务器端返回数据 if not data: break print data # 打印服务器端加了时间戳之后的返回数据,显示 tcpCliSock.close()
如果需要IPv6地址:需要将HOST改为 HOST = '::1' ,同时请求套接字的AF_INET6家族。
接着为了看他们的工作,先运行服务器端程序,然后启动客户端程序:
客户端显示如下:
服务器端显示如下:
waiting fo connection...
...connected from: ('127.0.0.1', 57551)
waiting fo connection...
通过以上的例子,给我们展示了数据如何从客户端到达服务器,并最后返回到客户端。这里服务器就是作为了一个“时间服务器”,获取服务器端的时间。
2.UDP连接
UDP连接与TCP连接通信的一个显著差异就是它不是面向连接的,无连接,无需监听传入的连接。这类服务器仅仅接受消息并有可能回复数据。只有创建套接字并将其绑定到地址中,然后无限循环接受客户端消息,处理,返回消息。
UDP服务器端:
# coding:utf-8 from socket import * from time import ctime HOST = '' # 对bind方法的标识,标识可以使用任何可用的地址 PORT = 21567 # 随机设置的端口号 BUFSIZE = 1024 # 缓冲区大小1KB ADDR = (HOST, PORT) udpSerSock = socket(AF_INET, SOCK_DGRAM) # 分配TCP服务器套接字 udpSerSock.bind(ADDR) # 将套接字绑定到服务器地址 while True: print 'waiting for message...' data, addr = udpSerSock.recvfrom(BUFSIZE) # 接收客户端数据 udpSerSock.sendto('[%s] %s' % (ctime(), data), addr) # 格式化客户端数据,并返回给客户端 print '... received from and returned tto:', addr udpSerSock.close() # 永远不会执行,只是提醒大家,可以用这种方式,关闭服务器套接字,退出服务
UDP客户端:
# coding:utf-8 from socket import * HOST = 'localhost' # 对bind方法的标识,标识可以使用任何可用的地址,使用本地连接localhost PORT = 21567 # 随机设置的端口号 BUFSIZE = 1024 # 缓冲区大小1KB ADDR = (HOST, PORT) udpSockCli = socket(AF_INET, SOCK_DGRAM) # 分配udp客户端套接字,主动调用并连接到服务器 while True: data = raw_input('> ') # 输入数据 if not data: # 如果输入数据为空,默认退出连接 break udpSockCli.sendto(data, ADDR) # 客户端发送输入的数据到服务器 data, addr = udpSockCli.recvfrom(BUFSIZE) # 客户端接收从服务器返回的数据 if not data: # 如果从服务器接收到的数据为空,默认断开连接 break print data udpSockCli.close() # 永远不会执行,只是提醒大家,可以用这种方式,关闭服务器套接字,退出服务
客户端显示:
服务器端显示:
在使用TCP连接的时候,我们必须先跑服务器端的程序,然后再起客户端的程序。但是在用UDP连接的时候,就不必管这样的启动顺序,可以先启动客户端程序,然后再启动服务器端程序进行通信。
3.SocketServer模块
服务器端:
# coding:utf-8 from SocketServer import (TCPServer as TCP, StreamRequestHandler as SRH) from time import ctime HOST = '' PORT = 21567 ADDR = (HOST, PORT) class MyRequestHandle(SRH): # 创建基于StreamRequestHandler的子类. def handle(self): # 重写Handle方法 print '...connected from:', self.client_address ''' StreamRequestHandler类将输入和输出套接字看作类似文件的对象.因此 通过write发送字符串给到客户端.readline获取客户端消息.因为采用的是 类似文件的处理方式,所以需要额外的回车和换行符,在客户端进行处理. ''' self.wfile.write('[%s] %s' %(ctime(), self.rfile.readline())) tcpSSSer = TCP(ADDR, MyRequestHandle) # 建立连接服务 print 'waiting for connection ...' tcpSSSer.serve_forever() # 无限循环等待,服务客户端请求
客户端:
# coding:utf-8 from socket import * HOST = 'localhost' PORT = 21567 BUFSIZE = 1024 ADDR = (HOST, PORT) while True: tcpSSCliSock = socket(AF_INET, SOCK_STREAM) tcpSSCliSock.connect(ADDR) data = raw_input('> ') if not data: break ''' 因为采用的是类似文件的处理方式,所以需要额外的回车和换行符, 在客户端进行处理. ''' tcpSSCliSock.send('%s\r\n' % data) data = tcpSSCliSock.recv(BUFSIZE) if not data: break print data.strip() tcpSSCliSock.close()
SocketServer请求处理程序的默认行为是接受连接,获取请求,然后关闭连接。由于这个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。
4.Twisted框架
服务器端:
# coding:utf-8 from twisted.internet import protocol, reactor from time import ctime PORT = 21567 # 定义连接端口 class TsSerProtocol(protocol.Protocol): # 定义基于protocol的子类 def connectionMade(self): # 重写connectionMade方法,客户端连接服务器默认调用该方法. clnt = self.clnt = self.transport.getPeer().host # 获取主机信息 print '...connected from:', clnt def dataReceived(self, data): # 重写dataReceived方法,客户端通过网络发送数据时默认调用该方法. self.transport.write('[%s] %s ' % (ctime(), data)) # 时间戳+发送的数据作为返回数据. factory = protocol.Factory() # 协议工厂,每个连接接入,制造一个协议实例 factory.protocol = TsSerProtocol print 'waiting for connection...' ''' 异步编程:这样的异步模式称为Reactor模式 1.监听事件 2.事件发生执行对应的回调函数 3.回调完成(可能产生新的事件添加进监听队列) 4.回到1,监听事件 ''' reactor.listenTCP(PORT, factory) # reactor 安装TCP监听器,检查服务请求,接收到请求后就创建一个TsSerProtocol实例来处理客户端事物 reactor.run() # 运行事件管理器
reactor是事件管理器,用于注册、注销事件,运行事件循环,当事件发生时调用回调函数处理。关于reactor有下面几个结论:
- Twisted的reactor只有通过调用reactor.run()来启动。
- reactor循环是在其开始的进程中运行,也就是运行在主进程中。
- 一旦启动,就会一直运行下去。reactor就会在程序的控制下(或者具体在一个启动它的线程的控制下)。
- reactor循环并不会消耗任何CPU的资源。
- 并不需要显式的创建reactor,只需要引入就OK了。
最后一条需要解释清楚。在Twisted中,reactor是Singleton(也就是单例模式),即在一个程序中只能有一个reactor,并且只要你引入它就相应地创建一个。上面引入的方式这是twisted默认使用的方法。
客户端:
# coding:utf-8 from twisted.internet import protocol, reactor import random from time import * HOST = 'localhost' # 定义本地127.0.0.1连接 PORT = 21567 # 定义默认端口 class TsCliProtocol(protocol.Protocol): # 定义基于protocol的子类 def sendData(self): # 添加默认的发送数据方法. # data = raw_input('> ') data = '123123' + str(random.randint(0, 1000)) # 为观察异步通信,而选择一直传输随机字符串 sleep(3) if data: print '...sending %s ...' % data self.transport.write(data) # 发送数据给到服务器 else: self.transport.loseConnection() # 断开客户端与服务器连接,关闭套接字,调用工厂函数clientConnectionLost(),停止reactor事物监听器 def connectionMade(self): # 建立连接 self.sendData() def dataReceived(self, data): # 接收到返回数据回调函数 print data self.sendData() class TsCliFactory(protocol.ClientFactory): # 客户端工厂类,继承于protocol.ClientFactory protocol = TsCliProtocol clientConnectionLost = clientConnectionFailed = lambda self, connector, reason: reactor.stop() reactor.connectTCP(HOST, PORT, TsCliFactory()) # 创建服务器TCP连接 reactor.run() # 运行事件管理器
运行上面的程序,一个服务器,两个客户端,我们可以看下如下情景:
服务器端:
客户端:
通过上面的几个尝试,我们不难发现单线程的通信是很容易实现的,但是在实际工作中使用呢?
可能存在以下一些问题,同时我们将在下一篇中进行探索:
1.异步通信,也是在不断的轮询排队处理中,如果采用服务器端多线程处理呢?
2.多线程与异步操作的异同
3.如果A - 服务器 -B该如何实现?