Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

时间:2021-10-25 09:37:29

1.黏包

# tcp协议在发送数据时,会出现黏包现象.
(1)数据粘包是因为在客户端/服务器端都会有一个数据缓冲区,
缓冲区用来临时保存数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。
(2)在收发数据频繁时,由于tcp传输消息的无边界,不清楚应该截取多少长度
导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包

例:下面三种方式采取的是,1.可以使用模块time,在客户端或者服务端发送方或者收到数据的一方时间稍微延缓,这样可以做到不黏包。

2.本例中的服务端发送数据时,先发一个数据给客户端,让客户端知道将要接受多少数据,这样也不会黏包。(方法1)

3.方法1的弊端就是一旦服务器发送的数据过多,客户端不需要每次去接受多少个数据然后再次就行赋值,直接写上接受8个字节就行,服务器端利用"00000120",让客户端知道你每次接受8个字节就行了,

res0 = int(sk.recv(8).decode("utf-8")) 也就是不需要每次去改动8这个数字,每次接受8个字节就行,服务器端按照这样的方式就行发送就可以。

上面的数字就是你需要接受的字节数。这样也不会造成黏包。

4.方法三是按照使用socketserver就行把要发送的任意数字使用stuct.pack()就行把数据就行打包成4个字节就行发送,然后让客户端知道你接受的是4个字节,然后再客户端处,使用struct.unpack()就是把4个字节的包就行解压,得到原有的数据,这样也不会造成黏包。

5.上面的情况都是按照服务器端给客户端发送2次数据建立而来的,一般2次数据不加任何东西的时候,由于tcp是无边界的,不清楚该接受多少个字节,客户端第一次接受recv(10) 客户端第二次接受recv(10) ,服务器方第一次发送的是helllo第二次发送的是world,由于无边界会造成第一次接受的是b"helloworl" 第二次接受的是b"d" 对此展开而来的。

解决方法一:(就不会造成黏包)

客户端代码:

# ### 客户端
import socket
import time
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

# time.sleep(0.2)
res0 = int(sk.recv(1).decode("utf-8")) #res0 "6"
print(res0)
res1 = sk.recv(res0)
print(res1)
# print(res1.decode("utf-8"))
res2 = sk.recv(10)
print(res2)
sk.close()

服务器端:
# ### 服务端
import socket
import time
sk = socket.socket()

# 在bind方法之前加上这句话,可以让一个端口重复使用
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
# 绑定地址端口(在网络上注册主机)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn, addr = sk.accept()
conn.send("6".encode("utf-8"))
message = "hello,"
conn.send(message.encode("utf-8"))
# time.sleep(0.1)
conn.send("world".encode("utf-8"))


# 四次挥手
conn.close()
# 退还端口
sk.close()
小结:在tcp就行发包的时候,可以先一个数据让客户端知道,我们将会发多大的数据包给他,如上就是先发一个6然后再发送"hello"和"world"
第一次接收到hello之后,接受第二个数据就不会造成数据的黏包。
 
 

 

 方法二:
利用recv(8)接受8个字节,这样服务器就可以发送数据。
客户端:
# ### 客户端
import socket
import time
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

# time.sleep(0.2)
res0 = int(sk.recv(8).decode("utf-8")) #res0 "6"
print(res0)
res1 = sk.recv(res0)
print(res1)
# print(res1.decode("utf-8"))
res2 = sk.recv(10)
print(res2)
sk.close()

Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

 服务器端
# ### 服务端
import socket
import time
sk = socket.socket()

# 在bind方法之前加上这句话,可以让一个端口重复使用
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定地址端口(在网络上注册主机)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn,addr = sk.accept()
conn.send("00000120".encode("utf-8"))
message = "hello," * 20
conn.send(message.encode("utf-8"))
# time.sleep(0.1)
conn.send("world".encode("utf-8"))


# 四次挥手
conn.close()
# 退还端口
sk.close()
# 在服务器端使用#time.sleep(0.1)延迟服务器端给客户端发数据也是可以的,也就是服务器端先发送一个数据然后,等待客户端接收到数据之后再次发送第二个数据这样子也行。

方法三:
# 使用模块socketserver

客户端代码:

# ### 客户端
import socket
import struct
import time
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )
time.sleep(0.1)
# 接受4个字节长度, 他是实际要发送的那个数字转化来的.
n = sk.recv(4)
# n
= struct.unpack("i",n)[0] print(n) # 接下来接受服务端发送过来的数据 res1 = sk.recv(n) print(res1.decode("utf-8")) res2 = sk.recv(1024) print(res2.decode("utf-8")) # 空格不是ascii编码中的,大家注意. sk.send(b"i_love_you") # 关闭连接 sk.close()
Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

 

服务器代码:

# ### 服务端
import socket
import struct

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn,addr = sk.accept()
inp = input(">>>msg:")
msg = inp.encode("utf-8")
# 发送数据的长度通过pack进行转换,变成具有固定长度的4个字节的值
res = struct.pack("i",len(msg))

conn.send(res)
# 接下来,开始真正的发送数据
conn.send(msg)
conn.send("world".encode("utf-8"))

res = conn.recv(1024)
print(res)
print(res.decode("utf-8"))
# 四次挥手
conn.close()
# 退还端口
sk.close()
Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发
小结:使用了,socketserver之后,服务器给客户端第一次发送数据,无论发送的发送的长度有多长,利用struct.pack("i",len(msg))计算之后的
值都是按按照4个字节长度发送,然后把这个字节发送给客户端,让它知道第一次接受4个字节长度的就行了,在客户端使用unpack(),就行把得到的数据就行
恢复原来的数据,这样就不会造成黏包。

 

 

 

 

2.黏包出现的两种情况

#黏包现象一:
在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成黏包
#黏包现象二:
在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包
#总结:
发送端,包之间时间间隔短 或者 接收端,接受不及时, 就会黏包
核心是因为tcp对数据无边界截取,不会按照发送的顺序判断

3.解决黏包问题

#解决黏包场景:
应用场景在实时通讯时,需要阅读此次发的消息是什么
#不需要解决黏包场景:
下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓.

4.模块socketserver

#网络协议的最底层就是socket,基于原有socket模块,又封装了一层,就是socketserver
socketserver 为了实现tcp协议,server端的并发.

 基本用法:(解决黏包的方法三种会使用到的知识点)

import struct
# pack     把任意长度的数字转化成具有固定4个字节长度的字节流
# unpack   将4个字节的值恢复成原本的数据,最后返回一个元组
# i => int 我要转化的当前数据类型是整型
res = struct.pack("i",304535345)
print(res)
print(len(res))

res = struct.unpack("i",res)
print(res)
print(res[0],type(res[0]))
Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发
 

 5.sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)加上可以实现端口的互用

6.socketserver并发

客户端1代码:(不带循环的客户端就行访问服务器)

# ### 客户端
import socket
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )
sk.close()

'''
try:
    print(name)
# 无论是否报错,都会执行finally里面代码块;
finally:
    pass
'''
# print(name)
"""
如果有一些语句,即使在发生报错的情况也仍然要调用或处理一些逻辑,那么使用finally
"""
# try:
    # print(name)
# finally:
    # print(123)
    # print(456)
    
'''
try这个代码块如果有错误,不执行else代码块中的内容
如果没有报错,那么执行else代码块中的内容.
try ... except ... else 要配合使用 else不能单独拿出来和try使用.
try ... finally ... 可以配合在一起使用.
'''
'''
try:
    # print(name)
    print(123)
except:
    pass
else:
    print(789)
'''

 

 

服务器1代码:

 
 
import socketserver
# 需要自定义一个类,并且继承socketserver.BaseRequestHandler
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        #<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 54088)>
        print(self.request) 
        print("--->执行这句话")

# Threading( (ip,端口号)   ,  自定义类 )
server = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) , MyServer)
# 循环调用
server.serve_forever()
服务器代码:
只要有客户端连接服务器就会打印出下面的代码。self.request相当于,接收到有人连接服务器,myserver这个类下面的函数handle就会自动去处理。看看是谁连接到了我,从而支持并发。
Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发
 Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

# 由于客户端没有给服务器发送消息,服务器这边采用可socketserver支持多人连接。

# 带循环的客户端2

import socket
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

while True:    
    sk.send(b"wangwen_dashuaguo")
    msg = sk.recv(1024)
    print(msg)


sk.close()
Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

 

 

 

# 服务器代码2:

import socketserver
class MyServer(socketserver.BaseRequestHandler):
    # 在handle里面自定义收发逻辑
    def handle(self):
        print("--->这句话被执行了")
        
        conn = self.request
        while True:    
            msg = conn.recv(1024).decode("utf-8")
            print(msg)
            conn.send(msg.upper().encode("utf-8"))
# 产生一个对象    
server = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) ,MyServer )
# 循环调用
server.serve_forever()

 

运行代码:

Day27 python 黏包 udp/tcp 解决黏包、模块socktserver、socketserver并发

 小结:

1.上面开的三个客户端都是发送wangwen_dashuaguo

2.服务器这边的逻辑就是只要有客户端来连接我我就使用

# 产生一个对象    
server = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) ,MyServer )
来产生一个对象就行处理,在这个类当中可以发现只要是客户端给我发消息我就可以接受到,然后再将他发送的内容发送给他。从而形成了并发,可以是多个客户端连接服务器。conn = self.request也很关键。