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()
tcp_server.py

 

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()
View Code

 

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()
server端

 

 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()
client端

 

 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()   # 关闭服务器套接字
udp_server.py

 

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_client.py

 

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)
server端
 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()
client端

 

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()
服务端代码.py

 

 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()
客户端代码.py

 

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()
tcp_server.py

 

 

 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'))
tcp_client.py

 

解决方案(二): <重点掌握>

 

  通过 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()
server.py

 

 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"))
client.py

 

posted @ 2019-05-28 11:50  mingtao.li  阅读(152)  评论(0编辑  收藏  举报