参考点击打开链接
Socket收发消息原理
发送端将要发送的信息存进缓存中,然后通过物理层,传输到接收端的缓存中,当程序要读取信息时再从自己的缓存中取。
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
须知:只有TCP有粘包现象,UDP永远不会粘包
TCP协议是面向连接的,面向流的,提供高可靠性服务。它会将间隔较小且数据量较小的数据进行封包发送造成接收端无法分辨消息的边界。即面向流的通信是无消息保护边界的。
UDP协议是是无连接的,面向消息的,提供高效率服务。它为每个消息打包了一个消息头,这样接收端就可以分辨出消息的边界。即面向消息的通信是有消息保护边界的。
两张粘包情况
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
服务端
from socket import *
ip_port=('218.197.223.92',800)#本机服务器的地址与端口
back_log=5 #半链池大小
buffer_size=1024 #接收字节数
tcp_serve=socket(AF_INET,SOCK_STREAM)#初始化socket,AF_INET是基于网络模型的套接字家族,SOCK_STREAM是数据流模式
tcp_serve.bind(ip_port) #绑定IP与端口
tcp_serve.listen(back_log)
conn,addr=tcp_serve.accept()
data1=conn.recv(buffer_size)
print('====>',data1)
data2=conn.recv(buffer_size)
print('===>',data2)
客户端
from socket import *
ip_port=('218.197.223.92',800) #要连接的服务器的IP与端口
buffer_size=1024 #要读取的字节数
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port) #连接服务器
tcp_client.send(b'hello')
tcp_client.send(b'world')
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
服务端
from socket import *
ip_port=('218.197.223.92',800)#本机服务器的地址与端口
back_log=5 #半链池大小
tcp_serve=socket(AF_INET,SOCK_STREAM)#初始化socket,AF_INET是基于网络模型的套接字家族,SOCK_STREAM是数据流模式
tcp_serve.bind(ip_port) #绑定IP与端口
tcp_serve.listen(back_log)
conn,addr=tcp_serve.accept()
data1=conn.recv(2)
print('====>',data1)
data2=conn.recv(6)
print('===>',data2)
客户端
from socket import *
ip_port=('218.197.223.92',800) #要连接的服务器的IP与端口
buffer_size=1024 #要读取的字节数
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port) #连接服务器
tcp_client.send(b'hello world')
UDP就不会出现这种情况
服务端
from socket import *
ip_port=('218.197.223.92',800)
buffer_size=1024
udp_serve=socket(AF_INET,SOCK_DGRAM)#创建基于UDP的服务器套接字,SOCK_DGRAM为数据报模式
udp_serve.bind(ip_port) #绑定主机地址与端口
data1=udp_serve.recvfrom(buffer_size)
print('===>',data1)
data2=udp_serve.recvfrom(buffer_size)
print('===>',data2)
客户端
from socket import *
ip_port=('218.197.223.92',800)
buffer_size=1024
udp_client=socket(AF_INET,SOCK_DGRAM)
udp_client.sendto(b'hello',ip_port)
udp_client.sendto(b'world',ip_port)
两张协议的优缺点:
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
粘包的解决
服务端
from socket import *
import subprocess
'''远程执行命令'''
ip_port=('218.197.223.92',800)
back_log=5
buffer_size=1024
tcp_shell_serve=socket(AF_INET,SOCK_STREAM)
tcp_shell_serve.bind(ip_port)
tcp_shell_serve.listen(back_log)
while True:
print('开始连接...')
conn,addr=tcp_shell_serve.accept()
print('连接到客户端:%s'%addr[0])
while True:
try:
cmd=conn.recv(buffer_size)
if not cmd:break #当客户端正常断开时传入为空
res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #运行命令提示符并执行指令 ,并把输出,异常和输入存在subprocess的管道(PIPE)中
stdout=subprocess.PIPE, #输出
stdin=subprocess.PIPE, #输入
stderr=subprocess.PIPE) #异常
err=res.stderr.read() #由管道读出异常
if err: #若存在异常则返回异常
res_cmd=err
else:
res_cmd=res.stdout.read() #否则返回输出结果(输出结果为二进制,编码方式由系统决定,Windowns为gbk编码)
if not res_cmd:
res_cmd='执行成功'.encode('gbk')
'''解决粘包情况'''
length=len(res_cmd) #得到要传输的长度
conn.send(str(length).encode('gbk'))#将长度传给客户端
sign=conn.recv(buffer_size) #为了防止小数据与下面的数据发生合并的粘包情况,让客户端传过来一个消息
if sign==b'ready':
conn.send(res_cmd) #把结果发送给客户端
except Exception: #当客户端非正常断开时会触发异常
print('客户端%s断开连接'%addr[0])
break
tcp_shell_serve.close()
客户端
from socket import *
ip_port=('218.197.223.92',800) #要连接的服务器的IP与端口
buffer_size=1024 #要读取的字节数
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port) #连接服务器
#与服务器持续对话
while True:
cmd=input('>>:').strip()
if not cmd:continue #当msg为空时,数据不会发送出去,服务器也就收不到信息,会导致程序阻塞
if cmd=='quit':break
tcp_client.send(cmd.encode('utf-8'))
'''解决粘包情况'''
data_length=tcp_client.recv(buffer_size) #得到要接收的数据的长度
tcp_client.send(b'ready') #为防止粘包给服务端发一个消息
length=int(data_length.decode('gbk'))
res_n=0
res_cmd=b''
while res_n<length: #用循环一直从缓存里取值,直到满足长度为止
res_cmd+=tcp_client.recv(buffer_size)
res_n=len(res_cmd)
print('收到的信息为',res_cmd.decode('gbk'))
tcp_client.close() #关闭客户端