【Python】网络编程之UDP、TCP

第十一节:UDP

一、Socket的定义

Socket的英文原义是“孔”或“插座”,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

建立网络通信连接至少要一对端口号(socket),socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。

基本上,Socket 是任何一种计算机网络通讯中最基础的内容。例如当你在浏览器地址栏中输入 http://www.baidu.com会打开一个套接字,然后连接到 http://www.baidu.com/ 并读取响应的页面然后然后显示出来。而其他一些聊天客户端如 qq和 skype 也是类似。任何网络通讯都是通过 Socket 来完成的。
Python 官方关于 Socket 的函数请看 http://docs.python.org/library/socket.html

二、Socket的类型

socket(family,type[,protocal]) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。

image.png

注意点:

  • TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的, 每次发送要指定是发给谁。
  • 服务端与客户端不能直接发送列表,元组,字典。需要字节化(data)。
  • 三、Socket相关函数

    1、服务端socket函数:

    image.png

    2、客户端Socket函数

    image.png

    3、公共的Socket函数

    socket函数 描述
    s.recv(bufsize[,flag]) 接受TCP,UDP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
    s.send(string[,flag]) 发送TCP,UDP数据。将string中的数据发送到连接的套接字。返回值是要 收的最大数据量。flag提供有关消发送的字节数量,该数量可能小于 息的其他信息,通常可以忽略。
    s.sendall(string[,flag]) 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
    s.recvfrom(bufsize[.flag]) 接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
    s.sendto(string[,flag],address) 发送UDP数据。将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
    s.close() 关闭套接字。
    s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
    s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
    s.setsockopt(level,optname,value) 设置给定套接字选项的值。
    s.getsockopt(level,optname[.buflen]) 设置给定套接字选项的值。
    s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
    s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
    s.fileno() 返回套接字的文件描述符。
    s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
    s.makefile() 创建一个与该套接字相关连的文件

    四、UDP

    UDP —(User Datagram Protocol) 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

    UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

    UDP特点

    UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。 UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。 UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。

    适用情况

    UDP是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP一般用于多点通信和实时的数据业务,比如

  • 语音广播
  • 视频
  • QQ
  • TFTP(简单文件传送)
  • 大型网络游戏
  • 相比较于TCP注重速度流畅

    UDP服务器的建立可以归纳这几步:

  • 创建 socket(套接字)
  • 绑定 socket 的 IP 地址和端口号
  • 接收客户端数据
  • 关闭连接
  • udp客户端的创建可总结为这几步:

  • 创建 socket(套接字)
  • 向服务器发送数据
  • 关闭连接
  • UDP简单案例

    服务端代码

    import socket
     
    # 创建 socket
    sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 绑定 IP 和端口号
    sk.bind(('127.0.0.1', 6000))
    while True:
      # 接收数据报
      msg, addr = sk.recvfrom(1024)
      # 打印
      print('来自[%s:%s]的消息: %s' % (addr[0], addr[1], msg.decode('utf-8')))
     
      # 等待输入
      inp = input('>>>')
      # 发送数据报
      sk.sendto(inp.encode('utf-8'), addr)
     
    # 关闭 socket
    sk.close()
    

    客户端代码:

    import socket
     
    # 创建 socket
    sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    addr = ('127.0.0.1', 6000)
    while True:
      # 等待输入
      msg = input('>>>')
      # 发送数据报
      sk.sendto(msg.encode('utf-8'), addr)
      # 接收数据报
      msg_recv, addr = sk.recvfrom(1024)
      # 打印
      print(msg_recv.decode('utf-8'))
     
    # 关闭 socket
    sk.close()
    

    实现简单TFTP(基于UDP协议)

  • tftp是基于udp的协议
  • 实现简单的tftp,首先要有tftp的协议图。
  • tftp默认接收端口为69,但每次有连接过来后,tftp会随机分配一个端口来专门为这个连接来服务。
  • 操作码:1.上传 2.下载 3.传数据 4.接收确认 5.错误码,6 文件传输完成
  • image.png

    因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)

    为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了,TFTP数据包的格式如下:

    image.png

    额外增加: 数据传输完成 :操作码=6, 块编号

    c语言和Python语音的格式表:

    image.png

    编程步骤

    1、编写客户端代码,发送第一个请求的数据包

    2、服务端代码:接受客户端发送过来的第一个数据包

    1. 解包
    2. 判断数据包是否正确
    3. 得到需要下载的文件名
    4. 判断操作码是否为:1
    5. 下载文件

    3、服务端代码:下载文件

    1. 创建一个新的socket
    2. 打开文件,如果打开文件错误,发送错误消息给客户端
    3. 循环读取文件的内容
    4. 每次读取512字节
    5. 判断是否小于512字节
    6. 发送文件数据包给客户端

    4、客户端代码:

    1. 循环接受:文件数据包
    2. 解包,并取得操作码
    3. 判断操作码==5,如果是:打印服务返回的错误,并结束循环
    4. 判断操作码==3 ,并且第一次接受文件的数据包,如果是:创建文件,并打开IO
    5. 把数据包的内容写入新文件中
    6. 判断操作码 == 6,如果是: xxx, 如果不是:xxx

    5、服务端代码:

    1. 接受客户端的ack数据包
    2. 判断操作码是否==6
    3. 判断操作是否!=4
    python实现
    服务端代码
  • 监听指定端口,等待客户端连接。
  • 接收到客户端的请求后,检查请求格式是否正确,提取文件名。
  • 尝试打开文件进行读取,如果文件不存在则发送错误信息给客户端。
  • 每次读取512字节的数据,并构建合适的数据包发送给客户端。
  • 在每次发送完数据包后等待客户端的确认(ACK),以确保数据成功接收。
  • import socket
    import os
    
    # 创建一个新的socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_address = ('localhost', 6969)
    server_socket.bind(server_address)
    
    print("Server is listening on {}:{}".format(*server_address))
    
    def send_file_to_client(file_name, client_address):
        try:
            with open(file_name, 'rb') as file:
                while True:
                    data = file.read(512)
                    if not data:
                        break
                    
                    # 构建数据包
                    opcode = b'\x00\x03' if len(data) == 512 else b'\x00\x04'
                    packet = opcode + data
                    
                    server_socket.sendto(packet, client_address)
                    
                    if len(data) < 512:
                        break
    
                    # 等待ACK
                    ack_packet, _ = server_socket.recvfrom(1024)
                    if ack_packet[:2] != b'\x00\x06':
                        print("Invalid ACK received")
                        return
    
        except FileNotFoundError:
            error_message = "File not found"
            error_packet = b'\x00\x05' + error_message.encode()
            server_socket.sendto(error_packet, client_address)
    
    while True:
        # 接收客户端发送过来的第一个数据包
        data_packet, client_address = server_socket.recvfrom(1024)
        
        # 解包并判断数据包是否正确
        if len(data_packet) >= 2 and data_packet[:2] == b'\x00\x01':
            # 得到需要下载的文件名
            file_name = data_packet[2:].decode().strip('\x00')
            
            # 判断操作码是否为:1
            if data_packet[:2] == b'\x00\x01':
                print(f"Client requested to download: {file_name}")
                send_file_to_client(file_name, client_address)
    
    客户端代码
  • 向服务端发送请求,指定要下载的文件名。
  • 进入循环接收来自服务端的数据包,根据操作码处理不同的情况。
  • 如果接收到的是错误消息,则打印错误信息并结束程序。
  • 如果接收到的是文件内容,则写入本地文件中,并在适当的时候创建新文件。
  • 当所有数据都接收完毕后,关闭文件并结束程序。
  • import socket
    
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_address = ('localhost', 6969)
    
    def request_file_download(file_name):
        # 发送第一个请求的数据包
        request_packet = b'\x00\x01' + file_name.encode() + b'\x00'
        client_socket.sendto(request_packet, server_address)
    
        file = None
        first_packet_received = False
    
        while True:
            # 接受文件数据包
            response_packet, _ = client_socket.recvfrom(1024)
            
            # 解包,并取得操作码
            opcode = response_packet[:2]
            
            if opcode == b'\x00\x05':  # 错误消息
                error_message = response_packet[2:].decode()
                print(f"Error from server: {error_message}")
                break
            
            elif opcode == b'\x00\x03' or opcode == b'\x00\x04':  # 文件数据包
                content = response_packet[2:]
                
                if not first_packet_received and opcode == b'\x00\x03':
                    # 创建文件,并打开IO
                    file = open('received_' + file_name, 'wb')
                    first_packet_received = True
                
                if file:
                    file.write(content)
                
                if opcode == b'\x00\x04':
                    # 最后一个数据包
                    file.close()
                    print("File download completed.")
                    break
                
                # 发送ACK
                ack_packet = b'\x00\x06'
                client_socket.sendto(ack_packet, server_address)
    
    if __name__ == "__main__":
        file_name = input("Enter the file name you want to download: ")
        request_file_download(file_name)
    

    第十二节:TCP

    一、什么是TCP协议

    TCP( Transmission control protocol )即传输控制协议,是一种面向连接、可靠的数据传输协议,它是为了在不可靠的互联网上提供可靠的端到端字节流而专门设计的一个传输协议。

  • 面向连接 :数据传输之前客户端和服务器端必须建立连接
  • 可靠的 :数据传输是有序的 要对数据进行校验
  • image.png

    二、TCP三次握手

    为了保证客户端和服务器端的可靠连接,TCP建立连接时必须要进行三次会话,也叫TCP三次握手,进行三次握手的目的是为了确认双方的接收能力和发送能力是否正常。

    下面用图文形式来解释一下TCP三次握手。

  • 第一次握手 TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT 同步已发送状态

  • 第二次握手 TCP服务器收到请求报文后,如果同意连接,则会向客户端发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了 SYN-RCVD 同步收到状态.

  • 第三次握手 TCP客户端收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED已建立连接状态 触发三次握手

  • 三次握手主要作用:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误

  • 第一次握手: 客户端向服务器端发送报文
  • 证明客户端的发送能力正常
  • 第二次握手:服务器端接收到报文并向客户端发送报文
  • 证明服务器端的接收能力、发送能力正常
  • 第三次握手:客户端向服务器发送报文
  • 证明客户端的接收能力正常
  • 三、TCP四次挥手

    建立TCP连接需要三次握手,终止TCP连接需要四次挥手。

  • 第一次挥手 客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态

  • 第二次挥手 服务器端接收到连接释放报文后,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT 关闭等待状态

  • 第三次挥手 客户端接收到服务器端的确认请求后,客户端就会进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文,服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

  • 第四次挥手 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态,但此时TCP连接还未终止,必须要经过2MSL后(最长报文寿命),当客户端撤销相应的TCB后,客户端才会进入CLOSED关闭状态,服务器端接收到确认报文后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了,四次挥手完成

  • 为什么客户端要等待2MSL?
    主要原因是为了保证客户端发送那个的第一个ACK报文能到到服务器,因为这个ACK报文可能丢失,并且2MSL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,这样新的连接中不会出现旧连接的请求报文。

    四、TCP实现一对一的简单聊天

    服务端代码

    增加:客户端想要离开,必须发给服务器一个’bye’ , 服务器发给客户端一个’^^^'。 这个时候就真正close连接。

    # coding=utf-8
    # 文件名:qq_server.py
    
    from socket import *
    
    # 创建socket
    tcp_server = socket(AF_INET, SOCK_STREAM)
    
    # 绑定本地信息
    host_port = ('', 12345)
    tcp_server.bind(host_port)
    
    # 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
    tcp_server.listen(5)
    
    while True:
    
        # 如果有新的客户端来链接服务器,那么就产生一个信心的套接字专门为这个客户端服务器
        # newSocket用来为这个客户端服务
        # tcpSerSocket就可以省下来专门等待其他新客户端的链接
        newSocket, host_port = tcp_server.accept()
    
        while True:
    
            # 接收对方发送过来的数据,最大接收1024个字节
            recvData = newSocket.recv(1024)
    
            # 如果接收的数据的长度为0,则意味着客户端关闭了链接
            if len(recvData) > 0:
                print('recv:', recvData)
            else:
                break
    
            # 发送一些数据到客户端
            sendData = input("send:")
            newSocket.send(sendData.encode('utf8'))
    
        # 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
        newSocket.close()
    
    # 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
    tcp_server.close()
    
    

    客户端代码

    # coding=utf-8
    # 文件名:qq_server.py
    
    
    from socket import *
    
    # 创建socket
    tcp_client = socket(AF_INET, SOCK_STREAM)
    
    # 链接服务器
    host_port = ('127.0.0.1', 12345)
    tcp_client.connect(host_port)
    
    while True:
    
        # 提示用户输入数据
        sendData = input("send:")
    
        if len(sendData) > 0:
            tcp_client.send(sendData.encode('utf8'))
        else:
            break
    
        # 接收对方发送过来的数据,最大接收1024个字节
        recvData = tcp_client.recv(1024)
        print('recv:', recvData.decode('uft8'))
    
    # 关闭套接字
    tcp_client.close()
    
    

    作者:道友老李

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python】网络编程之UDP、TCP

    发表回复