Python 全栈开发:网络编程
一 socket
1.什么是socket
看一看图中socket的位置
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
二 基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
服务器端
#!/usr/bin/env python3 # encoding: utf-8 # by fixdq import socket # ip,端口 ip_prot = ('127.0.0.1', 8884) # 缓冲区大小 buffer = 1024 # 创建 socket对象 socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定 ip 端口 socket_server.bind(ip_prot) # 服务端开始监听 socket_server.listen(5) while True: # 连接对象,客户端地址(ip:port) conn, address = socket_server.accept() while True: try: # 接收信息 () msg = conn.recv(buffer) # msg 字母变大写 发送个客户端 conn.send(msg.upper()) except ConnectionError: break # 关闭连接 conn.close() # 关闭socket服务 socket_server.close()
客户端
#!/usr/bin/env python3 # encoding: utf-8 # by fixdq import socket # ip,端口 ip_prot = ('127.0.0.1', 8884) # 缓冲区大小 buffer = 1024 # 获取socket socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 根据 ip_port 连接服务器 socket_client.connect(ip_prot) while True: # 接受输入 cmd = input("cmd>>:").strip() # 发送信息 socket_client.send(cmd.encode('utf-8')) # 接受信息 res = socket_client.recv(buffer) # 打印信息 print(res.decode('utf-8')) # 关闭socket socket_client.close()
注意点: tcp协议是基于字节流传输数据的的,所以收发信息的格式都是字节(bytes)格式
三 基于UDP的套接字
服务端
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Author : Fixdq # @File : server.py # @Software: PyCharm import socket # ip,端口 ip_port = ('127.122.114.15', 8080) # 获取socket server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定IP ,端口 server.bind(ip_port) # 循环执行 while True: # 接收信息 res, addr = server.recvfrom(1024) # 打印信息 print(res.decode('utf-8')) server.sendto('收到了'.encode('utf-8'), addr) # 回收资源 server.close()
客户端
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Author : Fixdq # @File : client.py # @Software: PyCharm import socket # IP port ip_port = ('127.122.114.15', 8080) # 创建客户端 client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 循环 while True: # 接收用户输入 msg = input('>>>>>>>:').strip() if not msg: continue # 发送信息 client.sendto(msg.encode('utf-8'), ip_port) # 接收信息 res,addr = client.recvfrom(1024) # 打印信息 print(res.decode('utf-8'),addr) # 回收资源 client.close()
四 粘包现象以及处理
1.什么是粘包现象
须知:只有TCP有粘包现象,UDP永远不会粘包
两种情况下会发生粘包:
----发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
----接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
2.怎么处理粘包
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
解决方案:
自定义报头,为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
需要用到 struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
具体用法,参照官方Doc
https://docs.python.org/3/library/struct.html?highlight=struct#module-struct
3.具体示例
import json,struct #假设通过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header={'file_size':1073741824000,
'file_name':'/a/b/c/d/e/a.txt',
'md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Author : Fixdq # @File : tcp_server.py # @Software: PyCharm import json from socket import * import subprocess import struct ip_port = ('127.155.101.25', 8090) server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(ip_port) server.listen(5) while True: conn, addr = server.accept() while True: cmd = conn.recv(1024) if not cmd: break print('--------->cmd----', cmd.decode('utf-8')) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ret = res.stdout.read() if ret: back = ret else: back = res.stderr.read() # 把真实数据的相关信息,包装成字典 head = { 'len': len(back) } # json 序列化 字典信息 head_str = json.dumps(head) # 只包含报头长度 (报头的报头) # i 模式,产生一个固定4字节长度bytes head_head = struct.pack('i', len(head_str)) # 发送 (报头的报头) conn.send(head_head) # 报头数据 conn.send(head_str.encode('utf-8')) # 真实数据 conn.send(back) conn.close() server.close()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Author : Fixdq # @File : tcp_client.py # @Software: PyCharm import json from socket import * import struct ip_port = ('127.155.101.25', 8090) client = socket(AF_INET, SOCK_STREAM) client.connect_ex(ip_port) while True: cmd = input('>>>>>:').strip() client.send(cmd.encode('utf-8')) # 接收 报头的报头 (固定长度 4 字节) head_head = client.recv(4) # 反解出 报头的长度 head_len = int(struct.unpack('i',head_head)[0]) # 接收 自定义报头 head = client.recv(head_len) # 拿到 自定义报头的长度 body_len = json.loads(head.decode('utf-8'))['len'] # 接收 真实的数据 data = client.recv(body_len) print(data.decode('utf-8')) client.close()
自定义报头总结:
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容