[toc]
python基础二十六 网络编程-tcp编程
1.TCP介绍
-
TCP 与 UDP 的区别相当大。它充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。
-
此外,TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。
-
根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现)。
2.TCP三次握手与四次挥手
2.1 TCP三次握手
- TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。
- 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发。
下面来看看三次握手的流程图:
三次握手过程
第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
2.2 TCP四次挥手
-
四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
-
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主 动关闭,而另一方则执行被动关闭。
下面来看看四次挥手的流程图:
中断连接端可以是客户端,也可以是服务器端。
第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次挥手。
上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况, 具体流程如下图:
3.TCP其他特性
3.1 通过序列号与确认应答提高可靠性
-
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。
-
在一定时间内没有等待到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。
-
未收到确认应答并不意味着数据一定丢失。也有可能是数据对方已经收到,只是返回的确认应答在途中丢失。这种情况也会导致发送端误以为数据没有到达目的地而重发数据。
-
此外,也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也屡见不鲜。此时,源主机只要按照机制重发数据即可。
-
对于目标主机来说,反复收到相同的数据是不可取的。为了对上层应用提供可靠的传输,目标主机必须放弃重复的数据包。为此我们引入了序列号。
-
序列号是按照顺序给发送数据的每一个字节(8位字节)都标上号码的编号。接收端查询接收数据 TCP 首部中的序列号和数据的长度,将自己下一步应该接收的序列号作为确认应答返送回去。通过序列号和确认应答号,TCP 能够识别是否已经接收数据,又能够判断是否需要接收,从而实现可靠传输。
3.2 重发超时的确定
-
**重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔。**如果超过这个时间仍未收到确认应答,发送端将进行数据重发。最理想的是,找到一个最小时间,它能保证“确认应答一定能在这个时间内返回”。
-
TCP 要求不论处在何种网络环境下都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差。将这个往返时间和偏差时间相加,重发超时的时间就是比这个总和要稍大一点的值。
-
在 BSD 的 Unix 以及 Windows 系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍。不过,最初其重发超时的默认值一般设置为6秒左右。
-
数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。
-
此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何 确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。
3.3 以段为单位发送数据
- 在建立 TCP 连接的同时,也可以确定发送数据包的单位,我们也可以称其为“最大消息长度”(MSS)。最理想的情况是,最大消息长度正好是 IP 中不会被分片处理的最大数据长度。
- TCP 在传送大量数据时,是以 MSS 的大小将数据进行分割发送。进行重发时也是以 MSS 为单位。
- MSS 在三次握手的时候,在两端主机之间被计算得出。两端的主机在发出建立连接的请求时,会在 TCP 首部中写入 MSS 选项,告诉对方自己的接口能够适应的 MSS 的大小。然后会在两者之间选择一个较小的值投入使用。
3.4利用窗口控制提高速度
-
TCP 以1个段为单位,每发送一个段进行一次确认应答的处理。这样的传输方式有一个缺点,就是包的往返时间越长通信性能就越低。
-
为解决这个问题,TCP 引入了窗口这个概念。确认应答不再是以每个分段,而是以更大的单位进行确认,转发时间将会被大幅地缩短。也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送。如下图所示:
- 窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。上图中窗口大小为4个段。这个机制实现了使用大量的缓冲区,通过对多个段同时进行确认应答的功能。
3.5 滑动窗口控制
-
上图中的窗口内的数据即便没有收到确认应答也可以被发送出去。不过,在整个窗口的确认应答没有到达之前,如果其中部分数据出现丢包,那么发送端仍然要负责重传。为此,发送端主机需要设置缓存保留这些待被重传的数据,直到收到他们的确认应答。
-
在滑动窗口以外的部分包括未发送的数据以及已经确认对端已收到的数据。当数据发出后若如期收到确认应答就可以不用再进行重发,此时数据就可以从缓存区清除。
-
收到确认应答的情况下,将窗口滑动到确认应答中的序列号的位置。这样可以顺序地将多个段同时发送提高通信性能。这种机制也别称为滑动窗口控制。
3.6 窗口控制中的重发控制
在使用窗口控制中, 出现丢包一般分为两 种情况:
-
① 确认应答未能返回的情况。在这种情况下,数据已经到达对端,是不需要再进行重发的,如下图:
-
② 某个报文段丢失的情况。接收主机如果收到一个自己应该接收的序列号以外的数据时,会针对当前为止收到数据返回确认应答。如下图所示,当某一报文段丢失后,发送端会一直收到序号为1001的确认应答,因此,在窗口比较大,又出现报文段丢失的情况下,同一个序列号的确认应答将会被重复不断地返回。而发送端主机如果连续3次收到同一个确认应答,就会将其对应的数据进行重发。这种机制比之前提到的超时管理更加高效,因此也被称为高速重发控制。
4.TCP编程流程
4.1 长连接与短连接
- 长连接
- 长连接指的是三次握手与四次挥手之间分多次传递完所有数据,会长时间占用某个套接字,例如在线看视频、打游戏就是长连接
- 短连接
- 短连接指的是三次握手与四次挥手之间传递少部分数据,多次握手挥手才传递完所有数据,会短时间占用某个套接字,例如浏览器静态页面就是短连接,因为浏览器静态页面可能会非常多,而浏览者不一定会全部阅读完,因此浏览器会先加载一部分页面,然后断开连接,将连接空闲出来提供其他请求,等到浏览者继续往下流览再把剩余的内容建立连接发送完成
4.2 TCP编程流程说明
过程说明
服务端
1.服务端创建socket套接字对象,用于收发数据 socket()
2.服务端绑定自身的IP及端口 bind()
3.服务端设置最大连接数 listen()
- 这里的最大连接数指的是,服务端达到最大连接数后,后续可以排队等待的请求数,超过这个数字就会被服务端拒绝服务,例如listen(5),这里设置为5,假如服务器达到了最大连接数1万,则可以排队等待的请求数为5
4.服务端创建accept(),等待接受socket连接,服务端的accept会与客户端创建的connect建立TCP三次握手
客户端
1.客户端创建socket套接字对象,用于收发数据 socket()
2.客户端创建connect,用于连接服务端,connect会与服务端创建的accept建立TCP三次握手
5.创建TCP服务器、客户端
5.1 最low版TCP服务器、客户端
编写一个最简单的TCP服务器
#导入socket模块
from socket import *
#创建socket套接字,用于tcp监听
tcpSocket = socket(AF_INET,SOCK_STREAM)
#绑定服务端IP地址和端口
tcpSocket.bind(("127.0.0.1",8080))
#设置最大连接数,这里的最大连接数指的是服务端达到最大连接数后可以排队等待的请求数
tcpSocket.listen(5)
#创建一个新的套接字,等待接受socket连接,于收发数据,接受到的数据解构为新套接字和客户端IP地址
newTcpSocket,addr = tcpSocket.accept()
#发送数据,编码根据实际情况指定
newTcpSocket.send("我是tcp服务端,快来连我!".encode())
print(newTcpSocket.recv(1024).decode())
#发送完数据后关闭用于收发数据的新套接字对象
newTcpSocket.close()
#关闭用于监听的套接字对象,关闭后程序不再接受任何新的客户端连接
tcpSocket.close()
编写一个最简单的TCP客户端
#导入socket模块
from socket import *
#创建socket套接字对象
tcpSocket = socket(AF_INET,SOCK_STREAM)
#创建connect,用于连接TCP服务器
tcpSocket.connect(("127.0.0.1",8080))
#接受数据
recvData = tcpSocket.recv(1024)
print(recvData.decode())
#向TCP服务端发送数据
tcpSocket.send("我是tcp客户端,我来了!".encode())
#关闭套接字对象
tcpSocket.close()
5.2 单进程TCP服务器
单进程的TCP服务器每次只能服务一个客户端
#导入socket模块
from socket import *
#创建只用来监听的套接字对象
serverSocket = socket(AF_INET,SOCK_STREAM)
#绑定TCP服务端IP和端口
addr = ("192.168.34.90",9999)
serverSocket.bind(addr)
#设置最大排队等待数
serverSocket.listen(3)
#这里要能多次处理客户端连接请求,因此写一个while循环
while True:
print("主进程等待新客户端连接")
#创建accept,用来等待客户端socket连接
newSocket,clientAddr = serverSocket.accept()
print(newSocket) #打印结果 <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.34.90', 9999), raddr=('192.168.34.90', 55255)>
print(clientAddr) #打印结果 ('192.168.34.90', 55255)
#clientAddr[0]就是客户端的IP地址,clientAddr[1]就是客户端的端口
print(f"主进程接下来负责处理{clientAddr[0]},端口{clientAddr[1]}的请求")
#传输过程可能会出错,因此写一个异常处理避免程序崩溃
try:
while True:
#接受数据并解码,编码类型根据实际情况填写
recvData = newSocket.recv(1024).decode()
#做一个判断,如果收到的数据内容长度大于0,则说明是在接受数据,并打印接受的数据,都则就提示客户端已关闭
if len(recvData) > 0:
print(f"接收到来自{clientAddr[0]},端口{clientAddr[1]}的数据:",recvData)
else:
print(f"{clientAddr[0]}客户端已关闭")
break
except Exception:
print("接收数据出错!")
#无论接受是否报错最后都执行关闭新建的用于收发数据的套接字
finally:
newSocket.close()
break
#关闭用于监听的套接字
serverSocket.close()
TCP客户端编写
#导入socket模块
from socket import *
#创建socket套接字对象
tcpSocket = socket(AF_INET,SOCK_STREAM)
#创建connect,用于连接TCP服务器
tcpSocket.connect(("192.168.34.90",9999))
#向TCP服务端发送数据
while True:
s = input("请输入要发送的内容>>>")
tcpSocket.send(s.encode())
#如果客户端输入的是Q或者q,则关闭套接字对象并退出程序
if s.lower() == "q":
tcpSocket.close()
break
服务端接受本机客户端发送的信息
服务端接收其他机器客户端发送的信息
5.3 并发TCP服务器
5.3.1 setsockopt(SOL_SOCKET,SO_REUSEADDR,1)方法
serSocket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
- serSokcet是套接字对象变量名,此选项意思为重新设置套接字选项,重复使用绑定的信息
这么做的原因?
- 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到SO_REUSEADDR选项。
在多进程中的作用?
- 使用多进程编写并发TCP服务,因为有多个子进程可以提供服务,因此每个子进程需要占用主进程的端口,但是端口只能被一个进程占用,这个时候就出现了端口被占用的情况,所以用到了**setsockopt(SOL_SOCKET,SO_REUSEADDR,1)**方法
在多线程中的作用?
- 使用多线程编写并发TCP服务,线程之间是共享数据的,不存在端口被占用情况,但是多线程中每个子线程使用主进程的端口后,系统会保留几分钟端口被占用的状态,不让别的线程使用,使用**setsockopt(SOL_SOCKET,SO_REUSEADDR,1)**方法让端口不被系统保留