Python进阶篇(三)-- TCP套接字与UDP套接字编程

2023-05-16

1 Python3 网络编程

1.1 介绍

        本文将首先利用 Python 实现面向TCP连接的套接字编程基础知识:如何创建套接字,将其绑定到特定的地址和端口,以及发送和接收数据包。其次还将学习 HTTP 协议格式的相关知识。在此基础上,本篇将用 Python 语言开发一个简单的 Web 服务器,它仅能处理一个HTTP连接请求。

        Web 服务器的基本功能是接受并解析客户端的 HTTP 请求,然后从服务器的文件系统获取所请求的文件,生成一个由头部和响应文件内容所构成成的 HTTP 响应消息,并将该响应消息发送给客户端。如果请求的文件不存在于服务器中,则服务器应该向客户端发送“404 Not Found”差错报文。

         具体的过程分为:

  • 当一个客户(浏览器)连接时,创建一个连接套接字;
  • 从这个连接套接字接收 HTTP 请求;
  • 解释该请求以确定所请求的特定文件;
  • 从服务器的文件系统获得请求的文件;
  • 创建一个由请求的文件组成的 HTTP 响应报文,报文前面有首部行;
  • 经 TCP 连接向请求浏览器发送响应。
  • 如果浏览器请求一个在该服务器中不存在的文件,服务器应当返回一个“404 Not Found”差错报文。

        要实现 Web 服务器,需使用套接字 Socket 编程接口来使用操作系统提供的网络通信功能。

        Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是一组编程接口。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。使用 Socket 后,无需深入理解 TCP/UDP 协议细节(因为Socket 已经为我们封装好了),只需要遵循 Socket 的规定去编程,写出的程序自然就是遵循 TCP/UDP 标准的。Socket 的地位如下图所示:

        从某种意义上说,Socket 由地址IP和端口Port构成。IP 是用来标识互联网中的一台主机的位置,而 Port 是用来标识这台机器上的一个应用程序,IP 地址是配置到网卡上的,而 Port 是应用程序开启的,IP 与 Port 的绑定就标识了互联网中独一无二的一个应用程序。

套接字类型

  • 流式套接字(SOCK_STREAM):用于提供面向连接、可靠的数据传输服务。——TCP
  • 数据报套接字(SOCK_DGRAM):提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。——UDP
  • 原始套接字(SOCK_RAW):主要用于实现自定义协议或底层网络协议。

        在本 WEB 服务器程序实验中,采用流式套接字进行通信。其基本模型如下图所示:

        其工作过程如下:服务器首先启动,通过调用 socket() 建立一个套接字,然后调用绑定方法 bind() 将该套接字和本地网络地址联系在一起,再调用 listen() 使套接字做好侦听连接的准备,并设定的连接队列的长度。客户端在建立套接字后,就可调用连接方法 connect() 向服务器端提出连接请求。服务器端在监听到连接请求后,建立和该客户端的连接,并放入连接队列中,并通过调用 accept() 来返回该连接,以便后面通信使用。客户端和服务器连接一旦建立,就可以通过调用接收方法 recv()/recvfrom() 和发送 方法 send()/sendto() 来发送和接收数据。最后,待数据传送结束后,双方调用 close() 关闭套接字。

        套接字这个词对很多不了解网络编程的人来说显得非常晦涩和陌生,其实说得通俗点,套接字就是一套用C语言写成的应用程序开发库,主要用于实现进程间通信和网络编程,在网络应用开发中被广泛使用。在Python中也可以基于套接字来使用传输层提供的传输服务,并基于此开发自己的网络应用。实际开发中使用的套接字可以分为三类:流套接字(TCP套接字)、数据报套接字和原始套接字。


2 创建TCP套接字

2.1 套接字

        套接字(Socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将 I/O 插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是 IP 地址与端口 Port 的组合。

        为了满足不同的通信程序对通信质量和性能的要求,网络系统提供了三种不同类型的套接字,以供用户在设计网络应用程序时根据不同的要求来选择。分别是:

  • 流式套接字(SOCK-STREAM)。提供一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送。流式套接字内设流量控制,被传输的数据看作是无记录边界的字节流。在 TCP/IP 协议簇中,使用 TCP 协议来实现字节流的传输,当用户想要发送大批量的数据或者对数据传输有较高的要求时,可以使用流式套接字。
  • 数据报套接字(SOCK-DGRAM)。提供一种无连接、不可靠的双向数据传输服务。数据包以独立的形式被发送,并且保留了记录边界,不提供可靠性保证。数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据。在 TCP/IP 协议簇中,使用 UDP 协议来实现数据报套接字。在出现差错的可能性较小或允许部分传输出错的应用场合,可以使用数据报套接字进行数据传输,这样通信的效率较高。
  • 原始套接字(SOCK-RAW)。该套接字允许对较低层协议(如 IP 或 ICMP )进行直接访问,常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网络设备。

        所谓TCP套接字就是使用TCP协议提供的传输服务来实现网络通信的编程接口。在Python中可以通过创建 socket 对象并指定type属性为SOCK_STREAM来使用TCP套接字。由于一台主机可能拥有多个IP地址,而且很有可能会配置多个不同的服务,所以作为服务器端的程序,需要在创建套接字对象后将其绑定到指定的IP地址和端口上。这里的端口并不是物理设备而是对IP地址的扩展,用于区分不同的服务,例如我们通常将HTTP服务跟80端口绑定,而MySQL数据库服务默认绑定在3306端口,这样当服务器收到用户请求时就可以根据端口号来确定到底用户请求的是HTTP服务器还是数据库服务器提供的服务。端口的取值范围是0~65535,而1024以下的端口我们通常称之为“著名端口”(留给像FTP、HTTP、SMTP等“著名服务”使用的端口,有的地方也称之为“周知端口”),自定义的服务通常不使用这些端口,除非自定义的是HTTP或FTP这样的著名服务。

        Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。

2.2 如何创建套接字

        套接字 Socket 实质上提供了主机间进程通信的连接点。进程通信之前,双方首先必须各自创建一个连接点。否则是没有办法建立联系并相互通信的。Python 中,我们用 socket() 函数来创建套接字,语法格式如下:

my_socket = socket(socket_family, socket_type, protocol=0)
'''
socket_family可以是如下参数之一:
    AF_INET IPv4(默认)
    AF_INET6 IPv6
    AF_UNIX 只能够用于单一的Unix系统进程间通信
socket_type可以是如下参数之一:
    SOCK_STREAM  流式socket , for TCP (默认)
    SOCK_DGRAM   数据报式socket , for UDP
    SOCK_RAW 原始套接字
'''

Socket 对象(内建)方法

函数描述
服务器端套接字
s.bind()绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.listen()开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept()被动接受TCP客户端连接,(阻塞式)等待连接的到来
客户端套接字
s.connect()主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex()connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv()接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send()发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall()完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom()接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto()发送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()创建一个与该套接字相关连的文件

2.3 如何为套接字绑定主机及端口

        一个完整的 Socket 可以用一个通信双方的相关描述:
协议 , 本地地址 , 本地端口 , 远程地址 , 远程端口 {协议,本地地址,本地端口,远程地址,远程端口} 协议,本地地址,本地端口,远程地址,远程端口

        实际应用中,在创建一个 Socket 时先用一个半相关描述(服务器这一半可以确定,而另一半尚不确定):
协议 , 本地地址 , 本地端口 {协议,本地地址,本地端口} 协议,本地地址,本地端口
        每一个 Socket 有一个本地的唯一端口号,由操作系统分配。

        绑定指为套接字绑定地址包含主机及其端口。 在 AF_INET 下,以元组(host,port)的形式表示地址。

  • host:用字符串表示主机的 IP 地址。表示本机'',也可用 127.0.0.1 表示回环地址,或者主机的一般 IP 地址。
  • port:端口号,数字表示。1024 以下为系统约定,自定义的用 1024 以上。

        绑定通过套接字的绑定方法 bind() 来完成,输入参数为元组 (host,port)
绑定示例:

my_socket.bind(('127.0.0.1', 1234))         # 绑定本地回环地址
my_socket.bind(('', 1234))                  # 自动获取IP地址

2.4 如何设置套接字监听

        服务器程序在调用创建套接字 socket() 和绑定 bind() 之后需要处于监听状态,因为不知客户端什么时候开始进行请求连接。为此,需调用套接字的监听方法 listen()

        一个服务端可能同时面对多个客户端的连接请求,为此服务器程序需创建一个连接队列来保存的连接请求,并依次为连接请求建立相应连接。为此需设置队列的大小作为监听方法的参数。
监听示例:

my_socket.listen(10)    # 设置连接队列大小为10,并使套接字处于监听状态。

2.5 服务端获取连接请求

2.5.1 如何获取客户端的连接请求

        当服务器中的套接字监听到了连接请求之后,内核和客户建立连接,并将连接放入连接队列中。典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用 accept() 返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有 accept 的客户端就处于连接等待状态。如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

示例:

connection_socket,addr = my_socket.accept()
'''
返回值: 
connectionSocket 客户端连接套接字
addr 连接的客户端地址
'''

        这里的 connectionSocket 称为客户端连接套接字,是 accept() 接收到一个客户端连接请求后返回的一个新的套接字,它代表了服务端和客户端的连接。后面可以用于读取数据以及关闭连接。

2.5.2 如何获取客户端发送的报文内容

        服务器与客户端的连接建立好之后,就可以接收或发送消息操作。相应有下面几组方法:

recv()/send()
recvmsg()/sendmsg()
recvfrom()/sendto()

        接收报文方法 recv() 用法如下:

data = socket.recv(buffersize)
'''
     功能 : 接收对应客户端消息
     参数 : 一次最多接收多少字节
     返回值 : 接收到的内容
       *  如果没有消息则会阻塞等待
'''

2.6 服务端读取请求文件内容

2.6.1 如何获取客户端请求的网页文件名

        HTTP 请求是客户端通过发送信息向服务器请求对资源的访问。HTTP 请求由三部分组成:请求行、请求头和请求正文。

POST /index.html HTTP/1.1   # 请求方法 url 协议及版本号
Host: localhost             # 主机地址
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: <a target=_blank href="http://localhost/" style="color: rgb(51, 102, 153); text-decoration: none;">http://localhost/</a>
Content-Length:25
Content-Type:application/x-www-form-urlencoded
`     `
username=aa&password=1234             # 请求体

        从上方代码可以看出,请求网页文件名位于请求行(第一行)中用空格分隔的第二个部分。

        在获得请求文件名后,读取文件内容使用文件操作来实现。Python 提供了必要的函数和方法进行默认情况下的文件基本操作。你可以用 file 对象做大部分的文件操作。

2.7 服务端响应请求头部信息

2.7.1 WEB服务器响应消息头部定义

        WEB 服务器在接收到客户端的连接请求后,接下来就会响应该请求。HTTP 响应报文由三部分组成:响应行、响应头、响应体。如下图所示。

  • 响应行:一般由协议版本、状态码及其描述组成,比如 HTTP/1.1 200 OK 其中协议版本 HTTP/1.1 或者 HTTP/1.0200 就是它的状态码,OK 则为它的描述。
  • 响应头:用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理它回送的数据。

        常见的响应头字段含义:

  • Allow:服务器支持哪些请求方法(如GET、POST等)。
  • Content-Encoding:文档的编码(Encode)方法。
  • Content-Length:表示内容长度。
  • Content-Type:表示后面的文档属于什么MIME类型。
  • Date:当前的GMT时间
  • Expires:告诉浏览器把回送的资源缓存多长时间,-1或0则是不缓存。
  • Last-Modified:文档的最后改动时间。
  • Location:用于重定向接收者到一个新URI地址。
  • Refresh:告诉浏览器隔多久刷新一次,以秒计。
  • Server:服务器通过这个头告诉浏览器服务器的类型。

        在这个 WEB 服务器返回的头部信息示例如下:

HTTP/1.1 200 OK
Connection: close
Content-Type: text/html
Content-Length: 24

2.7.2 发送响应消息头部内容

        在定义好响应消息的头部信息后,使用套接字的 send 方法发送即可。在发送前需要使用编码 encode() 方法,将字符串转换为字节数组后发送。
例如:

socket.send(header.encode())

2.7.3 如何捕获请求文件读取错误的异常

        在本服务器程序中,采用 try...except 结构来捕获异常。当请求的文件不存在(可能是文件名错误或路径错误)及其他可能导致文件访问错误(如没有相应权限)时,就会产生 IOError 异常。从而进入异常处理部分代码。

发送自定义的异常信息给客户端

        在异常处理代码中,定义响应客户端请求文件不存在的响应消息头代码 404 及消息内容not Found。

        将此响应消息头发给客户端,可以使用 socket 的发送方法 send() 完成,发送前需要使用编码方法 encode() 对响应消息进行编码。

完整代码如下:

from socket import *

serverSocket = socket(AF_INET, SOCK_STREAM) 
# Prepare a sever socket 
serverSocket.bind(("127.0.0.1",6789))
serverSocket.listen(1)

while True:
    print('开始WEB服务...')
    try:
            connectionSocket, addr = serverSocket.accept()
            message = connectionSocket.recv(1024) # 获取客户发送的报文
            
            # 读取文件内容
            filename = message.split()[1]       # message=["POST", "/index.html", "HTTP/1.1", ...]
            f = open(filename[1:])
            outputdata = f.read()
            
            # 向套接字发送头部信息
            header = ' HTTP/1.1 200 OK\nConnection: close\nContent-Type: text/html\nContent-Length: %d\n\n' % (len(outputdata))
            connectionSocket.send(header.encode())

            # 发送请求文件的内容
            for i in range(0, len(outputdata)):
                connectionSocket.send(outputdata[i].encode())
            
            # 关闭连接
            connectionSocket.close()
    except IOError:             # 异常处理
            # 发送文件未找到的消息
            header = ' HTTP/1.1 404 not Found'
            #########Begin#########
            connectionSocket.send(header.encode())
            #########End#########
            # 关闭连接
            connectionSocket.close()
    # 关闭套接字
    serverSocket.close()

        上面建立了一个只允许一个连接的服务器,在指定端口监听客户端的请求,从客户端发送的请求中提取文件名,若该文件存在于服务器上(如下文的"HelloWorld.html"),则生成一个状态码200的POST报文,并返回该文件;若该文件不存在,则返回一个404 Not Found报文。

HelloWorld.html

<head>Hello world!</head>

服务器端:

        在一台主机上的同一目录下放入WebServer.pyHelloWorld.html两个文件,并运行WebServer.py,作为服务器。

客户端:

        在另一台主机上打开浏览器,并输入"http://XXX.XXX.XXX.XXX:6789/HelloWorld.html" (其中"XXX.XXX.XXX.XXX"是服务器IP地址),以获取服务器上的HelloWorld.html文件。

        一切正常的话,可以看到如下页面:

        输入新地址"http://XXX.XXX.XXX.XXX:6789/abc.html",以获取服务器上不存在的abc.html。将出现以下页面(注意页面中的"HTTP ERROR 404"):

2.8 示例分析

1. 服务端

        下面的代码实现了一个提供时间日期的服务器。

# -*- encoding: utf-8 -*-
# @Author: CarpeDiem
# @Date: 230420
# @Version: 1.0
# @Description: 一个提供时间日期的服务器
# @Filename: server.py

from socket import socket, SOCK_STREAM, AF_INET, gethostname
from datetime import datetime

def main():
    # 1. 创建套接字对象并指定使用哪种传输服务
    # family=AF_INET - IPv4地址
    # family=AF_INET6 - IPv6地址
    # type=SOCK_STREAM - TCP套接字
    # type=SOCK_DGRAM - UDP套接字
    # type=SOCK_RAW - 原始套接字
    server = socket(family=AF_INET, type=SOCK_STREAM)
    # 2. 绑定IP地址和端口(端口用于区分不同的服务)
    # 同一个时间在同一个端口只能绑定一个服务否则报错
    # server.bind(('192.168.1.2', 1030))
    host = gethostname()            # 获取本地主机名
    port = 9999                     # 绑定端口号
    server.bind((host, port))
    # 3.开启监听 - 监听客户端连接到服务器
    # 参数512可以理解为连接队列的大小,超过后排队
    server.listen(512)
    print("服务器启动开始监听……")
    while True:
        # 4.通过循环接收客户端的连接并作出相应的处理(提供服务)
        # accept方法是一个阻塞方法如果没有客户端连接到服务器代码不会向下执行
        # accept方法返回一个元组其中的第一个元素是客户端对象
        # 第二个元素是连接到服务器的客户端的地址(由IP和端口两部分构成)
        client, addr = server.accept()
        print(str(addr) + "连接到了服务器.")
        # 5. 发送数据
        client.send(str(datetime.now()).encode('utf-8'))
        # 6. 断开连接
        client.close()

if __name__ == "__main__":
    main()

查找自己电脑IP和端口的方法:
第一步: Win+R
第二步: 输入:cmd 然后点击确定(Enter)进入
第三步: 输入:ipconfig 然后Enter
第四步: 输入:netstat 然后Enter 一般用第一个就行

        运行服务器程序后我们可以通过Windows系统的telnet来访问该服务器,结果如下图所示。

        Windows开启telnet服务,见下图所示:

        当然我们也可以通过Python的程序来实现TCP客户端的功能,相较于实现服务器程序,实现客户端程序就简单多了,代码如下所示。

2. 客户端

# -*- encoding: utf-8 -*-
# @Author: CarpeDiem
# @Date: 230420
# @Version: 1.0
# @Description: 一个接受时间日期的客户端
# @Filename: client.py

from socket import socket, SOCK_STREAM, AF_INET, gethostname

def main():
    # 1.创建套接字对象默认使用IPv4和TCP协议
    # client = socket()
    client = socket(family=AF_INET, type=SOCK_STREAM)

    # 2.连接到服务器(需要指定IP地址和端口)
    # client.connect(('10.69.164.78', 1030))
    host = gethostname()            # 获取本地主机名
    port = 9999                     # 绑定端口号
    client.connect((host, port))

    # 3.从服务器接受数据, 接收小于 1024 字节的数据
    print(client.recv(1024).decode('utf-8'))
    client.close()

if __name__ == "__main__":
    main()

        现在我们打开两个终端,第一个终端执行 server.py 文件:

python3 server.py

        第二个终端执行 client.py 文件:

python3 client.py
2023-04-19 17:04:30.293444

        这时我们再打开第一个终端,就会看到有以下信息输出:

('192.168.1.2', 11046)连接到了服务器.

        需要注意的是,上面的服务器并没有使用多线程或者异步I/O的处理方式,这也就意味着当服务器与一个客户端处于通信状态时,其他的客户端只能排队等待。很显然,这样的服务器并不能满足我们的需求,我们需要的服务器是能够同时接纳和处理多个用户请求的。下面我们来设计一个使用多线程技术处理多个用户请求的服务器,该服务器会向连接到服务器的客户端发送一张图片。

服务器端代码:

from socket import socket, SOCK_STREAM, AF_INET
from base64 import b64encode
from json import dumps
from threading import Thread


def main():
    
    # 自定义线程类
    class FileTransferHandler(Thread):

        def __init__(self, cclient):
            super().__init__()
            self.cclient = cclient

        def run(self):
            my_dict = {}
            my_dict['filename'] = 'guido.jpg'
            # JSON是纯文本不能携带二进制数据
            # 所以图片的二进制数据要处理成base64编码
            my_dict['filedata'] = data
            # 通过dumps函数将字典处理成JSON字符串
            json_str = dumps(my_dict)
            # 发送JSON字符串
            self.cclient.send(json_str.encode('utf-8'))
            self.cclient.close()

    # 1.创建套接字对象并指定使用哪种传输服务
    server = socket()
    # 2.绑定IP地址和端口(区分不同的服务)
    server.bind(('192.168.1.2', 5566))
    # 3.开启监听 - 监听客户端连接到服务器
    server.listen(512)
    print('服务器启动开始监听...')
    with open('guido.jpg', 'rb') as f:
        # 将二进制数据处理成base64再解码成字符串
        data = b64encode(f.read()).decode('utf-8')
    while True:
        client, addr = server.accept()
        # 启动一个线程来处理客户端的请求
        FileTransferHandler(client).start()


if __name__ == '__main__':
    main()

客户端代码:

from socket import socket
from json import loads
from base64 import b64decode


def main():
    client = socket()
    client.connect(('192.168.1.2', 5566))
    # 定义一个保存二进制数据的对象
    in_data = bytes()
    # 由于不知道服务器发送的数据有多大每次接收1024字节
    data = client.recv(1024)
    while data:
        # 将收到的数据拼接起来
        in_data += data
        data = client.recv(1024)
    # 将收到的二进制数据解码成JSON字符串并转换成字典
    # loads函数的作用就是将JSON字符串转成字典对象
    my_dict = loads(in_data.decode('utf-8'))
    filename = my_dict['filename']
    filedata = my_dict['filedata'].encode('utf-8')
    with open('/Users/Hao/' + filename, 'wb') as f:
        # 将base64格式的数据解码成二进制数据并写入文件
        f.write(b64decode(filedata))
    print('图片已保存.')


if __name__ == '__main__':
    main()

        在这个案例中,我们使用了JSON作为数据传输的格式(通过JSON格式对传输的数据进行了序列化和反序列化的操作),但是JSON并不能携带二进制数据,因此对图片的二进制数据进行了Base64编码的处理。Base64是一种用64个字符表示所有二进制数据的编码方式,通过将二进制数据每6位一组的方式重新组织,刚好可以使用0~9的数字、大小写字母以及“+”和“/”总共64个字符表示从000000到111111的64种状态。维基百科上有关于Base64编码的详细讲解,不熟悉Base64的读者可以自行阅读。


3 创建UDP套接字

        传输层除了有可靠的传输协议TCP之外,还有一种非常轻便的传输协议叫做用户数据报协议,简称UDP。TCP和UDP都是提供端到端传输服务的协议,二者的差别就如同打电话和发短信的区别,后者不对传输的可靠性和可达性做出任何承诺从而避免了TCP中握手和重传的开销,所以在强调性能和而不是数据完整性的场景中(例如传输网络音视频数据),UDP可能是更好的选择。可能大家会注意到一个现象,就是在观看网络视频时,有时会出现卡顿,有时会出现花屏,这无非就是部分数据传丢或传错造成的。

        数据包格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:

  • 强调快速传输而非传输顺序;
  • 传输的数据可能丢失也可能损毁;
  • 限制每次传输的数据大小;
  • 数据的发送和接收是同步的。

        数据包套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

        实际应用中,QQ 视频聊天和语音聊天主要使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。当然,SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。

        本部分将学习利用 UDP 套接字编程实现网络连通程序Ping。包含如何使用 UDP 套接字发送和接收数据报;如何设置适当的套接字超时;Ping 应用程序通信过程及计算网络统计信息(如丢包率)。

        Ping 程序的基本原理:利用客户端发送一个数据包到远程机器,远程机器将收到的数据包返回到客户端(称为回显),客户端根据是否收到发送的消息及计算数据包的往返时间来反映网络是否连通及网络状态。

        首先,要实现一个用 Python 编写的简单的 Ping 服务端程序,然后再实现对应的客户端程序。程序功能类似于现代操作系统中可用的标准 Ping 程序功能,不过这里使用简单的 UDP 协议,而不是标准互联网控制消息协议(ICMP)来进行通信的

3.1 基于 UDP 协议的 Socket 套接字编程

        UDP 协议是非连接的协议,通信双方不用建立连接,而是直接把要发送的数据发送给对方。UDP 协议适用于一次传输数据量很少,对可靠性要求不高的应用场景。但由于UDP 协议没有类似于 TCP 的三次握手、可靠传输机制等,所以通信效率比较高。

        UDP 协议的应用也非常广泛,比如知名的应用层协议:SNMP、DNS 都是基于 UDP的。一个常用的 UDP 通信的框架如下图所示:

        由图可以看出,客户端要发起一次请求,仅仅需要两个步骤(socket 和 sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。和 TCP 通信不同的是,UDP 通信不需要监听(listen)及建立连接(accept)步骤,在创建及套接字后,可以直接使用 sendto()recvform() 进行数据的发送及接收。

3.2 UDP Ping服务程序框架

        在这个简单的 UDP Ping 服务器程序中,完成套接字创建及绑定后,当接收到消息后进行简单处理(这里是转化为大写),再将消息回传给相应的客户端。

3.2.1 Ping服务端创建UDP套接字

        创建UDP套接字,绑定地址包含主机及其端口:

serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('0.0.0.0', 12000))

3.2.2 UDP通信中发送与接收数据

        在 UDP 通信中,使用 sendto() 函数发送 UDP 数据,将数据发送到套接字,输入参数 address 是形式为 (host, port) 的元组,指定远程地址,其中 host 表示服务器地址,port 表示服务器端口号。返回值是发送的字节数。

        接收数据使用 recvfrom() 函数实现。输入参数为接收缓冲区大小。该函数接收 UDP 数据,与 recv() 类似,但返回值是 (data, address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。

示例如下:

  • 接收数据
msg, addr = udp_server.recvfrom(BUFSIZE)   # 使用套接字对象udp_server的recvfrom()方法接收数据
  • 发送数据
udp_server.sendto(msg,addr)     # 使用套接字对象udp_server的sendto()方法发送数据

        完整的服务器程序一般都处于后台服务状态,通过不断循环等待客户端发送 Ping 消息,经过简单处理后,将消息发给相应的客户端。

        在本实验中,为了避免大量资源的消耗,设置了一个接收消息计数器,当接收到消息超过设定值后,服务程序就退出(break)循环。

        UDP为应用程序提供了不可靠的传输服务。消息可能因为路由器队列溢出,硬件错误或其他原因,而在网络中丢失。但由于在内网中很少丢包甚至不丢包,所以在本实验室的服务器程序添加人为损失来模拟网络丢包的影响。这里为了模拟,采用对接收到的消息计数器进行模运算,当模 3 的取值为 1 时,就不回传消息,返回接收下一条消息。

from socket import *
import random

# 创建UDP套接字
serverSocket = socket(AF_INET, SOCK_DGRAM)
# 绑定本机IP地址和端口号
serverSocket.bind(('', 12000))

num=0
while True:
    # 接收客户端消息
    message, address = serverSocket.recvfrom(1024)
    # 将数据包消息转换为大写
    message = message.upper()
        
    num = num + 1
    if num >= 8:
        break

    if num % 3 == 1:
        continue
    
    # 将消息传回给客户端
    serverSocket.sendto(message, address)

3.3 客户端创建UDP套接字

        创建 UDP 套接字:

udpSocket = socket(AF_INET, SOCK_DGRAM)

3.3.1 设置套接字超时时间

        在进行客户端向服务器发送 Ping 消息的过程中,有时候可能会因为网络原因造成一直连不上服务器(如服务器程序没有开启),这时如不手动停止,Socket 可能会一直尝试重连,造成资源的浪费。这就需要设置 timeout 来限制重连时间,当 Socket 尝试重连到指定的时间时,就会停止一切操作,并提示达到 timeout 设定阈值。设置超时时间一般在创建套接字后,在网络通信之前进行。示例如下:

mysocket.settimeout(10)

        代码作用为设定套接字的超时时间为 10 秒

        客户端程序在创建完套接字后,通过循环向服务器发送消息,然后接收服务器回传的消息,通过计算收到消息及发送消息的时间差,来反映网络的状况。如果超时时间过后还没收到消息,则报出超时异常。

3.3.2 客户端向服务器发送消息并接收消息

1. 消息编解码

        在网络通信中,网络线路中传输的是字节(二进制格式)流 bytes。但在我们发送的消息习惯用字符串 string 来表示,这时就需要用编码 encode() 和解码 decode() 函数来转换。

        encode() 函数:字符串类型(str)提供的方法,用于将字符串类型转换成 bytes 类型,这个过程也称为“编码”。其语法如下:

str.encode([encoding="utf-8"][,errors="strict"])

        注意,格式中用 [] 括起来的参数为可选参数,也就是说,在使用此方法时,可以使用 [] 中的参数,也可以不使用。

示例:

str.encode()

        采用默认的 UTF-8 字符集将 str 编码为字节流

str.encode('GBK')

        采用指定的 GBK 字符集将 str 编码为字节流

decode() 函数:用于将 bytes 类型的二进制数据转换为 string 类型,这个过程也称为“解码”。其格式如下:

bytes.decode([encoding="utf-8"][,errors="strict"])

示例:

        使用默认的 UTF-8 字符集进行解码为字符串

bytes.decode()

        如果编码时采用的不是默认的 UTF-8 编码,则解码时要选择和编码时一样的格式,否则会抛出异常。

bytes = str.encode("GBK")
bytes.decode()  #默认使用 UTF-8 编码,会抛出以下异常。
bytes.decode("GBK")  #不会抛出异常

        在 Ping 客户端程序中,发送消息时将发送消息的序号及发送时间发送到 Ping 服务器,然后接收消息,并将收到消息的时间与发送消息的时间差作为消息的延迟时间进行计算,并打印出来。

        客户端程序为:

  1. 使用UDP发送ping消息(注意:不同于TCP,您不需要首先建立连接,因为UDP是无连接协议。)
  2. 从服务器输出响应消息
  3. 如果从服务器受到响应,则计算并输出每个数据包的往返时延(RTT)(以秒为单位),
  4. 否则输出“请求超时”
from socket import *
import time

serverName = '127.0.0.1' 	# 服务器地址,本例中使用本机地址
serverPort = 12000 			# 服务器指定的端口
clientSocket = socket(AF_INET, SOCK_DGRAM) 	# 创建UDP套接字,使用IPv4协议
clientSocket.settimeout(1) 					# 设置套接字超时值1秒

for i in range(0, 9):
    sendTime = time.time()
    message = ('Ping %d %s' % (i+1, sendTime)).encode()     # 生成数据报,编码为bytes以便发送
    
    try:
        # 将信息发送到服务器
        clientSocket.sendto(message, (serverName, serverPort))
        # 从服务器接收信息,同时也能得到服务器地址
        modifiedMessage, serverAddress = clientSocket.recvfrom(1024)
    
        rtt = time.time() - sendTime    # 计算往返时间
        print('Sequence %d: Reply from %s    RTT = %.3fs' % (i+1, serverName, rtt))         # 显示信息
    except Exception as e:
        print('Sequence %d: Request timed out.' % (i+1))
        
clientSocket.close()            # 关闭套接字

服务器端:

        在一台主机上运行UDPPingerServer.py,作为接收ping程序数据的服务器。

        效果如下:

客户端:

        在另一台主机上运行UDPPinger.py,效果如下:

____

参考

  • 网络编程入门:https://gitee.com/zengyujin/Python-100-Days/blob/master/Day01-15/14.网络编程入门和网络应用开发.md
  • Python3 网络编程:https://www.nowcoder.com/tutorial/10005/99e037cb31a1486a8cf8ea61eb58dc8c
  • WEB服务器编程实现:https://www.educoder.net/shixuns/synqujxr/challenges
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python进阶篇(三)-- TCP套接字与UDP套接字编程 的相关文章

  • 51单片机学习笔记:串口接收和发送数据

    芯片STC89C52RC 在PC端向单片机发送任意数据后 单片机向PC端发送4行文本 由于在windos下 回车换行用 r n include lt reg52 h gt include 34 MY51 h 34 void initSer
  • 51单片机学习笔记:利用ADC0804模数转换器采集电压

    电位器调节待检测电压值 在数码管上显示出来 代码大多从书上搬过来的 书上例5 3 1要求前3个数码管显示AD转换后的8位数字量 即0 255 我这里让前4个数码管显示具体电压值 比如1 352 include lt reg52 h gt i
  • 51单片机学习笔记,模拟iic总线连续读写24c02存储器

    AT24C02A 2K SERIAL EEPROM Internally organized with 32 pages of 8 bytes each the 2K requires an 8 bit data word address
  • 51单片机学习笔记:定时器产生PWM可调方波,控制led灯亮度

    使用定时器T0产生PWM方波 用按键调整占空比 20级可调 控制led灯的亮度等级 include 34 my51 h 34 include 34 timer0 h 34 define grading 20 亮度20级变化 sbit key
  • 汉字与GBK内码互转工具(支持批量转换)

    注 原程序不是我写的 原作者没有署名 我只是让它更好用些而已 感谢未署名的原作者 汉字与GBK内码互转工具 exe 支持批量转换 修改记录如下 1 修改标题 2 修改文本框位置和大小 3 修改按钮位置和大小 修改按钮内容 4 修改光标的初始
  • 51单片机学习笔记:ST7920控制器的12864液晶使用总结

    总结 1 控制芯片不同 液晶接口定义 或者寄存器定义也可能不同 2 显示方式有并行和串行 串行方式据说不能读数据寄存器 DR 那指令暂存器IR是否可读 3 含字库芯片显示字符时不必对字符取模了 但字库有可能缺斤少两 就是说有一部分字 哪怕是
  • 启用SecureCRT自带的tftp服务

    以前用Tftpd32在win端开启tftp服务 Tftpd32功能很多 但我暂时用不到那些功能 今天试试SecureCRT自带的tftp功能 挺好的啊 只能说 SecureCRT 很好很强大 图解 测试 book 64 ubuntu wor
  • 十三、数据清洗

    1 清洗数据 数据清洗是指发现并纠正数据文件中可识别的错误的最后一道程序 xff0c 包括检查数据一致性 xff0c 处理无效值和缺失值等 xff1a 缺失值处理 xff1b 噪声数据处理 xff1b 不一致数据的处理 xff1b 清洗数据
  • Ubuntu 16 安装32位兼容库

    由于电脑是64位系统 但是linux下很多软件还是32的 所以需要32位运行库 如果没装的话 运行32位程序时会跳出来说没有这个文件或者目录 xff0c 但是ls看又有这个文件的 小白一脸懵逼啊 然后就是一阵gg搜 折腾了好久 终于搞定了
  • Ubuntu 16 安装samba

    sudo apt get install samba 安装samba 本来想用图形界面配置下 但是运行 system config samba失败 就手动配置 这里系统用户名sjj 创建的samba账户也为sjj 但为了安全的话最好不要相同
  • Ubuntu 16.04 安装tftp

    准备工作 新建一个777权限的tftpdir服务文件夹 例如 home sjj work tftpdir 放一个有内容的1 txt文本用来测试 安装tftp的增强版本 服务端tftpd hpa 客户端tftp hpa sudo apt ge
  • VS粘贴word时中文乱码修复工具v1

    部分VS版本复制代码到word时 中文注释变成乱码 就做了个小工具实时修复 以前在网上也下载过类似工具 但每次都要点一下 很麻烦 本工具可自动监控剪切板 自动修复 可谓全自动了
  • 正斜杠/和反斜杠\的区别

    近来研究源码时发现 xff0c 常常在路径中出现正斜杠 和反斜杠 xff0c 之前就一直不胜其扰 xff0c 所幸查资料把它一次弄懂 xff0c 求个明白 在这里做个记录 我认真搜了一下 xff0c 发现问这个问题的人还不少 xff0c 知
  • Gazebo需要的protoc版本与google-cartographer需要的版本不一致

    问题 xff1a 编译ROS工作空间时出现了问题 xff1a 错误提示是Gazebo代码由更新的protoc版本产生 xff0c 而当前安装的protobuf库的版本低了 xff11 xff0e ubuntu 18 04 Gazebo依赖的
  • vue 2.0项目 favicon.ico不显示 解决方案

    1 最好把favicon ico放到 index html放到同一目录 2 在webpack 配置文件里面配置 在webpack dev conf js 里面的 plugins配置 new HtmlWebpackPlugin filenam
  • 来自IT公司速查手册的各大IT公司薪资和待遇内幕

    来自IT公司速查手册的各大IT公司薪资和待遇内幕 xff08 转载于 http xuchaoyi99 cnblogs com xff09 编号 1 杭州 诺基亚 2 南京 趋势科技 Trend 3 北京 联想 xff08 北京 xff09
  • GCC中SIMD指令的应用方法

    X86架构上的多媒体应用开发 xff0c 如果能够使用SIMD指令进行优化 xff0c 性能将大大提高 目前 xff0c IA 32的SIMD指令包括MMX xff0c SSE xff0c SSE2等几级 在GCC的开发环境中 xff0c
  • ffmpeg mplayer x264 代码重点详解 详细分析

    ffmpeg和mplayer中求平均值得方法 1 ordinary c language level define avg2 a b a 43 b 43 1 gt gt 1 define avg4 a b c d a 43 b 43 c 4
  • 基于Region Proposal的深度学习目标检测简述

    转载链接 开篇需要跟大家道歉 xff0c 一切忙没时间的理由都是借口 xff0c 实际上就是偷懒了 xff0c 这么久才更新 xff0c 非常抱歉 xff01 本篇争取以最简明的叙述 xff0c 帮助大家理解下基于Region Propos
  • 3D电视,你知道多少?

    1 3D电视常见知识 系统概述篇 1 什么是3D电视 xff1f 答 xff1a 3D电视是一种能够模拟实际景物的真实空间关系的新型电视 xff0c 它利用人眼的视觉特性产生立体感 xff0c 让观众感受到观看的影像是具有深度特性的三维立体

随机推荐

  • 程序员成熟的标志《程序员成长路线图:从入门到优秀》

    对好书进行整理 xff0c 把好内容共享 我见证过许多的程序员的成长 xff0c 他们很多人在进入成熟期之后 xff0c 技术上相对较高 xff0c 一般项目开发起来比较自信 xff0c 没有什么太大的困难 xff0c 有的职位上也有所提升
  • 前端工程师的价值体现在哪里?

    这是一个很老的话题 前端工程师的价值体现在哪里 xff1f 有人说 xff1a 前端工程师之于网站的价值犹如化妆师之于明星的价值 一位好的Web前端开发工程师在知识体系上既要有广度 xff0c 又要有深度 当然 xff0c Web前端工程师
  • 设计公共API的六个注意事项

    摘要 xff1a 俗话说 xff1a 好东西就要贡献出来和大家一起分享 xff0c 尤其是在互联网业务高度发达的今天 xff0c 如果你的创业公司提供了一项很酷的技术或者服务 xff0c 并且其他用户也非常喜欢该产品 xff0c 在这种情况
  • 从工具的奴隶到工具的主人

    摘要 xff1a 我们每个人都是工具的奴隶 随着我们的学习 xff0c 我们不断的加深自己对工具的认识 xff0c 从而从它们里面解脱出来 现在我就来说一下我作为各种工具的奴隶 xff0c 以及逐渐摆脱它们的思想控制的历史吧 当我高中毕业进
  • C语言 常用API

    MySQL的C语言API接口 1 首先当然是连接数据库 xff0c 函数原型如下 xff1a MYSQL STDCALL mysql real connect MYSQL mysql const char host const char u
  • linux下的C语言开发

    linux下的C语言开发 xff08 开篇 xff09 在很多人的眼里 xff0c C语言和linux常常是分不开的 这其中的原因很多 xff0c 其中最重要的一部分我认为是linux本身就是C语言的杰出作品 当然 xff0c linux操
  • C读写ini文件

    read write ini file with c function 64 file testini c chinayaosir blog http blog csdn net chinayaosir connect ini databa
  • 职场必知的20条黄金法则

    时间紧张 xff0c 先记一笔 xff0c 后续优化与完善 1 办公室里只有两种人 xff0c 角主和龙套 职场上 xff0c 想要过的省力 xff0c 不想往上爬 xff0c 那就只能做一生的龙套 作龙套的处坏就是 xff1a 死送你先去
  • 检测是浏览器还是手机,需求为是否支持FLASH,此文为检测是否支持FLASH的代码(含demo下载)

    步骤 xff1a 1 导入swfobject js文件 2 写一个函数 3 在需要用的地方调用方法 xff0c 此处为页面加载时调用 以下为示例代码 xff1a Java代码 lt script src 61 34 Public js sw
  • Jetson NX安装opencv3.2.0报错及问题汇总

    先放一个完整的cmake命令 cmake D CMAKE BUILD TYPE 61 Release D ENABLE PRECOMPILED HEADERS 61 OFF D CMAKE EXE LINKER FLAGS 61 lcbla
  • Ubuntu16.04环境下安装python3.8

    一 首先下载安装包 xff1a python官网 xff1a https www python org downloads 下载相应版本的python xff0c 如下图所示为Linux版本的安装包 二 配置依赖环境 xff0c 如果不进行
  • 超好用的网站推荐(持续更新)

    1 在线学习 1 1 网课学习 网易公开课 链接 xff1a https open 163 com 中国大学MOOC 链接 xff1a https www icourse163 org 哔哩哔哩 链接 xff1a https www bil
  • ROS基础篇(五)-- C++如何实现Topic & Service(roscpp)

    文章目录 1 Client library与roscpp1 1 Client Library简介1 2 roscpp 2 节点初始化 关闭与NodeHandle3 Topic in roscpp3 1 Topic通信3 2 创建Person
  • ROS基础篇(一)-- 最新学习路线,快从这里开始

    资料目录 ROS官网必看资料Diego Robot系列机器人开发目录古月居大神专栏创客智造系列博客文字补充 ROS官网必看资料 学习ros在查看国内相关资料的同时 xff0c 一定要学会首先翻看官网英文文档 xff0c 尤其是教程 Code
  • Git基础篇(二)-- Git安装与配置

    文章目录 1 下载与安装2 Git配置2 1 初始配置2 2 Git项目搭建 3 和服务器同步3 1 GitHub3 2 Gitee 参考 1 下载与安装 1 下载 打开 Git 官网 xff0c 下载 Git 对应操作系统的版本 xff0
  • Arduino基础篇(六)-- 如何使用Arduino的IIC总线(Wire)

    文章目录 1 IIC总线1 1 IIC概述1 2 IIC通信协议 2 Wire类库2 1 成员函数2 2 IIC连接方法2 3 主机写数据 xff0c 从机接收数据2 4 从机发送数据 xff0c 主机读取数据 1 IIC总线 1 1 II
  • ROS基础篇(八)-- Arduino中如何使用ROS自定义的msg

    使用rosserial arduino时 xff0c 总会面对多传感器的处理 xff0c 由于不同传感器的数据类型不同 xff0c 为了方便起见 xff0c 我们需要自定义数据文件 那么如何自定义的传感器数据 xff0c 并在Arduino
  • 多视图学习 (Multi-View Learning)

    1 介绍 多视图学习也称作多视角学习 xff08 Multi view learning xff09 是陶大成提出的一个研究方向 在实际应用问题中 xff0c 对于同一事物可以从多种不同的途径或不同的角度进行描述 xff0c 这些不同的描述
  • sklearn基础篇(三)-- 鸢尾花(iris)数据集分析和分类

    后面对Sklearn的学习主要以 Python机器学习基础教程 和 机器学习实战基于scikit learn和tensorflow xff0c 两本互为补充进行学习 xff0c 下面是开篇的学习内容 1 初识数据 iris数据集的中文名是安
  • Python进阶篇(三)-- TCP套接字与UDP套接字编程

    1 Python3 网络编程 1 1 介绍 本文将首先利用 Python 实现面向TCP连接的套接字编程基础知识 xff1a 如何创建套接字 xff0c 将其绑定到特定的地址和端口 xff0c 以及发送和接收数据包 其次还将学习 HTTP