为什么要有socket
如何使用socket?
socket的配置
基于TCP协议的socket
基于UDP协议的socket
黏包现象
解决黏包问题(struct)
struct
使用struct解决黏包
Socket概念
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
简而言之,socket是一个模块,又称套接字,用来封装 互联网协议(应用层以下的层)
为什么要有socket
提高开发效率,自动进行应用层以下的层的封装工作,从而提高开发效率。
如何使用socket?
server端
import socket
server = socket.socket() # 调用socket产生一个对象
# localhost为本地回环地址:127.0.0.1,也可以直接写ip
server.bind(('localhost', 9527)) # 把地址绑定到套接字
server.listen(5) # 设置监听链接,也叫半连接池,设置为5,代表当前等待连接数为5,已在服务连接数为1,共计6个连接数
conn, addr = server.accept() # 接收客户端连接,建立简介
data = conn.recv(1024) # 接收客户端信息,1024代表一次可以接收1024bytes的数据(从当前内存中接收),可以自己调控大小
print(data) # 打印客户端信息
conn.send(b'hello') # 向客户端发送信息
conn.close() # 关闭客户端套接字
server.close() # 关闭服务器套接字
client端
import socket
client = socket.socket() # 调用socket产生一个对象
client.connect(('127.0.0.1', 9527)) # 尝试连接服务器
client.send(b'hi') # 向服务器发送信息
data = client.recv(1024) # 接收客户端信息,1024代表一次可以接收1024bytes的数据(从当前内存中接收),可以自己调控大小
print(data)
client.close() # 关闭客户端套接字
TCP是基于连接的,我们在实验的时候,必须先启动服务器,然后再去启动客户端去连接服务端。
socket的配置
注意:客户端发送信息到服务端,服务端必须先接受,才能再发送信息到客户端。
基于TCP协议的socket
服务端
import socket
server = socket.socket()
server.bind(
('localhost', 9527)
)
server.listen(3)
print('服务端正在接收信息'.center(50, '-'))
while True: # 此处循环,可以实现接收多个用户进行访问,就是所谓的循环建立连接
conn, addr = server.accept() # 建立连接
print(addr) # 可以显示对端的ip信息
while True: # 此处循环,实现服务器到客户端的循环通信
try: # 监听代码块是否有异常出现
data = conn.recv(1024) # 接收一次1024的字节
if data.decode('utf-8') is None:
break
if data.decode('utf-8') in ['q', 'Q']:
break
print(data.decode('utf-8'))
# 服务端往客户端发送信息
send_date = input('Server:').strip()
conn.send(send_date.encode('ustf-8'))
# 捕获异常,并打印 res:报错信息,res只是单纯的一个变量名
except Exception as res:
print(res)
break
conn.close() # 关闭连接
客户端
import socket
client = socket.socket()
client.connect(('localhost', 9527))
print('客户端正在接收数据'.center(50, '-'))
while True:
try:
# 向服务端发送请求
send_data = input('Client:').strip()
client.send(send_data.encode('utf-8'))
# 接收服务端请求
data = client.recv(1024)
if len(data) == 0:
break
if data.decode('utf-8') in ['q', 'Q']:
break
print(data.decode('utf-8'))
except Exception as res:
print(res)
break
client.close()
基于UDP协议的socket
server端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close() # 关闭服务器套接字
client端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
udp是无连接的,启动服务之后可以直接接受消息,不需要提前建立链接
黏包现象
黏包问题:
- 无法确认对方发送过来数据的大小
- 在发送数据间隔短并且数据量小的情况下,会将所有的数据一次性发送
模拟问题:
# 服务端代码
import socket
import subprocess
server = socket.socket()
server.bind(('localhost', 9527))
server.listen(5)
while True:
conn, addr = server.accept()
print(f'当前接入的用户IP为:{addr}')
while True:
try:
cmd = conn.recv(10) # 从内存中获取10个字节的数据
if len(cmd) == 0:
continue
cmd = cmd.decode('utf-8') # 将客户端传入过来的字符串解码为utf-8
if cmd in ['q', 'Q']:
break
# 调用subprocess连接终端,对终端进行操作,并获取操作后正确或错误的结果
obj = subprocess.Popen(
cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# 结果交给result变量名
result = obj.stdout.read() + obj.stderr.read()
print(len(result))
# 因为对端的的操作系统是windows,编码为GBK
print(result.decode('gbk'))
# 将结果返回给客户端
conn.send(result)
except Exception as res:
print(res)
break
# 客户端代码
import socket
client = socket.socket()
client.connect(('localhost', 9527))
while True:
cmd = input('>>>')
client.send(cmd.encode('utf-8'))
data = client.recv(100) # 接收100个字符
print(len(data))
print(data.decode('gbk'))
当我们从客户端向服务端发送windows的dir
查看目录的时候:
我们可以发现,我们设置了客户端可以接收100个字符,但是并没有将服务端返回的数据全部接收到,若我们无法确定对端返回的数据大小,就会造成黏包。
看一下服务端那边的信息:
可以发现,dir
命令在服务端执行后,得到的数据量是368,但是客户端却设置了接收100个,因此客户端显示不完整,这个是问题一。
模拟问题二:
服务端代码:
import socket
server = socket.socket()
server.bind(('localhost', 9527))
server.listen(5)
conn, addr = server.accept()
data = conn.recv(10)
print(data)
data = conn.recv(1024)
print(data)
data = conn.recv(1024)
print(data)
客户端
import socket
client = socket.socket()
client.connect(('localhost', 9527))
client.send(b'hello')
client.send(b'hello')
client.send(b'hello')
我们向服务端连续发送多个短和小的信息,观察服务端的接收情况:
可以看到,所有的数据一次性发送了,正是问题二。
解决黏包问题(struct)
无论哪一端先发送数据
- 客户端
- 先制定报头,并发送(struct)
- 发送真实数据
- 服务端
- 接收报头,并解包获取真实数据长度
- 根据真实数据长度,接收真实数据 recv(真实数据长度)
解决方案一
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
解决方案进阶
刚刚的方法,问题在于我们我们在发送
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
struct
struct是一个python内置的模块,它可以将将固定长度的数据,打包成固定格式的长度。
- 模式 i:4
作用:
可以将真实数据,做成一个固定长度多的报头,客户端发送给服务端,服务端可以接收报头,然后对报头进行解包,获取真实数据的长度,进行接收即可。
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
obj = struct.pack('i',123456)
print(len(obj)) # 4
obj = struct.pack('i',898898789)
print(len(obj)) # 4
# 无论数字多大,打包后长度恒为4
import struct
data = b'12345678'
print(len(data))
# 打包制作报头
header = struct.pack('i', len(data))
print(header)
print(len(header))
# 解包获取真实长度 ---> 得到一个元组,元组中第一个值是真实数据的长度
res = struct.unpack('i', header)[0]
print(res)
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接收时 |
---|---|
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
# 服务端
import socket
import subprocess
import struct
server = socket.socket()
server.bind(('localhost', 9527))
server.listen(5)
while True:
conn, addr = server.accept()
print(addr)
while True:
try:
# 获取客户端传过来的报头
header = conn.recv(4)
# 解包获取真实数据长度
data_len = struct.unpack('i', header)[0]
# 准备接收真实数据
cmd = conn.recv(data_len)
if len(cmd) == 0:
continue
cmd = cmd.decode('utf-8')
if cmd == 'q':
break
obj = subprocess.Popen(
cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = obj.stdout.read() + obj.stderr.read()
print('发给客户端端的真实数据长度', len(result))
header = struct.pack('i', len(result))
print(len(header))
conn.send(header)
conn.send(result)
except Exception as res:
print(res)
break
conn.close()
# 客户端
import socket
import struct
client = socket.socket()
client.connect(('localhost', 9527))
while True:
cmd = input('>>>')
cmd_bytes = cmd.encode('utf-8')
# 做一个报头
header = struct.pack('i', len(cmd_bytes))
print(len(header))
# 向服务端发送数据的长度
client.send(header)
# 待服务器确认长度后,发送真实数据长度
client.send(cmd_bytes)
# 接收服务器返回的报头
headers = client.recv(4)
# 解包,接收服务端返回的真实数据
data_len = struct.unpack('i', headers)[0]
result = client.recv(data_len)
print('接收服务端返回的真实数据长度', len(result))
print(result.decode('gbk'))
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时 | 接收时 |
---|---|
先发报头长度 | 先收报头长度,用struct取出来 |
再编码报头内容然后发送 | 根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 | 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 |
# server端
# 1. 先制作报头
header_dic = {
'filename': 'a.txt',
'md5': 'asdfasdf123123x1',
'total_size': len(stdout) + len(stderr)
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
# 2. 先发送4个bytes(包含报头的长度)
conn.send(struct.pack('i', len(header_bytes)))
# 3 再发送报头
conn.send(header_bytes)
# 4. 最后发送真实的数据
conn.send(stdout)
conn.send(stderr)
# client端
#1. 先收4bytes,解出报头的长度
header_size=struct.unpack('i',client.recv(4))[0]
#2. 再接收报头,拿到header_dic
header_bytes=client.recv(header_size)
header_json=header_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)
total_size=header_dic['total_size']
#3. 接收真正的数据
cmd_res=b''
recv_size=0
while recv_size < total_size:
data=client.recv(1024)
recv_size+=len(data)
cmd_res+=data
print(cmd_res.decode('gbk'))
总结:先发字典报头,再发字典数据,最后发真实数据