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()
服务器端
# ### 服务端 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()
服务器代码:
# ### 服务端 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()
小结:使用了,socketserver之后,服务器给客户端第一次发送数据,无论发送的发送的长度有多长,利用struct.pack("i",len(msg))计算之后的
值都是按按照4个字节长度发送,然后把这个字节发送给客户端,让它知道第一次接受4个字节长度的就行了,在客户端使用unpack(),就行把得到的数据就行
恢复原来的数据,这样就不会造成黏包。
2.
#黏包现象一:
在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成黏包
#黏包现象二:
在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包
#总结:
发送端,包之间时间间隔短 或者 接收端,接受不及时, 就会黏包
核心是因为tcp对数据无边界截取,不会按照发送的顺序判断
#解决黏包场景:
应用场景在实时通讯时,需要阅读此次发的消息是什么
#不需要解决黏包场景:
下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓.
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]))
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就会自动去处理。看看是谁连接到了我,从而支持并发。
# 由于客户端没有给服务器发送消息,服务器这边采用可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()
# 服务器代码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()
运行代码:
小结:
1.上面开的三个客户端都是发送wangwen_dashuaguo
2.服务器这边的逻辑就是只要有客户端来连接我我就使用
# 产生一个对象 server = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) ,MyServer )
来产生一个对象就行处理,在这个类当中可以发现只要是客户端给我发消息我就可以接受到,然后再将他发送的内容发送给他。从而形成了并发,可以是多个客户端连接服务器。conn = self.request也很关键。