返回顶部

3- TCP

tcp介绍

TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

tcp通信需要经过创建连接、数据传送、终止连接三个步骤。

tcp通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话""

 

 

 

 

TCP特点

1. 面向连接

通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。

双方间的数据传输都可以通过这一个连接进行。

完成数据交换后,双方必须断开此连接,以释放系统资源。

这种连接是一对一的,不适用于广播的应用程序,基于广播的应用程序适合使用UDP协议。

2. 基于数据流(字节流)

1)tcp数据流

发送端执行多次写操作(send)时,TCP模块先把这些数据放入TCP发送缓冲区中,当TCP模块真正可以发送数据时,才把TCP发送缓冲区等待发送的数据封装成一个或多个TCP报文段发出。

TCP会把数据流切分成一个或多个适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。

TCP模块发出的报文的个数与应用程序的写操作(send)的次数没有固定的数量关系。

接收端收到报文段后,TCP模块把它们携带的应用数据按照TCP报文段的序号依次放入TCP接收缓冲区中,同时通知应用程序读取数据,接收端应用程序可以一次或多次读取数据(取决于用户指定的应用程序读缓冲区大小)

应用程序执行读操作的次数和TCP模块接收到的TCP报文的个数没有固定的数量关系。

发送端执行的写操作(send)次数与接收端执行的读操作(recv)次数没有数量对应关系。

 

 

2)对比udp数据报

UDP发送端执行一次写操作(sendto),UDP模块把它封装成一个UDP数据报并发送。

接收端针对每一个数据报执行读操作(recvfrom),否则就会发生丢包,并且如果用户没有指定足够的应用程序缓冲区来读取数据报,则UDP数据报就会被截断(接收不完整)。

 

 

 

3. 可靠传输

1)TCP采用发送应答机制

TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功

2)超时重传

发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。

TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。

3)错误校验

TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

4) 流量控制和阻塞管理

流量控制用来避免主机发送得过快而使接收方来不及完全收下。

TCP与UDP的不同点

  • 面向连接(确认有创建三方交握,连接已创建才作传输。)
  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无差错的数据传输
  • 阻塞/流量控制

 

建立连接(三次握手)

TCP用三次握手(three-way handshake)过程创建一个连接。在连接创建过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。

 

 

我们把tcp通信的报文称为段。

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1。

    客户端发出段1,SYN位表示连接请求。序号是1000(实际是一个随机数,此处以1000为例),这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

  2. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。

    服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求SYN,序号是8000(实际也是一个随机数,此处以8000为例),同时声明最大尺寸为1024。

  3. 客户必须再次回应服务器端一个ACK报文,这是报文段3。

    客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。

在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三次握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused。

 

数据传输

 

 

 

 

  1. 客户端发出段4,包含从序号1001开始的20个字节数据。

  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据。

  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。

在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

 

关闭连接(四次挥手)

 

 

 

 

由于TCP连接是可以双向通信的(全双工),因此每个方向都必须单独进行关闭。

这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭连接的请求。

  2. 服务器发出段8,应答客户端的关闭连接请求。

  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。

  4. 客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

 

tcp程序流程

 

tcp服务器

生活中的电话机

如果想让别人能更够打通咱们的电话获取相应服务的话,需要做一下几件事情:

  1. 买个手机
  2. 插上手机卡
  3. 设计手机为正常接听状态(即能够响铃)
  4. 静静的等着别人拨打

tcp服务器

如同上面的电话机过程一样,在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:

  1. socket创建一个套接字
  2. bind绑定ip和port
  3. listen使套接字变为可以被动链接
  4. accept等待客户端的链接
  5. recv/send接收发送数据

一个很简单的tcp服务器如下:

 

import socket

# 创建socket
# 注意TCP协议对应的为SOCK_STREAM 流式
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定IP地址和端口
address = ("", 8000)
server_sock.bind(address)

# 让服务端的socket开启监听,等待客户端的连接请求
# listen中的参数表示已经建立链接和半链接的总数
# 如果当前已建立链接数和半链接数已达到设定值,那么新客户端不会立即connect成功,而是等待服务器能够处理时
server_sock.listen(128)

# 使用accept方法接收客户端的连接请求
# 如果有新的客户端来连接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_sock用来为这个客户端服务,与客户端形成一对一的连接
# 而server_sock就可以省下来专门等待其他新客户端的连接请求
# client_addr是请求连接的客户端的地址信息,为元祖,包含用户的IP和端口
client_sock, client_addr = server_sock.accept()
print("客户端%s:%s进行了连接!" % client_addr)

# recv()方法可以接收客户端发送过来的数据,指明最大收取1024个字节的数据
recv_data = client_sock.recv(1024)
# python3中收到的数据为bytes类型
# recv_data.decode()将bytes类型转为str类型
print("接收到的数据为:", recv_data.decode())

# send()方法向客户端发送数据,要求发送bytes类型的数据
client_sock.send("thank you!\n".encode())

# 关闭与客户端连接的socket
# 只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
client_sock.close()

# 关闭服务端的监听socket
# 要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
server_sock.close()
View Code

测试

我们暂时还没有写tcp客户端,可以用nc命令来作为客户端进行测试。

 

# nc 服务器的IP 服务器的端口
nc 127.0.0.1 8000

  

tcp客户端

tcp客户端,并不是像之前一个段子:一个顾客去饭馆吃饭,这个顾客要点菜,就问服务员咱们饭店有客户端么,然后这个服务员非常客气的说道:先生 我们饭店不用客户端,我们直接送到您的餐桌上

如果,不学习网络的知识是不是 说不定也会发生那样的笑话 ,哈哈

所谓的服务器端:就是提供服务的一方,而客户端,就是需要被服务的一方

tcp客户端构建流程

tcp的客户端要比服务器端简单很多,如果说服务器端是需要自己买手机、查手机卡、设置铃声、等待别人打电话流程的话,那么客户端就只需要找一个电话亭,拿起电话拨打即可,流程要少很多

示例代码:

import socket

# 创建客户端socket用以跟服务器连接通信
# tcp协议对应为SOCK_STREAM
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# connect方法用来连接服务器
server_addr = ("127.0.0.1", 8000)
client_sock.connect(server_addr)

# 提示用户输入要发送的数据
msg = input("请输入要发送的内容:")
# send()方法想服务器发送数据
client_sock.send(msg.encode())

# recv()接收对方发送过来的数据,最大接收1024个字节
recv_data = client_sock.recv(1024)
print("收到了服务器的回应信息:%s" % recv_data.decode())

# 关闭客户端套接字
client_sock.close()
View Code

测试

可以用我们刚才写的服务器程序,也可用nc命令进行测试

# -l参数表示服务器监听
# nc -l 绑定的服务器ip 端口
nc -l 127.0.0.1 8000

tcp十种状态

 

 

 

  • CLOSED:表示关闭状态(初始状态)。
  • LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
  • SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
  • SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
  • ESTABLISHED:表示连接已经建立。
  • FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是: FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。 FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
  • FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
  • TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
  • CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
  • LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。

tcp第十一种状态

  • CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

tcp的2MSL问题

 

 

 

2MSL (Maximum Segment Lifetime) TIME_WAIT状态的存在有两个理由:

  1. 让4次挥手关闭流程更加可靠;4次挥手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。

  2. 防止lost duplicate对后续新建正常链接的传输造成破坏。

    lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。

    另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,则会对我们的传输造成致命的错误。

    TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000,len=1000, 则TCP认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。

该状态为什么设计在主动关闭这一方:

  1. 发最后ACK的是主动关闭一方。
  2. 只要有一方保持TIME_WAIT状态,就能起到避免incarnation connection在2MSL内的重新建立,不需要两方都有。

如何正确对待2MSL TIME_WAIT?

RFC (Request For Comments),是一系列以编号排定的文件。收集了有关因特网相关资讯,以及UNIX和因特网社群的软件文件。

RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。

若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。这甚至比RFC限制更为严格,RFC仅仅是要求socket pair不一致,而实现当中只要这个port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。

解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端口去连它,TCP协议会告诉我们连接失败,原因为Address already in use.

RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

 

tcp服务器(REUSEADDR)

为了解决服务器socket可能的2MSL延迟问题,我们可以为服务器socket设置SO_REUSEADDR选项。

import socket

# 创建socket
# 注意TCP协议对应的为SOCK_STREAM 流式
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 使用setsockopt()方法设置socket的选项参数
# SOL_SOCKET = Set Option Level _ SOCKET 设置选项级别为socket级
# SO_REUSEADDR = Socket Option _ REUSEADDR 设置socket的选项参数为重用地址功能
# 1 表示开启此选项功能,即开启重用地址功能
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定IP地址和端口
address = ("", 8000)
server_sock.bind(address)

# 让服务端的socket开启监听,等待客户端的连接请求
server_sock.listen(128)

# 使用accept方法接收客户端的连接请求
client_sock, client_addr = server_sock.accept()
print("客户端%s:%s进行了连接!" % client_addr)

# recv()方法可以接收客户端发送过来的数据,指明最大收取1024个字节的数据
recv_data = client_sock.recv(1024)
print("接收到的数据为:", recv_data.decode())

# send()方法向客户端发送数据,要求发送bytes类型的数据
client_sock.send("thank you!\n".encode())

# 关闭与客户端连接的socket
client_sock.close()

# 关闭服务端的监听socket
server_sock.close()
View Code

应用:模拟QQ聊天

服务器参考代码

 

import socket

# 创建socket
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置socket可以重用地址
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定本地信息
address = ('', 8000)
server_sock.bind(address)

# 开启监听
server_sock.listen(128)

while True:
    # 接收客户端的连接请求
    client_sock, client_addr = server_sock.accept()
    print("与客户端%s:%s建立了连接" % client_addr)

    while True:

        # 接收对方发送过来的数据,最大接收1024个字节
        recv_data = client_sock.recv(1024)

        # 如果接收的数据的长度为0,则意味着客户端关闭了链接
        if len(recv_data) > 0:
            print("客户端说:%s" % recv_data.decode())
            # 发送一些数据到客户端
            msg = input("请输入回复的内容:")
            client_sock.send(msg.encode())
        else:
            break

    print("客户端%s:%s已下线" % client_addr
          )
    # 关闭客户端socket
    client_sock.close()

# 关闭服务器socket
server_sock.close()
View Code

客户端参考代码

 

import socket

# 创建socket
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 链接服务器
server_addr = ('127.0.0.1', 8000)
client_sock.connect(server_addr)

while True:

    # 提示用户输入数据
    msg = input("请输入要发送的消息:")

    # 若用户输入了内容,则发送,否则退出
    if len(msg) > 0:
        client_sock.send(msg.encode())

        # 接收对方发送过来的数据,最大接收1024个字节
        recv_data = client_sock.recv(1024)
        print("收到的消息:%s" % recv_data.decode())
    else:
        break

# 关闭套接字
client_sock.close()
View Code

tcp长连接和短连接

TCP在真正的读写操作之前,server与client之间必须建立一个连接,

当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,

连接的建立通过三次握手,释放则需要四次握手,

所以说每个连接的建立都是需要资源消耗和时间消耗的。

TCP通信的整个过程,如下图:

 

1. TCP短连接

模拟一种TCP短连接的情况:

  1. client 向 server 发起连接请求
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,此时双方任何一个都可以发起 close 操作

在第 步骤5中,一般都是 client 先发起 close 操作。当然也不排除有特殊的情况。

从上面的描述看,短连接一般只会在 client/server 间传递一次读写操作!

2. TCP长连接

再模拟一种长连接的情况:

  1. client 向 server 发起连接
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,连接不关闭
  6. 后续读写操作...
  7. 长时间操作之后client发起关闭请求

3. TCP长/短连接操作过程

3.1 短连接的操作步骤是:

建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接

 

3.2 长连接的操作步骤是:

建立连接——数据传输...(保持连接)...数据传输——关闭连接

 

4. TCP长/短连接的优点和缺点

  • 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。

    对于频繁请求资源的客户来说,较适用长连接。

  • client与server之间的连接如果一直不关闭的话,会存在一个问题,

    随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,

    如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;

    如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,

    这样可以完全避免某个蛋疼的客户端连累后端服务。

  • 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
  • 但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。

5. TCP长/短连接的应用场景

  • 长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。

    每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,

    再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,

    再次处理时直接发送数据包就OK了,不用建立TCP连接。

    例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,

    而且频繁的socket 创建也是对资源的浪费。

  • 而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,

    而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,

    如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,

    那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

 

posted @ 2017-12-08 19:03  Crazymagic  阅读(184)  评论(0编辑  收藏  举报