Python--Socket与粘包
一、 C/S 架构:Client/Server 客户端/ 服务端
B/S 架构:Browser/Server 前端/ 服务端
二、网络编程通信流程
网卡--> mac地址-->ip地址-->子网掩码-->网关-->DNS服务器(进行域名domain name 和与之相对应的ip地址转换的服务器)
DHCP(自动分配IP) NAT(网络地址转换) 端口 路由器
交换机 集线器 广播 单播 广播风暴 arp协议 路由协议
三、网络通信协议
1. TCP五层通信 OSI七层通信
网络通信协议是网络传输的灵魂,非常重要,协议即准则,准则是传输消息的格式要求。
2. TCP/IP 协议存在:传输层
TCP:面向连接,消息可靠,效率相对差,<面向流的消息格式>,无消息保护边界,比如:打电话、web浏览器、文件传输程序
UDP: 面向无连接,消息不可靠,效率高,<面向包的消息格式>,有消息保护边界,域名系统(DNS)、视频流、IP语音(VoIP)
TCP三次握手:
1. client --> [SYN] --> server client请求建立连接
2. client <--[SYN/ACK] <--server server收到syn 发送[SYN/ACK]确认
3. client --> [ACK] --> server client收到[SYN/ACK]再发一个[ACK]确认
TCP四次挥手:
1. client --> [ACK/FIN] --> server client发送包,请求关闭
2. client <--[ACK] <-- server server收到包,同意关闭
3. clinet <-- [ACK/FIN] <-- server server收到包,client是否收到 同意关闭 消息
4. client -->[ACK/FIN] --> server client发送确认收到 关闭包
四、基于TCP和UDP两个协议下socket通讯流程
概念: Socket 是任何一种计算机网络通讯中最基础的内容,它是 [应用层] 和 TCP/IP协议簇通信的中间软件抽象层,它是一组接口。
套接字有两种:
基于文件类型:AF_UNIX
基于网络类型:AF_INET ---- 使用最广泛的一个 这里还有一个AF_INET6 被用于ipv6
TCP和UDP对比:
TCP:是面向流的,如果多次发送的数据很小,并且每次发送间隔时间很短,就有可能会被拼到一个数据流里面。TCP是有序的,可以一段一段取值,只能从头开始
UDP:是面向包的,udp不能取半个包,会报错,如果数据包很大,那么一次性接收的时候,设置的接收大小也要很大,否则会报错,缓冲区错误。
TCP和UDP下socket差异对比图:
1. TCP协议下的Socket
server端
import socket server = socket.socket() #创建了一个socket对象 ip_port = ("127.0.0.1",8080) server.bind(ip_port) #将套接字绑定到地址 server.listen() #监听ip地址和端口 conn,addr = server.accept() #阻塞住,等待连接 from_client_msg = conn.recv(1024) #接收消息#1024为消息大小,单位B,MB=1024KB,1KB=1024B from_client_mst = from_client_msg.decode("utf-8") #接收的消息是bytes类型,需要转换为字符串 print(from_client_msg) conn.send("人生苦短".encode("utf-8")) #发送消息 conn.close() #关闭连接 server.close()
client端:
import socket client = socket.socket() server_ip_port = ("127.0.0.1",8080) client.connect = (server_ip_port) #连接服务器端 client.send("我用python".encode("utf-8")) #发送消息 #send里面的消息必须是字节类型的 from_server_msg = client.recv(1024) from_server_msg = from_server_msg.decode("utf-8") print(from_server_msg) client.close()
TCP通讯总结: 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时候客户端与服务端的连接就建立好了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
2. TCP长连接:
解释:长连接就是一直占用着这个链接,这个连接的端口被占用了,第二个客户端过来连接的时候,他是可以连接的,但是处于一个占线的状态,就只能等着去跟服务端建立连接,除非一个客户端断开了(优雅的断开可以,如果是强制断开就会报错,因为服务端的程序还在第一个循环里面),然后就可以进行和服务端的通信了。什么是优雅的断开呢?看代码。
1 import socket
2
3 sk = socket.socket()
4 ip_port = ("127.0.0.1",8090)
5 sk.bind(ip_port)
6 sk.listen()
7
8 while True:
9 conn,addr = sk.accept()
10 while True:
11 ret = conn.recv(1024)
12 ret = ret.decode("utf-8")
13 print(ret)
14 if ret == "bye":
15 break
16 msg = input("服务端>>")
17 conn.send(msg.encode("utf-8"))
18 if msg =="bye":
19 break
20 conn.close()
1 import socket 2 3 sk = socket.socket() 4 ip_port = ("127.0.0.1",8090) 5 sk.connect(ip_port) 6 7 while True: 8 msg = input("客户端>>") 9 sk.send(msg.encode("utf-8")) 10 if msg == "bye": 11 break 12 ret = sk.recv(1024) 13 ret = ret.decode("utf-8") 14 print(ret) 15 if ret == "bye": 16 break 17 sk.close()
3. UDP协议下的socket
先上代码:
1 ort socket 2 udp_sk = socket.socket(type=socket.SOCK_DGRAM) # 实力化一个udp socket对象(创建一个服务器的套接字) 3 ip_port = ("127.0.0.1",9000) 4 udp_sk.bind(ip_port) # 绑定服务器套接字 5 msg,addr = udp_sk.recvfrom(1024) # 接收来自客户端的信息 6 print(msg) 7 udp_sk.sendto(b"hi",addr) # 向客户端发送信息 8 udp_sk.close() # 关闭服务器套接字
1 import socket 2 ip_port = ("127.0.0.1",9000) 3 udp_sk = socket.socket(type=socket.SOCK_DGRAM) 4 udp_sk.sendto(b"hello",ip_port) 5 back_msg,addr = udp_sk.recvfrom(1024) 6 print(back_msg.decode("utf-8"),addr)
UDP通讯总结: 服务器端先初始化socket,然后与端口绑定(bind),recvfrom接收消息,这个消息有两项:消息内容和对方客户端的地址,然后回复消息也要带着这个客户端的地址发送回去(否则服务器不知道收到的是哪个客户端的消息),最后关闭连接,一次交互结束。
类似QQ聊天的代码示例:
1 import socket 2 ip_port=('127.0.0.1',8081) 3 udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #DGRAM:datagram 数据报文的意思,象征着UDP协议的通信方式 4 udp_server_sock.bind(ip_port)#你对外提供服务的端口就是这一个,所有的客户端都是通过这个端口和你进行通信的 5 while True: 6 qq_msg,addr=udp_server_sock.recvfrom(1024)# 阻塞状态,等待接收消息 7 print('来自[%s:%s]的一条消息:%s' %(addr[0],addr[1],qq_msg.decode('utf-8'))) 8 back_msg=input('回复消息: ').strip() 9 10 udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
1 while True: 2 qq_name=input('请选择聊天对象: ').strip() 3 while True: 4 msg=input('请输入消息,回车发送,输入q结束和他的聊天: ').strip() 5 if msg == 'q':break 6 if not msg or not qq_name or qq_name not in qq_name_dic:continue 7 udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])# 必须带着自己的地址,这就是UDP不一样的地方,不需要建立连接,但是要带着自己的地址给服务端,否则服务端无法判断是谁给我发的消息,并且不知道该把消息回复到什么地方,因为我们之间没有建立连接通道 8 9 back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)# 同样也是阻塞状态,等待接收消息 10 print('来自[%s:%s]的一条消息:%s' %(addr[0],addr[1],back_msg.decode('utf-8'))) 11 udp_client_socket.close()
4. socketserver
它是在socket的基础之上进行了一层封装,也就是说底层还是调用的socket。需要用它来实现并发,也就是同时也多个客户端进行通信,多个人可以同时进行上传和下载。
1 import socketserver 2 3 # 定义一个类,类里面继承socketserver.BaseRequestHandler 4 class MyServer(socketserver.BaseRequestHandler): 5 # 类里面定义一个handle方法,handle名称不能变 6 def handle(self): 7 while 1: 8 from_client_date = self.request.recv(1024).decode("utf-8") 9 # self.request # conn连接通道 10 print(from_client_date) 11 12 server_input = ("听说:") 13 self.request.send(server_input.encode("utf-8")) 14 15 16 if __name__ == '__main__': 17 18 # 服务端的ip地址和端口 19 ip_port = ("127.0.0.1",9001) 20 socketserver.TCPServer.allow_reuse_address = True 21 # 绑定ip地址和端口,并且启动我上面定义的类 22 server = socketserver.ThreadingTCPServer(ip_port,MyServer) 23 # 永久执行 24 server.serve_forever()
1 import socket 2 3 tcp_client = socket.socket() 4 5 server_ip_port = ("127.0.0.1",9001) 6 7 tcp_client.connect(server_ip_port) 8 9 while 1: 10 client_msg = input("她说>>>") 11 if client_msg == "q":break 12 else: 13 tcp_client.send(client_msg.encode("utf-8")) 14 15 from_server_msg = tcp_client.recv(1024).decode("utf-8") 16 print(from_server_msg) 17 18 tcp_client.close()
socket相关常用操作:
sk.bind(address): 将套接字绑定到地址。address的格式取决与地址族,在AF_INET下,以元祖的形式(host,port)
sk.listen(backlog): 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没调用accept进行处理的连接个数最大为5
sk.accept(): 接受连接并返回(conn,address),阻塞式。conn代表连接管道,用来接收和发送数据。address是客户端的地址
sk.connect(address):连接到address处的套接字。address的格式为元祖(address,port),如果连接出错,返回socket.error错误
sk.connect_ex(): 同上,只不过会有返回值,连接成功时返回0,失败返回编码,如:10061
sk.close(): 关闭套接字
sk.recv(bufsize[,flag]): 接收套接字的数据。数据以字符串的形式返回,bufsize指定最多可以接收的数量,flag提供有关消息的其它信息,通常可以忽略。
sk.recvfrom(bufsize[.flag]): 与recv()类似,但返回值是(data,address)。data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag]): 将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
sk.sendall(string[,flag]): 将string中的数据发送到连接的套接字。但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
内部通过递归调用send,将所有内容发送出去。
sk.sendto(string[,flag],address): 将数据发送到套接字,address是形式为(address,port)的元祖,指定远程地址,返回值是发送的字节数。
该函数主要应用于UDP
sk.getpeername(): 返回套接字的远程地址,通常是一个元祖(address,port)
sk.fileno(): 套接字的文件描述符
五、粘包
1. 概念性描述
缓冲区:每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
暂时存在传输数据的,防止程序在发送数据的时候卡住,提高代码运行效率。
输入缓冲区:recv
输出缓冲区:send
缓冲区有长度限制,输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取
MTU:最大传输单元,网络层限制是1500B,每次发送数据的时候最好不要超过这个数。
粘包现象:
(现象1)连续发送小的数据,间隔时间很短,接收端有可能一次就接收到了这几个连续的拼接在一起的小数据。
#原因:为了提高tcp传输效率,内部提供了一个叫做Nagel的算法和ACK机制,它的意思就是为了避免连续发送大量小包,会让数据出现粘包。
(现象2)接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘 包)
粘包的根本原因:两端互相不知道发送数据的长度。
补充问题一:为何tcp是可靠传输,udp是不可靠传输 tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。 而udp发送数据,对端是不会返回确认信息的,因此不可靠 补充问题二:send(字节流)和sendall send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失,一般的小数据就用send,因为小数据也用sendall的话有些影响代码性能,简单来讲就是还多while循环这个代码呢。 用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送) 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
六、粘包解决方案
1. 方案(一)
让发送端在发送数据之前,把自己将要发送的字节流总大小让接收端知道, 然后接收端发一个确认消息给发送端,然后发送端再发送真实的数据,然后接收端再来一个死循环接收完所有的数据。
上代码:
1 import socket,subprocess 2 ip_port=('127.0.0.1',8080) 3 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 5 6 s.bind(ip_port) 7 s.listen(5) 8 9 while True: 10 conn,addr=s.accept() 11 print('客户端',addr) 12 while True: 13 msg=conn.recv(1024) 14 if not msg:break 15 res=subprocess.Popen(msg.decode('utf-8'),shell=True,\ 16 stdin=subprocess.PIPE,\ 17 stderr=subprocess.PIPE,\ 18 stdout=subprocess.PIPE) 19 err=res.stderr.read() 20 if err: 21 ret=err 22 else: 23 ret=res.stdout.read() 24 data_length=len(ret) 25 conn.send(str(data_length).encode('utf-8')) 26 data=conn.recv(1024).decode('utf-8') 27 if data == 'recv_ready': 28 conn.sendall(ret) 29 conn.close()
1 import socket,time 2 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 3 res=s.connect_ex(('127.0.0.1',8080)) 4 5 while True: 6 msg=input('>>: ').strip() 7 if len(msg) == 0:continue 8 if msg == 'quit':break 9 10 s.send(msg.encode('utf-8')) 11 length=int(s.recv(1024).decode('utf-8')) 12 s.send('recv_ready'.encode('utf-8')) 13 send_size=0 14 recv_size=0 15 data=b'' 16 while recv_size < length: 17 data+=s.recv(1024) 18 recv_size+=len(data) 19 20 21 print(data.decode('utf-8'))
解决方案(二): <重点掌握>
通过 struck 模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到对端,对端只取出前4个字节,然后对这4个字节的数据进行解包,拿到你要发送的内容的长度,然后根据这个长度来继续接收我们实际要发送的内容。
struck模块的使用: struck模块中最重要的两个函数是pack()打包,和unpack解包(unpack返回的是tuple!)。
1 import socket,struct,json 2 import subprocess 3 4 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 5 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 地址重用 6 7 ip_port = ("127.0.0.1",8090) 8 phone.bind(ip_port) 9 phone.listen(5) 10 11 while True: 12 conn,add = phone.accept() 13 while True: 14 cmd = conn.recv(1024) 15 if not cmd:break 16 print("cmd: %s" %cmd) 17 res = subprocess.Popen(cmd.decode("utf-8"), 18 shell=True, 19 stdout=subprocess.PIPE, 20 stderr=subprocess.PIPE 21 ) 22 err = res.stderr.read() 23 if err: 24 back_msg = err 25 else: 26 back_msg=res.stdout.read() 27 conn.send(struct.pack("i",len(back_msg))) # 先发送back_msg的长度 28 conn.sendall(back_msg) # 再发送真实的内容 29 # 其实就是连续的将长度和内容一起发出去,那么整个内容的前4个字节就是我们打包的后面内容的长度 30 31 conn.close()
1 import socket,time,struct 2 3 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 ip_port = ("127.0.0.1",8090) 5 res = s.connect_ex(ip_port) 6 7 while True: 8 msg = input(">>: ".strip()) 9 if len(msg) == 0:continue 10 if msg == "quit":break 11 s.send(msg.encode("utf-8")) # 发送一个指令 12 l=s.recv(4) # 先接收4字节的数据,因为我们把将要发过来的内容打包成了4字节,所以先取出4字节 13 x=struct.unpack("i",l)[0] # 解包,是一个元祖,第一个元素就是我们的内容的长度 14 print(type(x),x) 15 16 r_s=0 17 data=b"" 18 while r_s < x: #根据内容的长度来继续接收4个字节后面的内容 19 r_d= s.recv(1024) 20 data+=r_d 21 r_s+=len(r_d) 22 23 print(data.decode("utf-8"))