socket套接字编程、实现通信循环、解决黏包问题与优酷小项目之视频上传与下载(一)

上期内容回顾

  • 面向对象复习(json序列化类)
对象、类、父类的概念

面向对象的三大特征:
	封装 继承 多态
    
双下开头的方法(达到某个条件自动触发):
	__init__:对象实例化自动触发
	__str__:对象执行打印操作自动触发
	__call__:对象加括号调用的时候自动触发
	...

反射:
	利用字符串操作对象的属性或方法
	hasattr getattr
    
json序列化非默认的python数据类型:
	常见操作是手动转字符串
	不常见操作重写cls指向的类
  • 软件开发架构
c/s 架构

b/s 架构
	b/s架构本质上也是c/s架构
  • 远程传输数据的发展史
所有前言的技术几乎都是诞生于军事

要想实现远程传输首先需要满足的条件是:"物理连接介质"
  • OSI七层协议
'应表会传网数物'

应用层
传输层
网络层
数据链路层
网络连接层
  • 各种协议及常见硬件介绍
# 物理连接层
	网线 网卡
    
# 数据链路层
	电信号分组方式
	以太网协议
	mac地址 1216进制数
		mac地址只能在局域网内实现数据交互
        
交换机
路由器
局域网
互联网

'上网其实就是顺着网线访问其他计算机上面的资源(网络只有更安全)'

# 网络层
	IP协议
		IP地址用于表示接入互联网的一台计算机
		IPV4与IPV6
	PORT协议
		端口地址用于表示计算机上面某一个应用程序
		动态分配、范围限制(0-65535)
        
'''IP+PORT:唯一标识计算机上面的某一个应用程序'''

# 传输层
	TCP、UDP
    
# 应用层
	HTTP、FTP、HTTPS
  • TCP与UDP
# TCP	
	可靠协议 流式协议
	三次握手建链接
	四次挥手断链接

# UDP 
	不可靠协议 数据报协议
	
"""
TCP类似于打电话
UDP类似于发短信
"""

今日内容概要

  • socket套接字编程

    • 掌握基本的客户端与服务端代码编写
  • 通信循环

  • 代码健壮性校验

  • 链接循环

  • TCP黏包现象(流式协议)

  • 报头制作、struct模块、封装形式

  • 优酷项目

内容详细

1、socket套接字编程

要求:自己想写一款可以数据交互的程序

应用:socket模块

# 测试:
	创建两个py文件 分别写好 客户端 服务端代码
	交互程序架构启动 肯定是先启动服务端再启动客户端
	客户端输入的话 会自动在服务端输出
	服务端回复的话 也会自动在客户端输出
    
    
    
"""
导入模块的两种方式
    import句式
    from...import...句式
第三方模块下载
    pip3 install 模块名==版本号 -i 仓库地址
"""

1.1、简易版代码(客户端)

import socket


# 1.先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 2.同样绑定IP 端口
client.connect(('192.168.11.70', 8080))
# 3.开始给服务端说话
client.send(b'I am from client!')  # send 说给对方
# 4.也要听对方想说什么
data = client.recv(1024)  # recv 听对方说(接收)
print(data)  # 打印听到的对方说的话
# 5.说完可以关机了
client.close()

1.2、简易版代码(服务端)

import socket


# 1.定义传输协议
server = socket.socket()
# 2.绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 3.半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)
# 4.开始监听客户端说话 三次握手建立连接
sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
# 5.开始听客户端说话
data = sock.recv(1024)  # recv(1024) 最大接收1024个字节 听对方说(接收)
print(data)  # 打印听到的对方说的话
# 6.听完对方说话 要回复对方
sock.send(b'I AM FROM SERVER!')  # send 说给对方
# 7.说完可以 挂断电话
sock.close()
# 8.挂完电话 服务端可以关机了
server.close()

2、通信循环及代码优化

1.客户端校验消息不能为空

2.服务端添加兼容性代码(主要针对:mac linux系统)

3.服务端重启频繁报端口占用错误
	from socket import SOL_SOCKET, SO_REUSEADDR
	server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
    
4.客户端异常关闭服务端报错的问题
	加入异常捕获
    
5.服务端链接循环

6.半连接池
	设置可以等待的客户端数量
    
"""
优化项:
	1.想要可以循环说话 所以从说话开始加入循环
	2.说话要自己随机输入 所有加入获取用户输入
	3.如果输入为空 不能报错 要重新获取用户输入
	4.解决客户端异常关闭 服务端会报错的问题
	5.服务端提供循环服务 
		断开一台客户端 就去服务另一台客户端 服务端不会关闭
	6.可以同时连接多位客户端(但是同时只能给一个客户提供服务 其他客户要等待)
"""

2.1、通信循环及代码优化客户端

import socket


# 1.先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 2.同样绑定IP 端口
client.connect(('192.168.11.70', 8080))

while True:
    speak = input('输入想说的话>>>:').strip()
    if len(speak) == 0 :
        continue
    # 3.开始给服务端说话 因为获取的用户输入是字符串 不能直接传输 所以要先转码
    client.send(speak.encode('utf8'))
    # 也可以用手动转码
    # client.send(bytes(speak, 'utf8'))
    # 4.也要听对方想说什么
    data = client.recv(1024)
    print(data.decode('utf8'))  # 打印听到的对方说的话

2.2、通信循环及代码优化服务端

import socket
from socket import SOL_SOCKET, SO_REUSEADDR


# 1.定义传输协议
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
# 2.绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 3.半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)

while True:
    # 4.开始监听客户端说话 三次握手建立连接
    sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
    print(address)
    while True:
        try:
            # 5.开始听客户端说话
            data = sock.recv(1024)  # recv(1024) 最大接收1024个字节
            if len(data) == 0: continue
            print(data.decode('utf8'))  # 打印听到的对方说的话
            # 6.听完对方说话 要回复对方
            sock.send(data+b'123')
        except ConnectionResetError as e:
            print(e)
            break

3、黏包问题

'''
subprocess模块:
	可以将代码命令给到计算机 cmd窗口执行 并拿回执行结果
'''

# 客户端
import socket


# 1.先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 2.同样绑定IP 端口
client.connect(('192.168.11.70', 8080))

while True:
    speak = input('输入cmd命令>>>:').strip()
    if len(speak) == 0 :
        continue
    # 3.开始给服务端说话 因为获取的用户输入是字符串 不能直接传输 所以要先转码
    client.send(speak.encode('gbk'))
    # 也可以用手动转码
    # client.send(bytes(speak, 'utf8'))
    # 4.也要听对方想说什么
    data = client.recv(1024)  # 获取的回复数据如果太大 一次回复不完 那么第二次无论告诉服务端什么请求 也都是继续先把第一次的数据继续传完为止 就导致错乱
    print(data.decode('gbk'))  # 打印听到的对方说的话


# 服务端
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
import subprocess

# 1.定义传输协议
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
# 2.绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 3.半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)

while True:
    # 4.开始监听客户端说话 三次握手建立连接
    sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
    print(address)
    while True:
        try:
            # 5.开始听客户端说话
            data = sock.recv(1024)  # recv(1024) 最大接收1024个字节
            if len(data) == 0: continue
            # print(data.decode('utf8'))  # 打印听到的对方说的话

            command_cmd = data.decode('utf8')
            sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            res = sub.stdout.read() + sub.stderr.read()  # cmd执行的正确结果和错误结果全都获取

            # 6.将获得的cmd执行结果回复给客户端
            sock.send(res)
        except ConnectionResetError as e:
            print(e)
            break

            
1.例如:
	第一次输入指令 tasklist
	在客户端打印的结果其实只有服务端真实要传输的结果的其中一部分
	第二次输入指令 dir
	第三次输入指令 ipconfig
	会发现客户端打印的结果依然是 第一次指令tasklist的剩余的数据
	这就是所谓的 '黏包问题'


"""
原理:
	第一次数据管道的数据没有被完全取出
	那么后面不管到第几次数据传输 都会先将第一次堆积在传输管道中的数据传输完毕
	由此 会发现 只要有某一次数据未能全部传输的 后面的多次获取的结果并不是正确的对应结果 而是那一次未能全部传输的剩下的数据
	
	一句话总结:
		数据管道的数据没有被完全取出
"""   


2.TCP协议有一个特性:
	当数据量比较小 且时间间隔比较短的多次数据
	那么TCP会自动打包成一个数据包发送
 
# TCP客户端
import socket


client = socket.socket()
client.connect(('192.168.11.70', 8080))

client.send(b'123')
client.send(b'456')
client.send(b'789')

# TCP服务端
import socket


server = socket.socket()
server.bind(('192.168.11.70', 8080))
server.listen(5)

sock, address = server.accept()
data = sock.recv(1024)
print(data)
sock.recv(1024)
print(data)
data = sock.recv(1024)
print(data)

打印结果:
b'123456789'
b'123456789'
b''


# 解决以上两种情况
	报头:能够标识即将到来的数据具体信息
	比如:数据量多大
	报头的长度必须是固定的

3.1、struct模块

'''该模块可以把一个类型 如数字 转成固定长度的bytes'''

import struct


res = b'13515313513516354140'
print(len(res))  # 20
res1 = struct.pack('i', len(res))
print(len(res1))
res2 = b'13515313513516354140dsadasdadada'
print(len(res2))
res3 = struct.pack('i', len(res2))
print(len(res3))

打印结果:
20
4
32
4


# 但是如果数据特别大的时候 是无法打包成固定长度的

image

# 因此可以使用字典方式进行数据传输(无论真实数据多大)
import struct
import json


d = {
    'file_name': '很好看.mv',
    'file_size': 1231283912839123123424234234234234234324324912,
    'file_desc': '拍摄的很有心 真的很好看!!!',
    'file_desc2': '拍摄的很有心 真的很好看!!!'
}
d = json.dumps(d)
res = struct.pack('i',len(d))
print(len(res))
res1 = struct.unpack('i',res)[0]
print(res1)

# 打印结果
4
186

3.2、简易版本报头

# 客户端
import socket
import struct


# 1.先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 2.同样绑定IP 端口
client.connect(('192.168.11.70', 8080))

while True:
    speak = input('输入cmd命令>>>:').strip()
    if len(speak) == 0 :
        continue
    # 3.开始给服务端说话 因为获取的用户输入是字符串 不能直接传输 所以要先转码
    client.send(speak.encode('utf8'))
    # 先接收固定长度为4的报头数据
    recv_first = client.recv(4)
    # 解析报头
    real_length = struct.unpack('i', recv_first)[0]
    # 接收真实数据
    real_data = client.recv(real_length)

    print(real_data.decode('gbk'))
    
    
    
# 服务端
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
import subprocess
import struct


# 1.定义传输协议
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
# 2.绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 3.半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)

while True:
    # 4.开始监听客户端说话 三次握手建立连接
    sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
    print(address)
    while True:
        try:
            # 5.开始听客户端说话
            data = sock.recv(1024)  # 接收amd命令
            if len(data) == 0: continue
            # print(data.decode('utf8'))  # 打印听到的对方说的话

            command_cmd = data.decode('utf8')
            sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            res = sub.stdout.read() + sub.stderr.read()  # cmd执行的正确结果和错误结果全都获取 但是结果可能很大
            # 制作报头
            data_first = struct.pack('i', len(res))
            # 发送报头
            sock.send(data_first)
            # 发送真实数据
            sock.send(res)
        except ConnectionResetError as e:
            print(e)
            break

3.3、利用字典报头上传文件数据(升级版)

'''
从服务端向客户端传输
	按照此方法就解决了 黏包问题
'''

# 客户端
import json
import socket
import struct


# 先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 同样绑定IP 端口
client.connect(('192.168.11.70', 8080))

while True:
    speak = input('输入cmd命令>>>:').strip()
    if len(speak) == 0 :
        continue
    # 开始给服务端说话 因为获取的用户输入是字符串 不能直接传输 所以要先转码
    client.send(speak.encode('utf8'))
    # 1.先接收固定长度为4的报头数据
    recv_first = client.recv(4)
    # 2.解析字典报头
    dict_length = struct.unpack('i', recv_first)[0]
    # 3.接收字典数据
    real_data = client.recv(dict_length)
    # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化)
    real_dict = json.loads(real_data)
    print(real_dict)
    # 5.获取字典中的各项数据
    data_lenfth = real_dict.get('size')
    data_bytes = client.recv(data_lenfth)
    print(data_bytes.decode('gbk'))
    
    
# 服务端
import json
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
import subprocess
import struct

# 定义传输协议
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
# 绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)

while True:
    # 开始监听客户端说话 三次握手建立连接
    sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
    print(address)
    while True:
        try:
            # 开始听客户端说话
            data = sock.recv(1024)  # 接收amd命令
            if len(data) == 0: continue
            # print(data.decode('utf8'))  # 打印听到的对方说的话

            command_cmd = data.decode('utf8')
            sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            res = sub.stdout.read() + sub.stderr.read()  # cmd执行的正确结果和错误结果全都获取 但是结果可能很大

            # 1.预防数据过大 定义字典传输
            data_dict = {
                'desc': '这是非常重要的数据',
                'size': len(res),
                'info':'视频很提神醒脑!'
            }
            # 2.将字典做成报头 但是字典不能直接打包成报头 所以先json序列化
            data_json = json.dumps(data_dict)
            # 3.制作字典报头
            data_first = struct.pack('i', len(data_json))
            # 4.发送字典报头
            sock.send(data_first)
            # 5.发送字典
            sock.send(data_json.encode('utf8'))
            # 6.发送真实数据
            sock.send(res)
        except ConnectionResetError as e:
            print(e)
            break

4、优酷项目中 视频的上传与下载

# 从客户端向服务端上传


# 客户端
import json
import socket
import struct
import os


# 先要有通讯工具 也就是定义传输协议
client = socket.socket()
# 同样绑定IP 端口
client.connect(('192.168.11.70', 8080))

while True:
    DATA_PATH = r'D:\飞秋下载文件\python\02、python提升\网络并发day01\视频'
    movie_name_list = os.listdir(DATA_PATH)  # ['01 后期课程安排.mp4', '02 面向对象复习扩展.mp4', '03 今日内容概要.mp4', '04 软件开发架构.mp4', '05 网络传输前戏.mp4', '06 七层协议简介.mp4', '07 常见硬件.mp4', '08 网络层.mp4', '09 传输层之端口协议.mp4', '10 TCP与UDP协议.mp4']
    for i, j in enumerate(movie_name_list, 1):
        print(i, j)
    choice = input('选择想要上传的电影编号>>>:').strip()
    if choice.isdigit():
        choice = int(choice)
        if choice in range(1, len(movie_name_list)+1):  # range顾头不顾尾
            # 获取文件名称
            movie_name = movie_name_list[choice-1]
            print(movie_name)
            # 拼接文件绝对路径 以找到文件
            movie_path = os.path.join(DATA_PATH, movie_name)
            # 1.预防数据过大 定义字典传输
            data_dict = {
                'file_name': '波多野老师合集.mp4',
                'desc': '这是非常重要的数据',
                'size': os.path.getsize(movie_path),
                'info': '视频很提神醒脑!'
            }
            # 2.将字典做成报头 但是字典不能直接打包成报头 所以先json序列化
            data_json = json.dumps(data_dict)
            # 3.制作字典报头
            data_first = struct.pack('i', len(data_json))
            # 4.发送字典报头
            client.send(data_first)
            # 5.发送字典
            client.send(data_json.encode('utf8'))
            # 6.发送真实数据
            with open(movie_path, 'rb') as f:
                for line in f:
                    client.send(line)
                    
                    
                    
# 服务端
import json
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
import struct


# 定义传输协议
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加该行代码
# 绑定IP 端口
server.bind(('192.168.11.70', 8080))
# 半连接池(同时可以有六个客户端访问 五个排队等待 一个接受服务)
server.listen(5)

while True:
    # 开始监听客户端说话 三次握手建立连接
    sock, address = server.accept()  # sock 监听的对象 | address 监听对象的地址
    print(address)
    while True:
        # 1.先接收固定长度为4的报头数据
        recv_first = sock.recv(4)
        # 2.解析字典报头
        dict_length = struct.unpack('i', recv_first)[0]
        # 3.接收字典数据
        real_data = sock.recv(dict_length)
        # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化)
        real_dict = json.loads(real_data)
        print(real_dict)
        # 5.获取字典中的各项数据
        data_length = real_dict.get('size')
        file_name = real_dict.get('file_name')

        recv_size = 0
        with open(file_name, 'ab') as f:
            while recv_size < data_length:
                data = sock.recv(1024)
                recv_size += len(data)
                f.write(data)
                
'''
本期只简单完成上传功能
细节之处后续继续优化
'''

知识拓展

在阅读源码的时候:
	1.变量名后面跟冒号 表示的意思是该变量名需要指代的数据类型
	2.函数后更横杆加大于号表示的意思是该函数的返回值类型
posted @   Deity_JGX  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示