当前位置:   article > 正文

Python-Level2-day11:TCP客户端/服务端传输(循环模型之短连接与长连接形态);TCP粘包问题;与UDP对比;数据传输过程原理;_python使用tcp长连接发送数据

python使用tcp长连接发送数据
前情回顾
​
1. 网络概念
​
  什么是网络 : 数据传输
​
  OSI七层模型 : 网络通信标准化流程模型
​
  tcp/ip模型 : 实际工作模型
​
  网络协议 : 网络通信过程中都遵循的规定
​
  网络地址 : IP(公网,内网)   端口
​
  服务端客户端:  C/S结构    B/S
​
​
 cookie:
  面试理论问题
  这是什么->具体描述->特点(优点,缺点)->话题引申->我用它干什么
​
2. 套接字编程  (写功能,确定地址,选择服务)
​
  UDP套接字 (也叫数据报套接字)  (udp协议)
​
  服务端: socket()->bind()->recvfrom/sendto->close()
   客户端: socket()--------->sendto/recvfrom->close()
​
​
 训练1:
 改写代码,让客户端可以循环收发消息,
 直到输入##两边都退出
​
注意:1.address error  当非法中断服务端端口,而不是##程序中断时候,系统认为网络断开
 与服务连接不上,此时重新运行服务端会报端口地址重复错误,过几分钟就好了。
 操作系统自动将这个地址丢弃。
   2.broken pipe ereror --在客户端不通知服务端的情况下退出(自己非法结束终端),
 那么服务端的recv不在阻塞了,他会返回一个空字节串,然后打印空,发送thanks,
 再循环一遍,再返回空,再打印空,然后就不发thanks了,开始报错。
   3.连接就意味责任,一方退出必须告诉对方,否者报错。
 解决方法:服务端写上if not data or data == b"##",python的客户端是
 无法发送空字节串。
​
​
 训练2:
 改写代码,让客户端退出但是服务端不退出,
 服务端可以继续处理下一个客户端连接
​
练习01:
 完成一个图片上传练习,将一个图片从客户端
 上传到服务端,在服务端以 20210409.jpg为
 名字保存
 图片自选 注意,图片可能比较大,不建议一次性read读取
​
​
 练习02: 模拟一个问答机器人
 从客户端输入问题发送给服务端,
 服务端根据问题中是否有关键字返回对应的回答
​
​
 你多大了 : 我今年2岁了
 男生女生 : 我是机器人
 叫什么 : 我叫小美
     人家还小不知道啦
​
​
 作业 : 1. 今天的重点代码 自己独立完成
    \2. 两天的练习自己没写出来的 独立完成

1.3 TCP 传输方法

1.3.1 TCP传输特点

  • 面向连接的传输服务:即传输之前必须建立连接关系

    • 传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复

    • 可靠性保障机制(都是操作系统网络服务自动帮应用完成的):

      • 在通信前需要建立数据连接

      • 确认应答机制

      • 通信结束要正常断开连接

  • 三次握手(建立连接)

    • 客户端向服务器发送消息报文请求连接

    • 服务器收到请求后,回复报文确定可以连接

    • 客户端收到回复,发送最终报文连接建立

img编辑

  • 四次挥手(断开连接)

    • 主动方发送报文请求断开连接(谁先发起都可以)

    • 被动方收到请求后,立即回复报文,表示准备断开

    • 被动方准备就绪,再次发送报文表示可以断开

    • 主动方收到确定,发送最终报文完成断开

img编辑

1.3.2 TCP服务端处理

img编辑

服务端通信流程:创建TCP套接字->绑定自己服务端地址->具备监听功能,可以被客户端连接listen->阻塞等待客户端连接accept->收发消息->关闭套接字。因此对比于udp,多了listen与accept是处理连接三次握手用的。

  • 创建套接字

    sockfd=socket.socket(family,type)
    功能:创建套接字
    参数:family  网络地址类型 AF_INET表示ipv4
         type  套接字类型 SOCK_STREAM默认,表示tcp套接字 (也叫流式套接字) 
    返回值: 套接字对象
  • 绑定地址 (与udp套接字相同)

  • 设置监听(使得具备被客户端连接的功能)

    sockfd.listen(n)
    功能 : 将套接字设置为监听套接字,确定监听队列大小,即具备被客户端连接能力。实现缓冲效果。
    参数 : 监听队列大小(只能一个一个处理客户端,给他一个队列排队等待)
    在Linux下这个参数就是摆设,操作系统自动帮你设置监听队列缓冲区大小,设置越大占内存越大,并且三次握手几乎在瞬间完成。

img编辑

  • 处理客户端连接请求

    connfd,addr = sockfd.accept()
    功能: 阻塞等待处理客户端请求
    返回值: connfd  客户端连接套接字
            addr    连接的客户端地址

img

服务端accept阻塞等待客户端发送数据建立连接关系后,为每一个客户端单独创建一个connfd套接字,应对每一个客户端的数据传输功能,而socket专门用于建立连接,连接完成一个客户端创建一个connfd,他是客户端的专属服务管家。如果客户端退出了,对应的connfd随之销毁。

  • 消息收发

  data = connfd.recv(buffersize)
    功能 : 接受客户端消息
    参数 :每次最多接收消息的大小
    返回值: 接收到的内容
​
    n = connfd.send(data)
    功能 : 发送消息
    参数 :要发送的内容  bytes格式
    返回值: 发送的字节数

  • 关闭套接字 (与udp套接字相同)

"""
   tcp服务端数据传输案例:重点代码
"""
from socket import *
​
# 1.创建tcp套接字
tcp_socket = socket(AF_INET, SOCK_STREAM)  # 默认选择也是TCP协议
​
# 2.绑定地址:只有服务端才需要固定地址
tcp_socket.bind(("0.0.0.0", 8888))
​
# 3.设置为监听功能:同一时刻允许5个客户端发起连接给他一个缓冲队列等待
tcp_socket.listen(5)
​
# 4.阻塞等待客户端处理连接
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr) #发现客户端需要与我建立连接完成
​
# 5.先收后发消息
data = connfd.recv(1024)  # 阻塞等待接收数据:一次连接接收数据后就断开,tcp不循环接收数据照样丢失
print("服务端收到数据为:", data.decode())
connfd.send(b"Thinks")
​
# 6.关闭
connfd.close()  # 关闭与某个客户端连接:完成四次挥手
tcp_socket.close()  # 关闭套接字,断开所有客户端
​

1.3.3 TCP客户端处理

img

创建与服务端相同类型的套接字->客户端发起连接->数据的发送与接受->关闭套接字

  • 创建TCP套接字

  • 请求连接

    sockfd.connect(server_addr)
    功能:连接服务器
    参数:元组  服务器地址
  • 收发消息

注意: 防止两端都阻塞,recv send要配合用

  • 关闭套接字

"""
    tcp客户端数据传输案例
"""
from socket import *
​
# 1.创建与服务端相同tcp套接字
tcp_socket = socket(AF_INET, SOCK_STREAM)  # 默认选择也是TCP协议
​
# 2.请求连接,三次握手,与服务端accept对应:写服务器地址
tcp_socket.connect(("127.0.0.1", 8888))
​
# 3.先发后收消息
tcp_socket.send(b"hello")
data = tcp_socket.recv(1024)  # 阻塞等待接收数据
print("客户端收到数据为:", data.decode())
​
# 4.关闭tcp套接字
tcp_socket.close()
​

img编辑

"""
     改写代码,让客户端可以循环收发消息,
     直到输入##两边都退出
"""
from socket import *
from time import sleep
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
print("wait for connect...")
connfd, addr = tcp_socket.accept()
print("Connect from ", addr)
​
while True:
    data = connfd.recv(1024)  # 阻塞等待接收数据
    # 当连接的两端有一端(通常是客户端)突然退出的时候
    # 另外一端的recv就不再阻塞,会得到一个空字节串
    if data.decode() == "##" or not data:
        break
    print("服务端收到数据为:", data.decode())
    connfd.send(b"Thinks")
    # sleep(0.1)
​
connfd.close()  # 关闭与某个客户端连接:完成四次挥手
tcp_socket.close()  # 关闭套接字,断开所有客户端
​
​
"""客户端程序"""
from socket import *
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
​
while True:
    senddata = input(">>")
    tcp_socket.send(senddata.encode())#如果发送##通知服务端要断开连接
    if senddata == "##":#不然客户端不打招呼给服务端就退出容易管道破裂
        break#客户端发不了空字节串,因为发他没有意义
    data = tcp_socket.recv(1024)  # 阻塞等待接收数据
    print("客户端收到数据为:", data.decode())
tcp_socket.close()
​


"""
    改写代码,让客户端退出但是服务端不退出,
    服务端可以继续处理下一个客户端连接
​
    这里程序是循环模型,不是并发模型,不能应对多个客户端同时发出数据做处理,只能在
    这个客户端处理完成退出后,才能等待下一个客户端连接。即tcp 长连接形态
    udp也是循环模型,虽然任何人发数据都接收,但只是处理快的让你感觉多个客户端
    可以同时处理
"""
from socket import *
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
while True:
    print("wait for connect...")
    connfd, addr = tcp_socket.accept()
    print("Connect from ", addr)
    while True:
        data = connfd.recv(1024)  # 阻塞等待接收数据
        if data.decode() == "##" or not data:
            break
        print("服务端收到数据为:", data.decode())
        connfd.send(b"Thinks")
    connfd.close()
​
"""
   客户端程序
"""
from socket import *
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
​
while True:
    senddata = input(">>")
    tcp_socket.send(senddata.encode())
    if senddata == "##":
        break
    data = tcp_socket.recv(1024)  # 阻塞等待接收数据
    print("客户端收到数据为:", data.decode())
​
tcp_socket.close()
​
​
​


"""
 完成一个图片上传练习,将一个图片从客户端
上传到服务端,在服务端以 20210409.jpg为名字保存
图片自选 注意,图片可能比较大,不建议一次性read读取
"""
from socket import *
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.connect(("127.0.0.1", 8888))
file1 = open("caihua.jpeg", "rb")
while True:
    data = file1.read(1024)
    if not data:
        break
    tcp_socket.send(data)
tcp_socket.close()
#发送完成客户端关闭,客户端退出后服务端收到系统发的空字符串
​
​
"""
    服务端程序
"""
"""
 完成一个图片上传练习,将一个图片从客户端
上传到服务端,在服务端以 20210409.jpg为名字保存
图片自选 注意,图片可能比较大,不建议一次性read读取
"""
from socket import *
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
file2 = open("20210402.jpg", "wb")
​
while True:
    print("wait for connect...")
    connfd, addr = tcp_socket.accept()
    print("Connect from ", addr)
    while True:
        data = connfd.recv(1024)  # 阻塞等待接收数据
        file2.write(data)
        if not data:
            break
    connfd.close()
    file2.close()
​
​
​
"""
升级
    tcp服务端例子:短连接形态,看起来好像和udp一样同一时间可以应对多个客户端
    其实并不是,只是应对的客户端只接收4字节后就迅速断开,不是真正的同时
    即只处理一次数据交互就断开连接。如果下次还想进行交互就必须建立连接
    之前是一次只应对一个客户端,一次连接长久有效,效率较高
    现在是不利用并发技术下 一次应对多个客户端,由于每次需要三次挥手,效率较低
"""
from socket import *
​
def main():
    tcp_sock = socket()
    tcp_sock.bind(("0.0.0.0", 8888))
    tcp_sock.listen(5)
    while True:
        connfd, add = tcp_sock.accept()
        print("connect from:", add)
        handle(connfd)  # 只进行一次客户端交互数据就断开,不能长期占有服务器端口代码
        connfd.close()
​
​
def handle(connfd):
    data = connfd.recv(4)
    print(data.decode())
    connfd.send(b"ok")
​
​
if __name__ == '__main__':
    main()
​
"""
    客户端1
"""
from socket import *
​
​
def main():
    while True:
        msg = input(">>")
        if not msg:
            break
        tcp_connect(msg)
​
​
def tcp_connect(msg):
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    tcp_socket.connect(("127.0.0.1", 8888))
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("客户端收到数据为:", data.decode())
​
    tcp_socket.close()
​
​
if __name__ == '__main__':
    main()
​
"""
    客户端2:如果服务端1的数据传输需要占用很长时间,那么客户端2也连接不上服务端。因此这个循环模型适合处理小数据传输,传输完成后立即等待下一个客户端连接。
"""
from socket import *
​
​
def main():
    while True:
        msg = input(">>")
        if not msg:
            break
        tcp_connect(msg)
​
​
def tcp_connect(msg):
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    tcp_socket.connect(("127.0.0.1", 8888))
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("客户端收到数据为:", data.decode())
​
    tcp_socket.close()
​
​
if __name__ == '__main__':
    main()
"""
练习02: 模拟一个问答机器人
从客户端输入问题发送给服务端,
服务端根据问题中是否有关键字返回对应的回答
​
客户端2
你多大了 :  2ages
​
客户端3:
男生女生 :  robort
​
客户端1
叫姓名什么 : xiao yi
"""
from socket import *
​
def main():
    tcp_sock = socket()
    tcp_sock.bind(("0.0.0.0", 8888))
    tcp_sock.listen(5)
    while True:
        connfd, add = tcp_sock.accept()
        print("connect from:", add)
        handle(connfd)
        connfd.close()
​
​
def handle(connfd):
    data = connfd.recv(1024)
    if "age" in data.decode():
        connfd.send(b"2 ages")
    elif "name" in data.decode():
        connfd.send(b"xiao yi")
    elif "sex" in data.decode():
        connfd.send(b"robot")
    else:
        connfd.send(b"sorry I not know")
​
​
if __name__ == '__main__':
    main()
​
 """客户端1""" 
from socket import *
​
​
def main():
    while True:
        msg = input(">>")
        if not msg:
            break
        tcp_connect(msg)
​
​
def tcp_connect(msg):
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    tcp_socket.connect(("127.0.0.1", 8888))
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("客户端1收到数据为:", data.decode())
​
    tcp_socket.close()
​
​
if __name__ == '__main__':
    main()
​
 
"""
客户端2
"""
 from socket import *
​
​
def main():
    while True:
        msg = input(">>")
        if not msg:
            break
        tcp_connect(msg)
​
​
def tcp_connect(msg):
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    tcp_socket.connect(("127.0.0.1", 8888))
    tcp_socket.send(msg.encode())
    data = tcp_socket.recv(1024)
    print("客户端2收到数据为:", data.decode())
    tcp_socket.close()
​
​
if __name__ == '__main__':
    main()
​

1.3.4 TCP套接字细节

  • tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会收到一个空字串。

  • tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError

  • 一个服务端可以同时连接多个客户端,也能够重复被连接,即循环模型的短连接形态

  • tcp粘包问题

    • 产生原因

      • 为了解决数据再传输过程中可能产生的速度不协调问题,例如发送端发10个字节,接收端按照5个字节接收。如果不是长连接模型那么另外的5个字节就丢了,但操作系统设置了缓冲区专门给tcp提供可靠不丢失传输服务,分两次发送。

      • 实际网络工作过程比较复杂,导致消息收发速度不一致

      • tcp以字节流方式进行数据传输,udp是数据报方式传输,在接收时不区分消息边界,发送端发送的数据都连接堆积在缓冲区里面,如同水流不间断。

      • img编辑

    • 带来的影响

      • 如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。

    • 处理方法

      • 消息格式化处理,如人为的添加消息边界,用作消息之间的分割。

      • 软件sleep来控制发送的速度

from socket import *
from time import sleep
​
tcp_socket = socket(AF_INET, SOCK_STREAM)
tcp_socket.bind(("0.0.0.0", 8888))
tcp_socket.listen(5)
while True:
    print("wait for connect...")
    connfd, addr = tcp_socket.accept()
    print("Connect from ", addr)
    while True:
        data = connfd.recv(2)  # 阻塞等待接收数据
        if data.decode() == "##" or not data:
            break
        print("服务端收到数据为:", data.decode())
        connfd.send(b"Thinks#")#区分粘包边界
        sleep(0.1)#等完全发出去Thinks后再接收服务端缓冲区剩余字节
    connfd.close()
    

1.3.5 TCP与UDP对比

  • 传输特征

    • TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性

    • TCP传输数据处理为字节流,而UDP处理为数据包形式

    • TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高

  • 套接字编程区别

    • 创建的套接字类型不同

    • tcp套接字会有粘包,udp套接字有消息边界不会粘包

    • tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要

    • tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom

  • 使用场景

    • tcp更适合对准确性要求高,传输数据较大的场景

      • 文件传输:如下载电影,访问网页,上传照片

      • 邮件收发

      • 点对点数据传输:如点对点聊天,登录请求,远程访问,发红包

    • udp更适合对可靠性要求没有那么高,传输方式比较自由的场景

      • 视频流的传输: 如直播,视频聊天

      • 广播:如网络广播,群发消息

      • 实时传输:如游戏画面

    • 在一个大型的项目中,可能既涉及到TCP网络又有UDP网络

1.4 数据传输过程

1.4.1 传输流程

  • 发送端由应用程序发送消息,上到下逐层添加首部信息,最终在物理层发送消息包。如果数据过大,拆分成一帧一帧发送,帧头与帧尾将这大数据按照顺序连接起来。

  • 发送的消息经过多个网络设备节点(交换机,路由器)传输,最终到达目标主机。

  • 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。

img编辑

1.4.2 传输层的TCP协议首部信息(了解)

img编辑

  • 源端口和目的端口 各占2个字节,分别写入源端口和目的端口。

  • 序号seq占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。

  • 确认号ack占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。

  • 确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。

  • 同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。

  • 终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号