Python 网络编程
TCP套接字
面向连接的套接字,即在通信前建立一条连接(TCP的三次握手和四次挥手),这种通信方式也被称为"虚电路"或"流套接字"。
面向连接的通信方式提供了顺序的,可靠地,不会重复的数据传输,而且也不会被加上数据边界。
这个也意味着每一个要发送的信息,可能会被拆分成多分,每一份都会不多不少的正确到达目的地,然后被重新按顺序拼装起来,传给正在等待的应用程序
实现这种连接的主要协议就是传输控制协议(即TCP)。
要创建TCP套接字就要在创建的时候指定套接字类型为SOCK_STREAM
服务端
from socket import * server = socket(AF_INET,SOCK_STREAM) # 创建套接字对象 server.bind(("127.0.0.1",8888)) # 绑定IP 端口 server.listen(5) # 监听连接 while True: # 服务器循环 conn,addr = server.accept() # 等待连接,返回的第一个参数是套接字对象,第二个参数是客户端信息 while True: # 通信循环 cmd = conn.recv(1024) # 回话,接收数据 conn.send(cmd ) # 回话,发送数据 conn.close() # 关闭客户端套接字, 断开连接 server.close() # 关闭服务器套接字(可选)
客户端
from socket import * client = socket(AF_INET,SOCK_STREAM) # 创建TCP套接字 client.connect(("127.0.0.1",8888)) # 指定IP端口连接服务器 while True: # 通信循环 client.send(img) # 通信, 发送数据 header = client.recv(1024) # 通信, 接收数据 client.close()
解决端口占用
# 假如端口被socket使用过,并且利用socket.close()来关闭连接,但此时端口还没有释放,要经过一个 # TIME_WAIT的过程之后才能使用,这是TNN的相当烦银的,为了实现端口的马上复用,可以选择 # setsockopt()函数来达到目的。(以下是网上找到的一篇文章的一小段相关例子,试用之后,相当有效果,# 特此提取出来收藏) # 端口复用的实现,我在这里用Python举个TCP端口复用的例子,UDP套接字要做的完全一样。 import socket tcp1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#在绑定前调用setsockopt让套接字允许地址重用 tcp1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)#接下来两个套接字都也可以绑定到同一个端口上
粘包
套接字是从操作系统中收发数据,然后操作系统包ip头和以太网头, 收发数据都是对操作系统的缓冲区进行的操作
粘包问题产生的原因
#由接收方造成的粘包 # 当接收方不能及时接收缓冲区的包,造成多个包接收就产生了粘包 # 客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据 #由传输方造成的粘包 # tcp协议中会使用Nagle算法来优化数据。发送时间间隔短,数据量小的包会一起发送,造成粘包
结局粘包问题的基本套路
粘包问题的根源在于,接收端不知道发送端将要传送的字节流长度,所以解决粘包的方法就是围绕
如何让发送端在发送数据前把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接受完所有数据
方法:自定义一个报头,传输数据的大小,这个报头是固定字节,在发送数据前发送
# 服务端 import struct from socket import * server = socket(AF_INET,SOCK_STREAM) # 创建套接字对象 server.bind(("127.0.0.1",8888)) server.listen(5) while True: conn,addr = server.accept() # 等待连接,返回的第一个参数是套接字对象,第二个参数是客户端信息 while True: try: cmd = conn.recv(1024) # 接收数据 stdout = 'XXX' # 第一步:制作报头: total_size = len(stdout) header = struct.pack("i",total_size) #存储包含数据长度的固定字节的字节码 # 第二步:先发报头: conn.send(header) # 第三步:发送正文 conn.send(stdout) except ConnectionResetError: break conn.close() server.close() # 服务端 import struct from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8888)) while True: cmd = input(">>>").strip() client.send(cmd.encode("utf-8")) # 第一步:收到报头 header = client.recv(4) # 已知报头长度为四个字节 total_size = struct.unpack("i",header)[0] # 拿到数据长度 # 第二步:收完整真实数据 recv_size = 0 res = b"" while recv_size < total_size: # 计数器小于报头传入的数据长度 recv_data = client.recv(1024) # 从操作系统拿数据 res += recv_data # 拼接到res recv_size += len(recv_data) # 计数器加上拿到的数据的长度 print(res.decode("gbk")) client.close()
初级解决粘包问题的弊端
一 能够表示的大小有限。struct.pack能够转化的数字长度有限,不能够解决某些问题,比如下载一个大文件
二 有时报头中还需要传递其他信息,比如对于文件的描述
基于此类问题,我们会创建一个字典,其中存储了关于文件的各类信息
# 服务端 import subprocess import struct # 完成报头中的数字转换为固定长度的字节码 import json # 序列化模块 将数据类型信息转化为字符串 from socket import * # 套接字模块 server = socket(AF_INET,SOCK_STREAM) server.bind(("127.0.0.1",8888)) server.listen(5) while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) stdout = 'XXX' # 制作报头,报头里放数据大小,MD5,文件名 header_dict ={ "total_size":len(stdout), "md5":"********", "filename":"文件名" } header_json = json.dumps(header_dict) # 序列化报头 header_bytes = header_json.encode("utf-8") # 将报头转码编程字节码 header_size = struct.pack("i",len(header_bytes)) # 将转码后的报头长度转换成固定长度的字节码 # 先发报头长度 conn.send(header_size) # 再发报头 conn.send(header_bytes) # 在发送真实数据 conn.send(stdout) except ConnectionResetError: break conn.close() server.close() # 客户端 import struct import json from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8888)) while True: cmd = input(">>>").strip() if not cmd:continue client.send(cmd.encode("utf-8")) # 先收报头长度-已知长度 obj = client.recv(4) header_size = struct.unpack("i",obj)[0] # 得到了报头长度 # 接收报头,解出报头内容 header_bytes = client.recv(header_size) # 根据报头长度得到报头 header_json = header_bytes.decode('utf-8') # 报头转码成str header_dic = json.loads(header_json) # 反序列化的到字典 total_size = header_dic['total_size'] # 得到真实数据长度 # 3:循环收完整数据 recv_size = 0 res = b"" while recv_size < total_size: # 计数器小于报头传入的数据长度 recv_data = client.recv(1024) # 从操作系统拿数据 res += recv_data # 拼接到res recv_size += len(recv_data) # 计数器加上拿到的数据的长度 print(res.decode("gbk")) client.close()
UDP套接字
tcp是基于数据流的,于是收发的消息不能为空(如果发送数据为空 服务端端会得不到内容 一直等待接受内容 客户端也会等待接收服务端发送的内容,),这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头(存在边界)
服务端
from socket import * server=socket(AF_INET,SOCK_DGRAM) # 创建套接字 server.bind(('127.0.0.1',8083)) # 绑定IP端口 while True: # 等待接收数据 data,client_addr=server.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组 print('客户端的数据: ',data) # 发送数据 server.sendto(data.upper(),client_addr) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组
客户端
from socket import * client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() # 发送数据 client.sendto(msg.encode('utf-8'),('127.0.0.1',8083)) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组 # 接收数据 data,server_addr=client.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组 print(data.decode('utf-8')) print(server_addr)
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。