TCP/UDP传输的其实是通过套接字来进行传输的,那么我就首先讲一下什么是套接字吧~
什么是套接字?
Socket(套接字)通信原理 - codedot - 博客园 (cnblogs.com)https://www.cnblogs.com/myitnews/p/13790067.html
Socket 就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个 Socket 实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP 地址和端口”,我就接通谁。
套接字是网络上运行的两个程序之间的双向通信链路的一个端点。套接字机制通过建立发生通信的命名接触点来提供进程间通信 (IPC) 的方法。就像“管道”用于创建管道,套接字是使用“套接字”系统调用创建的。插座通过网络提供双向FIFO通信设施。在通信的每一端创建一个连接到网络的套接字。每个套接字都有一个特定的地址。此地址由 IP 地址和端口号组成。套接字通常用于客户端服务器应用程序。服务器创建一个套接字,将其附加到网络端口地址,然后等待客户端与其联系。客户端创建一个套接字,然后尝试连接到服务器套接字。建立连接后,将进行数据传输。
实际上,Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。Socket 起源于 UNIX,在 UNIX 一切皆文件的思想下,进程间通信就被冠名为文件描述符(file descriptor)
,Socket 是一种“打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
Socket类型
世界上有很多种套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。我们只介绍第一种套接字——Internet 套接字,它是最具代表性的,也是最经典最常用的。以后我们提及套接字,指的都是 Internet 套接字。根据数据的传输方式,可以将 Internet 套接字分成两种类型。
流格式套接字(SOCK_STREAM)
流格式套接字(Stream Sockets)也叫“面向连接的套接字”,是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。在计算机操作系统中,流套接字是进程间通信套接字或网络套接字的类型,它提供面向连接的、有序的和唯一的数据流,没有记录边界,具有明确定义的机制来创建和销毁连接以及检测错误。它类似于电话。在电话之间建立连接(两端)并进行对话(数据传输)。
其特点:
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。
可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。
------------------------------------
Function Call Description
------------------------------------
Create() To create a socket
Bind() It’s a socket identification like a telephone number to contact
Listen() Ready to receive a connection
Connect() Ready to act as a sender
Accept() Confirmation, it is like accepting to receive a call from a sender
Write() To send data
Read() To receive data
Close() To close a connection
-------------------------------------
为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。
你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。
TCP协议是端到端的传输控制协议,之所以是“端到端”的协议,是因为”路由“是由IP协议负责的,TCP协议负责为两个通信端点提供可靠性保证,这个可靠性不是指一个端点发送的数据,另一个端点肯定能收到(这显然是不可能的),而是指,数据的可靠投递或者故障的可靠通知。
TCP的可靠性通过以下方式来保证:
1.超时重传:TCP每发送出一个报文段后,都会启动一个定时器,对目的端传回的确认信息进行确认计时,超时后便重传。
2.确认信号:当TCP收到一个来自TCP的报文段后,便会发送回一个确认信号。
3.检验和:TCP将始终保持首部和数据的检验和,如果收到的报文段的检验和有差错,便将其丢弃,希望发送端超时重传。
4.重新排序:由于IP数据报的达到可能失序,因此TCP将会数据进行重新排序,以正确的顺序交给应用层。
5.丢弃重复:由于IP数据报有可能重复,因此TCP将会丢弃重复的数据。
6.流量控制:TCP连接的两端都有固定大小的缓冲区空间,TCP接受端只允许对端发送本端缓冲区能容纳的数据。TCP提供流量控制。在双方进行交互时,会彼此通知自己目前接收缓冲区最多可以接收的数据量(通告窗口),以此确保发送方发送的数据不会溢出接收缓冲区。
那么,“数据的发送和接收不同步”该如何理解呢?
假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。
流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。
也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
流格式套接字有什么实际的应用场景吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。
数据报格式套接字(SOCK_DGRAM)
数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
有以下特征:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的
众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。
另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
Socket通信过程
Socket 保证了不同计算机之间的通信,也就是网络通信。对于网站,通信模型是服务器与客户端之间的通信。两端都建立了一个 Socket 对象,然后通过 Socket 对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。
下面是面向连接的 TCP 时序图:
Client
客户端的过程比较简单,创建 Socket,连接服务器,将 Socket 与远程主机连接(注意:只有 TCP 才有“连接”的概念,一些 Socket 比如 UDP、ICMP 和 ARP 没有“连接”的概念),发送数据,读取响应数据,直到数据交换完毕,关闭连接,结束 TCP 对话。
import socket
import sys
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建 Socket 连接
sock.connect(('127.0.0.1', 8001)) # 连接服务器
while True:
data = input('Please input data:')
if not data:
break
try:
sock.sendall(data)#这里也可用 send() 方法:不同在于 sendall() 在返回前会尝试发送所有数据,并且成功时返回 None,而 send() 则返回发送的字节数量,失败时都抛出异常。
except socket.error as e:
print('Send Failed...', e)
sys.exit(0)
print('Send Successfully')
res = sock.recv(4096) # 获取服务器返回的数据,还可以用 recvfrom()、recv_into() 等
print(res)
sock.close()
Tips:
套接字send与sendall的区别
send()
使用send()
进行发送的时候,Python
将内容传递给系统底层的send
接口,也就是说,Python
并不知道这次调用是否会全部发送完成,比如MTU
是1500,但是此次发送的内容是2000,那么除了包头等等其他信息占用,发送的量可能在1000左右,还有1000未发送完毕
但是,send()
不会继续发送剩下的包,因为它只会发送一次,发送成功之后会返回此次发送的字节数,如上例,会返回数字1000给用户,然后就结束了
如果需要将剩下的1000发送完毕,需要用户自行获取返回结果,然后将内容剩下的部分继续调用send()
进行发送
sendall()
sendall()
是对send()
的包装,完成了用户需要手动完成的部分,它会自动判断每次发送的内容量,然后从总内容中删除已发送的部分,将剩下的继续传给send()
进行发送;因此我们最好使用sendall来发送我们的数据包,这样才能保证我们发送的数据包完整。
Server
服务端先初始化 Socket,建立流式套接字,与本机地址及端口进行绑定,然后通知 TCP,准备好接收连接,调用 accept()
阻塞,等待来自客户端的连接。如果这时客户端与服务器建立了连接,客户端发送数据请求,服务器接收请求并处理请求,然后把响应数据发送给客户端,客户端读取数据,直到数据交换完毕。最后关闭连接,交互结束。
import socket
import sys
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建 Socket 连接(TCP)
print('Socket Created')
try:
sock.bind(('127.0.0.1', 8001)) # 配置 Socket,绑定 IP 地址和端口号
except socket.error as e:
print('Bind Failed...', e)
sys.exit(0)
sock.listen(5) # 设置最大允许连接数,各连接和 Server 的通信遵循 FIFO 原则
while True: # 循环轮询 Socket 状态,等待访问
conn, addr = sock.accept()
try:
conn.settimeout(10) # 如果请求超过 10 秒没有完成,就终止操作
# 如果要同时处理多个连接,则下面的语句块应该用多线程来处理
while True: # 获得一个连接,然后开始循环处理这个连接发送的信息
data = conn.recv(1024)
print('Get value ' + data, end='\n\n')
if not data:
print('Exit Server', end='\n\n')
break
conn.sendall('OK') # 返回数据
except socket.timeout: # 建立连接后,该连接在设定的时间内没有数据发来,就会引发超时
print('Time out')
conn.close() # 当一个连接监听循环退出后,连接可以关掉
sock.close()
调用 accept()
时,Socket 会进入waiting状态。客户端请求连接时,建立连接并返回服务器。accept()
返回一个含有两个元素的元组 (conn, addr)。第一个元素 conn 是新的 Socket 对象,服务器必须通过它与客户端通信;第二个元素 addr 是客户端的 IP 地址及端口。
data = conn.recv(1024)
接下来是处理阶段,服务器和客户端通过 send()
和 recv()
通信(传输数据)。
服务器调用 send()
,并采用字符串形式向客户端发送信息,send()
返回已发送的字符个数。
服务器调用 recv()
从客户端接收信息。调用 recv()
时,服务器必须指定一个整数,它对应于可通过本次方法调用来接收的最大数据量。recv()
在接收数据时会进入blocked状态,最后返回一个字符串,用它表示收到的数据。如果发送的数据量超过了 recv()
所允许的,数据会被截短。多余的数据将缓冲于接收端,以后调用 recv()
时,会继续读剩余的字节,如果有多余的数据会从缓冲区删除(以及自上次调用 recv()
以来,客户端可能发送的其它任何数据)。传输结束,服务器调用 Socket 的 close()
关闭连接。
TCP的信令交互流程
一次完整的TCP通讯包括:建立连接、数据传输、关闭连接
建立连接(三次握手):
1.客户端通过向服务器端发送一个SYN来建立一个主动打开,作为三路握手的一部分。
2.服务器端应当为一个合法的SYN回送一个SYN/ACK。
3.最后,客户端再发送一个ACK。这样就完成了三路握手,并进入了连接建立状态。
数据传输:
1.发送数据端传输PSH数据包
2.接收数据端回复ACK数据包
关闭连接(四次分手):
1. 一端主动关闭连接。向另一端发送FIN包。
2. 接收到FIN包的另一端回应一个ACK数据包。
3. 另一端发送一个FIN包。
4. 接收到FIN包的原发送方发送ACK对它进行确认。
将TCP的握手分手与Socket的传输流程结合起来理解:
首先是TCP的三次握手中的Socket的交互流程
- 服务器调用
socket()
、bind()
、listen()
完成初始化后,调用accept()
阻塞等待; - 客户端 Socket 对象调用
connect()
向服务器发送了一个 SYN 并阻塞; - 服务器完成了第一次握手,即发送 SYN 和 ACK 应答;
- 客户端收到服务端发送的应答之后,从
connect()
返回,再发送一个 ACK 给服务器; - 服务器 Socket 对象接收客户端第三次握手 ACK 确认,此时服务端从
accept()
返回,建立连接。
然后是四次分手
- 某个应用进程调用
close()
主动关闭,发送一个 FIN; - 另一端接收到 FIN 后被动执行关闭,并发送 ACK 确认;
- 之后被动执行关闭的应用进程调用
close()
关闭 Socket,并也发送一个 FIN; - 接收到这个 FIN 的一端向另一端 ACK 确认。
说明:上面的服务端代码只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力很弱,而实际中服务器都需要有并发处理能力,为了达到并发处理,服务器就需要 fork 一个新的进程或者线程去处理请求。
TCP客户端、服务器实时图像传输小Demo
客户端
#--------------------------------------------------------------
# https://blog.csdn.net/qq_42688495/article/details/108279618
# TCP实时图像传输
# 目前为止找到的时延最低的实现方式
# 能够实现传输完视频关闭窗口,服务器不需要重新启动
#--------------------------------------------------------------
import cv2
import time
import socket
# 服务端ip地址
HOST = '192.168.2.108'
# 服务端端口号
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接远程ip
tcpClient.connect(ADDRESS)
cap = cv2.VideoCapture("clock.mp4")
i = 0
while True:
# 计时
start = time.perf_counter()#用于计算端到端时延的计算器
# 读取图像
ref, cv_image = cap.read()
if not ref: #检查视频是否读取完毕,如果读取完毕则ref=0,那麽退出循环
tcpClient.send(b"over")
break
# 压缩图像(如果端到端时延过大,一般是图像未压缩,调整至30~60之间的压缩范围即可)
img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 30])[1]
# 转换为字节流
bytedata = img_encode.tostring()
# 标志数据,包括待发送的字节流长度等数据,用‘,’隔开
flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
#发送图片数据流
tcpClient.send(flag_data)
# 接收服务端的应答
data = tcpClient.recv(1024)
if ("ok" == data.decode()):
# 服务端已经收到标志数据,开始发送图像字节流数据
tcpClient.send(bytedata)
# 接收服务端的应答
data = tcpClient.recv(1024)
if ("ok" == data.decode()):
# 计算发送完成的延时
print("延时:" + str(int((time.perf_counter() - start) * 1000)) + "ms")
i = i+1 #自加,计算总的数据帧数
#跳出循环之后意味着视频全部输出完毕,这时候要
print('视频已经全部传输完毕')
cap.release()
cv2.destroyAllWindows()
服务器
#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as np
HOST = '192.168.2.108'
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip
tcpServer.bind(ADDRESS)
# 开始监听
tcpServer.listen(5)
while True:
print("等待连接……")
client_socket, client_address = tcpServer.accept()
print("连接成功!")
try:
while True:
# 接收标志数据
data = client_socket.recv(1024)
if ("over" == data.decode()):
print("已传输完毕!")
break
if data:
# 通知客户端“已收到标志数据,可以发送图像数据”
client_socket.send(b"ok")
# 处理标志数据
flag = data.decode().split(",")
# 图像字节流数据的总长度
total = int(flag[0])
# 接收到的数据计数
cnt = 0
# 存放接收到的数据
img_bytes = b""
while cnt < total:
# 当接收到的数据少于数据总长度时,则循环接收图像数据,直到接收完毕
data = client_socket.recv(65535)#256000
img_bytes += data
cnt += len(data)
print("receive:" + str(cnt) + "/" + flag[0])
# 通知客户端“已经接收完毕,可以开始下一帧图像的传输”
client_socket.send(b"ok")
# 解析接收到的字节流数据,并显示图像
img = np.asarray(bytearray(img_bytes), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
cv2.imshow("img", img)
cv2.waitKey(1)
else:
print("已断开!")
break
finally:
cv2.destroyAllWindows()
视频demo
tcp视频传输demo