先来回顾一下昨天的内容

黏包现象

粘包现象的成因 :

tcp 协议的特点 面向流的 为了保证可靠传输 所以有很多优化的机制

无边界 所有在连接建立的基础上传递的数据之间没有界限

收发消息很有可能不完全相等

缓存机制,导致没发过去的消息会在发送端缓存

没接收完的消息会在接收端缓存

解决:

给应用层定制协议

解决方案一:只发送一条信息

先发送一个定长表示待发送数据长度的 bytes 先接收一个固定长度

再发送要发送的数据 再按照长度接收数据

解决方案二 :发送的多条信息

先发送一个定长表示待发送字典长度的 bytes 先接收一个固定长度

再发送要发送字典(字典中存储的是文件信息) 再按照长度接收字典

再发送文件 再根据字典中的信息接收相应的内容

那么这 2 种方案,如何选择呢?

如果发送数据之前,发送的定制协议超过 1 个变量时,就应该使用字典

否则,就需要发送多次,比如:文件名的长度,文件名,文件大小的长度,文件的内容…

发送的次数越多,在网络上,就比较浪费时间。

如果将上面的 5 个变量,放到一个字典里面,只需要发送一次,就可以了。

如果只有 1 个变量,就使用第一种方案

作业讲解(大文件下载以及进度条展示)

大文件传输:

server.py

  1. import os
  2. import json
  3. import socket
  4. import struct
  5. filepath = r'E:\BaiduYunDownload\[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4'
  6. sk = socket.socket()
  7. sk.bind(('127.0.0.1',9000))
  8. sk.listen()
  9. conn,addr = sk.accept()
  10. filename = os.path.basename(filepath)
  11. filesize = os.path.getsize(filepath)
  12. dic = {'filename':filename,'filesize':filesize}
  13. str_dic = json.dumps(dic).encode('utf-8')
  14. len_dic = len(str_dic)
  15. length = struct.pack('i',len_dic)
  16. conn.send(length) # dic的长度
  17. conn.send(str_dic) # dic
  18. with open(filepath,'rb') as f: # 文件
  19. while filesize:
  20. content = f.read(4096)
  21. conn.send(content)
  22. filesize -= len(content)
  23. '''
  24. 这里不能减等4096,因为文件,最后可能只有3字节。
  25. 要根据读取的长度len(content),来计算才是合理的。
  26. '''
  27. conn.close()
  28. sk.close()

client.py

  1. import json
  2. import struct
  3. import socket
  4. sk = socket.socket()
  5. sk.connect(('127.0.0.1',9000))
  6. dic_len = sk.recv(4)
  7. dic_len = struct.unpack('i',dic_len)[0]
  8. dic = sk.recv(dic_len)
  9. str_dic = dic.decode('utf-8')
  10. dic = json.loads(str_dic)
  11. with open(dic['filename'],'wb') as f: # 使用wb更严谨一些,虽然可以使用ab
  12. while dic['filesize']:
  13. content = sk.recv(4096) #这边的4096,可以不用和server对应,改成1024,也可以
  14. dic['filesize'] -= len(content)
  15. f.write(content)
  16. sk.close()

先执行 server.py,再执行 client.py

等待 30 多秒,当前目录就会出现一个视频文件,打开确认,是否可以播放。

客户体验太差了,用户不知道啥时候能接收完,程序到底有没有卡住?下载花了多长时间?都不知道

下面来一个进阶版的,

增加进度条和下载时间

主要是修改 client.py,代码如下:

  1. import json
  2. import struct
  3. import socket
  4. import sys
  5. import time
  6. def processBar(num, total): # 进度条
  7. rate = num / total
  8. rate_num = int(rate * 100)
  9. if rate_num == 100:
  10. r = '\r%s>%d%%\n' % ('=' * rate_num, rate_num,)
  11. else:
  12. r = '\r%s>%d%%' % ('=' * rate_num, rate_num,)
  13. sys.stdout.write(r)
  14. sys.stdout.flush
  15. start_time = time.time() # 开始时间
  16. sk = socket.socket()
  17. sk.connect(('127.0.0.1',9000))
  18. dic_len = sk.recv(4)
  19. dic_len = struct.unpack('i',dic_len)[0]
  20. dic = sk.recv(dic_len)
  21. str_dic = dic.decode('utf-8')
  22. dic = json.loads(str_dic)
  23. with open(dic['filename'],'wb') as f: # 使用wb更严谨一些,虽然可以使用ab
  24. content_size = 0
  25. while True:
  26. content = sk.recv(4096)<br> f.write(content) # 写入文件
  27. content_size += len(content) # 接收大小
  28. processBar(content_size,dic['filesize']) # 执行进度条函数
  29. if content_size == dic['filesize']:break # 当接收的总大小等于文件大小时,终止循环
  30. sk.close() # 关闭连接
  31. end_time = time.time() # 结束时间
  32. print('本次下载花费了{}秒'.format(end_time - start_time))

执行效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582658-2397d2a0-2ab4-415f-9ad6-dfd89f409b87.png)

上面效果展示了 100 个等号,太长了,那么要缩减到 1/3 呢?

修改进度条函数

  1. def processBar(num, total): # 进度条
  2. rate = num / total
  3. rate_num = int(rate * 100)
  4. if rate_num == 100:
  5. r = '\r%s>%d%%\n' % ('=' * int(rate_num / 3), rate_num,) # 控制等号输出数量,除以3,表示显示1/3
  6. else:
  7. r = '\r%s>%d%%' % ('=' * int(rate_num / 3), rate_num,)
  8. sys.stdout.write(r)
  9. sys.stdout.flush

再次执行:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582652-127941cb-3a64-496f-b24b-ded0680249b7.png)

再来一个高级版,显示绿色的飞机

代码如下:

  1. def processBar(num, total): # 进度条
  2. rate = num / total
  3. rate_num = int(rate * 100)
  4. pretty = '✈'
  5. if rate_num == 100:
  6. r = '\r\033[32m{}\033[0m{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
  7. else:
  8. r = '\r\033[32m{}\033[0m{}%'.format(pretty * int(rate_num / 5), rate_num,)
  9. sys.stdout.write(r)
  10. sys.stdout.flush

效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582681-9ca6ddd8-374e-4a18-a12a-c3569dc5e05f.png)

再来一个每秒换色

导入一个随机换色类

  1. import random
  2. class Prompt(object): # 提示信息显示
  3. colour_dic = {
  4. 'red': 31,
  5. 'green': 32,
  6. 'yellow': 33,
  7. 'blue': 34,
  8. 'purple_red': 35,
  9. 'bluish_blue': 36,
  10. 'white': 37,
  11. }
  12. def __init__(self):
  13. pass
  14. @staticmethod
  15. def display(msg, colour='white'):
  16. choice = Prompt.colour_dic.get(colour)
  17. # print(choice)
  18. if choice:
  19. info = "\033[1;{};1m{}\033[1;0m".format(choice, msg)
  20. return info
  21. else:
  22. return False
  23. def random_color(msg): # 随机换色
  24. colour_list = []
  25. for i in Prompt.colour_dic:
  26. colour_list.append(i)
  27. length = len(colour_list) - 1 # 最大索引值
  28. index = random.randint(0, length) # 随机数
  29. ret = Prompt.display(msg, colour_list[index]) # 随机颜色
  30. return ret

修改 client.py

  1. from Prompt import Prompt
  2. def processBar(num, total): # 进度条
  3. rate = num / total
  4. rate_num = int(rate * 100)
  5. pretty = Prompt.random_color('✈') # 随机换色
  6. if rate_num == 100:
  7. r = '\r{}{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
  8. else:
  9. r = '\r{}{}%'.format(pretty * int(rate_num / 5), rate_num,)
  10. sys.stdout.write(r)
  11. sys.stdout.flush

执行效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582820-9800d823-25f9-42a5-bdfe-037276529974.png)

增加 MD5 校验

server.py

  1. import os
  2. import json
  3. import socket
  4. import struct
  5. import hashlib
  6. sk = socket.socket()
  7. sk.bind(('127.0.0.1', 9000))
  8. sk.listen()
  9. conn, addr = sk.accept()
  10. filename = '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4' # 文件名
  11. absolute_path = os.path.join('E:\BaiduYunDownload',filename) # 文件绝对路径
  12. buffer_size = 1024*1024 # 缓冲大小,这里表示1MB
  13. md5obj = hashlib.md5()
  14. with open(absolute_path, 'rb') as f:
  15. while True:
  16. content = f.read(buffer_size) # 每次读取指定字节
  17. if content:
  18. md5obj.update(content)
  19. else:
  20. break # 当内容为空时,终止循环
  21. md5 = md5obj.hexdigest()
  22. print(md5) # 打印md5值
  23. dic = {'filename':filename,
  24. 'filename_md5':str(md5),'buffer_size':buffer_size,
  25. 'filesize':os.path.getsize(absolute_path)}
  26. str_dic = json.dumps(dic).encode('utf-8')
  27. len_dic = len(str_dic)
  28. length = struct.pack('i', len_dic)
  29. conn.send(length) # dic的长度
  30. conn.send(str_dic) # dic
  31. with open(absolute_path, 'rb') as f: # 文件
  32. while dic['filesize']:
  33. content = f.read(dic['buffer_size'])
  34. conn.send(content)
  35. dic['filesize'] -= len(content)
  36. '''
  37. 这里不能减等4096,因为文件,最后可能只有3字节。
  38. 要根据读取的长度len(content),来计算才是合理的。
  39. '''
  40. conn.close()

client.py

  1. import json
  2. import struct
  3. import socket
  4. import sys
  5. import time
  6. import hashlib
  7. import os
  8. from Prompt import Prompt
  9. def processBar(num, total): # 进度条
  10. rate = num / total
  11. rate_num = int(rate * 100)
  12. pretty = Prompt.random_color('✈')
  13. if rate_num == 100:
  14. r = '\r{}{}%\n'.format(pretty * int(rate_num / 5), rate_num,)
  15. else:
  16. r = '\r{}{}%'.format(pretty * int(rate_num / 5), rate_num,)
  17. sys.stdout.write(r)
  18. sys.stdout.flush
  19. start_time = time.time() # 开始时间
  20. sk = socket.socket()
  21. sk.connect(('127.0.0.1',9000))
  22. dic_len = sk.recv(4)
  23. dic_len = struct.unpack('i',dic_len)[0]
  24. dic = sk.recv(dic_len)
  25. str_dic = dic.decode('utf-8')
  26. dic = json.loads(str_dic)
  27. md5 = hashlib.md5()
  28. with open(dic['filename'],'wb') as f: # 使用wb更严谨一些,虽然可以使用ab
  29. content_size = 0
  30. while True:
  31. content = sk.recv(dic['buffer_size']) # 接收指定大小
  32. f.write(content) # 写入文件
  33. content_size += len(content) # 接收大小
  34. md5.update(content) # 摘要
  35. processBar(content_size,dic['filesize']) # 执行进度条函数
  36. if content_size == dic['filesize']:break # 当接收的总大小等于文件大小时,终止循环
  37. md5 = md5.hexdigest()
  38. print(md5) # 打印md5值
  39. if dic['filename_md5'] == str(md5):
  40. print(Prompt.display('md5校验正确--下载成功','green'))
  41. else:
  42. print(Prompt.display('文件验证失败', 'red'))
  43. os.remove(dic['filename']) # 删除文件
  44. sk.close() # 关闭连接
  45. end_time = time.time() # 结束时间
  46. print('本次下载花费了{}秒'.format(end_time - start_time))

执行输出:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582632-c97e8af4-bd80-48eb-9ec8-34db202d6f14.png)

socket 的更多方法介绍

更多方法

  1. 服务端套接字函数
  2. s.bind() 绑定(主机,端口号)到套接字
  3. s.listen() 开始TCP监听
  4. s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
  5. 客户端套接字函数
  6. s.connect() 主动初始化TCP服务器连接
  7. s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
  8. 公共用途的套接字函数
  9. s.recv() 接收TCP数据
  10. s.send() 发送TCP数据
  11. s.sendall() 发送TCP数据
  12. s.recvfrom() 接收UDP数据
  13. s.sendto() 发送UDP数据
  14. s.getpeername() 连接到当前套接字的远端的地址
  15. s.getsockname() 当前套接字的地址
  16. s.getsockopt() 返回指定套接字的参数
  17. s.setsockopt() 设置指定套接字的参数
  18. s.close() 关闭套接字
  19. 面向锁的套接字方法
  20. s.setblocking() 设置套接字的阻塞与非阻塞模式
  21. s.settimeout() 设置阻塞套接字操作的超时时间
  22. s.gettimeout() 得到阻塞套接字操作的超时时间
  23. 面向文件的套接字的函数
  24. s.fileno() 套接字的文件描述符
  25. s.makefile() 创建一个与该套接字相关的文件

send和sendall方法

  1. 官方文档对socket模块下的socket.send()和socket.sendall()解释如下:
  2. socket.send(string[, flags])
  3. Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data.
  4. send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。
  5. socket.sendall(string[, flags])
  6. Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent.
  7. 尝试发送string的所有数据,成功则返回None,失败则抛出异常。
  8. 故,下面两段代码是等价的:
  9. #sock.sendall('Hello world\n')
  10. #buffer = 'Hello world\n'
  11. #while buffer:
  12. # bytes = sock.send(buffer)
  13. # buffer = buffer[bytes:]

验证客户端链接的合法性

使用 hashlib.md5 加密

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582716-e0e53e85-6191-4612-b5fc-fdc4f6de12c1.png)

为什么要随机字符串,是为了防止客户端的数据被窃取

生成随机的 bytes 类型数据,它是解不出来的

  1. import os
  2. print(os.urandom(32))

执行输出:

b’PO\xca8\xc8\xf3\xa0\xb5,\xdd\xb8K \xa8D\x9cN”\x82\x03\x86g\x18e\xa7\x97\xa77\xb9\xa5VA’

server.py

  1. import os
  2. import socket
  3. import hashlib
  4. secret_key = '老衲洗头用飘柔' # 加密key
  5. sk = socket.socket()
  6. sk.bind(('127.0.0.1',9000))
  7. sk.listen()
  8. while True:
  9. try:
  10. conn,addr = sk.accept()
  11. random_bytes = os.urandom(32) # 随即产生32个字节的字符串,返回bytes
  12. conn.send(random_bytes) # 发送随机加密key
  13. md5 = hashlib.md5(secret_key.encode('utf-8')) # 使用secret_key作为加密盐
  14. md5.update(random_bytes) #得到MD5消息摘要
  15. ret = md5.hexdigest() #以16进制返回消息摘要,它是一个32位长度的字符串
  16. msg = conn.recv(1024).decode('utf-8') # 接收的信息解码
  17. if msg == ret:print('是合法的客户端') # 如果接收的摘要和本机计算的摘要一致,就说明是合法的
  18. else:conn.close() # 关闭连接
  19. finally: # 无论如何,都执行下面的代码
  20. sk.close() # 关闭连接
  21. break

client.py

  1. import socket
  2. import hashlib
  3. secret_key = '老衲洗头用飘柔' # 加密key
  4. sk = socket.socket()
  5. sk.connect(('127.0.0.1',9000))
  6. urandom = sk.recv(32) # 接收32字节,也就是os.urandom的返回值
  7. md5_obj = hashlib.md5(secret_key.encode('utf-8')) # 使用加密盐加密
  8. md5_obj.update(urandom)
  9. sk.send(md5_obj.hexdigest().encode('utf-8')) # 发送md5摘要
  10. print('-----')
  11. sk.close() # 关闭连接

先执行 server.py,再执行 client.py

client 输出:——-

server 输出:是合法的客户端

如果 100 客户端,来连接呢?秘钥都是通用的。

一般情况下,用在哪些场景呢?

比如公司级别,比如 1 台机器,向 100 台服务器获取数据

假如黑客渗透到内网,得知到服务器 IP 地址。就可以做端口扫描,一台计算机的端口范围是 0~65535

扫描 6 万多次,就能知道了。

使用 hmac 加密

hmac 是专门来做客户端合法性的

  1. import hmac
  2. obj = hmac.new(key=b'secret_key',msg=b'100212002155')
  3. print(obj.hexdigest())

执行输出:

27111d37764a2fe5bc79d297e7b54c35

客户端也使用 hmac,验证一下,就可以了。

改造 server 和 client

server.py

  1. import os
  2. import socket
  3. import hmac
  4. secret_key = '老衲洗头用飘柔'.encode('utf-8')
  5. sk = socket.socket()
  6. sk.bind(('127.0.0.1',9000))
  7. sk.listen()
  8. while True:
  9. try:
  10. conn,addr = sk.accept()
  11. random_bytes = os.urandom(32)
  12. conn.send(random_bytes)
  13. obj = hmac.new(key=secret_key,msg=random_bytes)
  14. ret = obj.hexdigest()
  15. msg = conn.recv(1024).decode('utf-8')
  16. if msg == ret:print('是合法的客户端')
  17. else:conn.close()
  18. finally:
  19. sk.close()
  20. break

client.py

  1. import socket
  2. import hmac
  3. secret_key = '老衲洗头用飘柔'.encode('utf-8')
  4. sk = socket.socket()
  5. sk.connect(('127.0.0.1', 9000))
  6. urandom = sk.recv(32)
  7. hmac_obj = hmac.new(key=secret_key, msg=urandom)
  8. sk.send(hmac_obj.hexdigest().encode('utf-8'))
  9. print('-----')
  10. sk.close()

执行效果,同上

socketserver

SocketServer 内部使用 IO 多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的 Socket 服务端。即:每个客户端请求连接到服务器时,Socket 服务端都会在服务器是创建一个“线程”或者“进 程” 专门负责处理当前客户端的所有请求。

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582664-5ba7965c-3ba0-4b99-9e9f-633e435f5c85.png)

它能实现多个客户端,同时连接,它继承了 socket

ThreadingTCPServer 实现的 Soket 服务器内部会为每个 client 创建一个 “线程”,该线程用来和客户端进行交互。

使用 ThreadingTCPServer:

创建一个继承自 SocketServer.BaseRequestHandler 的类,必须继承

类中必须定义一个名称为 handle 的方法,必须重写

解读socketserver源码 —— http://www.cnblogs.com/Eva-J/p/5081851.html

看 BaseRequestHandler 的源码,它的 hendle 方法,是空的

  1. def handle(self):
  2. pass

需要自己去实现

server.py

  1. import socketserver
  2. class MyServer(socketserver.BaseRequestHandler):
  3. def handle(self):
  4. print(self.request)
  5. server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
  6. server.serve_forever()

client.py

  1. import socket
  2. sk = socket.socket()
  3. sk.connect(('127.0.0.1',9000))
  4. sk.close()

先执行 server.py,再执行 client.py

server 输出

E:/python_script/day30/socketserver/server.py

每连接一个 client.就会触发 handle,输出 request

监听,等待连接,接收,全由 hendle 完成了。

server.py

  1. import socketserver
  2. class MyServer(socketserver.BaseRequestHandler):
  3. def handle(self):
  4. print(self.request)
  5. self.request.send(b'hello') # 跟所有的client打招呼
  6. print(self.request.recv(1024)) # 接收客户端的信息
  7. server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
  8. server.serve_forever()

client.py

  1. import socket
  2. sk = socket.socket()
  3. sk.connect(('127.0.0.1',9000))
  4. print(sk.recv(1024))
  5. inp = input('>>>').encode('utf-8')
  6. sk.send(inp)
  7. sk.close()

先执行 server.py,再执行 client.py

client 输出:

b’hello’

hi

server 输出:

b’hi’

开多个客户端,也可以执行

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582719-6551f08f-9d63-40df-8cfb-fea66a0f94c8.png)

server 能够和多个 client 通信

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582698-b3b53ddb-9f63-49ef-9c81-b3805b55b52a.png)

连续发送

client 连续发送:

  1. import socket
  2. sk = socket.socket()
  3. sk.connect(('127.0.0.1',9000))
  4. while True:
  5. print(sk.recv(1024))
  6. #inp = input('>>>').encode('utf-8')
  7. sk.send(b'hahaha')
  8. sk.close()

server 连续接收:

  1. import socketserver
  2. class MyServer(socketserver.BaseRequestHandler):
  3. def handle(self):
  4. while True:
  5. print(self.request)
  6. self.request.send(b'hello') # 跟所有的client打招呼
  7. print(self.request.recv(1024)) # 接收客户端的信息
  8. server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
  9. server.serve_forever()

执行效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597977582661-35f1d5cc-f615-44db-9d32-fa963a32f9b0.png)

如果 server 端口重复,使用以下代码:

  1. # 设置allow_reuse_address允许服务器重用地址
  2. socketserver.TCPServer.allow_reuse_address = True

完整代码如下:

  1. import socketserver
  2. class MyServer(socketserver.BaseRequestHandler):
  3. def handle(self):
  4. while True:
  5. print(self.request) # 这里不能使用input,否则卡住了
  6. self.request.send(b'hello') # 跟所有的client打招呼
  7. print(self.request.recv(1024)) # 接收客户端的信息
  8. if __name__ == '__main__':
  9. socketserver.TCPServer.allow_reuse_address = True
  10. server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
  11. server.serve_forever()

明日默写:

  1. import socketserver
  2. class MyServer(socketserver.BaseRequestHandler):
  3. def handle(self):
  4. self.request.send(b'hello')
  5. msg = self.request.recv(1024)
  6. print(msg)
  7. if __name__ == '__main__':
  8. socketserver.TCPServer.allow_reuse_address = True
  9. server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),MyServer)
  10. server.serve_forever()