Python-网络编程TCP

TCP 简介


1. TCP 介绍


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

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

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

2. TCP 特点


2.1 面向连接


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

双方间的数据传输都可以通过这一个连接进行。完成数据交换后,双方必须断开此连接,以释放系统资源。这种连接是一对一的,因此 TCP不适用于广播的应用程序,基于广播的应用程序请使用 UDP 协议。

2.2 可靠传输


1)TCP 采用发送应答机制

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

2)超时重传

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

3)错误校验

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

4)流量控制和阻塞管理

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

TCP 与 UDP 的不同点


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

3. UDP 通信模型


UDP 通信模型中,在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中的 "写信"。

4. TCP 客户端


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

4.1 客户端构建流程


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

示例代码:

import socket

def main():
    # 1.创建tcp的套接字
    tcp_socket =socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.链接服务器
    server_ip = input("请输入要链接的服务器的ip:")
    server_port = int(input("请输入要链接的服务器的port:"))
    server_addr = (server_ip, server_port)
    tcp_socket.connect(server_addr)
    # 3.发送数据/接收数据
    send_data = input("请输入要发送的数据:")
    tcp_socket.send(send_data.encode("gbk"))
    # 4.关闭套接字
    tcp_socket.close()

if __name__ == "__main__":
    main()

 

 

5. TCP 服务器


5.1 生活中的手机


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

 

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

5.2 TCP 服务器


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

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

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

import socket

def main():
    # 1.买个手机(创建套接字 socket)
    tcp_server_socket = socket.socket(socket.AF_INET, 
    socket.SOCK_STREAM)
    # 2.插入手机卡(绑定本地信息 bind)
    tcp_server_socket.bind(("", 7890))
    # 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
    # 创建出来的套接字默认是链接别人,不是别人链接你
    tcp_server_socket.listen(128)
    print("------1------")
    # 4.等待别人的电话到来(等待客户端的链接 accept)
    new_client_socket, client_addr = tcp_server_socket.accept()
    """
    accept 有一个返回值,并且返回值是一个元组。如果等号的右边是一个元组,
    等号的左边是两个变量,这叫拆包!
    """
    print("------2------")
    """
    accept()接受一个客户端的链接请求,并返回一个新的套接字,
    不同于以上socket.socket()返回的是用于监听和接受客户端的链接请求的
    套接字;与此客户端通信是通过这个新的套接字上发送和接收数据来完成的。
    每个链接进来的客户端,都会通过accept函数返回一个不同的客户端的
    socket 对象和属于客户端的套接字
    """
    # new_client_socket 用来接收一个新的套接字,
    # client_ddr 用来接收一个链接你的客户端的地址
    # 监听套接字负责等待有新的客户端进行链接,
    # accept 产生的新的套接字用来为客户端服务
    # 谁给你打的电话
    print(client_addr)

    # 服务器接收客户端发送过来的请求
    recv_data = new_client_socket.recv(1024)
    print(recv_data)

    # 回送一部分数据给客户端
    new_client_socket.send("收到请求...".encode("gbk"))

    # 关闭套接字
    new_client_socket.close()
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

 

没有显示 ------2------ ,说明堵塞,等别人来的时候才会解堵塞。什么时候解堵塞?也就是说有一个新的客户端链接你就会解堵塞(也就是说只要一个客户端调用 connect,accept 就会解堵塞,不仅解堵塞,解完堵塞 accept 还有一个返回值,返回值是一个元组,里面分为两部分).

 

accept 的返回值是一个元组,元组里面有两个参数。为了更好地去利用这个元组的返回值,直接写成两个变量的拆包。第一个参数 new_client_socket 用来接收一个新的套接字,第二个参数 client_ddr 用来接收一个链接你的客户端的地址。还拿打电话的例子来说,谁给你打的电话啊,就存放在 client_ddr 里面,为这个客户服务的时候呢,找了一个新的套接字。怎么更好地理解呢?

我们可以把最开始创建的套接字 tcp_server_socket 理解成某个公司买的一个比较大的电话机,这个电话机用的端口是 7890,也就是相当于这个电话机向电信局申请了一个号码 7890,因为这个电话机比较大,是公司里面的电话机,和家用的不一样。不太一样他想做什么呢?它想做一个客服系统。什么叫客服?你给 10086 打电话,打的是 10086,将来真正为我们服务的是谁?是客服。

 

人工服务,找一个人为这个客户,他是怎么办的呢?

你打电话的时候, accept 就有响应, accept 响应的的结果就是一看到这个人给我打电话,就找了个客服,为这个客户端服务。为这个客户端服务的时候,设想一个日常生活中的场景。你和客服人员说,您好,我想把我的套餐升级一下,客服往往会说,先生咱们接下来确认一下身份信息,请问您是 xxxx 的机主么?客服是怎么知道的你的号码?你给10086 电话,那里面就有来电显示,找客服为你服务的时候,就把来电显示号码顺便告诉了客服人员,这个客服人员 就得到了号码,得到了号码现在有几个东西为这个客户端服务呢?一个客服为他服务,客服也知道这个客户的号码(ip)是谁,因此 accept 的返回值是一个元组,这个元组里面有两部分。第一部分找了一个客服(tcp_server_socket),就是套接字,第二部分客户端连接你的时候,客户端的地址(tcp_addr)。

 

服务器10086 这个系统先买了一个电话机,这个电话机默认用来打电话的,先 listenー下,tcp_server_socket 就变成了一个被动的 套接字 了。只要做好了之后,这个套接字就可以等待别人给你打电话了。那么这个时候,客户往套接字( tcp_server_socket)里面打电话,这个套接字 tcp_server_socket 对应的端口就是 7890,所以说这个套接字可以接收来自7890 端口的数据了,收到了数据之后,通过这个套接字( tcp_server_socket) accept,电话机( tcp_server_socket)是用来单独等待别人打电话的,为了服务这个客户,又找了一个新的套接字( new_client_socket)这个新的套接字为这个客户服务,而上面的那个箭头至始至终就没了。

如果这个时候又来了个新的客户端(客户2),这个人也需要打 10086,他会往创建的监听套接字里面打,监听套接字创建完之后,找个客服为客户2服务,他又找了一个客服,即又找了一个套接字,这个套接字为客户2服务,tcp 通信的大体是,服务器先创建一个套接字,放在那等着,来一个客户端给服务器打电话,就找一个客服为这个客户服务,客服拿着信息走了,就是这个道理!

有一个客户2打电话,10086 又找了另一个客服,客服为客户2服务。服务器作为 tcp 的服务器方,客户端打电话往服务器的监听套接字里面打,将来服务器会通过 accept 得到一个新的套接字,这个新的套接字标记了这个客户,有多少个客户端,直白的讲服务器里面就有多少个新的套接字。

接下来客户端所发送的所有数据以及接收到的所有数据,通通都是通过新的套接字来的。监听套接字(tcp_server_socket)只负责等待别人打电话,新的套接字(new_client_socket)负责通信。

监听套接字默认会堵塞,堵塞到什么时候为止?有一个新的客户端链接你,你就会解堵塞,而且解堵塞的时候有个返回值(new_client_socket, client_addr)。

 

这种 拆包 有个特点,后面的元组有多少个元素,前面就多少个变量,这种方式适合你得到的元组里面的元素个数是固定的。

代码升级(循环为多个客户端的服务器.py):

import socket

def main():
    # 1.买个手机(创建套接字 socket)
    tcp_server_socket = socket.socket(socket.AF_INET, 
    socket.SOCK_STREAM)
    # 2.插入手机卡(绑定本地信息 bind)
    tcp_server_socket.bind(("", 7890))
    tcp_server_socket.listen(128)
    while True:
        print("等待客户端的到来...")
        # 4.等待别人的电话到来(等待客户端的链接 accept)
        new_client_socket, client_addr = tcp_server_socket.accept()
        print("一个新的客户端已经到来 %s" % str(client_addr))
        # 服务器接收客户端发送过来的请求
        recv_data = new_client_socket.recv(1024)
        print("客户端发送过来的请求是 %s" % recv_data.decode("gbk"))

        # 回送一部分数据给客户端
        new_client_socket.send("收到请求...".encode("gbk"))

        # 关闭套接字
        # 关闭accept返回的套接字 意味着 不会再为这个客户端服务了
        new_client_socket.close()
        print("服务完毕!")
    # 如果将监听套接字关闭了,那么会导致 不能再次等待新客户的到来,
    # 即xxxx.accept就会失败      
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

 

再升级后(循环为多个客户端服务器并且多次服务一个客户端.py):

import socket


def main():
    # 1.买个手机(创建套接字 socket)
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.插入手机卡(绑定本地信息 bind)
    tcp_server_socket.bind(("", 7890))
    # 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
    # 创建出来的套接字默认是链接别人,不是别人链接你
    tcp_server_socket.listen(128)

    # 这个while True循环为多个客户端服务
    while True:
        print("等待客户端的到来...")
        # 4.等待别人的电话到来(等待客户端的链接 accept)
        new_client_socket, client_addr = tcp_server_socket.accept()
        print("一个新的客户端已经到来 %s" % str(client_addr))
        # new_client_socket 用来接收一个新的套接字,clientAddr 用来接收一个链接你的客户端的地址
        # 监听套接字负责等待有新的客户端进行连接,accept 产生的新的套接字用来为客户端服务
        # 谁给你打的电话

        # 这个while True循环多次为同一个客户端服务多次
        while True:
            # 服务器接收客户端发送过来的请求
            recv_data = new_client_socket.recv(1024)
            print("客户端发送过来的请求是 %s" % recv_data.decode("gbk"))

            # 如果recv解堵塞,那么有两种方式:
            # 1.客户端发送过来数据
            # 2.客户端调用close导致了这里recv解堵塞
            if recv_data:
                # 回送一部分数据给客户端
                new_client_socket.send("收到请求...".encode("gbk"))
            else:
                break

        # 关闭套接字
        new_client_socket.close()
        print("服务完毕!")
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

TCP 注意点:

  1. TCP 服务器一般情况下都需要绑定,否则客户端找不到这个服务器。
  2. TCP 客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的 ip、port 等信息就好,本地客户端可以随机
  3. TCP 服务器中通过 listen 可以将 socket 创建出来的主动套接字变为被动的,这是做 TCP 服务器时必须要做的
  4. 当客户端需要链接服务器时,就需要使用 connect 进行链接,UDP 是不需要链接的而是直接发送,但是 TCP必须先链接,只有链接成功才能通信
  5. 当一个 TCP客户端连接服务器时,服务器端会有 1 个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
  6. listen 后的套接字是被动套接字,用来接收新的客户端的链接请求的,而 accept返回的新套接字是标记这个新客户端的
  7. 关闭 listen 后的套接字是意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
  8. 关闭 accept 返回的套接字意味着这个客户端已经服务完毕
  9. 当客户端的套接字调用 close 后,服务器端会 recv 解堵塞,并且返回的长度为 0,因此服务器可以通过返回数据的长度来区分客户端是否已经下线

 

解堵塞有两种方式:

1. 收到了消息解堵塞

2. 对方调用了 close

 

recv 和 recvfrom 有什么区别?

recvfrom 里面不仅有数据,还有谁发过来的信息;

recv 里面只有数据,因为之前已经知道这个客户端的信息是谁了。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值