Python 第十章 网络编程
1.网络编程基础知识
计算机网络:把分布在不同地理区域的计算机与专门的外部设备用通信线路互联成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息,共享硬件软件,数据信息等资源。
通信协议:在计算机网络中实现通信必须有一些约定,被叫做通信协议。通信协议负责对传输速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。
IP协议:通信协议是网络通信的基础,IP协议则是一种非常重要的通信协议。IP又称网际协议,是支持网间互联地数据报协议,提供了网间连接的完善功能,包括IP数据报规定的互联网络范围内的地址格式。
TCP协议:即传输控制协议,规定了一种可靠的数据信息传递服务。
在实际使用中常把这两个协议统称为TCP/IP协议。它的网络分层为应用层、传输层、网络层、网络接口层。各层都有对应的网络协议提供支持。
网络层协议主要是IP,它是所有互联网协议的基础。
传输层协议主要是TCP和UDP,python提供了socket等模块针对传输层协议进行编程。
应用层有FTP、HTTP、TELNET等等。
IP地址和端口号:IP地址用于唯一标识网络中的一个通信实体,这个通信实体既可以是一个主机也可以是一台打印机,或者是路由器的某一个端口。而在基于IP协议的网络中传输的数据包,都必须使用IP地址来标识。
被传输的每一个数据包都要包括一个源IP地址和一个目的IP地址。IP地址是一个32位的整数,通常分成4个8位的二进制数,每8位之间用圆点隔开,每个8位整数都可以转换成一个0-255的十进制整数,因此通常看到的IP地址形式为:202.9.128.88
端口:IP地址用于唯一标识网络上的一个通信实体,但一个通信实体可以有多个通信程序同时提供网络服务,此时还需要使用端口。端口是一个16位的整数,用于表示将数据交给哪个通信程序处理。因此,端口就是应用程序与外界交流的出入口,是一种抽象的软件结构,包括一些数据结构和I/O缓冲区。
如果把应用程序比做人,把计算机网络比作邮递员,把IP地址理解为某个人所在的地址,但仅有地址是找不到这个人的,还需要知道房间号,也就是端口。因此,当一个程序需要发送数据,需要知道目的地的IP地址和端口号。
2.使用urllib.parse子模块
URL对象代表统一资源定位符,是指向互联网资源的指针。资源可以是简单的文件或目录,也可以是对复杂对象的引用,例如数据库或搜索引擎的查询。在通常情况下,URL可以由协议名、主机、端口、和资源路径组成,即满足以下格式:
protocol://host:port/path,比如:http://www.abs.org/index.php
urllib模块包含了多个用于处理URL的子模块:
- urllib.request:这是核心的子模块,包含了打开和读取URL的各种函数。
- urllib.error:主要包含由urllib.request子模块所引发的各种异常。
- urllib.parse:用于解析URL。
- urllib.robotparser:主要用于解析robots.txt文件。
下面介绍urllib.parse子模块中用于解析URL地址和查询字符串的函数:
- urllib.parse.urlparse(urlstring,scheme='',allow_fragments=True):用于解析URl字符串。程序返回一个ParseResult对象,可以获取解析出来的数据。
- urllib.parse.urlunparse(parts):是上一个函数的反向操作,用于将解析结果反向拼接成URL地址。
- urllib.parse.parse_qs(qs,keep_blank_values=False,strict_parsing=False,encoding='utf-8',errors='replace'):用于解析查询字符串(application/x-www-form-urlencoded类型的数据),并以字典形式返回解析结果。
- urllib.parse.parse_qsl(qs,keep_blank_values=False,strict_parsing=False,encoding='utf-8',errors='replace'):用于解析查询字符串(application/x-www-form-urlencoded类型的数据),并以列表形式返回解析结果。
- urllib.parse.urlencode(query,doseq=False,safe='',encoding=None,errors=None,quote_via=quote_plus):将字典形式或列表形式的请求参数恢复成字符串。
- urllib.paese.urljoin(base,url,allow_fragment=True):用于将一个base URL和另一个资源URL连接成代表绝对地址的URL。
下面使用urlparse解析URL字符串:
from urllib.parse import *
# 解析URL字符串
result = urlparse('http://www.crazyit.org:80/index.php;yeeku?name=fkit#frag')
print(result)
# 通过属性名和索引来获取URL的各部分
print('scheme:', result.scheme, result[0])
print('主机和端口:', result.netloc, result[1])
print('主机:', result.hostname)
print('端口:', result.port)
print('资源路径:', result.path, result[2])
print('参数:', result.params, result[3])
print('查询字符串:', result.query, result[4])
print('fragment:', result.fragment, result[5])
print(result.geturl())
# 输出
ParseResult(scheme='http', netloc='www.crazyit.org:80', path='/index.php', params='yeeku', query='name=fkit', fragment='frag')
scheme: http http
主机和端口: www.crazyit.org:80 www.crazyit.org:80
主机: www.crazyit.org
端口: 80
资源路径: /index.php /index.php
参数: yeeku yeeku
查询字符串: name=fkit name=fkit
fragment: frag frag
http://www.crazyit.org:80/index.php;yeeku?name=fkit#frag
如果使用urlunparse函数,则可以把一个ParseResult对象或元组恢复成URL字符串,例如:
result = urlunparse(('http','www.crazyit.org:80','index.php','yeeku','nsme=fkit','frag'))
print('URL为:',result)
http://www.crazyit.org:80/index.php;yeeku?name=fkit#frag
如果被解析的URL以双斜线开头,那么urlparse函数可以识别出主机,只是缺少scheme部分;
如果被解析的URL没有scheme、也没有以//开头,那么urlparse函数会把这些URL当成资源路径:
# 解析以//开头的URL
result = urlparse('//www.crazyit.org:80/index.php')
print('scheme:', result.scheme, result[0])
print('主机和端口:', result.netloc, result[1])
print('资源路径:', result.path, result[2])
print('-----------------')
# 解析没有scheme,也没有以双斜线(//)开头的URL
# 从开头部分开始就会被当成资源路径
result = urlparse('www.crazyit.org/index.php')
print('scheme:', result.scheme, result[0])
print('主机和端口:', result.netloc, result[1])
print('资源路径:', result.path, result[2])
#
scheme:
主机和端口: www.crazyit.org:80 www.crazyit.org:80
资源路径: /index.php /index.php
-----------------
scheme:
主机和端口:
资源路径: www.crazyit.org/index.php www.crazyit.org/index.php
parse_qs()和parse_qsl()两个函数都用于解析查询字符串,只不过返回字典和列表。urlencode是他们的逆函数:
# 解析查询字符串,返回dict
result = parse_qs('name=fkit&name=%E7%96%AF%E7%8B%82java&age=12')
print(result)
# 解析查询字符串,返回list
result = parse_qsl('name=fkit&name=%E7%96%AF%E7%8B%82java&age=12')
print(result)
# 将列表格式的请求参数恢复成请求参数字符串
print(urlencode(result))
#
{'name': ['fkit', '疯狂java'], 'age': ['12']}
[('name', 'fkit'), ('name', '疯狂java'), ('age', '12')]
name=fkit&name=%E7%96%AF%E7%8B%82java&age=12
urljoin()函数负责将两个URL拼接在一起,返回代表绝对地址的URL。这里主要会出现三种情况:
# 被拼接URL不以斜线开头
result = urljoin('http://www.crazyit.org/users/login.html', 'help.html')
print(result) # http://www.crazyit.org/users/help.html
result = urljoin('http://www.crazyit.org/users/login.html', 'book/list.html')
print(result) # http://www.crazyit.org/users/book/list.html
# 被拼接URL以斜线(代表根路径path)开头
result = urljoin('http://www.crazyit.org/users/login.html', '/help.html')
print(result) # http://www.crazyit.org/help.html
# 被拼接URL以双斜线(代表绝对URL)开头
result = urljoin('http://www.crazyit.org/users/login.html', '//help.html')
print(result) # http://help.html
3.使用urllib.request读取资源
urllib.request.urlopen(url,data=None):用于打开url指定的资源,并从中读取数据。如果url是一个HTTP地址,那么该方法返回一个http.client.HTTPResponse对象:
from urllib.request import *
# 打开URL对应的资源
result = urlopen('http://www.crazyit.org/index.php')
# 按字节读取数据
data = result.read(326)
# 将字节数据恢复成字符串
print(data.decode('utf-8'))
# 用context manager来管理打开的URL资源
with urlopen('http://www.crazyit.org/index.php') as f:
# 按字节读取数据
data = f.read(326)
# 将字节数据恢复成字符串
print(data.decode('utf-8'))
在使用urlopen函数时,可以通过data属性向被请求的URL发送数据:
from urllib.request import *
# 向https://localhost/cgi-bin/test.cgi发送请求数据
#with urlopen(url='https://localhost/cgi-bin/test.cgi',
with urlopen(url='http://localhost:8888/test/test', #①
data='测试数据'.encode('utf-8')) as f:
# 读取服务器全部响应
print(f.read().decode('utf-8'))
上面为data属性指定了一个bytes字节数据,该数据会以原始二进制流的方式提交给服务器。
如果使用urlopen函数向服务器页面发送get请求参数则无需使用data属性,直接把请求的参数附加在URL之后即可:
import urllib.parse
params = urllib.parse.urlencode({'name': 'fkit', 'password': '123888'})
# 将请求参数添加到URL的后面
url = 'http://localhost:8888/test/get.jsp?%s' % params
with urlopen(url=url) as f:
# 读取服务器全部响应
print(f.read().decode('utf-8'))
如果想通过urlopen函数发送POST请求参数,也可通过data属性来实现:
import urllib.parse
params = urllib.parse.urlencode({'name': '疯狂软件', 'password': '123888'})
params = params.encode('utf-8')
# 使用data指定请求参数
with urlopen("http://localhost:8888/test/post.jsp", data=params) as f:
print(f.read().decode('utf-8'))
通过request对象发送PUT请求:
from urllib.request import *
params = 'put请求数据'.encode('utf-8')
# 创建Request对象,设置使用PUT请求
req = Request(url='http://localhost:8888/test/put',
data=params, method='PUT')
with urlopen(req) as f:
print(f.status)
print(f.read().decode('utf-8'))
管理cookie:
使用urlopen函数可以发送GET请求、POST、PUT、DELETE、PATCH等请求。
有时候用户可能需要访问web应用中的被保护的页面,如果使用浏览器则十分简单,通过系统提供的登录页面登陆系统,浏览器会负责维护与服务器之间的session,如果用户登录的用户名和密码符合要求就可以访问了。
如果使用urllib.request模块访问被保护页面,同样需要维护与服务器之间的session,此时要借助cookie管理器。
客户端向服务器发送请求获取响应,服务器如何辨别两次请求的客户端是同一个客户端呢,答案是session id。
为了有效管理session,可以引入http.cookiejar模块。
此外,程序还需使用OpenerDirector对象来发送请求。
使用urllib.request模块通过cookie来管理session步骤:
- 创建http.cookiejar.CookieJar对象或其子类的对象。
- 以CookieJar对象为参数,创建urllib.request.HTTPCookieProcessor对象,该对象负责调用CookieJar来管理cookie。
- 以HTTPCookieProcessor对象为参数,调用urllib.request.build_opener()函数创建OpenerDirector对象。
- 使用OpenerDirector对象来发送请求,该对象将会通过HTTPCookieProcessor调用CookieJar来管理cookie。
下面程序示范了先登录web应用,然后访问web应用中的被保护页面:
from urllib.request import *
import http.cookiejar, urllib.parse
# 以指定文件创建CookieJar对象,对象将可以把cookie保存在文件中
cookie_jar = http.cookiejar.MozillaCookieJar('a.txt')
# 创建HTTPCookieProcessor对象
cookie_processor = HTTPCookieProcessor(cookie_jar)
# 创建OpenerDirector对象
opener = build_opener(cookie_processor)
# 定义模拟Chrome浏览器的user_agent
user_agent = r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36' \
r' (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
# 定义请求头
headers = {'User-Agent':user_agent, 'Connection':'keep-alive'}
#-------------下面代码发送登录的POST请求----------------
# 定义登录系统的请求参数
params = {'name':'crazyit.org', 'pass':'leegang'}
postdata = urllib.parse.urlencode(params).encode()
# 创建向登录页面发送POST请求的Request
request = Request('http://localhost:8888/test/login.jsp',
data = postdata, headers = headers)
# 使用OpenerDirector发送POST请求
response = opener.open(request)
print(response.read().decode('utf-8'))
# 将cookie信息写入磁盘文件
cookie_jar.save(ignore_discard=True, ignore_expires=True) # ①
#-------------下面代码发送访问被保护资源的GET请求----------------
# 创建向"受保护页面"发送GET请求的Request
request = Request('http://localhost:8888/test/secret.jsp',
headers=headers)
response = opener.open(request)
print(response.read().decode())
第5行先创建了一个CookieJar对象,此处使用它的子类:MozillaCookieJar,该对象负责把cookie信息保存在文件中。
第7行创建HTTPCookieProcessor对象;
第9行调用build_opener创建OpenerDirector对象,接下来程序会通过该对象来发送请求,而底层的CookieJar对象就负责处理cookie。
第29行会把服务器响应的session id等cookie持久化保存在a.txt文件中,后面程序可以读取该文件信息,这样程序就可以模拟前面登陆过的客户端,访问被保护的页面:
from urllib.request import *
import http.cookiejar, urllib.parse
# 以指定文件创建CookieJar对象,对象将可以把cookie保存在文件中
cookie_jar = http.cookiejar.MozillaCookieJar('a.txt')
# 直接加载a.txt中的Cookie信息
cookie_jar.load('a.txt',ignore_discard=True,ignore_expires=True)
# 遍历a.txt中保存的cookie信息
for item in cookie_jar:
print('Name ='+ item.name)
print('Value ='+ item.value)
# 创建HTTPCookieProcessor对象
cookie_processor = HTTPCookieProcessor(cookie_jar)
# 创建OpenerDirector对象
opener = build_opener(cookie_processor)
# 定义模拟Chrome浏览器的user_agent
user_agent = r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36' \
r' (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
# 定义请求头
headers = {'User-Agent':user_agent, 'Connection':'keep-alive'}
#-------------下面代码发送访问被保护资源的GET请求----------------
# 创建向"受保护页面"发送GET请求的Request
request = Request('http://localhost:8888/test/secret.jsp',
headers=headers)
response = opener.open(request)
print(response.read().decode())
4.基于TCP协议的网络编程
TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个socket,从而形成虚拟的网络链路。从而两端的程序通过该链路可以进行通信。python的socket模块为基于TCP协议的网络通信提供了良好的封装,python使用socket对象来代表两端的通信端口,通过socket进行网络通信。
4.1TCP协议基础:
IP是Internet上使用的一个关键协议,全称是Internet Protocol,即Internet协议。通过使用IP协议,使Internet成为一个允许连接不同类型的计算机和不同操作系统的网络。
TCP被称作端对端协议,这是因为他在两台计算机的连接中起了重要作用,当一台计算机需要与另一台远程计算机连接时,TCP协议会让他们之间建立一个虚拟链路,用于发送接收数据。
只有把TCP和IP两个协议结合起来才能保证Internet正常运行。
4.2使用socket创建TCP服务器端:
在使用socket之间要先创建其对象,可通过该类的构造器创建socket实例。
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
family参数用于指定网络类型。该参数支持socket.AF_UNIX(UNIX网络)、socket.AF_INET(基于IPv4协议的网络)和socket.AF_INET6(基于IPv6协议的网络)。
type参数用于指定网络sock类型。该参数支持SOCK_STREAM(默认值,创建基于TCP协议的socket)、SOCK_DGRAM(创建基于UDP协议的socket)和SOCK_RAM(创建原始socket)。
proto参数用于指定协议号。没有特殊要求参数默认为0可忽略。
在创建了socket之后接下来需要将两个socket连接起来。作为服务器端使用的socket必须被绑定到指定IP地址和端口,并在该IP地址和端口进行监听,接收来自客户端的连接。
socket对象提供了如下常用方法:
- socket.accept():作为服务器端使用的socket调用该方法接受来自客户端的连接。
- socket.bind(address):作为服务器端使用的socket调用该方法,将该socket绑定到指定address,该address可以是一个元组,包含IP地址和端口。
- socket.close():关闭连接,回收资源。
- socket.connect(address):作为客户端使用的socket调用该方法连接远程服务器。
- socket.listen([backlog]):作为服务器端使用的socket调用该方法进行监听。
- socket.send(bytes[,flags]):向socket发送数据,该socket必须与远程socket建立了连接。
- socket.sendto(bytes[,flags]):向socket发送数据,该socket应该没有与远程socket建立连接。
- socket.recvfrom(bufsize[,flags]):接收数据,返回元组。
TCP通信的服务器编程的基本步骤:
- 服务器端先创建一个socket对象。
- 服务器端socket将自己绑定到指定IP地址和端口。
- 服务器端socket调用listen方法监听网络。
- 程序采用循环不断调用socket的accept()方法接受来自客户端的连接。
连接代码:
# 创建socket对象
s = socket.socket()
# 将socket绑定到本机IP地址和端口
s.bind(('192.168.1.88'),30000)
# 服务器开始监听来自客户端的连接
s.listen()
while True:
# 每当接收到客户端socket的请求时,该方法就返回对应的socket和远程地址
c,addr = s.accrpt()
...
4.3使用socket通信
客户端也是先创建一个socket对象然后调用socket的connect方法建立与服务器端的连接,这样就可建立一个基于TCP协议的网络连接。
TCP通信的客户端编程的基本步骤大致归纳如下:
- 客户端先创建一个socket对象。
- 客户端socket调用connect方法连接远程服务器
s = socket.socket()
s.connect(('192.168.1.88',30000))
客户端和服务器端互相连接后通过各自的socket进行通信:
- 发送数据:使用send方法
- 接收数据:使用recv_xxx方法。
下面服务器端仅仅建立连接并监听来自客户端的连接,只要客户端连接进来程序就会向socket发送简单信息:
# 导入 socket 模块
import socket
# 创建socket对象
s = socket.socket()
# 将socket绑定到本机IP和端口
s.bind(('192.168.1.88', 30000))
# 服务端开始监听来自客户端的连接
s.listen()
while True:
# 每当接收到客户端socket的请求时,该方法返回对应的socket和远程地址
c, addr = s.accept()
print(c)
print('连接地址:', addr)
c.send('您好,您收到了服务器的新年祝福!'.encode('utf-8'))
# 关闭连接
c.close()
下面是客户端,仅使用socket建立与指定IP地址和端口的连接,并从socket中获取服务器端发送的数据。
# 导入socket模块
import socket
# 创建socket对象
s = socket.socket()
# 连接远程主机
s.connect(('192.168.1.88', 30000)) # ①
print('--%s--' % s.recv(1024).decode('utf-8'))
s.close()
先运行服务器端程序,将看到服务器一直处于等待状态,因为服务器使用了死循环接收来自客户端的请求;再运行客户端程序,将看到程序输出结果。
4.4加入多线程
前面的服务器端和客户端只是进行了简单的通信操作:服务器端接收客户端的连接之后向客户端输出一个字符串,而客户端也只是读取服务器端的字符串后就退出了。在实际应用中,客户端则可能需要和服务器端保持长时间通信,即服务器端需要不断读取客户端数据并向客户端写入数据;客户端也需要不断地读取服务器端数据,并向服务器端写入数据。
由于socket的recv方法在成功读取到数据之前,线程会被阻塞,程序无法继续执行。考虑到这个原因服务器端应该为每个socket都单独启动一个线程,每个线程负责与一个客户端进行通信。
客户端读取服务器端数据的线程同样会被阻塞,所以系统应该单独启动一个线程,专门负责读取服务器端数据。
现在考虑实现一个命令行界面的C/S聊天室应用,服务器端应该包含多个线程,每个socket对应一个线程,该线程负责从socket读取数据(从客户端发送过来的数据),并将所读取到的数据向每个socket发送一次(将一个客户端发送过来的数据广播给其他客户端),因此需要在服务器端使用list保存所有socket。
import socket
import threading
# 定义保存所有socket的列表
socket_list = []
# 创建socket对象
ss = socket.socket()
# 将socket绑定到本机IP和端口
ss.bind(('192.168.1.88', 30000))
# 服务端开始监听来自客户端的连接
ss.listen()
def read_from_client(s):
try:
return s.recv(2048).decode('utf-8')
# 如果捕获到异常,则表明该socket对应的客户端已经关闭
except:
# 删除该socket
socket_list.remove(s); # ①
def server_target(s):
try:
# 采用循环不断地从socket中读取客户端发送过来的数据
while True:
content = read_from_client(s)
print(content)
if content is None:
break
for client_s in socket_list:
client_s.send(content.encode('utf-8'))
except Exception as e:
print(e.strerror)
while True:
# 此行代码会阻塞,将一直等待别人的连接
s, addr = ss.accept()
socket_list.append(s)
# 每当客户端连接后启动一个线程为该客户端服务
threading.Thread(target=server_target, args=(s, )).start()
5.基于UDP协议的网络编程
5.1UDP协议基础
UDP(用户数据报协议),主要用来支持那些需要在计算机之间传输数据的网络连接。是一种面向非连接的协议面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送数据。至于对方是否可以接收到这些数据,UDP协议无法控制,所以说UDP协议是一种不可靠协议。适用于一次只传送少量数据,对可靠性要求不高的应用环境。
UDP协议的主要作用是完成网络数据流和数据报之间的转换,在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去;在信息的接收端,将数据报转换成实际数据内容。
UDP和TCP协议简单对比:
UDP协议:不可靠,差错控制开销较小,传输大小限制在64kb以下,不需要建立连接。
TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
5.2使用socket发送和接收数据
不同电脑之间的通信需要使用socket,也可以在同一个电脑的不同程序通信
1.创建socket并连接
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
AF_INET:表示socket是用来进行网络连接
程序在创建socket时可通过type参数指定该socket的类型,如果将该参数指定为SOCK_DGRAM,则意味着创建基于UDP协议的socket。
2.发送接收数据:
然后可以通过两个方法发送和接收数据:
- socket.sendto(bytes,address):将bytes数据发送到address地址。
- socket.recvfrom(bufsize[,flags]):接收数据。可以同时返回socket中的数据和数据来源地址。
s.sendto('hello'.encode('utf8',(192.168.31.199),9000))给这个IP地址和端口号的电脑发消息。
s.bind(('192.168.31.199',9090)) 绑定IP地址和端口号
content = s.recvfrom(1024) 接收数据
下面程序使用UDP协议的socket实现了C/S结构的网络通信。本程序的服务器端通过循环1000次来读取socket中的数据报,每当读取到内容之后,便向该数据报的发送者发送一条信息。服务器端程序的代码如下:
import socket
PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
# 定义一个字符串数组,服务器端发送该数组的元素
books = ("疯狂Python讲义",
"疯狂Kotlin讲义",
"疯狂Android讲义",
"疯狂Swift讲义")
# 通过type属性指定创建基于UDP协议的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 将该socket绑定到本机的指定IP和端口
s.bind(('172.21.212.1', PORT))
# 采用循环接收数据
for i in range(1000):
# 读取s中的数据的数据的发送地址
data, addr = s.recvfrom(DATA_LEN)
# 将接收到的内容转换成字符串后输出
print(data.decode('utf-8'))
# 从字符串数组中取出一个元素作为发送数据
send_data = books[i % 4].encode('utf-8')
# 将数据报发送给addr地址
s.sendto(send_data, addr)
s.close()
客户端采用循环不断地读取用户的键盘输入内容,每当读取到用户输入的内容就通过数据报发送出去;接下来再读取来自socket中的信息。
import socket
PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
DEST_IP = "172.21.212.1";
# 通过type属性指定创建基于UDP协议的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 不断地读取键盘输入
while True:
line = input('')
if line is None or line == 'exit':
break
data = line.encode('utf-8')
# 发送数据报
s.sendto(data, (DEST_IP, PORT))
# 读取socket中的数据
data = s.recv(DATA_LEN)
print(data.decode('utf-8'))
s.close()