Day9 - Python基础9 socket基础、粘包
本节内容:
1.socket的介绍
2.基于tcp的socket
3.基于tcp的问题分析
4.基于udp的socket
5.基于udp的问题分析
6.基于udp的ntp服务
7.基于tcp的远程执行命令服务
8.粘包
9.粘包的两种解决办法
1.socket的介绍
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求
注意的一点:socket 的应用程序在内存的用户空间中,而操作系统在内存空间中,socket的应用程序要想使用网卡(硬件) 必须通过操作系统间接去内存的缓冲区去收发数据。也就是说socket的 收 --》去缓冲区拿 发---》送到缓冲区,跟socket有关的就是缓冲区了。
2.基于tcp的socket
####server端###### ss = socket() #创建服务器套接字 ss.bind() #把地址绑定到套接字 ss.listen() #监听链接 inf_loop: #服务器无限循环 cs = ss.accept() #接受客户端链接 comm_loop: #通讯循环 cs.recv()/cs.send() #对话(接收与发送) cs.close() #关闭客户端套接字 ss.close() #关闭服务器套接字(可选) ###client端##### cs = socket() # 创建客户套接字 cs.connect() # 尝试连接服务器 comm_loop: # 通讯循环 cs.send()/cs.recv() # 对话(发送/接收) cs.close() # 关闭客户套接字
3.tcp的一些问题分析
0:实现循环收发数据 (采用循环) 1:客户输了一个 回车 导致 都阻塞在recv阶段 分析: 收发都是去内存的缓存区去取数据,而你发送了空 取不到数据呀,自然就服务端就阻塞在了recv, 而对于客户端他发送了空数据后,下一步就接受来自服务端的数据。而你的服务端还没收到数据, 怎么给你客户端发数据,自然也阻塞住了。 解决办法:加上一个if 条件判断,当用户输入为空时,continue 2:客户端的强制关闭 导致服务端发生错误 分析: 我服务端recv得到的是data 和conn 你把conn干掉,服务端的recv肯定报错呀 解决办法:try except 当出现错误,就break退出收发循环,并关闭客户端的链接 3:客户端正常关闭close,导致了服务端一直循环 解决办法:服务端加个if not data:break 退出通信循环 4:现在的服务器只能接收一个accept 我要接收多个客户端。 解决办法:while true 循环接收accept 接收到的链接都被挂起来了。
4:有的同学在重启服务端时可能会遇到
这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)
解决方法:
方法一:
#加入一条socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决, vi /etc/sysctl.conf 编辑文件,加入以下内容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 然后执行 /sbin/sysctl -p 让参数生效。 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
4.基于UDP的socket
######udp服务端####### ss = socket() #创建一个服务器的套接字 ss.bind() #绑定服务器套接字 inf_loop: #服务器无限循环 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送) ss.close() ########udp客户端######## cs = socket() # 创建客户套接字 comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收) cs.close() # 关闭客户套接字
通过以上代码:我们可以看出服务端少了一个listen的过程,那么分析下为什么需要listen?
因为tcp是基于一个双向链接的协议,当多个客户端来之后 只会为其中的一个客户端提供服务,其他的客户端都处于链接挂起的状态。那udp没有链接啊,自然就没有listen 和 accept的过程。
5.udp套接字的一些问题分析:
1:接收和发送问题 收recvfrom() (收到的是一个元祖的格式,第一个是发送过来的数据 第二个又是个客户端的套接字元祖 ); 发sendto (数据,服务端的套接字); [因为你udp没有链接了呀,所以每次发送数据需要带上服务端的套接字] 2:实现自由收发信息 (while循环) 3:udp 发送回车,可以接受到数据;(recv默认从缓存区拿不到数据就阻塞住,而recvfrom默认可以从缓存区拿到空数据) 问题保留?等会解答 4:tcp和udp一个服务端,对应多个客户端,udp可以实现并发 因为他本根不就需要建立连接。
6.基于udp的ntp服务实现
from socket import * import time ip_port = ('127.0.0.1',9009) buffer_size = 1024 sk_server = socket(AF_INET,SOCK_DGRAM) sk_server.bind(ip_port) while True: print("服务端开启..") data,addr = sk_server.recvfrom(buffer_size) print("客户端的地址:",addr) if not data: back_data = time.strftime('%Y-%m-%d %H:%M:%S').encode('utf-8') else: data = str(data,'utf-8') print("收到服务的命令:", data) back_data = time.strftime(data).encode('utf-8') sk_server.sendto(back_data,addr)
import socket ip_port = ('127.0.0.1',9009) cs = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: input_data = input('请输入你的数据:').encode('utf-8') if input_data == 'quit':break cs.sendto(input_data,ip_port) data,addr = cs.recvfrom(1024) data = data.decode('utf-8') print("服务端返回的命令:",data)
客户端的输出: 请输入你的数据: 服务端返回的命令: 2018-03-13 11:04:02 请输入你的数据:%Y 服务端返回的命令: 2018 请输入你的数据:%Y-%m 服务端返回的命令: 2018-03 请输入你的数据:
7.基于tcp的远程执行命令服务
from socket import * import subprocess import struct ip_port = ('127.0.0.1',9014) buffer_size = 1024 sk_server = socket(AF_INET,SOCK_STREAM) sk_server.bind(ip_port) sk_server.listen(5) while True: print("服务端开启..") conn,addr=sk_server.accept() print("客户端的地址:",addr) while True: try: data = conn.recv(buffer_size).decode('utf-8') print("收到服务的命令:",data) back_cmd = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE,stdin=subprocess.PIPE) err_cmd = back_cmd.stderr.read() if not err_cmd: ##如果命令执行成功 cmd_back = back_cmd.stdout.read() if not cmd_back: ##如果是执行命令cd .. 默认,客户端从缓冲区是拿不到数据的,给个默认的返回 cmd_back = '执行成功'.encode('gbk') else: cmd_back = back_cmd.stderr.read() ###直接发送 默认会两个一起发送,不过长度固定占4个字节 conn.send(struct.pack('i',len(cmd_back))) conn.send(cmd_back) except Exception as e : print(e);break conn.close()
import socket import struct sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sk.connect(('127.0.0.1',9014)) while True: input_data = input('请输入你的数据:') if not input_data:continue if input_data == 'quit':break ##发送命令 sk.send(input_data.encode('utf-8')) ##接收固定占的长度4个字节 cmd_len = sk.recv(4) cmd_int_len = struct.unpack('i',cmd_len)[0] recv_size = 0 recv_msg = b'' while recv_size < cmd_int_len: recv_msg += sk.recv(1024) recv_size = len(recv_msg) print("服务端返回的命令:",recv_msg.decode('gbk'))
8.粘包
udp是不存在粘包的,那么粘包的产生是因为:
1:我们客户端定义了recv(1024) 默认接收1024个字节, 2:而服务端一次发送大于1024字节的数据 到客户端的缓冲区, 3:客户端从缓冲区一次只取1024字节,剩下的数据就只能留着下次再去, 4:这就是所谓的粘包
粘包的进一步产生原因分析:
粘包1:TCP有网络延迟,采用使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 粘包2:数据量发送的大,接收的少,再接收的话就是上次未接收完的数据。
为什么udp不存在粘包?
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。 而udp是一个消息一个消息的发送,一次recvfrom 可以接收到数据加ip、端口的元组, 那么我只要找到ip_and_port的位置 到下一个消息ip_and_port ,中间的位置都是需要的数据, 这样我就知道了我一次需要提取多少个字节。因为消息跟消息之间存在了界限
9.解决粘包
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
low版本的解决方法
服务端 客户端 发要要发送的数据长度 接收要接收数据的长度 接收一个ready【主要避免TCP的Nagle算法】 发送一个数据 ready 发送 要发送的数据 while 判断,接收数据
import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客户端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True,\ stdin=subprocess.PIPE,\ stderr=subprocess.PIPE,\ stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) ###发送数据长度 data=conn.recv(1024).decode('utf-8') ##接收客户端数据,为了解决粘包 if data == 'recv_ready': conn.sendall(ret) conn.close()
import socket,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))
为何low:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
前戏:将命令的长度 转为固定的4个字节
关于:struct 模块的使用 移步:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
>>> import subprocess >>> cmd_b = subprocess.Popen('ipconfig',shell=True,stdout=subprocess.PIPE,stderr =subprocess.PIPE) >>> data = cmd_b.stdout.read() >>> len(data) ##获取执行命令结果的长度是多少 1799 >>> cmd_len = struct.pack('i',len(data)) ##将命令的长度转为对应的占4个字节 >>> cmd_len b'\x07\x07\x00\x00' >>> struct.unpack('i',cmd_len) (1799,) >>> struct.unpack('i',cmd_len)[0] ##将4个字节,转为int长度 1799 >>> len(data) 1799 >>>
实现:
from socket import * import subprocess import struct ip_port = ('127.0.0.1',9014) buffer_size = 1024 sk_server = socket(AF_INET,SOCK_STREAM) sk_server.bind(ip_port) sk_server.listen(5) while True: print("服务端开启..") conn,addr=sk_server.accept() print("客户端的地址:",addr) while True: try: data = conn.recv(buffer_size).decode('utf-8') print("收到服务的命令:",data) back_cmd = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE,stdin=subprocess.PIPE) err_cmd = back_cmd.stderr.read() if not err_cmd: ##如果命令执行成功 cmd_back = back_cmd.stdout.read() if not cmd_back: ##如果是执行命令cd .. 默认,客户端从缓冲区是拿不到数据的,给个默认的返回 cmd_back = '执行成功'.encode('gbk') else: cmd_back = back_cmd.stderr.read() ###直接发送 默认会两个一起发送,不过长度固定占4个字节 conn.send(struct.pack('i',len(cmd_back))) conn.send(cmd_back) except Exception as e : print(e);break conn.close()
1 import socket 2 import struct 3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 5 sk.connect(('127.0.0.1',9014)) 6 7 while True: 8 input_data = input('请输入你的数据:') 9 if not input_data:continue 10 if input_data == 'quit':break 11 12 ##发送命令 13 sk.send(input_data.encode('utf-8')) 14 15 ##接收固定占的长度4个字节 16 cmd_len = sk.recv(4) 17 cmd_int_len = struct.unpack('i',cmd_len)[0] 18 19 recv_size = 0 20 recv_msg = b'' 21 while recv_size < cmd_int_len: 22 recv_msg += sk.recv(1024) 23 recv_size = len(recv_msg) 24 25 print("服务端返回的命令:",recv_msg.decode('gbk'))