《python核心编程》读书笔记 第二章 网络编程

一、服务器/客户端的概念

想象客户端/服务器架构如何工作的一个方法就是,在你的脑海中创建一个画面,那就是一个银行出纳员,他既不吃不睡,也不休息,服务一个又一个的排队客户,似乎永远不会结束。这个队列可能很长,也可能空无一人,但在任何给定的某个时刻,都可能会出现一个客户。当然,在几年前这样的出纳员完全是一种幻想,但是现在的自动取款机(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()

注:第五六节的注释是直接复制原文的,因为我也没看太懂,用到的时候再慢慢看吧

猜你喜欢

转载自blog.csdn.net/weixin_39655021/article/details/85344048
今日推荐