socket 套接字
1: socket 以及 小内容补充
1. socket : 又称为套件字,在python中使用需要导入这个模块(import socket)
2. 本机回环地址: (127.0.0.1) 只能自己识别自己,其他人无法访问
2: 基于TCP协议的socket使用
由于TCP协议都是基于链接的,所以必须要先启用服务端,再启动客户端
服务端:
import socket # 导入socket模块 sk = socket.socket() # 创建一个socket对象 sk.bind(('127.0.0.1',8080)) # 绑定ip和端口,注意: bind方法只接收一个参数,所以里面要将ip和port先到一个元组里面 sk.listen(5) # 开始监听 注意:listen方法后面跟的int类型,表示待连接的最大监听个数,不写默认最大 conn,addr = sk.accept() # 阻塞, accept接收一个元组,conn是一个链接,也就是建立起来一个TCP的全双工,addr是监听到客户端的地址 ret = conn.recv(1024) # 阻塞 ,接收客户端消息,接收1024个字节,可以改变,推荐为1024的倍数,不可超出当前可用内存 print(ret) # 打印客户端发来的信息 conn.send(b'hi') # 向客户端发送信息,注意发送的数据必须先转为二进制 conn.close() # 断开与客户端的链接 sk.close() # # 关闭服务器套接字, 可以不写,因为服务器通常都是24小时提供服务的
客户端:
import socket sk = socket.socket() # 创建客户端套接字对象 sk.connect(('127.0.0.1',8080)) # 连接服务器,connect 绑定要访问的服务器的ip+port 元组形式 sk.send(b'hello') # 给服务器发送数据 ret = sk.recv(1024) # 接收服务器返回的数据 print(ret) sk.close() # 关闭客户端的套接字
由于再操作时候由于刚才使用端口操作系统没有及时没有回收,再使用时后会报错:
Traceback (most recent call last): File "D:/py/网络编程/blog/基于TCP协议的socket使用/server.py", line 4, in <module> sk.bind(('127.0.0.1',8080)) # 绑定ip和端口,注意: bind方法只接收一个参数,所以里面要将ip和port先到一个元组里面 OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
我们可以这样处理:
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 加入这个配置,重用ip和端口
如
import socket # 导入socket模块 sk = socket.socket() # 创建一个socket对象 sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 加入这个配置,重用ip和端口 sk.bind(('127.0.0.1',8080)) # 绑定ip和端口,注意: bind方法只接收一个参数,所以里面要将ip和port先到一个元组里面 sk.listen(5) # 开始监听 注意:listen方法后面跟的int类型,表示待连接的最大监听个数,不写默认最大 conn,addr = sk.accept() # 阻塞, accept接收一个元组,conn是一个链接,也就是建立起来一个TCP的全双工,addr是监听到客户端的地址 ret = conn.recv(1024) # 阻塞 ,接收客户端消息,接收1024个字节,可以改变,推荐为1024的倍数,不可超出当前可用内存 print(ret) # 打印客户端发来的信息 conn.send(b'hi') # 向客户端发送信息,注意发送的数据必须先转为二进制 conn.close() # 断开与客户端的链接 sk.close() # # 关闭服务器套接字, 可以不写,因为服务器通常都是24小时提供服务的
练习: client端:每隔10秒把当前时间的时间戳发送给客户端 server端:将client端发来的时间戳转化成格式化时间返回给client端
练习server端:
import socket import time sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while 1: ret = conn.recv(1024).decode('utf-8') print(ret) res = float(ret) ret = time.localtime(res) my_time = time.strftime('%Y-%m-%d %H:%M:%S',ret) conn.send(my_time.encode('utf-8'))
练习client端:
import socket import time sk = socket.socket() sk.connect(('127.0.0.1',8080)) while 1: time.sleep(10) sk.send(bytes(str(time.time()).encode('utf-8'))) ret = sk.recv(1024).decode('utf-8') print(ret)
3: 黏包:主要是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
1: 基于TCP的传输当接收的数据小于对方发送的数据后,得到的数据是不完整的,再次发送请求时接收到的不是当前需要的数据而是上次未接收完的数据,如:
服务端:
import socket import subprocess sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(('127.0.0.1',8080)) sk.listen() while 1: conn,addr = sk.accept() while True: try: cmd = conn.recv(1024).decode('utf-8') if cmd == 0: break obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) ret = obj.stdout.read() + obj.stderr.read() conn.send(ret) except ConnectionResetError: break conn.close()
客户端:
import socket sk = socket.socket() sk.connect(('127.0.0.1',8080)) while 1: cmd = input('>>>') if len(cmd) == 0: continue sk.send(cmd.encode('utf-8')) ret = sk.recv(1024).decode('gbk') print(ret)
2:TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据.如:
服务端:
import socket sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() res = conn.recv(1024) print(res) # b'hello jayhello tomhello rose' res1 = conn.recv(1024) print(res1) # b'' res2 = conn.recv(1024) print(res2) # b'' conn.close() sk.close()
客户端:
import socket sk = socket.socket() sk.connect(('127.0.0.1',8080)) sk.send(b'hello jay') sk.send(b'hello tom') sk.send(b'hello rose') sk.close()
4: 解决黏包的办法:可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了,如:
服务端:
import socket import json import struct import subprocess sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(('127.0.0.1',8080)) sk.listen() while 1: conn,addr = sk.accept() while 1: try: cmd = conn.recv(1024).decode('utf-8') if len(cmd) == 0: # 针对在linux,mac系统中客户端异常退出会一直接收到空 break obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) ret = obj.stdout.read() + obj.stderr.read() # 先制作一个发送给客户端的字典 d = {'name':'小明','size':len(ret)} # 为了能将字典发送.将字典序列化 json_d = json.dumps(d) # 制作字典的报头,导入struct模块将字典打包成固定长度 len_d = struct.pack('i',len(json_d)) # 发送报头 conn.send(len_d) # 发送字典 conn.send(json_d.encode('utf-8')) # 发送真实数据 conn.send(ret) # 避免客户端强制中断连接报错 except ConnectionResetError: break conn.close()
客户端:
import socket import json import struct sk = socket.socket() sk.connect(('127.0.0.1',8080)) while 1: cmd = input('>>>').strip().encode('utf-8') if len(cmd) == 0: continue sk.send(cmd) # 接收字典的报头 len_d = sk.recv(4) # 解析拿到字典数据的真实长度 len_d = struct.unpack('i',len_d)[0] # 接收字典,并将其反序列化 json_d = sk.recv(len_d).decode('utf-8') d = json.loads(json_d) info_len = 0 with open('test','w',encoding='utf-8') as f: while info_len < d.get('size'): # 接收真实数据并循环写到文件中.避免文件过大内存溢出 info = sk.recv(1024) # window系统默认GBK编码 f.write(info.decode('gbk')) info_len += len(info)
5: struct 模块的使用:该模块可以把一个类型,如数字,转成固定长度的bytes
1: struct.pack : 将一个类型打包成固定长的的bytes
import struct d = {'name':'xiaoming','age':18} print(len(d)) # 2 ret = struct.pack('i',len(d)) print(len(ret)) # 4
2: struct.unpack: 将打包的解析成原先的长度
import struct d = {'name':'xiaoming','age':18} print(len(d)) # 2 ret = struct.pack('i',len(d)) print(len(ret)) # 4 res = struct.unpack('i',ret)[0] # 注意返回一个元组,索引0就时他的长度 print(res) # 2
3.它是可以选择打包模式的.如'i'对应的就时打包成4个字节
6: 使用socket 客户端上传文件到服务端实例
服务端:
import socket import os import json import struct sk = socket.socket() sk.connect(('127.0.0.1',8080)) # 将上传文件目录的路径存到变量中 BASE_DIR = 'D:\学习资料\day28\视频' # 获取当前目录下的所有文件 dir_list = os.listdir(BASE_DIR) while 1: # 循环打印选择要上传的文件 for index,name in enumerate(dir_list,1): print(index,name) choice = input('请输入上传文件的序号').strip() if not choice.isdigit() or not (int(choice)-1 in range(len(dir_list))): continue name = dir_list[int(choice)-1] # 拼接路径 path = os.path.join(BASE_DIR,name) # 获取文件的大小 size = os.path.getsize(path) # 定义存储文件信息的字典 d = {'name':name,'size':size} # 序列化字典 json_d = json.dumps(d) # 制作字典的报头 len_d = struct.pack('i',len(json_d)) # 发送报头 sk.send(len_d) # 发送字典 sk.send(json_d.encode('utf-8')) # 循环发送真实数据.避免内存溢出 with open(path,'rb') as f: for line in f: sk.send(line)
客户端:
import socket import struct import json sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(('127.0.0.1',8080)) sk.listen(5) while 1: conn,addr = sk.accept() while 1: try: len_d = conn.recv(4) if len_d == 0: break len_d = struct.unpack('i',len_d)[0] d = json.loads(conn.recv(len_d).decode('utf-8')) recv_len = 0 with open(d.get('name'),'wb') as f: while recv_len < d.get('size'): info = conn.recv(1024) f.write(info) recv_len += len(info) except ConnectionResetError: break
7: 基于UDP协议的socket使用
客户端:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) IP_PORT = ('127.0.0.1',8080) while True: msg = input('>>>>').encode('utf-8') sk.sendto(msg,IP_PORT) info,addr = sk.recvfrom(1024) print(info.decode('utf-8'))
服务端:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 指定基于UDP协议,type默认TCP协议 sk.bind(('127.0.0.1',8080)) # 绑定IP和端口号 while True: info,addr = sk.recvfrom(1024) # recvfrom 接收一个元组,内容与对方的ip+port print(info.decode('utf-8')) msg = input('>>>>').encode('utf-8') sk.sendto(msg,addr) # sendto 要发送给对方的消息,以及指定对方的地址
UDP协议的4个特点:
1: udp协议客户端允许发空
2: udp协议不会黏包
3: udp协议负端不存在的情况下,客户端照样不会报错
3: udp协议支持并发
8: socketserver模块:可以使服务器实现并发
import socketserver class MyServer(socketserver.BaseRequestHandler): # 必须继承 def handle(self): # 必须定义此方法 while 1: msg = self.request.recv(1024).decode('utf-8') print(msg) info = input('>>>>').encode('utf-8') self.request.send(info) if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) # 实现并发,参数1:ip+port 参数2:自定义类 server.serve_forever() # 永远启动
9: 怎么看源码:
1:多个类之间的继承关系要先整理
2: 每一个类中有哪些方法要大致列出来
3: 所有的self对象调用要清楚的了解,到底使谁的对象
4. 所有的方法调用要退回最子类的类中开始寻找.逐级向上