网络技术(下)
网络编程
- 传输层之TCP与UDP
- 应用层
- socket模块简介
- socket模块基本使用
- 代码优化处理
- socket黏包问题
- 黏包问题的解决问题
- TCP/IP协议簇
由多个协议组成,其中以TCP于IP为代表,本文详细介绍TCP和UDP
- UDP协议
UDP协议定义了端口,同一个主机上的每个应用程序都需要指定唯一的端口号,并且规定网络中传输的数据包必须加上端口信息,当数据包到达主机以后,就可以根据端口号找到对应的应用程序了。UDP协议比较简单,实现容易,但它没有确认机制,数据包一旦发出,无法知道对方是否收到,因此可靠性较差,为了解决这个问题,提高网络可靠性,TCP协议就诞生了。 - TCP协议
TCP即传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。简单来说TCP就是有确认机制的UDP协议,每发出一个数据包都要求确认,如果有一个数据包丢失,就收不到确认,发送方就必须重发这个数据包。为了保证传输的可靠性,TCP协议在UDP基础之上建立了三次对话的确认机制,即在正式收发数据前,必须和对方建立可靠的连接。TCP数据包和UDP一样,都是由首部和数据两部分组成,唯一不同的是,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
- TCP和UDP主要区别
简单来说一个TCP需要建立通道确认应答,UDP则直接发送
这里借鉴鸡哥博客中的原理图

TCP重要原理图,三次握手四次挥手
- 应用层
应用层相当于是相当于程序员自己写的程序,本层具有相当多的协议
这里常需要使用的是httpshttp FTP
- socket模块
Socket 是应用层于TCP/IP协议簇通信的之间软件抽象层,是一组接口,在设计模式中,Socket其实就是一个门面模式 把复杂的TCP/IP协议簇隐藏在Socket接口后面,对于用户来说直接调用接口,已符合指定的协议
- 套接字(socket)的发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
- Socket-TCP在python中的应用
本质上来说python中的Socket是一个类,也是一个内置模块,需要导入使用
1. Server
import socket
"""
在写代码时建议查看源码写代码的思路
"""
# 1. 产生一个Socket对象并指定采用的通信版本和协议(TCP)
server = socket.socket() # 括号内不写参数,默认使用TCP协议,查看源码得知family=AF_INET基于网络的套接字, type=SOCK_STREAM流式协议即TCP
# 2. 绑定一个固定的地址,需要设定对外提供服务的端口和IP
server.bind(('127.0.0.1', 8080))
# 3. 设立半连接池
server.listen(5)
# 4. 等待客户端访问
sock, addr = server.accept() # sock为建立的双向通道,addr为客户端地址
print(sock, addr)
# 5. 响应客户端
data = sock.recv(1024) # 消息现在为1024字节
print(data.decode('utf8'))
sock.send('我是服务端'.encode('utf8'))
# 6. 关闭双向通道
sock.close() # 四次挥手
# 7. 关闭服务端
server.close()
2. Client
import socket
# 1. 生成socket对象指定的类型和协议
client = socket.socket()
# 2. 通过服务端的地址链接服务端
client.connect(('127.0.0.1', 8080))
# 3. 直接给服务端发送消息
client.send('我是客户端'.encode('utf8')) # 使用send是一次性发送消息
# 4. 接受服务端返回的消息
data = client.recv(1024)
print(data.decode('utf8'))
# 5. 断开于服务端的链接
client.close()
- 基于基础代码优化
1. server
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept()
while True:
try:
date = sock.recv(1024)
if len(date) == 0:
break
print(f'来自客户端{addr}的消息>>>:', date.decode('utf8'))
msg = input('请输入发送给客户端的消息>>>>: ')
sock.send(msg.encode('utf8'))
except BaseException:
break
2. client
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
msg = input('请输入你想发送给服务端的消息>>>>: ').encode('UTF-8')
if len(msg) == 0:
print('不能为空')
continue
client.send(msg)
data = client.recv(1024)
print('来自服务器发送的消息>>>:', data.decode('UTF-8'))
-
更新 2022-11-17
-
黏包现象
在使用上述的网络编程的方式实现了网络的通讯的同时带来了新的问题
黏包:简单来说就是服务端不清楚需要接受多少次的数据算作完整的数据包,黏包显现的出现,会将后面的数据包的内容切到前一个数据包的尾部
一、黏包成因
1、tcp协议的拆包机制
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
这样会产生很多数据包碎片,增加丢包率,降低网络速度。
2、tcp的合包机制
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,
使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
但是这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,
而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
3、说明
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
也就是:
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,
则函数会返回错误。(丢弃这个包,不进行发送)
用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。
而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
例如:
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,
通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- 如何避免黏包
需要明确服务端需要接收多大的数据包,再次之前如何将数据的长度作为报文的元数据提前发送给服务端并解析
- 解决方案
客户端
1. 制作真实数据的信息字典(数据长度,数据简介, 数据名称)
2. 利用struct模块制作字典的报头
3. 发送固定长度的报头(解析出来是字典的长度)
4. 发送字典数据
5. 发送真实数据
import socket
import os
import struct
import json
import time
client = socket.socket()
client.connect(('127.0.0.1', 8080))
# 需要获取真实数据大小
file_size = os.path.getsize(r'/Users/wesley/PycharmProjects/pythonProject5/间谍过家家第13集.mp4')
# 制作真实数据的字典描述数据
data_dict = {
'file_name': 'lala.mp4',
'file_size': file_size,
'file_desc': '这是一个好看的视频',
'file_info': '请准备好瓜子花生矿泉水'
}
# 制作字典报头
data_dict_bytes = json.dumps(data_dict).encode('utf8')
data_dict_len = struct.pack('i', len(data_dict_bytes))
# 发送字典报头
client.send(data_dict_len) # 报头本身也是bytes类型,我们在看的使用用len长度是4
# 发送字典数据
client.send(data_dict_bytes)
# 最后发送真实数据
with open(r'/Users/wesley/PycharmProjects/pythonProject5/间谍过家家第13集.mp4', 'rb' ) as f:
for line in f:
client.send(line)
time.sleep(0.0001)
# 服务端
1. 接收固定长度的字典报头
2. 解析出字典长度并接收
3. 通过字典获取到真实数据的各项信息
4. 接收真实数据长度
import socket
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
sock, addr = server.accept()
# 需要接送用于描述真实信息的字典报头
data_dict_head = sock.recv(4)
# 根据报头解析出字典数据的长度
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 接收字典数据
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes) # 自动解码再反序列化
# 获取数据的各项信息,一般比较建议使用循环的方式来获取
total_size = data_dict.get('file_size')
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
while recv_size < total_size:
data = sock.recv(1024)
f.write(data)
recv_size += len(data)
print(recv_size)
"""
在使用上述代码对数据进行传输时,如果跨网络需要对时间进行一定的限制,客户端需要稍微限制一下发送间隔不然较为容易被操作系统认作为攻击
"""

浙公网安备 33010602011771号