python(八)socket网络编程

目录

python(七)回顾
socket练习
SocketServer
开发一个支持多用户在线的FTP程序

一、python(七)回顾

1、面向对象

静态方法
  与类无关,不能访问类里的任何属性和方法,不用self
类方法
  只能访问类变量
属性方法@property
  把一个方法变成一个静态属性
  flight.status
  @status.setter
  flight.status = 3
  @status.delter
反射
  hasattr(obj,str)
  setattr(obj,str,val)
  getattr(obj,str)
  delattr(obj,str)
动态导入模块
  __import__(‘import_lib.metaclass’)
  importlib.import_module(‘import_lib.metaclass’) #官方建议用这个
__new__,早于__init__
类名()():执行__call__方法
__metaclass__ 用了定义类以怎样的形式被创建
异常处理:

try :
    ……
except (ValueError,KeyError) as e:
    print(e)
except Exception as e:
    ……
else:
    ……
finally:
    ……

raise:

断言:(断定)
assert type(“fgf”) is str # 断定是str类型,不是就报断言错误
print(“hehe”)

2、socket

所有的网络协议本质上都是对数据的收发

socket 对以下封装:
    tcp/ip  send,recv
    udp

    family address  #  地址簇
        AF.INET ipv4
        AF.INET6 
        AF.UNIX   # 本地 local 
    socket protocol type
    sock.SOCK_STREAM  # tcp/ip
    sock.SOCK_DGRAM   # 数据报式socket,for UDP

socket服务端

server = socket.socket(AF.INET,sock.SOCK_STREAM)
server.bind((ip,port))
server.listen()
while True:  # 不断接收新连接
    conn,addr = server.accept()  # 阻塞
    while True:  # 接收连接,多次通信
        print("new conn",addr)
        date = conn.recv(1024)  #官方建议最大8192
        conn.send(data.upper())
        # recv 默认是阻塞的
        if not data :  
            break  # 客户端一断开,conn.recv 收的的就都是空数据,就进入死循环了
# 只能同时服务一个连接
server.close()

socket客户端

client = socket.socket()
client.connect((serverip,port))
client.send(data)
[...  # 发多个]
client.recv(data)
client.close()

python3.x 发送只能是bytes类型

io缓冲区 buffer
缓冲区满了肯定会发
send(“”),强制触发

二、socket练习

1、上节socket SSH练习程序

在上节(python(七)下:初识socket网络编程)中的问题:
如果执行命令结果超过1024,导致接收结果不全,执行新指令会显示上次的结果。如何解决这个问题?
则客户端需要重复接收多次结果,问题需要接收多少次?这就需要服务端在发送结果前就先把结果的大小发送给客户端。
SSH服务端

#/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket, os

server = socket.socket()
server.bind(('localhost',9999))
server.listen()

while True:
    conn, addr = server.accept()
    print("new conn", addr)
    while True:
        print("等待新指令")
        data = conn.recv(1024)
        if not data:
            print("客户端已断开")
            break
        print("执行指令:",data)
        cmd_res = os.popen(data.decode()).read()
        print("before send",len(cmd_res))
        if len(cmd_res) == 0:
            cmd_res = "cmd has no output..."
        # 发送结果大小给客户端(整数不能encode,先转为str)
        # 注意:这里是len结果的encode形式,和客户端len的方式要一致
        conn.send( str(len(cmd_res.encode())).encode('utf-8') )
        # 发送执行结果
        conn.send(cmd_res.encode('utf-8'))
        print("send done")

server.close()

# 防止客户端不断开的时候,服务端重启提示端口占用,可以如下实例化解决
# server = socket.socket() #获得socket实例
# server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

SSH客户端

#/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket

client = socket.socket()
client.connect(('localhost', 9999))

while True:
    cmd = input('>>> ').strip()
    if len(cmd) == 0 : continue
    client.send(cmd.encode('utf-8'))
    cmd_res_size = client.recv(1024)  # 接收命令结果的长度
    print("命令结果大小:",cmd_res_size)
    received_size = 0  # 记录接收长度
    received_data = b''  # 声明空bytes
    # 循环接收命令结果
    while received_size != int(cmd_res_size.decode()):
        data = client.recv(1024)
        received_size += len(data)  # 每次收到的有可能小于1024,所以必须用len判断
        print("当前接收大小:",received_size)
        received_data += data  # 接收命令结果
    else:  # 接收完命令结果之后,打印
        print("cmd res receive done ...",received_size)
        print(received_data.decode())

client.close()

2、网络开发过程中注意粘包问题

上面的代码在windows中运行没有问题,但是在linux中运行客户端会有如下报错:

    while received_size != int(cmd_res_size.decode()):
ValueError: invalid literal for int() with base 10: '26anaconda-ks.cfg\nc.py\ns.py\n'

明明接收大小是整数,怎么后面还连有其他数据?这就出现了粘包问题
 
看服务端代码:

        conn.send( str(len(cmd_res.encode())).encode('utf-8') )
        conn.send(cmd_res.encode('utf-8'))

虽然send可以强制发送数据,但是由于两次发送的间隔非常小,所以系统会当成一次send发送数据,从而数据粘在一起了。
由于是python3.x版本,如果是python2.x版本,在windows上也会出现粘包问题。3.x上偶尔也会出现在windows上
 
那么怎么解决粘包问题
方法1:sleep
两次发送send中,sleep0.5秒

import time
    ……
        conn.send( str(len(cmd_res.encode())).encode('utf-8') )
        time.sleep(0.5)
        conn.send(cmd_res.encode('utf-8'))

方法二:接收客户端确实信息
两次send中间,不立刻执行第二条send。
服务端

        conn.send( str(len(cmd_res.encode())).encode('utf-8') )
        client_ack = conn.recv(1024)  # wait client to confirm
        conn.send(cmd_res.encode('utf-8'))

客户端

    cmd_res_size = client.recv(1024)  # 接收命令结果的长度
    client.send("准备好接收命令结果了".encode())

3、模拟FTP收发文件

FTP server 代码基本步骤:
 1. 读取文件名
 2. 检测文件是否存在
 3. 打开文件
 4. 检测文件大小
 5. 发送大小给客户端
 6. 等客户端确认
 7. 开始边读边发数据
 8. 发送MD5值
ftp服务端

#/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket, os, hashlib

server = socket.socket()
server.bind(('localhost',9999))
server.listen()

while True:
    conn, addr = server.accept()
    print("new conn",addr)
    while True:
        print("等待新指令")
        data = conn.recv(1024)
        if not data :
            print("客户端已经断开")
            break
        cmd, filename = data.decode().split()
        print(filename)
        if os.path.isfile(filename):  # 判断是否是一个文件,而不是判断是否存在
            f = open(filename, 'rb')
            m = hashlib.md5()
            file_size = os.stat(filename).st_size  # 读取出文件大小
            conn.send( str(file_size).encode() )  # 发送文件大小
            conn.recv(1024)  # 接收客户端确认,ack
            for line in f:  # hashlib 是不能直接对文件做MD5的
                m.update(line)  # update 连续的,hash只能bytes类型
                conn.send(line)  # 循环发数据
            print("file md5", m.hexdigest())
            f.close()
            conn.send(m.hexdigest().encode())  # 发送MD5
        print("send done")

server.close()

ftp客户端

#/usr/bin/env python
# -*- coding: UTF-8 -*-
import socket, hashlib

client = socket.socket()
client.connect(('localhost', 9999))

while True:
    cmd = input(">>>").strip()
    if len(cmd) == 0: continue
    if cmd.startswith("get"):  # 代表下载文件
        client.send(cmd.encode())  # 发送命令
        server_response = client.recv(1024)  # 接收文件大小
        print("server response:", server_response)
        client.send(b"ready to recv file")  # 发送确认信息
        file_total_size = int(server_response.decode())
        received_size = 0
        filename = cmd.split()[1]  # 获取文件名
        f = open(filename+'.new', 'wb')  # 打开写文件
        m = hashlib.md5()
        while received_size < file_total_size :
            if file_total_size - received_size > 1024:
                size = 1024  # 如果超过1024,只接收1024大小
            else :  # 最后一次,剩多少收多少
                size = file_total_size - received_size
                # 如果小于1024,只接收那一小部分,通过这种方式就避免了粘包现象
                print("last receive:", size)
            data = client.recv(size)
            received_size += len(data)
            m.update(data)
            f.write(data)
        else:
            new_file_md5 = m.hexdigest()
            print("file recv done", received_size, file_total_size)
            f.close()

        server_file_md5 = client.recv(1024)
        print("server file md5:", server_file_md5)
        print("client file md5:", new_file_md5)

client.close()

三、SocketServer

socket不支持多并发,socketserver最主要的作用:就是实现一个并发处理,前面只是铺垫。
socketserver就是对socket的再封装。简化网络服务器版的开发。

1、socketserver一共有这么几种类型:

  • TCP 协议
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
  • UDP 协议
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
  • Unix 本机之间进程的TCP、UDP (不常用)
class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate=True)

类型之间的关系:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

2、创建一个socketserver 至少分以下几步:

  • 首先,您必须创建一个请求处理类,继承BaseRequestHandlerclass类并且重写父类的handle()方法,该方法将处理传入的请求。
  • 其次,你必须实例化一个上面类型中的一个类(如TCPServer)传递服务器的地址和你上面创建的请求处理类 给这个TCPServer。
  • 然后,调用handle_request()或者serve_forever()方法来处理一个或多个请求。
ser.handle_request()  # 只处理一个请求,处理完就退出了
ser.serve_forever()   # 处理多个请求,永远执行。
  • 最后,调用server_close()关闭socket。

3、创建一个简单的socketserver

socketserver 服务端

#/usr/bin/env python
# -*- coding: UTF-8 -*-
import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):  # 自己写的请求处理类
    # 重写handle,跟客户端所有的交互都是在handle里完成的,请求处理过程都是handle规定的
    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()  # 等同socket的server.recv
                print("{} wrote:".format(self.client_address[0]))
                print(self.data)
                self.request.send(self.data.upper())
            # 不支持像socket那样,通过 if not self.data: break 方式断开
            except ConnectionResetError as e:
                print("客户端已断开",e)
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    # server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)  # 和socket一样,并没加入多并发功能
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)  # 多线程
    # server = socketserver.ForkingTCPServer((HOST, PORT), MyTCPHandler)  # 多进程,在windows上不可以,不支持os.fork
    server.serve_forever()
    # 具体的调用关系可以看ThreadingTCPServer和ForkingTCPServer的源码

客户端

import socket

client = socket.socket() #声明socket类型,同时生成socket连接对象

client.connect(('localhost',9999))
while True:
    msg = input(">>:").strip()
    if len(msg) == 0:continue
    client.send(msg.encode("utf-8"))
    data = client.recv(10240)
    print("recv:",data.decode())

client.close()

4、socketserver.BaseServer(server_address, RequestHandlerClass) 主要有以下方法

fileno()  # 返回文件描述符
handle_request()  # 处理单个请求
server_forever(poll_interval=0.5)  # 每0.5秒检测一下是否发了让我shutdown的信号,做一些清理子进程或线程工作
server_close()  # 告诉server_forever(),让它停掉
server_close()  # 关闭
address_family  # 地址簇
RequestHandlerClass # 请求处理类
server_address  # IP地址
allow_reuse_address  # 允许重用地址,用于服务器断开,客户端未断开,端口需要等待几十秒才能用
# 使用如下:
server = socket.socket() #获得socket实例
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

finish_request()  # 和handle关系如下,看源码(BaseRequestHandler类构造函数):
def __init__(self, request, client_address, server):
    ......
    self.setup()
    try:
        self.handle()
    finally:
        self.finish()

四、开发一个支持多用户在线的FTP程序

要求:

用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件的断点续传


转载请务必保留此出处:http://blog.csdn.net/fgf00/article/details/52752746

你可能感兴趣的:(Python开发)