封面图片来源:沙沙野
内容概览
- 服务端持续等待的解决方式
- 使用 socket 上传文件
- UDP 协议基本用法
- 使用 UDP 协议实现多人聊天功能
- 验证客户端的合法性
- 使用 socketserver 来实现 TCP 协议 socket 的并发
服务端持续等待的解决方式
- 之前的示例都是如果客户端停止通信了,服务端也会跟着停止。那么,如何让服务端与一个客户端的用户聊天退出后,还能继续等待下一个客户端的用户聊天呢?——解决思路:还是可以尝试用 whie 循环
- server.py 中的内容
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
while 1:
conn,addr = sk.accept()
while 1:
send_msg = input('请输入消息:')
conn.send(send_msg.encode())
if send_msg == 'q':break
msg = conn.recv(1024).decode()
if msg == 'q':break
print(msg)
conn.close()
sk.close()
3. client.py 中的内容
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
while True:
msg = sk.recv(1024).decode()
if msg == 'q':break
print(msg)
send_msg = input('请输入消息: ')
sk.send(send_msg.encode())
if send_msg == 'q':break
sk.close()
4. 运行两个文件,发现如果客户端输入 q 退出程序后,再次重启客户端(相当于另一个用户进入),还能继续和服务端通信
# server.py
(base) yanfa@yanfa-H110SD3-C:~/Desktop$ python server.py
# 与客户端的第一次交流
请输入消息:晚上好
你好
请输入消息:再见
# 此时客户端已经退出,但服务端没有停止运行
# 只是在等待另一个客户端的链接请求,除非手动强制关掉它
请输入消息:你好
晚上好
请输入消息:再见
# client.py
# 第一次链接服务端
(base) yanfa@yanfa-H110SD3-C:~/Desktop$ python client.py
晚上好
请输入消息:你好
再见
请输入消息:q
# 第二次链接服务端
(base) yanfa@yanfa-H110SD3-C:~/Desktop$ python client.py
你好
请输入消息:晚上好
再见
请输入消息:q
使用 socket 上传文件
- 整体的解决思路先捋一遍,首先客户端在上传文件之前:
- 先向服务端传递该文件的相关信息,比如文件大小、文件名以及要做的操作等,这时就要用到 send
- 但是又有可能与下一个 send 发生粘包现象,因此还要使用 struct 模块。
2. 下一步,客户端要开始向服务端传文件,大概步骤:
- 打开文件
- 读取文件内容:按照固定字节读取,比如该文件有 5000 个字节,每次向服务端发送 1024 个字节,这样的话要 send 五次,但是不用担心粘包问题
3. 最后,服务端的接收步骤:
- 先接收 4 字节,知道了文件信息的长度
- 按照长度接收文件信息
- 从文件信息中得到文件的大小
- 开始接收,直到收完文件大小这么多的数据
4. 按照上面的思路,先写 client.py 的内容
import os
import json
import struct
import socket
# 假设同一目录下有个视频文件
# 注意:r 表示字符串里面的内容写的什么就是什么
# 不会被 Python 误以为比如 是转义字符
file_path = r"G:learntest.mp4"
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)
print(file_size) # 143799118
print(file_name) # test.mp4
file_info = {"filesize": file_size, "filename": file_name, "operate": "upload"}
json_info = json.dumps(file_info)
file_info_bytes = json_info.encode()
bytes_len = len(file_info_bytes)
sk = socket.socket()
sk.connect(("127.0.0.1", 8080))
# 先发送文件信息的长度,再发送文件信息
sk.send(struct.pack("i", bytes_len))
sk.send(file_info_bytes)
with open(file_path, "rb") as f:
while filesize > 0:
content = f.read(1024)
filesize -= len(content)
# 这里已经是bytes类型
sk.send(content)
sk.close()
5. 接着写 server.py 的内容
import struct
import socket
import json
sk = socket.socket()
sk.bind(("127.0.0.1", 8080))
sk.listen()
conn, addr = sk.accept()
bytes_len = conn.recv(4)
info_len = struct.unpack("i", bytes_len)[0]
json_info = conn.recv(info_len).decode()
info_dic = json.loads(json_info)
print(info_dic)
# 把接收到的视频文件放在与 server.py 同一目录下
with open(info_dic["filename"], "wb") as f:
while info_dic["filesize"]:
content = conn.recv(1024)
f.write(content)
info_dic["filesize"] -= 1024
conn.close()
sk.close()
6. 这里有问题,如果把这里的『 content = conn.recv(1024) 』的 1024 和 client.py 里的 1024 都改为 2048 或者更大的数,运行结束后发现查看该文件时文件大小变小了。原因是这里虽然指定每次接收 1024 个字节,但不代表每次一定就能接收到 1024 个字节,比如最后还剩 200 多字节,就会发生拆包现象。为了解决这个问题,可以这样:
# server.py
import struct
import socket
import json
sk = socket.socket()
sk.bind(("127.0.0.1", 8080))
sk.listen()
conn, addr = sk.accept()
bytes_len = conn.recv(4)
info_len = struct.unpack("i", bytes_len)[0]
json_info = conn.recv(info_len).decode()
info_dic = json.loads(json_info)
print(info_dic)
with open(info_dic["filename"], "wb") as f:
while info_dic["filesize"]:
content = conn.recv(1024)
f.write(content)
# info_dic["filesize"] -= 1024
info_dic["filesize"] -= len(content)
conn.close()
sk.close()
UDP 协议基本用法
- 还记得之前发的 TCP 协议与 UDP 协议的流程图吗?再来回顾一下,看看两者之间的区别
2. server.py 内容
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(("127.0.0.1", 8080))
# 这里不用写sk.listen(),没有三次握手
msg, client_addr = sk.recvfrom(1024)
print(msg) # b'hello'
sk.sendto(b"hello", client_addr)
sk.close()
3. client.py 的内容
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.sendto(b"hello", ("127.0.0.1", 8080))
msg, addr = sk.recvfrom(1024)
print(msg) # b'hello'
sk.close()
使用 UDP 协议实现多人聊天功能
- server.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(("127.0.0.1", 8080))
# 和一个人多次聊天
while True:
msg, client_addr = sk.recvfrom(1024)
print(msg.decode())
content = input(">>>")
sk.sendto(content.encode(), client_addr)
sk.close()
2. client.py
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
# 和一个人多次聊天
# 可以再创建一个client.py,内容一模一样,然后依次运行
# 都可以跟server端聊天
while True:
content = input(">>>")
# 多人聊天时,可以这样区分谁跟server端聊天
content = "%s : %s" % ("小明", content)
sk.sendto(content.encode(), ("127.0.0.1", 8080))
msg, addr = sk.recvfrom(1024)
print(msg.decode())
sk.close()
验证客户端的合法性 (了解)
- server.py
import os
import hmac
import socket
def auth(conn):
secret_key = b"jane"
rand_b = os.urandom(32)
conn.send(rand_b)
obj = hmac.new(secret_key, rand_b)
res1 = obj.digest()
res2 = conn.recv(1024)
cmp_res = hmac.compare_digest(res1, res2)
return cmp_res
sk = socket.socket()
sk.bind(("127.0.0.1", 8080))
sk.listen()
conn, addr = sk.accept()
res = auth(conn)
if res:
print("是合法的客户端")
conn.send("您好".encode())
else:
conn.close()
sk.close()
2. client.py
import socket
import hmac
def auth(sk):
secret_key = b"jane"
rand_b = sk.recv(32) # 注意这里
obj = hmac.new(secret_key, rand_b)
res2 = obj.digest()
sk.send(res2)
sk = socket.socket()
sk.connect(("127.0.0.1", 8080))
auth(sk)
msg = sk.recv(1024)
print(msg.decode())
sk.close()
3. 运行后得出结果:是合法的客户端。只要两边的secret_key的值不一样,服务端就知道不是合法的客户端
使用 socketserver 来实现 TCP 协议 socket 的并发
- socket是基于socketserver的下层模块,socketserver是基于socket的上层模块,因此 socketserver 可以实现并发
- server.py
import socketserver
class Myserver(socketserver.BaseRequestHandler):
# 注意:有多少个client端,就相当于这里有多少个handle
def handle(self):
# 可以与多个client端运行,即并发
while True:
conn = self.request
conn.send(b"hello")
# 这里不需要实例化一个对象
server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), Myserver)
server.serve_forever()
3. client.py,写几个内容一样的客户端文件,进行测试即可
import socket
sk = socket.socket()
sk.connect(("127.0.0.1", 8080))
while True:
msg = sk.recv(1024)
print(msg)
sk.close()