一、服务器/客户端的概念
想象客户端/服务器架构如何工作的一个方法就是,在你的脑海中创建一个画面,那就是一个银行出纳员,他既不吃不睡,也不休息,服务一个又一个的排队客户,似乎永远不会结束。这个队列可能很长,也可能空无一人,但在任何给定的某个时刻,都可能会出现一个客户。当然,在几年前这样的出纳员完全是一种幻想,但是现在的自动取款机(ATM)似乎比较接近这种模型。
出纳员就是一个运行在无限循环中的服务器,每个客户就是一个客户端。
二、服务器/客户端网络编程
在服务器响应客户端请求之前,必须进行一些初步的设置来为之后的工作做准备,首先会创建一个通信端点,它能够使服务器监听请求——这个服务端点可以类比于电话号码和设备安装成功而且接线员到达,此时监听服务器可以进入无限循环中,等待客户端的连接并响应它们的请求。
客户端要做的事情更简单,就是创建它的单一通信端点,然后建立一个到服务器的连接,发出请求,和服务器进行通信。
三、套接字(socket)
套接字就体现了上文的“通信端点”,类似于打电话时的电话插口。服务器就像一个大插排,包含很多插座,客户端就是像一个插头,每一个线程代表一条电线,客户端将电线的插头插到服务器插排上对应的插座上,就可以开始通信了。它是一种机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。
为了确定一个套接字,我们需要知道通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。IP地址和端口号确定的是通信的对象,而传输层协议分为两种。一种是面向连接的套接字(传输控制协议,简称TCP,使用SOCK_STREAM作为套接字类型),在进行TCP通信之前必须建立一个连接;一种是无连接的套接字(用户数据报协议,简称UDP,使用SOCK_DGRAM作为套接字类型),这意味着通信开始之前不需要建立连接,可能存在重复和遗漏问题,但是成本低廉。由于这些套接字都是用互联网协议(IP)来寻找网络中的主机,因此又分别被称为TCP/IP和UDP/IP协议。
四、python中的网络编程
这里主要使用socket模块,在这个模块中可以找到socket函数,该函数主要用于创建套接字对象,套接字也有自己的方法集,这些方法可以实现基于套接字的网络通信。
(1)TCP
创建TCP服务器的伪代码:
python3代码如下:
from socket import *
from time import ctime
Host='' #对bind方法的标识,表示它可以使用任何可用的地址
Port=21567 #可以使用0~65535中间任何一个未被占用的端口号
Bufsize=1024 #缓冲区
Addr=(Host,Port) #在连接被转接或拒绝之前,传入连接请求的最大数。
tcpSersock=socket(AF_INET,SOCK_STREAM) #tcp套接字。如果要走ipv6仅仅需要将地址家族中的AF_INET(IPv4)修改成AF_INET6(IPv6)
tcpSersock.bind(Addr) #将套接字绑定到服务器
tcpSersock.listen(5) #开启监听
while True:
print('waiting for connection')
tcpclisock,addr=tcpSersock.accept() #accept的实现是堵塞的,也就是说除非收到数据,不然它会一直停在这里
print('--- connect from:',addr)
while True: #如果消息是空白的,这意味着客户端已经退出,所以此时我们将跳出对话循环,关闭当前客户端连接,然后等待另一个客户端连接。
#如果确实得到了客户端发送的消息,就将其格式化并返回相同的数据,但是会在这些数据中加上当前时间戳的前缀。
data=tcpclisock.recv(Bufsize)
if not data:
break
tcpclisock.send(b'[%s] %s'%(bytes(ctime(),'utf-8'),data))
tcpclisock.close()
tcpSersock.close() #最后一行永远不会执行,它只是用来提醒应当考虑一个更加优雅的退出方式,比如给代码加一个异常处理
创建客户端的伪代码:
python3代码:
from socket import *
Host='127.0.0.1' #也可以使用"localhost"这个字符串,如果你的服务器运行在另一台主机上,那么需要进行相应修改)
#端口号PORT 应该与你为服务器设置的完全相同(否则,将无法进行通信)。
#若使用ipv6则本机地址变为"::1"
Port=21567
Bufsize=1024
Addr=(Host,Port)
tcpclisock=socket(AF_INET,SOCK_STREAM)
tcpclisock.connect(Addr)
while True:
data=input('> ')
if not data: #若用户没有输入
break
tcpclisock.send(data.encode())
data=tcpclisock.recv(Bufsize) #或服务器终止且对recv的调用失败,则跳出,否则,客户端接收到加了时间戳的字符串,并显示在屏幕上
if not data:
break
print(data.decode('utf-8'))
tcpclisock.close()
下面我们来看运行结果:
客户端
服务器:
服务器这里记录的是连接而非会话,比如如果我们用空输入结束客户端,再重新开始,那么
客户端:
服务器:
这里把连接情况打印出来只是为了说明TCP协议中有连接这一步,把会话信息也打印出来是可以的,但没必要。
(2)UDP
UDP 服务器不需要TCP 服务器那么多的设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作。伪代码:
除了普通的创建套接字并将其绑定到本地地址(主机名/端口号对)外,并没有额外的工作。无限循环包含接收客户端消息、打上时间戳并返回消息,然后回到等待另一条消息的状态。再一次,close()调用是可选的,并且由于无限循环的缘故,它并不会被调用,但它提醒我们,它应该是我们已经提及的优雅或智能退出方案的一部分。
UDP 和TCP 服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”的操作。这些服务器仅仅接受消息并有可能回复数据。
from socket import *
from time import ctime
Host=''
Port=21567
Bufsize=1024
Addr=(Host,Port)
udpsersock=socket(AF_INET,SOCK_DGRAM)
udpsersock.bind(Addr) #因为UDP 是无连接的,所以这里没有调用“监听传入的连接”。
while True:
print('waiting for message ') #被动地等待信息并处理
data,addr=udpsersock.recvfrom(Bufsize)
print('from:',addr)
udpsersock.sendto(b'[%s] %s'%(bytes(ctime(),'utf-8'),data),addr)
udpsersock.close()
客户端:
UDP 客户端循环工作方式几乎和TCP 客户端完全一样。唯一的区别是,事先不需要建立与UDP 服务器的连接,只是简单地发送一条消息并等待服务器的回复。在时间戳字符串返回后,将其显示到屏幕上,然后等待更多的消息。最后,当输入结束时,跳出循环并关闭套接字。
from socket import *
Host='127.0.0.1'
Port=21567
Bufsize=1024
Addr=(Host,Port)
udpserSock=socket(AF_INET,SOCK_DGRAM)
while True:
data=input('> ')
if not data:
break
udpserSock.sendto(data.encode(),Addr)
data,addr=udpserSock.recvfrom(Bufsize)
if not data:
break
print(data.decode())
代码本身没什么好说的,运行结果如下。
客户端:
服务器:
这里我们就只能waiting for message了,因为没有connection给我们waiting。
另一个问题是,在运行代码时尝试了开一个服务器——多个客户端这样的情况,对于UDP来说是没有任何问题的。
但是对于TCP协议来说,后一个打开的客户端并不能建立连接。除非前一个连接已经断开。考虑我们的代码是一个简单的单进程连接,这更说明了TCP和UDP的不同之处。
五、SocketServer模块。
SocketServer 是标准库中的一个高级模块(Python 3.x 中重命名为socketserver),它的目标是简化很多样板代码,它们是创建网络客户端和服务器所必需的代码。除了为你隐藏了实现细节之外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,这意味着只有在系统中的事件发生时,它们才会工作。事件包括消息的发送和接收。事实上,你会看到类定义只包括一个用来接收客户端消息的事件处理程序。所有其他的功能都来自使用的SocketServer 类。
from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime
Host=''
Port=21567
Addr=(Host,Port)
class MyRequesHandler(SRH):
def handle(self): #我们得到了请求处理程序MyRequestHandler,作为SocketServer中StreamRequestHandler 的一个子类,并重写了它的handle()方法,该方法在基类Request 中默认情况下没有任何行为。
#当接收到一个来自客户端的消息时,它就会调用handle()方法。而StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送回客户端
print('... connected from:',self.client_address)
self.wfile.write(('[%s] %s'%(ctime(),self.rfile.readline().decode())).encode())
tcpServ=TCP(Addr,MyRequesHandler)
print('waiting for connect')
tcpServ.serve_forever()
from socket import *
Host='127.0.0.1'
Port=21567
Bufsize=1024
Addr=(Host,Port)
while True:
tcpclisock=socket(AF_INET,SOCK_STREAM) #SocketServer 请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。这种行为使得TCP 服务器更像是一个UDP 服务器
tcpclisock.connect(Addr)
data=input('> ')
if not data:
break
tcpclisock.send(('%s \r\n'%data).encode())
data=tcpclisock.recv(Bufsize)
if not data:
break
print(data.decode())
tcpclisock.close()
六、Twisted 框架介绍
与 SocketServer 类似,Twisted 的大部分功能都存在于它的类中。特别是对于该示例,我们将使用Twisted 因特网组件中的reactor 和protocol 子包中的类。
安装Twisted框架稍微费了一点事,主要是安装版本不对了,直接在cmd里面用pip是会安装失败的,需要手动下载执行,只要下载whl文件的时候注意对上python版本和系统位数即可。cpxx对应python版本号,amd64对应64位的python(注意是64位的python,不是64位的电脑~~)下载地址在这里https://www.lfd.uci.edu/~gohlke/pythonlibs/
import os
from twisted.internet import protocol, reactor
from time import ctime
PORT = 21568
class TSServProtocol(protocol.Protocol):
#我们获得protocol 类并为时间戳服务器调用TSServProtocol。然后重写了connectionMade()和dataReceived()方法,当一个客户端连接到服务器时就会执行connectionMade()方法,而当服务器接收到客户端通过网络发送的一些数据时就会调用dataReceived()方法。reactor 会作为该方法的一个参数在数据中传输,这样就能在无须自己提取它的情况下访问它。
##此外,传输实例对象解决了如何与客户端通信的问题。你可以看到我们如何在connectionMade()中使用它来获取主机信息,这些是关于与我们进行连接的客户端的信息,以及如何在dataReceived()中将数据返回给客户端。
def connectionMade(self):
clnt = self.clnt = self.transport.getPeer().host
print('...connected from:', clnt)
def dataReceived(self, data):
self.transport.write(bytes('%s current time is : [%s] %s' %(os.listdir
(),ctime(), data.decode("utf-8")),"utf-8"))
#在服务器代码的最后部分中,创建了一个协议工厂。它之所以被称为工厂,是因为每次得到一个接入连接时,都能“制造”协议的一个实例。然后在reactor 中安装一个TCP 监听器,以此检查服务请求。当它接收到一个请求时,就会创建一个TSServProtocol 实例来处理那个客户端的事务。
factory = protocol.Factory()
factory.protocol = TSServProtocol
print('waiting for connection...')
reactor.listenTCP(PORT, factory)
reactor.run()
from twisted.internet import protocol, reactor
HOST = '127.0.0.1'
PORT = 21568
#类似于服务器,我们通过重写connectionMade()和dataReceived()方法来扩展Protocol,并且这两者都会以与服务器相同的原因来执行。另外,还添加了自己的方法sendData(),当需要发送数据时就会调用它。
#以上行为会在一个循环中继续,直到当提示输入时我们不输入任何内容来关闭连接。此时,并非调用传输对象的write()方法发送另一个消息到服务器,而是执行loseConnection()来关闭套接字。当发生这种情况时,将调用工厂的clientConnectionLost()方法以及停止reactor,结束脚本执行。此外,如果因为某些其他的原因而导致系统调用了clientConnectionFailed(),那么也会停止reactor。
class TSClntProtocol(protocol.Protocol):
def sendData(self):
data = (input('> ')).encode("utf-8")
if data:
print("...sending %s ..."%data.decode("utf-8"))
self.transport.write(data)
else:
self.transport.loseConnection()
def connectionMade(self):
self.sendData()
def dataReceived(self, data):
print(data.decode("utf-8"))
self.sendData()
class TSClntFactory(protocol.ClientFactory):
protocol = TSClntProtocol
clientConnectionLost = clientConnectionFailed = \
lambda self, connector, reason: reactor.stop()
#在脚本的最后部分创建了一个客户端工厂,创建了一个到服务器的连接并运行reactor。
#注意,这里实例化了客户端工厂,而不是将其传递给reactor,正如我们在服务器上所做的那样。这是因为我们不是服务器,需要等待客户端与我们通信,并且它的工厂为每一次连接都创建一个新的协议对象。因为我们是一个客户端,所以创建单个连接到服务器的协议对象,而服务器的工厂则创建一个来与我们通信。
reactor.connectTCP(HOST, PORT, TSClntFactory())
reactor.run()
注:第五六节的注释是直接复制原文的,因为我也没看太懂,用到的时候再慢慢看吧