socket套接字编程
前言:学习套接字编程需要掌握的网络基础知识
包含(TCP/IP 5层模型,TCP协议建立的三次握手与4次断开,网络通信过程等)
点击进行了解~
套接字介绍
-
套接字 : 实现网络编程进行数据传输的一种技术手段
-
Python实现套接字编程:import socket
-
套接字分类
流式套接字(SOCK_STREAM): 以字节流方式传输数据,实现tcp网络传输方案。(面向连接–tcp协议–可靠的–流式套接字)
数据报套接字(SOCK_DGRAM):以数据报形式传输数据,实现udp网络传输方案。(无连接–udp协议–不可靠–数据报套接字)
tcp套接字编程
服务端流程
- 创建套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)
功能:创建套接字
参数:socket_family 网络地址类型 AF_INET表示ipv4
socket_type 套接字类型 SOCK_STREAM(流式) SOCK_DGRAM(数据报)
proto 通常为0 选择子协议
返回值: 套接字对象
- 绑定地址
本地地址 : 'localhost' , '127.0.0.1'
网络地址 : '172.40.91.185'
自动获取地址: '0.0.0.0'
sockfd.bind(addr)
功能: 绑定本机网络地址
参数: 二元元组 (ip,port) ('0.0.0.0',8888)
- 设置监听
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小
- 等待处理客户端连接请求
connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求
返回值: connfd 客户端连接套接字
addr 连接的客户端地址
- 消息收发
data = connfd.recv(buffersize)
功能 : 接受客户端消息
参数 : 每次最多接收消息的大小
返回值:接收到的内容
n = connfd.send(data)
功能 : 发送消息
参数 : 要发送的内容 bytes格式
返回值:发送的字节数
- 关闭套接字
sockfd.close()
功能:关闭套接字
代码展示
"""
tcp_server.py tcp套接字服务端功能流程
注意 : 注意流程和函数使用
"""
import socket
from time import sleep
# 创建TCP套接字(AF_INET表示ipv4 SOCK_STREAM(流式))
sockfd = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 重用服务端的IP和端口 (如果服务端的IP及端口在短时间内释放掉
# 那么就把之前的IP及端口重用上,就可以解决端口被占用问题)
sockfd.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定地址
sockfd.bind(('0.0.0.0',8889))
# 设置监听
sockfd.listen(5)
# 阻塞等待客户端连接
while True:
print("Waiting for connect...")
try:
connfd,addr = sockfd.accept()
print("Connect from",addr) # 打印连接的客户端
except KeyboardInterrupt:
print("服务端退出")
break
except Exception as e:
print(e)
continue
# 收发消息
while True:
data = connfd.recv(5)
# 连接的客户端退出,recv会立即返回空字符串
if not data:
break
print(data.decode())
sleep(0.1)
n = connfd.send(b"Thanks#")
print("Send %d bytes"%n)
connfd.close()
# 关闭套接字
sockfd.close()
客户端流程
- 创建套接字
注意:只有相同类型的套接字才能进行通信
- 请求连接
sockfd.connect(server_addr)
功能:连接服务器
参数:元组 服务器地址
- 收发消息
data = connfd.recv(buffersize)
功能 : 接受客户端消息
参数 : 每次最多接收消息的大小
返回值:接收到的内容
n = connfd.send(data)
功能 : 发送消息
参数 : 要发送的内容 bytes格式
返回值:发送的字节数
注意: 防止两端都阻塞,recv send要配合
- 关闭套接字
sockfd.close()
功能:关闭套接字
代码展示
"""
tcp_client.py tcp客户端流程
重点代码
"""
from socket import *
# 创建tcp套接字
sockfd = socket() # 默认参数-->tcp套接字
# 连接服务端程序
server_addr = ('127.0.0.1',8889)
sockfd.connect(server_addr)
# 发送接收消息
while True:
data = input("Msg:")
# data为空退出循环
if not data:
break
sockfd.send(data.encode()) # 发送字节串
data = sockfd.recv(1024)
print("Server:",data.decode())
# 关闭套接字
sockfd.close()
tcp 套接字数据传输特点
- tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
- tcp连接中如果一端已经不存在,仍然试图通过send发送则会产生BrokenPipeError
- 一个监听套接字可以同时连接多个客户端,也能够重复被连接
网络收发缓冲区
- 网络缓冲区有效的协调了消息的收发速度
- send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。
服务端->客户端数据发送流程:服务端的应用程序把数据包发给操作系统,缓存到机器的内存里面,然后由操作系统(经过层层封装)把数据发给客户端的机器,客户端机器收到数据包后存到机器的缓存里面,然后由客户端的应用程序从机器的内存里面获取数据。
tcp粘包
原因:tcp以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。
影响:如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
处理方法
- 人为的添加消息边界
- 控制发送速度
1、粘包的原因:
针对TCP:服务端可以是1K,1K地发送数据,而接收端的应用程序可以2K,2K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
通俗理解: 服务端应用程序(等机器内存缓冲区满后)然后一次性发送数据包(发送数据时间间隔很短,数据流很小,会合到一起,产生粘包)到达客户端服务器,而客户端应用程序在接收数据包时(客户端不及时接收缓冲区的包,造成多个包接收),服务端发送了一段数据,客户端只收了一小部分,客户端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包。
2、TCP 协议 (又称为流式协议):
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,
提供高可靠性服务(可靠协议)。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法:将多次间隔较小且数据量小的数据合并成一个大的数据块,然后进行封包)。 这样接收端就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。)
TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
TCP的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
TCP协议的数据并不是 “一发(数据发送)” 对应 “一收(数据接收)”,每次发送都是由操作系统决定的,操作系统可能把多个数据合并成一个包发送。
3、UDP 协议:
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务(不可靠协议) 。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
UDP协议在传输层通过本身自带的报头属性,以及一发(发送数据包)一收(接收数据包)的机制解决了数据粘包的问题。 UDP协议一般不用来传文件,通常用来做与查询相关的数据包的发送,UDP协议稳定有效的数据包传输量最大为512字节(协议本身的原因造成)。
UDP协议一般不用来传文件,通常用来做与查询相关的数据包的发送,UDP协议稳定有效的数据包传输量最大为512字节(协议本身的原因造成)。
UDP的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着 UDP根本不会粘包,但是会丢数据,不可靠。
UDP协议一般用在:DNS查询,NTP时间服务器
4、TCP/UDP 协议的可靠性
TCP协议在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的,而udp发送数据,对端是不会返回确认信息的,因此不可靠
5、send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。
解决tcp粘包
1、解决粘包现象
方法: 为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据(直到收干净为止)。
通俗解释: 给字节流加上自定义固定长度报头:客户端在接收时,先去读报头的长度,从而拿到数据包的长度。就相当于手动给数据包划分成一段一段的,客户端每次都会接收完一段在接受另外一段。
通过在应用层通过封装报头的形式来解决粘包问题,但是并没有改变TCP协议(流式协议)发送数据包的属性。
2、解决粘包问题需要先了解一个struct模块的用法:
该模块可以把一个类型,如数字,转成固定长度的bytes
使用例子一:
import struct
obj=struct.pack('i',1231231) #i表示:int 整型,1231231表示:整型的大小为 1231231
print(obj,len(obj))
# b'\x7f\xc9\x12\x00' 4 #struct.pack得到的是一个二进制形式的数据,长度为4
res=struct.unpack('i',obj)[0] #使用struct.unpack解包得到整型i的值为1231231
print(res)
# 1231231
使用例子二:(在解决粘包问题有用到这种方法)
import struct
import json
header_dic={ #定义字典的格式与内容
'filenema': 'a.txt',
'total_size': 123123123123123123123123123123123123123123123123123,
'md5':'sssxxxadwc123asd123',
}
header_json=json.dumps(header_dic) #由字典序列化为json格式
print(header_json) #得到序列化内容如下
# >>:{"filenema": "a.txt", "total_size": 123123123123123123123123123123123123123123123123123, "md5": "sssxxxadwc123asd123"}
header_bytes=header_json.encode('utf-8') #在由json格式转化为bytes格式
print(header_bytes) #得到bytes格式内容如下
# >>:b'{"filenema": "a.txt", "total_size": 123123123123123123123123123123123123123123123123123, "md5": "sssxxxadwc123asd123"}'
print(len(header_bytes)) #bytes格式的长度
# >>:118
res=struct.pack('i',len(header_bytes)) #使用struct.pack把'i(int)'及长度len(header_bytes)转换成固定长度的bytes
print(res,len(res)) #bytes的值为:b'v\x00\x00\x00'
# >>:b'v\x00\x00\x00' 4 #bytes的长度为:4
obj=struct.unpack('i',res)[0] #使用struct.unpack解包得到bytes格式的长度(得到的值是一个元组:(118,)),这里只需要取118即可
print(obj)
# >>:118
解决粘包问题–初级版:
服务端代码:
from socket import *
import subprocess
import struct
socket=socket(AF_INET,SOCK_STREAM)
socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
socket.bind(('127.0.0.1',8888))
socket.listen(5)
print('等待连接中。。。')
while True:
coo,addr=server.accept()
while True: #通信循环
try:
data=con.recv(1024)
if not data:break
obj=subprocess.Popen(data.decode('utf-8'),shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
# 1、先发送固定长度的报头 #如何制作固定长度的报头(用到struct)
total_size=len(stdout) + len(stderr) #数据的总长度
coon.send(struct.pack('i',total_size)) #把包含数据长度的报头发送到服务端
# 2、发送真实数据
con.send(stdout)
con.send(stderr)
except ConnectionResetError:
break
coon.close() #关闭连接状态 (回收的是操作系统的资源)
server.close() #关闭服务端 (回收的是操作系统的资源)
客户端代码:
from socket import *
import struct
socket=socket(AF_INET,SOCK_STREAM) #SOCK_STREAM==流式协议:指的就是TCP协议
socket.connect(('127.0.0.1',8086)) #这里的IP和端口都是服务端的
while True:
data=input('>>:').strip()
if not data:continue
socket.send(data.encode('utf-8')) #在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')制定字符集的方式发送
print('发送中...')
# 1、先收报头,从报头里取出对真实数据的描述信息
header=socket.recv(4) #接收4个字节即可,struct.pack('i',1231231)的长度为4,所以只用接收4个即可
total_sisz=struct.unpack('i', header)[0] #解包报头,拿到数据的总长度
# 2、循环接收真实数据,直到收完为止
recv_size=0 #接收数据包的大小
res=b'' #把接收到的数据包拼接到一起
while recv_size < total_sisz:
recv_data = socket.recv(1024) #循环接收服务端传过来的数据
res += recv_data #res把接收到的数据全部拼接起来
recv_size += len(recv_data) #接收到的数据的长度直到等于数据的总长度为止
print(res.decode('gbk')) #就收客户端操作系统(windows默认使用gbk)发过来的数据,想要输出到屏幕得使用gbk解码
client.close() #关闭客户端
解决粘包问题–终极版:
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时(服务端):
1、先发报头长度。
2、再编码报头内容然后发送。
3、最后发真实内容。
接收时(客户端):
1、先手报头长度,用struct取出来。
2、根据取出的长度收取报头内容,然后解码,反序列化。
3、从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容。
服务端代码:
from socket import *
import subprocess
import struct
import json
socket=socket(AF_INET,SOCK_STREAM)
socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
socket.bind(('127.0.0.1',8086))
socket.listen(5)
print('等待连接中。。。')
while True:
con,addr=server.accept()
# 通信循环
while True:
try:
data=con.recv(1024)
if not data:break
obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
#制作报头
header_dic = { #设置报头为字典格式
'filenema': 'a.txt', #文件名
'total_size': len(stdout) + len(stderr), #数据总长度
'md5': 'sssxxxadwc123asd123', #md5值
}
header_json = json.dumps(header_dic) #把报头,序列化为json格式
header_bytes = header_json.encode('utf-8') #在由json格式转化为bytes格式(数据包发送通过bytes形式发送)
#1、先发送报头的长度(客户端拿到报头的长度后可以知道要接受的数据大小)
con.send(struct.pack('i',len(header_bytes))) #struct.pack用法:#i表示:int 整型,len(header_bytes)表示:报头的长度(int类型)
#2、在发送报头(报头的内容)
con.send(header_bytes)
#3、最后发送真实数据
con.send(stdout)
con.send(stderr)
except ConnectionResetError:
break
# 关闭连接状态 (回收的是操作系统的资源)
con.close()
#关闭服务端 (回收的是操作系统的资源)
socket.close()
客户端代码:
from socket import *
import struct
import json
socket=socket(AF_INET,SOCK_STREAM) #SOCK_STREAM==流式协议:指的就是TCP协议
socket.connect(('127.0.0.1',8086)) #这里的IP和端口都是服务端的
while True:
data=input('>>:').strip()
if not data:continue
socket.send(data.encode('utf-8')) #在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')制定字符集的方式发送
print('发送中...')
# 1、先收报头的长度(服务端先发送的是报头的长度,所有要先接收报头的长度)
obj=socket.recv(4) #报头长度
header_size=struct.unpack('i',obj)[0] #拿到报头长度后,通过struct.unpack拿到报头的大小(即服务端header_dic的大小)
# 2、在接收报头
header_bytes=client.recv(header_size) #通过client.recv接收报头
header_json=header_bytes.decode('utf-8') #接收报头后对报头的格式做反序列化处理(因为报头在服务端被json序列化了)
header_dic=json.loads(header_json) #通过json.loads拿到报头的字典格式及内容
print(header_dic)
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')) #就收客户端操作系统(windows默认使用gbk)发过来的数据,想要输出到屏幕得使用gbk解码
# 5、关闭客户端
client.close()