Python C/S 网络编程(二)之 UDP 实现英汉词典查询小程序

一、传输层协议

IP协议:支持数据包传输至正确的机器

但若两个应用程序要维护一个会话怎么办?

IP层以上的协议,需两个额外特性:多路复用可靠传输(UDP\TCP)

多路复用即为两台主机间传送的大量数据包打上标签,由此与该机器正在进行的其他网络会话使用的数据包分隔开。使用端口号实现。

二、端口号

注:端口号不是一一对应的,比如你的电脑作为客户机访问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。

端口分类:
• 知名端口(0~1023):被分配给最重要的、最常用的服务。普通程序无法监听这些端口。例如:80端口分配给HTTP服务、21端口分配给FTP服务
• 动态端口(1024~65535):动态申请,动态释放。

三、套接字

  • 首先理解什么是API?
    API(Application Programming Interface):操作系统为网络应用提供应用编程的接口,是一些预先定义的函数。其中,API为网络提供的应用程序接口叫socket。

Python中用socket.socekt()函数表示套接字。

  • Python socket编程介绍

Python 提供了两个基本的 socket 模块:

(1)第一个是 Socket,它提供了标准的 BSD Sockets API。
(2)第二个是 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。

  • 下面讲的是Socket模块功能

1. socket类型

套接字格式:
socket( family , type [ , protocal ] ) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。
在这里插入图片描述

2. socket函数

注意点:
1)TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的,每次发送要指定是发给谁。
2)服务端与客户端不能直接发送列表,元组,字典。需要字符串化repr(data)
在这里插入图片描述

3. socket编程思路

在这里插入图片描述

  • UDP服务端:

    1 创建socket(套接字)
    2 绑定套接字到本地IP与端口
    3 接受客户端数据
    4 发送数据
    5 关闭套接字

  • UDP客户端:

    1 创建socket(套接字)
    2 绑定套接字到本地IP与端口
    3 发送数据
    4 接受数据
    5 关闭套接字

四、代码上手

1.先写一个简单的C/S通信模型

服务端:

#!/usr/bin/envpython3
#-*-coding:utf-8-*-
	
import socket
	
MAX_BYTES=1024#定义缓冲区大小
s_addr=('127.0.0.1',50001)#元祖形式
	
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#定义socket类型,网络通信,UDP
	
s.bind(addr)#绑定IP和端口号
print('Listeningat{}'.format(s.getsockname()))#getsockname返回当前套接字信息(IP,端口号)
	
while  True:
	data,c_addr=s.recvfrom(MAX_BYTES)#从缓冲区读取字节
	text=data.decode('utf-8')#接收到的是utf-8编码的字节流,所以解码为Unicode字符
	if text =='EXIT':
		break
	else:
		print('The client at {} says {!r}'.format(c_addr,text))
		data=text.encode('utf-8')#发送时将Unicode字符编码为utf-8字节流
		s.sendto(data,c_ addr)
	
s.close()#关闭套接字

客户端:

#!/usr/bin/envpython3
#-*-coding:utf-8-*-
	
import socket
	
MAX_BYTES=1024
c_addr=('127.0.0.1',50001)
	
c=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#定义socket类型,网络通信,UDP
	
while True:
	data=input('Please  input  the  word (EXIT for exit) :')
	text=data.encode('utf-8')
	c.sendto(text,c_addr)
	
	if text== b'EXIT':
		break
	else:
		print('The server assigned me the address{}'.format(c.getsockname()))
		data,s_addr=c.recvfrom(MAX_BYTES)
		text=data.decode('utf-8')#终端不一定支持utf-8编码字符显示,先解码
		print('The server {} replied {!r}'.format(s_addr,text))
	
c.close()#关闭套接字

测试结果:
在这里插入图片描述
在这里插入图片描述

错误心得:

别看代码这么短,本菜鸡真的调了好多错才过,菜哭了嘤嘤嘤…

(1)data,addr=s.recvfrom(MAX_BYTES)#从缓冲区读取字节 被我这个马虎怪写成了:address,结果就是client输一次,server开始疯狂死循环一句话:The client at (‘127.0.0.1’, 62791) says ‘3123’

(2)第二个错就是client输入EXIT时,client和server都不会终止。原因是我写的时候:client是以utf-8编码后的字节流传输,所以判断时 text==b’EXIT’,而server接到数据后先解码为Unicode,所以判断时 text==‘EXIT’

2.再写一个简单的词典查询函数

#!/usr/bin/envpython3
#-*-coding:utf-8-*-

def dicsearch(n):
	f=open("words.txt",encoding="gb18030")
	line=f.readline()
	while line:
	line=f.readline().strip()#跳过空行
	#print(line)
	result=line.find(n)
	#print(result)
	if result==0:
		print(line)
		break
	if result==-1:
		print('no such word in this dictionary! ')
		#break
	f.close()
	return

if__name__=='__main__':
	dicsearch('fun')

测试结果:
在这里插入图片描述
在这里插入图片描述

3.然后就是移花接木大法啦:

服务端:

#!/usr/bin/envpython3
#-*-coding:utf-8-*-
	
import  socket
	
def  dicsearch(n):
	f = open("words.txt",'rb')          #,encoding="gb18030"
	line = f.readline()          #跳过空行
	while  line:
		line = f.readline().strip()        #跳过空行
		#print(line)
		result = line.find(n)
		#print(result)
		if  result == 0:
			return  line.decode('gb18030','ignore')
			break
	if  result == -1:
		return'no  such  word  in  this  dictionary!'
	f.close()
	
MAX_BYTES = 1024#定义缓冲区大小
s_addr = ('127.0.0.1',50001)#元祖形式
	
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#定义socket类型,网络通信,UDP
	
s.bind(s_addr)#绑定IP和端口号
print('Listeningat{}'.format(s.getsockname()))#getsockname返回当前套接字信息(IP,端口号)
	
while  True:
	data,c_addr = s.recvfrom(MAX_BYTES)#从缓冲区读取字节
#print(type(data))
	text = data.decode('utf-8')#接收到的是utf-8编码的字节流,所以解码为Unicode字符
	if  text == 'EXIT':
		break
	else:
		print('The  client  at  {}  want  to  look  for:  {!r}'.format(c_addr,text))
		data = text.encode('utf-8')#发送时将Unicode字符编码为utf-8字节流
		y = dicsearch(data).encode('utf-8')
		#print(type(y))
		#print(y,c_addr)
		s.sendto(y,c_addr)
		
s.close()#关闭套接字

客户端:

#!/usr/bin/envpython3
#-*-coding:utf-8-*-
	
import  socket
	
MAX_BYTES = 1024
addr = ('127.0.0.1',50001)
	
c = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#定义socket类型,网络通信,UDP
	
while  True:
	data = input('Please  input  the  word  (EXIT  for  exit):')
	text = data.encode('utf-8')
	c.sendto(text,addr)
		
	if  text == b'EXIT':
		break
	else:
			#print('Theserverassignedmetheaddress{}'.format(c.getsockname()))
		data,addr = c.recvfrom(MAX_BYTES)
		text = data.decode('utf-8')#终端不一定支持utf-8编码字符显示,先解码
		print('The  word  you  looking  for  is:  {!r}'.format(text))
			
c.close()#关闭套接字

测试结果:
在这里插入图片描述
在这里插入图片描述

错误心得:

  • 死在编码与解码上太多次了…(哭哭),存储方式默认为二进制,所以f.open 一定要用二进制方式打开,不然会报错:TypeError: write() argument must be str, not bytes刚开始打开那个txt文件时,f = open(“words.txt”,‘r’),输出中文全是乱码,后面发现office word可以查看用什么编码,f = open(“words.txt”,‘r’,encoding = “gb18030” ), 函数没错,接到服务端就由出错,因为只能用bytes存储和发送,bytes不能编码,最后只能分开解码。

  • 然后,我居然犯了调用函数没有设返回值(return)的错误,导致后面一直说:TypeError: must be str, not Nonetype. 想锤死自己…

  • 注意区分read()、readline()、realines()

    read():
    1、读取整个文件,将文件内容放到一个字符串变量中
    2、如果文件大于可用内存,不能使用这种处理

    readline():
    1、readline()每次读取一行,比readlines()慢得多
    2、readline()返回的是一个字符串对象,保存当前行的内容

    readlines():
    1、一次性读取整个文件。
    2、自动将文件内容分析成一个行的列表。

五、混杂客户端与垃圾回复

通过下面这个例子来讲解他的缺点:

使用自环接口的UDP服务器和客户端——这个程序值调用了一次Python标准库的socket.socket()函数,其他所有调用都是通过返回的套接字对象来进行的

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_local.py
# UDP client and server on localhost

import argparse, socket        #其中argparse库是用来处理命令行参数的
from datetime import datetime     #datetime是Python中处理日期和时间的标准库

MAX_BYTES = 65535         #定义缓冲区大小

def server(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)     #创建套接字
    sock.bind(('127.0.0.1', port))       #绑定IP和端口号
    print('Listening at {}'.format(sock.getsockname()))      #返回当前套接字信息
    while True:
        data, address = sock.recvfrom(MAX_BYTES)         #从缓冲区读取字节
        text = data.decode('ascii')            #以ACSII形式解码
        print('The client at {} says {!r}'.format(address, text))        #!r调用了repr()函数,转化为适合解释器读取的形式
        text = 'Your data was {} bytes long'.format(len(data))
        data = text.encode('ascii')            #以ACSII形式编码
        sock.sendto(data, address)             #发送到客户端

def client(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    text = 'The time is {}'.format(datetime.now())      #获取当前时间
    data = text.encode('ascii')
    sock.sendto(data, ('127.0.0.1', port))         #发送到服务器端
    print('The OS assigned me the address {}'.format(sock.getsockname()))
    data, address = sock.recvfrom(MAX_BYTES)  # Danger! 
    text = data.decode('ascii')
    print('The server {} replied {!r}'.format(address, text))

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send and receive UDP locally')
    parser.add_argument('role', choices=choices, help='which role to play')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='UDP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]        #function 是根据args.role的值从choices中取出的函数。
    function(args.p)

补充:

  • 1.关于repr()函数:

Python 有办法将任意值转为字符串将它传入repr() 或str() 函数

函数str() 用于将值转化为适于人阅读的形式,而repr() 转化为供解释器读取的形式。

  • 2.关于argparse库(使用步骤):

(1)导入argparse模块:import argparse

(2)创建解析器对象ArgumentParser,可以添加参数:parser = argparse.ArgumentParser(description=…(用于描述程序))

(3)add_argument()方法,用来指定程序需要接受的命令参数

#定位参数
parser.add_argument('role', choices=choices, help='which role to play')   
#可选参数   
parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='UDP port (default 1060)') 

执行程序时,定位参数必选
(4)解析一个命令行:args = parser.parse_args(),parse_args() 的返回值是一个命名空间,包含传递给命令的参数。

  • 3.如何运行程序?当然是在命令行运行啦!小傻瓜~

服务器端(采用Ctrl+C终止):
在这里插入图片描述
客户端:
在这里插入图片描述

  • 下面就是理性分析时间

首先服务器的启动和运行经历了3步:
(1) 服务器使用socket()创建了一个空套接字。(这个套接字没有与任何IP地址或者端口号绑定,也没有进行任何连接)
(2) 服务器使用bind()命令请求绑定一个UDP网络地址。(这个网络地址由一个简单的Python二元组构成)但如果另一个程序已经占用了该UDP端口,将导致服务器脚本无法获取这个端口,绑定失败,抛出异常。
(3) 准备好接受请求,服务器会进入一个循环不断运行recvfrom()。每个数据报recvfrom()都会返回两个值(客户端地址,数据报内容)

那么问题出在哪里呢?

尽管recvfrom()返回了传入数据报的地址,但是代码没有检查该数据报的源地址,也就没有验证该数据报是否是该服务器发回的响应。

因为UDP是不面向连接的
像这样不考虑地址是否正确,接受并处理所有收到的数据包的网络监听客户端被称为混杂客户端

在安全层面,混杂客户端无疑是危险的,有两个解决方案(当然最好还是TCP):
(1)设计或使用在请求中包含唯一标识符或者请求ID的协议,在响应中重复该ID。
(2)检查响应数据包的地址与请求数据包的地址是否相同(==,connect()之类)

六、不可靠性、退避、阻塞和超时

上面的程序C(客户端)、S(服务器端)都是在一台主机上运行的。因为C/S通过自环接口通信,没有使用可能会产生信号故障的物理网卡,然而,数据包是可能会丢失的。

下面这个程序中的S并未始终响应C的请求,而是随机选择。我们把S的IP地址指定位一个空字符串(任何本地接口)——理论上是应该开虚拟机演示,but 我的centos的python有点问题。所以就都在MacOS环境下将就看下:

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_remote.py
# UDP client and server for talking over the network

import argparse, random, socket, sys

MAX_BYTES = 65535

def server(interface, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((interface, port))
    print('Listening at', sock.getsockname())
    while True:
        data, address = sock.recvfrom(MAX_BYTES)
        if random.random() < 0.5:                 #一半数据包会被丢包
            print('Pretending to drop packet from {}'.format(address))
            continue
        text = data.decode('ascii')
        print('The client at {} says {!r}'.format(address, text))
        message = 'Your data was {} bytes long'.format(len(data))
        sock.sendto(message.encode('ascii'), address)

def client(hostname, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    hostname = sys.argv[2]
    sock.connect((hostname, port))
    print('Client socket name is {}'.format(sock.getsockname()))

    delay = 2                                # 等待时间2s,过后重发    
    text = 'This is another message'
    data = text.encode('ascii')
    while True:
        sock.send(data)                #已经和S connect过,所以不用sendto,用send
        print('Waiting up to {} seconds for a reply'.format(delay))
        sock.settimeout(delay)
        try:
            data = sock.recv(MAX_BYTES)
        except socket.timeout as exc:
            delay *= 2                        # 指数退避
            if delay > 2.0:
                raise RuntimeError('I think the server is down') from exc
        else:
            break                             # 中断

    print('The server says {!r}'.format(data.decode('ascii')))

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send and receive UDP,'
                                     ' pretending packets are often dropped')
    parser.add_argument('role', choices=choices, help='which role to take')
    parser.add_argument('host', help='interface the server listens at;'
                        ' host the client sends to')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='UDP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host, args.p)

操作结果:

S:
在这里插入图片描述
C:
在这里插入图片描述

  • 相比自环接口,C不会在调用recv()后永久等待(称为被阻塞),而是调用套接字的settimeout()方法。通知OS,C进行一个套接字的最长等待时间的delay秒,超过就抛出socket.timeout异常,recv()调用就中断。
  • 对超时的处理,C采用指数退避,尝试重发数据包的频率会越来越低。但对于需要不断重发数据包的守护程序代码时,不要严格遵守指数退避,最好取一个较大的延时参数,比如5分钟,一旦指数退避到最大值就不再延时了。

七、连接UDP套接字

UDP绑定:包括显示bind()调用隐式绑定

显式bind()调用:发生在服务器端,用来指定服务器要使用的IP地址和端口。
隐式绑定:发生在客户端,当客户端第一次尝试使用一个套接字时,操作系统会为其随机分配一个临时端口。

这个时候我们就可以使用connect()。
如果使用sendto(),那么每次向服务器发送信息的时候都必须显式地给出服务器端的IP地址和端口;
而如果使用connect(),即操作系统事先就已经知道数据包要发送到的远程地址,就可以简单地把要发送的数据作为参数传入send()调用,而无需重复给出服务器地址。

但其实,connect()还可以解决客户端混杂的问题,因为只要操作系统发现传入的数据包的返回地址与已连接的地址不同,就会将该数据包丢弃。但这也并不意味着确保安全,因为黑客可以实施电子欺骗,即用另一台电脑的返回地址来发送数据包。

八、请求ID

如果要自己设计一套UDP请求和响应机制的话,,应该考虑给每个请求加上一个序列号,以此保证接受的响应包含相同的序列号。在服务器端,只需包请求的序列号复制到相应的响应中即可。ID号可以用random模块来生成大整数。

但是这也不能算是真正的安全,只有在攻击者无法获取我们的网络通信信息而只能进行最简单的电子欺骗时才能起到保护作用。真正的安全意味着即使攻击者可以获取通信数据并插入任何信息,我们的客户端仍能受到保护

九、广播

只有UDP能支持广播,TCP不可以,因为TCP是面向连接的。

通过广播,可以将数据包的目标地址设置为本机连接的整个子网,然后使用物理网卡将数据包广播。

但是现在,广播已经过时了,用的更多的是多播,多播能支持非本地子网上的主机。

广播使用方法:在使用套接字对象时,先调用setsockopt()方法,设为允许进行广播。除此之外,与之前无异。设置允许对UDP数据包进行广播并不会禁用其发送与接收特定地址数据包的正常使用。

实际上,只有在每次只发送一条信息然后等待响应的时候呀,UDP才是高效的。

十、总结一下

用户数据报协议使得用户级程序能够在IP网络中发送独立的数据包。通常情况下,客户端程序向服务器发送一个数据包,而服务器通过每个UDP数据包中包含的返回地址发送响应的数据包。

使用UDP一定要会区分绑定客户端的连接。绑定指定了要使用的特定的UDP端口,而连接限制了客户端可以接收的响应,表示只接收从正在连接的服务器发送的数据包。

猜你喜欢

转载自blog.csdn.net/weixin_41206209/article/details/84643746
今日推荐