socket是应用层和传输层中的tcp/udp协议族通信的中间抽象层,它是一组接口.在设计模式中,socket其实就是一个门面模式,它把复杂的协议隐藏在socket接口好眠,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议.如下图:
套接字在TCP和UDP协议中的使用流程
在python中使用socket只需要导入socket模块即可
一个简单的socket版本
1 import socket 2 3 client = socket.socket() # 生成一个套接字 4 client.connect(('127.0.0.1',8080)) # 绑定服务器的地址 5 6 client.send(b'hello') # 发消息 7 res = client.recv(1024) # 接收消息 8 print(res) # 打印服务端接受数据 9 10 client.close() # 关闭套接字
1 import socket 2 3 client = socket.socket() # 创建套接字 4 client.connect(('127.0.0.1',8080)) # 连接服务器 5 6 while True: 7 msg = input('>>>:') 8 client.send(msg.encode('utf-8')) # 发送信息到服务端 9 if msg == 'exit': 10 break 11 res = client.recv(1024).decode('utf-8') # 从服务端接受信息 12 if res == 'exit':break 13 print(res) # 打印接受到的信息 14 15 16 client.close() # 关闭套接字
socket使用注意:
127.0.0.1是本机回环地址,只能自己识别自己,其他人无法访问,可以用来测试代码
客户端和服务端的send和recv要对应出现,不能出现两边都是相同的情况
socket是基于网络传输协议工作的,所以传输和接收的都只能是二进制数据,所以数据一定要转换为二进制
通信循环以及代码健壮性补充
通信循环就是可以让客户端和服务端进行多次通信,并设置一个条件,触发这个条件就退出连接
在交互的过程中会存在各种问题,比如发空信息会导致
1 import socket 2 3 client = socket.socket() # 生成一个套接字 4 client.connect(('127.0.0.1',8080)) # 绑定服务器的地址 5 6 while True: 7 msg = input('>>>:') 8 if len(msg) == 0: 9 continue 10 client.send(msg.encode('utf-8')) # 发消息 11 if msg == 'exit': 12 print('退出程序') 13 break 14 res = client.recv(1024).decode('utf-8') # 接收消息 15 if res == 'exit': 16 print('退出程序') 17 break 18 print(res) # 打印服务端接受数据 19 20 client.close() # 关闭套接字
1 import socket 2 3 server = socket.socket() # 生成一个套接字(买手机) 4 5 server.bind(('127.0.0.1',8080)) # 绑定ip地址和端口号(插电话卡) 6 server.listen() # 监听客户请求 半连接池(开机) 7 8 conn, addr = server.accept() # 等待客户连接(等待接听电话) 9 10 while True: 11 # 异常捕获,在客户端强制退出之后,服务端会报错 12 try: 13 res = conn.recv(1024).decode('utf-8') # 接收客户的发送(听对方说话) 14 if len(res) == 0:break # 在mac和linux系统中,客户端强制退出不会报错,只会发空,所以如果为空,直接退出 15 if res == 'exit': 16 print('退出程序') 17 break 18 print(res) # 打印客户发送 19 msg = input('>>>:') 20 if len(msg) == 0: 21 continue 22 conn.send(msg.encode('utf-8')) # 给客户发消息(说话),只能发送二进制数据 23 if msg == 'exit': 24 print('退出程序') 25 break 26 except ConnectionResetError as e: # 报错信息:[WinError 10054] 远程主机强迫关闭了一个现有的连接。 27 print(e) # 打印报错信息并退出 28 break 29 30 conn.close() # 关闭通信连接 31 server.close() # 关闭套接字
连接循环就是可以跟多个客户端交互,但一次只能和一个客户端交互,只有在这个客户端交互完成之后才能跟下一个客户端交互.在等待客户连接的代码块前加上一个while True循环就能实现
半连接池规定了交互以及等待交互的客户端的数量,等待交互的客户端不能大于半连接池,否则会直接报错
如下图,半连接池的数值是5,但是等待交互的客户端超过了5个,直接报错
TCP协议会将数据量较小的并且间隔时间较短的数据一次性打包发给对方,这就会发生粘包问题.
粘包问题是由于TCP协议本身造成的,TCP为了提高传输效率,往往都需要收集到足够的数据之后才会发送出去,当多次发送的数据一次性的发送出去后,接收方不知道这些数据从哪里开始,从哪里结束,这些数据就是粘包数据.
粘包问题
1 import socket 2 3 client = socket.socket() 4 client.connect(('127.0.0.1',8080)) 5 6 client.send(b'hello') # 客户端连续发送了三条数据 7 client.send(b'world') 8 client.send(b'happy') 9 10 client.close()
1 import socket 2 3 server = socket.socket() 4 server.bind(('127.0.0.1',8080)) 5 server.listen(5) 6 7 conn,addr = server.accept() 8 9 res = conn.recv(1024) # 只会在第一条数据收到 10 print(res) # 打印了三个发送信息 b'helloworldhappy' 11 res = conn.recv(1024) # 收到的是空 12 print(res) # b'' 13 res = conn.recv(1024) 14 print(res) # b'' 15 16 conn.close() 17 server.close()
struct模块
解决粘包问题需要引入一个struct模块,他能帮助我们存储数据的长度,并打包成一个4位或者8位的数据,解包之后就能获得原数据长度
1 import struct 2 3 t = 'asdasfasfasf43243421321safsafase23fsaasfsafas' 4 print(len(t)) 5 6 res1 = struct.pack('i',len(t)) # 打包数据 7 print(len(res1)) # 在i模式下,打包的数据始终都只有4位 8 9 res2 = struct.unpack('i',res1)[0] # 解包数据 10 print(res2) # 解包之后能得到原始数据的长度
当传输的数据过大时,struct模块并不能帮我们有效的存储数据,这时候我们可以声明一个字典,把数据存储在字典中,再把字典的数据传输进去,这样原数据就算非常大,在字典中也只是一个数字,并不会占用很大的空间
字典存储
1 import struct 2 3 # t = 'asdasfasfasf43243421321safsafase23fsaasfsafas' 4 # print(len(t)) 5 6 # 当传输的数据过大时,我们就不能使用len的方法 7 d = { 8 'name': 'sxc', 9 'size': 15646545656465111111146546565 10 } 11 import json 12 json_d = json.dumps(d) 13 print(len(json_d)) 14 15 16 res1 = struct.pack('i',len(json_d)) # 打包数据 17 print(len(res1)) # 在i模式下,打包的数据始终都只有4位 18 19 res2 = struct.unpack('i',res1)[0] # 解包数据 20 print(res2) # 解包之后能得到原始数据的长度
解决粘包问题的方法
服务端:
1.先制作一个发送给客户端的字典
2.制作字典的报头
3.发送字典的报头
4.发送字典
5.发送真实数据
客户端:
1.先接受字典的报头
2.解析拿到字典的数据长度
3.接受字典
4.从字典中拿到真实数据的长度
5.接受真实数据
解决粘包问题的完整版本
1 import json 2 import socket 3 import struct 4 5 client = socket.socket() # 生成一个套接字 6 client.connect(('127.0.0.1',8080)) # 绑定服务端的ip地址和端口 7 8 while True: 9 cmd = input('>>>:') 10 if len(cmd) == 0: 11 continue 12 client.send(cmd.encode('utf-8')) 13 # 1.接收字典报头 14 dict_header = client.recv(4) 15 # 2.解包这个报头,获取字典长度 16 dict_len = struct.unpack('i',dict_header)[0] 17 # 3.接收字典 18 d = client.recv(dict_len) 19 recv_d = json.loads(d) 20 # 4.获取字典中的信息 21 print(recv_d) 22 recv_size = 0 23 recv_data = b'' 24 while recv_size < recv_d.get('size'): 25 date = client.recv(1024) 26 recv_data += date 27 recv_size += len(date) 28 print(recv_data.decode('gbk'))
1 import socket 2 import subprocess 3 import struct 4 import json 5 6 server = socket.socket() # 生成一个套接字 7 server.bind(('127.0.0.1',8080)) # 绑定ip地址和端口 8 server.listen(5) # 监听客户端 9 10 while True: 11 conn, addr = server.accept() # 生成通道 12 while True: 13 try: 14 cmd = conn.recv(1024).decode('utf-8') # 接受用户输入 15 if cmd == b'': break 16 obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 17 res = obj.stdout.read() + obj.stderr.read() 18 d = { 19 'name':'sxc', 20 'size':len(res) 21 } 22 # 1.生成一个字典长度的报头 23 json_d = json.dumps(d) 24 header = struct.pack('i',len(json_d)) 25 # 2.发送这个报头 26 conn.send(header) 27 # 3.发送这个字典 28 conn.send(json_d.encode('utf-8')) 29 # 4.发送这个真实数据 30 conn.send(res) 31 32 except ConnectionResetError: 33 break 34 conn.close()
作业:写一个上传电影的功能
1.循环打印某一个文件夹下面的所有文件
2.用户选取想要上传的文件
3.将用户选择的文件上传到服务器
4.服务端保存该文件
28
1 import socket 2 import json 3 import os 4 5 client = socket.socket() 6 client.connect(('127.0.0.1',8080)) 7 8 9 # 获取选择函数 10 def choose_movie(movie_list): 11 for index, name in enumerate(movie_list): 12 print(index+1, name) 13 choice = input('请输入您想选择的电影编号>>>:').strip() 14 if not choice.isdigit(): 15 print('请输入正确的编号') 16 choice = int(choice) 17 if not (choice > 0 and choice <= len(movie_list)): 18 print('请输入正确的编号') 19 movie_name = movie_list[choice-1] 20 return movie_name 21 22 23 while True: 24 cmd = input('是否想下载电影y/n>>>:').strip() 25 if cmd == 'n': 26 print('退出程序') 27 break 28 elif cmd == 'y': 29 client.send(cmd.encode('utf-8')) 30 else: 31 print('请输入正确的命令') 32 continue 33 movie_list = client.recv(1024).decode('utf-8') # 接收收到的电影列表 34 35 movie_list = json.loads(movie_list) 36 movie_name = choose_movie(movie_list) 37 38 client.send(movie_name.encode('utf-8')) # 发送选择 39 40 movie_dict = client.recv(1024).decode('utf-8') 41 movie_dict = json.loads(movie_dict) 42 print(movie_dict) 43 recv_size = 0 44 45 path = os.path.dirname(__file__) 46 save_path = os.path.join(path,movie_name) 47 with open(save_path, 'wb') as f: 48 while recv_size < movie_dict.get('size'): # real_size = 102400 49 data = client.recv(1024) 50 # print(len(data)) 51 f.write(data) 52 recv_size += len(data) 53 client.close()
1 import socket 2 import os 3 import json 4 5 server = socket.socket() # 创建套接字 6 server.bind(('127.0.0.1',8080)) # 绑定ip和端口 7 server.listen() # 侦听客户请求 8 9 conn, addr = server.accept() # 接受客户连接 10 11 data = conn.recv(1024).decode('utf-8') # 接受客户输入 12 13 if data == 'y': 14 path = 'F:迅雷下载' 15 movie_list = os.listdir(path) 16 print(movie_list) 17 movie_list = json.dumps(movie_list) 18 19 conn.send(movie_list.encode('utf-8')) # 输出 20 movie_name = conn.recv(1024).decode('utf-8') # 接受客户输入 21 # print(movie_name) 22 23 movie_path = os.path.join(path, movie_name) 24 size = os.path.getsize(movie_path) 25 movie_dict = { 26 'name': movie_name, 27 'size': size 28 } 29 movie_dict = json.dumps(movie_dict) 30 31 conn.send(movie_dict.encode('utf-8')) # 发送电影字典 32 send_size = 0 33 with open(movie_path,'rb')as f: 34 while send_size < size: 35 data = f.read(1024) 36 conn.send(data) 37 send_size += len(data) 38 print(send_size) 39 else: 40 print('upload successful') 41 conn.close()