且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Python自动化开发学习8

更新时间:2022-09-27 17:59:44

Socket 模块回顾

服务端的例子:

import socket
server = socket.socket()
server.bind(('localhost',11111))
server.listen()
print("监听已经开始")
count = 0
# 加个计数器,服务3次后停止服务
while count<3:
    # accept是等待连接请求,所以在没有客户端连接的时候,希望回到这里
    conn,addr = server.accept()
    print("发现连接请求:\n%s\n%s"%(conn,addr))
    # 持续接收数据,发回给客户端
    while True:
        data = conn.recv(1024)
        if not data: break  # 这样可以正常退出循环,没有这句客户端断开后会报错
        print("recv:",data.decode("utf-8"))
        conn.send("收到:".encode("utf-8") + data)
    print("断开与 %s 的连接,再次开始监听等待"%str(addr))
    count += 1
print("停止服务")
server.close()

一、定好协议

实例化一个socket
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
一般用IPv4的TCP协议,所以全部用默认参数就好了,也就是 socke.socket() 。前2个参数比较重要,其他选项如下。

Socket Families(地址簇):

  • socket.AF_UNIX unix本机进程间通信
  • socket.AF_INET IPV4 
  • socket.AF_INET6 IPV6

Socket Types:

  • socket.SOCK_STREAM #for tcp
  • socket.SOCK_DGRAM #for udp
  • socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
  • socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
  • socket.SOCK_SEQPACKET #废弃了

二、绑定套接字

socket.bind('localhost,9999')

三、开启监听

socket.listen()

四、接收连接请求

conn,addr = server.accept()

开始收发数据

上面4步后,连接已经建立,就可以收发数据了。
conn.recv(1024) :接收数据。参数是一次接收的字节数(Byte),这个数字不要超过8192(官方建议)。虽然可以更大,但是还是按照建议的来。按经验,一般都是设成2的N次方。
recv默认是阻塞的,如果没有发来数据,就会一直等着。如果客户端断开,就会收到空数据。可以用if not data: break 可以在客户端断开后退出循环
conn.send() :
conn.sendall() :

客户端

客户端的例子:

import socket
client = socket.socket()
client.connect(('localhost',11111))
msg = input(">>:")
# 把input的内容持续发送给服务器,如果发送空内容,就不发送直接跳出循环
while msg:
    client.send(msg.encode("utf-8"))  # 发送数据要转码成bytes类型
    data = client.recv(1024)  # 接收服务器端的回复
    print('recv:',data.decode("utf-8"))  # 打印出回复的内容
    msg = input(">>:")
else:
    input("准备断开连接,现在服务端还没断开\n"
          "回车后客户端close,服务端也同时close")
client.close()

客户端具体就3步:
一、 实例化:定义好协议,实例化socket,同服务端一样 client = socket.socket()
二、 连接:绑定套接字,请求连接服务器 client.connect(('localhost',11111))
三、 收发数据:与服务端通信,通服务端一样

通过socket实现简单的SSH

就是客户端通过socket将命令发送给服务端
服务端通过os模块或者subprocess模块执行命令,并且把执行结果(***还要有标准错误)再通过socket返回给客户端
例子课上讲的比较简单,上次笔记里有,不过并不包括错误的返回

Socket接收大数据

问题:接收数据的命令 conn.recv(1024) 必须要有参数,而且这个参数也不能无限大,前面已经讲过。如果一次要接收的数据大于这个参数,就需要多次接收。但是我们现在并没有方法判断几次才能接收完,并且如果进入了recv但是又没有新数据进来,这时候就会出现阻塞。所以多recv一次会阻塞,少recv一次则收不全数据,必须要通过已经接收到的数据来判断出是否接收完成。
解决办法:在发送数据前,先发送本次数据的大小。可以用len()来获取到数据的大小。这里课上踩了一个坑,用len()计算数据大小的时候一定要先传码变成bytes类型,然后再len()计算大小。因为非ASCII码字符计算字符长度是1,但是转成bytes类型的话长度可能是2或3(看你什么编码了)

str1 = "abcDEF"
print(len(str1))
print(len(str1.encode('utf-8')))  # 默认编码应该就是utf-8。这个参数可省略
str2 = '你好'
print(len(str2))
print(len(str2.encode('utf-8')))  # utf-8一个中文字占3个字节
print(len(str2.encode('gbk')))  # unicode一个中文字占2个字节

粘包

如果发送方连续发送这个两个包出去,接收方一次接收过来,把两个包当做了一个包处理了,就会出现问题。
粘包出现的原因

  1. 发送端需要等缓冲区满才发送出去,造成粘包:
    缓冲区满或者等待超时都会讲缓冲区的内容发送出去。如果发送了一个包后,再超时前有发了一个包,这是缓冲区里就是两个包,然后会一起发出去。
  2. 接收方不及时接收缓冲区的包,造成多个包接收:
    这个情况是一个包送达接收端的缓冲区,但是接收方没收,然后又进来一个包。然后接收端再进行接收,这是也是接收到了缓冲区里全部的数据,也就是两个包当做一个包一起收了

解决办法:还是从编程上来避免粘包的出现。就是一端发送了一个包后,不要再发包了,而是先等待接收。而另外一端在接收到包之后,必须给一个答复。一定要一收一发交替出现。不要连续send两个包,中间要插入一次等待回复,然后才能发第二个包。
详细讲:
一次发送和接收的数据有字节数的限制,对于大数据无法一次发送和接收完。所以发送方和接收发都得有一个循环把包完全发送和接收,并且包本身要带一个信息让接收方通过接收到的内容判断一个包已经接收完毕。多一次recv会进入阻塞,而少一次recv会导致包无法接收完全并且还有残留的数据在缓冲区里,导致下一次接收的数据也有错误。
发送数据可以偷个懒,使用sendall方法。但是接收数据只能用循环接收的方式,并且每次recv都要判断一下是否收完了。
判断一个包接收完毕的方法可以有很多,推荐的是包头先带上包大小的信息,也可以在包结尾加上特定的字符串标记。具体后面再讲,或者不讲了。
一个完整的包接收完毕,无论接收方是否需要回复,编程设计上都应该让接收方回复一个包。回包表示接收方和发送方的缓冲区此时都是空的了,所以就避免粘包的情况。至于插入一个回包而产生的等待,在极端场景下可能会有效率问题吧,虽然一般情况下可能也感受不到。
除了上面说的,还可以根据包头的数据大小的信息,让接收方一次只接收正好的字节数。如果有剩下的数据则留在缓冲区里下次再收,避免粘包。课上就是上面两种方法结合使用。应该方法也不止这些,不同的场景使用合适的方法,具体情况还要具体分析。

socketserver模块

socket无法支持多并发,所以前面的都只是铺垫,现在要再来学学SocketServer。
socketserver模块主要的作用是,简化网络服务器的开发。
创建一个SocketServer的步骤:

  1. 创建一个处理请求的类,继承SocketServer模块的BaseRequestHandler类,并且要重构父类的handle方法
  2. 实例化一个SocketServer的对象,并且传递服务器地址簇和上面创建的请求处理的类作为这个实例的前两个参数
  3. 开始监听
    3.1. server.handle_request() 只处理一次请求(不用这个)
    3.2. server.serve_forver() 处理多次请求,永远执执行着(就用这个)

简单的例子

服务端的例子:

import socketserver
# 第一步:创建一个处理请求的类
class MyTCPHandler(socketserver.BaseRequestHandler):
    # 第一步:重构handle方法
    def handle(self):
        # 每次收到一个连接请求。就会开始执行handle方法
        print("收到客户端连接请求:",self.client_address)
        # 接收数据,self.request是连接进来的套接字
        self.data = self.request.recv(1024)
        # 打印收到的数据
        print(self.data)
        # 把收到的内容转换成全大写然后发回去
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    # 第二步:实例化,第一个参数是TCP的地址簇,第二个参数是第一步建的类
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    # 第三部:开启监听
    # 如果是request方法,一个客户请求处理完毕后就会就会继续往后执行
    #server.handle_request()
    # forever方法,一个客户断开后,程序不会向下执行,而是继续等待下一个客户的请求
    # 执行到这里服务器就是一直执行的状态,直到你Ctrl-C中断程序
    server.serve_forever()
    # 所有的交互都是在handle方法里,所以之后就是执行handle中的程序
    print("运行结束")

和客户端所有的交互都是在handle方法里写的,上面例子中的handle方法比较简单。就是收一次数据,然后再回一条,此时handle执行结束。返回主函数的 server.serve_forever() 继续等待下一次请求。
客户端还是继续使用socket模块,所以之前的例子就好了:

import socket
def run():
    '''
    1 和服务器建立连接
    2 等待用户输入,如果输入为空就退出
    3 将用户输入的数据转成bytes发送
    4 接收回复的数据,然后打印出来
    5 返回第2步,继续让用户输入下一条信息
    '''
    client = socket.socket()
    client.connect(('localhost',9999))
    msg = input(">>:")
    while msg:
        client.send(msg.encode("utf-8"))
        print('发送:',msg.encode("utf-8"))
        data = client.recv(1024)
        print('recv:',data.decode("utf-8"))
        msg = input(">>:")
    else:
        print("主动断开连接...")
    client.close()

if __name__ == "__main__":
    run()

现在可以测试了,开启服务端和客户端,然后客户端输入消息发送。这里每次客户端只能发送一条消息,再要发送一条就会报错。因为此时服务端已经将连接中断了。这是因为服务端的handle中顺序执行完每一条语句后就会返回主函数的 server.serve_forever() 等待下一个连接请求。如果想要可以多次交互,需要在handle里写一个循环。
上面的例子中,一个客户端断开后,服务端还是继续监听的,别的客户端此时可以继续连接。这个就是 server.serve_forever() 的效果

自定义类中的其他方法

创建socketserver的第一步,创建的类中除了要重构handle方法外,还有两个方法也可以选择新的重构它们。
setup() :在 handle() 之前执行,可以用来执行所需的初始化操作。默认什么也不做。
finish() :在 handle() 之后执行,可以用来执行所需的任何清理操作。默认什么也不做。如果 setup() 中有抛出异常,该函数将不会被调用。
上面的两个方法都是可选的,其实代码写在handle方法里应该也一样,不过如果有初始化或者清理的操作,写到对应的方法里面,结构应该是更加的清晰。后面一起举例。

多次交互的例子

这里改写一下handle方法,加入一个while循环,实现多次交互,并且检测如果客户端断开就跳出循环返回主程序。顺便加上前面的setup和finish方法

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def setup(self):
        print("This is in setup")

    def handle(self):
        print("收到客户端连接请求:",self.client_address)
        while True:
            self.data = self.request.recv(1024)
            # 如果客户端断开连接,就退出循环,最后会返回主程序
            if not self.data: break
            print(self.data)
            self.request.sendall(self.data.upper())
        print("客户端断开了连接")

    def finish(self):
        print('This is in finish')

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    print("准备监听,等待用户接入")
    server.serve_forever()
    print("运行结束")

这里handle的代码和之前socket的服务的的代码是一样的。退出连接的检测也是一样的:如果客户的断开,这里recv获取的数据就会得到空,然后我们break。客户端如果吧断开,recv如果没有数据,只会继续阻塞在这一步,等待缓冲区进来数据。
到这里就和socket里基本一样了,差别只是在socketserver是修改handle方法。

多并发

上面已经测试过了,目前仍然还是不支持多并发,一次只能连接一个客户端。因为我们调用的方法不对。

多线程实现并发

要实现多并发只需要使用 socketserver.ThreadingTCPServer 替代 socketserver.TCPServer 就可以了。这个是通过多线程实现的,有关多线程以后会学。不过目前只需要知道socket的多并发是通过多线程来实现的就可以了。

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # 现在可以有多个客户端同时接入了,每次print都带上客户端连接信息
        print("收到客户端连接请求:",self.client_address)
        while True:
            self.data = self.request.recv(1024)
            if not self.data: break
            print(self.client_address,':',self.data)
            self.request.sendall(self.data.upper())
        print(self.client_address,':',"客户端断开了连接")

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    # 只要修改这里调用的方法就可以了
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
    print("准备监听,等待用户接入")
    server.serve_forever()
    print("运行结束")

客户端还是老样子,现在可以多开几个客户端同时接入试一下了。

多进程实现并发

除了用多线程方法外,也可以通过多进程的方法来实现多并发。同样是换个命令 socketserver.ForkingTCPServer 就好了,不过windownds上用不了。这个可以去源码里看一下,文件是哪个,可以用 __file__ 属性查到文件位置

import socketserver
print(socketserver.__file__)

现在知道了 \lib\socketserver.py ,源码就在这个位置。然后开头的注释后是一堆模块的导入。

import socket
import selectors
import os
import errno
import sys
try:
    import threading
except ImportError:
    import dummy_threading as threading
from io import BufferedIOBase
from time import monotonic as time

__all__ = ["BaseServer", "TCPServer", "UDPServer",
           "ThreadingUDPServer", "ThreadingTCPServer",
           "BaseRequestHandler", "StreamRequestHandler",
           "DatagramRequestHandler", "ThreadingMixIn"]
if hasattr(os, "fork"):
    __all__.extend(["ForkingUDPServer","ForkingTCPServer", "ForkingMixIn"])
if hasattr(socket, "AF_UNIX"):
    __all__.extend(["UnixStreamServer","UnixDatagramServer",
                    "ThreadingUnixStreamServer",
                    "ThreadingUnixDatagramServer"])

这里Forking的几个方法导入之前,先确认了当前系统是否有fork这个属性。试了一下确实没有。所以windows上用不了Foring的几个方法。但是多并发,之前用多线程的方法已经实现了,如果有别的系统可以用 hasattr(os,"fork") 看一下,Linux应该是支持的。

其他方法

既然已经打开了源码文件,可以看到里面定义了很多方法。比如TCPServer类中的:

    def server_close(self):
        """Called to clean-up the server.

        May be overridden.

        """
        self.socket.close()

还有的甚至什么都没有直接一句pass,在BaseServer类中有很多,比如:

    def server_activate(self):
        """Called by constructor to activate the server.

        May be overridden.

        """
        pass

这些方法都是在别的过程中可能被调用到的,比如上面的方法会在实例化的时候再构造函数中被调用。我们也可以运用之前学习的面向对象的方法来重构这些方法,实现一些别的功能或需求。

作业

高级FTP服务器开发:

  1. 用户加密认证
  2. 多用户同时登陆
  3. 每个用户有自己的家目录且只能访问自己的家目录
  4. 对用户进行磁盘配额、不同用户配额可不同
  5. 用户可以登陆server后,可切换目录
  6. 查看当前目录下文件
  7. 上传下载文件,保证文件一致性
  8. 传输过程中现实进度条
  9. 支持断点续传











本文转自骑士救兵51CTO博客,原文链接:http://blog.51cto.com/steed/2050273,如需转载请自行联系原作者