Python学习篇22-网络编程

如果对您有一丁点帮助,劳烦动动手指点个赞,支持和鼓励是搬砖人不断创作的动力!

网络编程

自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。 计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是 如何在程序中实现两台计算机的通信。

网络编程对所有开发语言都是一样的,Python 也不例外。网络是一个互联网应用的重 要组成部分,在 Python 语言中提供了大量的内置模块和第三方模块用于支持各种网络访问, 而且 Python 语言在网络通信方面的优点特别突出,远远领先其他语言。

通过阅读本章,你可以:

  • 了解 TCP 和 UDP
  • 掌握编写 UDP Socket 客户端应用
  • 掌握编写 UDP Socket 服务器端应用
  • 掌握编写 TCP Socket 客户端应用
  • 掌握编写 TCP Socket 服务器端应用
  • 掌握使用 socketserver 模块中的 API 编写 TCP 服务端应用

基本概念

IP 地址与端口

IP 地址

用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互 联网的每个服务器都要有自己的 IP 地址,而每个局域网的计算机要通信也要配置 IP 地址。

路由器是连接两个或多个网络的网络设备。
在这里插入图片描述

​ 目前主流使用的 IP 地址是 IPV4,但是随着网络规模的不断扩大,IPV4 面临着枯竭的 危险,所以推出了 IPV6。

​ IP 地址实际上是一个 32 位整数(称为 IPv4),以字符串表示的 IP 地址如 192.168.0.1

实际上是把 32 位整数按 8 位分组后的数字表示,目的是便于阅读。

​ IPv6 地址实际上是一个 128 位整数,它是目前使用的 IPv4 的升级版,以字符串表示类 似于 2001:0db8:85a3:0042:1000:8a2e:0370:7334。

注意事项

  • 127.0.0.1 本机地址
  • 192.168.0.0–192.168.255.255 为私有地址,属于非注册地址,专门为组织机构内部使用。

端口

IP 地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来 区分这些不同的程序呢?这就要用到端口。
在这里插入图片描述

端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机 上运行多个网络应用程序。 端口的表示是一个 16 位的二进制整数,对应十进制的 0-65535。

Oracle、MySQL、Tomcat、QQ、msn、迅雷、电驴、360 等网络程序都有自己的端口。

总结

  • IP 地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定 IP 地址和端 口号才能够正确的发送数据。
  • IP 地址好比为电话号码,而端口号就好比为分机号。

网络通信协议

  • 网络通信协议 通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必

须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定 标准。就像两个人想要顺利沟通就必须使用同一种语言一样,如果一个人只懂英语而另外一 个人只懂中文,这样就会造成没有共同语言而无法沟通。

国际标准化组织(ISO,即 International Organization for Standardization)定义了网络通信协 议的基本框架,被称为 OSI(Open System Interconnect,即开放系统互联)模型。要制定通 讯规则,内容会很多,比如要考虑 A 电脑如何找到 B 电脑,A 电脑在发送信息给 B 电脑时

是否需要 B 电脑进行反馈,A 电脑传送给 B 电脑的数据格式又是怎样的?内容太多太杂, 所以 OSI 模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得 标准的制定没那么复杂。OSI 模型制定的七层标准模型,分别是:应用层,表示层,会话层, 传输层,网络层,数据链路层,物理层。

OSI 七层协议模型如图所示:

在这里插入图片描述

虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用 最多的网络通信协议是 TCP/IP 网络通信协议。

TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层, 网络接口层(物理+数据链路层)。

那么 TCP/IP 协议和 OSI 模型有什么区别呢?OSI 网络通信协议模型,是一个参考模型, 而 TCP/IP 协议是事实上的标准。TCP/IP 协议参考了 OSI 模型,但是并没有严格按照 OSI 规定的七层标准去划分,而只划分了四层,这样会更简单点,当划分太多层次时,你很难区 分某个协议是属于哪个层次的。TCP/IP 协议和 OSI 模型也并不冲突,TCP/IP 协议中的应用 层协议,就对应于 OSI 中的应用层,表示层,会话层。就像以前有工业部和信息产业部, 现在实行大部制后只有工业和信息化部一个部门,但是这个部门还是要做以前两个部门一样 多的事情,本质上没有多大的差别。TCP/IP 中有两个重要的协议,传输层的 TCP 协议和互 连网络层的 IP 协议,因此就拿这两个协议做代表,来命名整个协议族了,再说 TCP/IP 协议

时,是指整个协议族。

  • 网络协议的分层

​ 由于网络结点之间联系很复杂,在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一 层,而与再下一层不发生关系。

​ 把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干 层,规定每层处理的任务,也规定每层的接口标准。

​ ISO 模型与 TCP/IP 模型的对应关系如图所示。
在这里插入图片描述

TCP/UDP

  • 联系和区别

​ TCP 协议和 UDP 协议是传输层的两种协议。Socket 是传输层供给应用层的编程接口,所以 Socket 编程就分为 TCP 编程和 UDP 编程两类。

​ 在网络通讯中,TCP 方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建 立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该 数据。而 UDP 方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的 虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。

​ 这两种传输方式都在实际的网络编程中使用,重要的数据一般使用 TCP 方式进行数据 传输,而大量的非核心数据则可以通过 UDP 方式进行传递,在一些程序中甚至结合使用这 两种方式进行数据传递。

​ 由于 TCP 需要建立专用的虚拟连接以及确认传输是否正确,所以使用 TCP 方式的速度 稍微慢一些,而且传输时产生的数据量要比 UDP 稍微大一些。

总结

  • TCP 是面向连接的,传输数据安全,稳定,效率相对较低。
  • UDP 是面向无连接的,传输数据不安全,效率较高。

套接字编程

应用程序通常通过“套接字”(socket)向网络发出请求或者应答网络请求,使用主机 之间或者一台计算机上的进程间可以通信。Python 语言提供了两种访问网络服务的功能。 其中低级别的网络服务通过套接字实现,而高级别的网络服务通过模块 SocketServer 实现, 它提供了服务中心类,可以简化网络服务器的开发。

**socket()**函数介绍

在 Python 语言标准库中,通过使用 socket 模块提供的 socket 对象,可以在计算机网络 中建立可以互相通信的服务器与客户端。在服务器端需要建立一个 socket 对象,并等待客户 端的连接。客户端使用 socket 对象与服务器端进行连接,一旦连接成功,客户端和服务器端 就可以进行通信了。

在 Python 中,通常用一个 Socket 表示“打开了一个网络连接”,语法格式如下: socket.socket([family[, type[, proto]]])

其中参数 family: 套接字家族可以使 AF_UNIX 或者 AF_INET;type: 套接字类型可以 根据是面向连接的还是非连接分为 SOCK_STREAM 或 SOCK_DGRAM;protocol: 一般不填 默认为 0。

Socket 主要分为面向连接的 Socket 和无连接的 Socket。无连接 Socket 的主要协议是用 户数据报协议,也就是常说的 UDP,UDP Socket 的名字是 SOCK_DGRAM。创建套接字 UDP/IP 套接字,可以调用 socket.socket()。示例代码如下:

udpSocket=socket.socket (AF_INET,SOCK_DGRAM)

socket 对象的内置函数和属性

在 Python 语言中 socket 对象中,提供如表 12-1 所示的内置函数。

12-1 内置函数

函数功能
服务器端套接字函数
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()创建一个与该套接字相关连的文件

UDP 编程

TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对 TCP,UDP则是 面向无连接的协议。使用 UDP协议时,不需要建立连接,只需要知道对方的 IP地址和端口 号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用 UDP传输数据不可靠,但 它的优点是和 TCP比,速度快,对于不要求可靠到达的数据,就可以使用 UDP协议。

创建 Socket 时,SOCK_DGRAM指定了这个 Socket 的类型是 UDP。绑定端口和 TCP一样, 但是不需要调用 listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返 回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用 sendto()就可以把数 据用 UDP发给客户端。

发送数据:为看到效果借助“网络调试助手”。双击 NetAssist.exe 就完成安装。使用 详细如下图所示:

在这里插入图片描述

【示例】UDP 发送数据

from socket import *

s = socket(AF_INET, SOCK_DGRAM) #创建套接字

addr = (‘192.168.121.1’, 8080) #准备接收方地址

data = input(“请输入:”)

#发送数据时,python3 需要将字符串转成 byte s.sendto(data.encode(‘gb2312’),addr) #默认的网络助手使用的编码是 gb2312 s.close()

【示例】UDP 先发送数据,再接收数据

from socket import *
s = socket(AF_INET, SOCK_DGRAM) #创建套接字
s.bind(('', 8788)) #绑定一个端口,ip 地址和端⼝号,ip⼀般不⽤写 
addr = ('192.168.121.1', 8080) #准备接收方地址
data = input("请输入:")
s.sendto(data.encode('gb2312'),addr)
redata = s.recvfrom(1024) #1024 表示本次接收的最⼤字节数
print(redata)
print('接收到%s 的消息:%s'%(redata[1],redata[0].decode('gb2312')))
s.close()

【示例】UDP 实现简单聊天

from socket import *

udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定端口
udp_socket.bind(('',8989))
#接收数据
while True:
	recv_data = udp_socket.recvfrom(1024)
	print('接收到%s 的消息是:%s' % (recv_data[1], recv_data[0].decode('gb2312')))
udp_socket.close()

【示例】UDP 实现多线程聊天

from socket import *
from threading import Thread 

udp_socket=socket(AF_INET,SOCK_DGRAM) 
#绑定端口
udp_socket.bind(('',8989))

#不停接收
def recv_data():
	while True:
		recv_msg=udp_socket.recvfrom(1024) 
		print('>>%s:%s'%(recv_msg[1],recv_msg[0].decode('gb2312')))

#不停发送
def send_data():
	while True:
		data=input('<<:') addr=('192.168.121.1',8080) 		
		udp_socket.sendto(data.encode('gb2312'),addr)

if __name__=='__main__':
	创建两个线程 
	t1=Thread(target=send_data) 
	t2=Thread(target=recv_data) 
	t2.start()
	t1.start()
	t1.join()
	t2.join()

TFTP 文件下载器

TFTP(Trivial File Transfer Protocol,简单⽂件传输协议)使用这个协议,就可以实现简 单文件的下载,tftp 端⼝号为 69。

实现 TFTP 下载器: 下载:从服务器上将一个文件复制到本机上 下载的过程: 在本地创建一个空文件(与要下载的文件同名) 向里面写数据(接收到一点就向空文件里写一点) 关闭(接受完所有数据关闭文件)

(1)TFTP 文件下载过程

在这里插入图片描述

(2)TFTP 下载格式

在这里插入图片描述

当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过 TFTP 协议发 送给客户端。如果文件的总大小较大(比如 3M),那么服务器分多次发送,每次会从文件 中读取 512 个字节的数据发送过来。因为发送的次数可能会很多,所以为了让客户端对接收 到的数据进行排序,所以在服务器发送那 512 个字节数据的时候,会多发 2 个字节的数据, 用来存放序号,并且放在 512 个字节数据的前面,序号是从 1 开始的。

因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误 的信息过来,为了区分服务器的是文件内容还是错误的提示信息,所以又用了 2 个字节来表 示这个数据的功能(称为操作码),并且在序号的前面。操作码对应功能如下表所示:

操作码功能
1读请求,及下载
2写请求,即上传
3表示数据包,即 DATA
4确认码,及 ACK
5错误

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

什么时候就认为数据包已发送完毕?当客户端接收到的数据⼩于 516(2 字节操作码+2 个字节的序号+512 字节数据) 时,就意味着服务器发送完毕了 (如果恰好最后一次数据 长度为 516,会再发一个长度为 0 的数据包)。struct 模块可以按照指定格式将 Python 数据 转 换 为 字 符 串 ,该 字 符 串 为 字 节 流 。 struct 模 块 中 最 重 要 的 三 个 函 数 是 pack(), unpack(),

calcsize()。

函数名描述
pack(fmt, v1, v2, …)按照给定的格式(fmt),把数据封装成字 符串(实际上是类似于 c 结构体的字节流)
unpack(fmt, string)按照给定的格式(fmt)解析字节流 string, 返回解析出来的元组
calcsize(fmt)

计算给定的格式(fmt)占用多少字节的内

【示例】构造下载请求数据:“1test.jpg0octet0

import struct
cmb_buf = struct.pack(“!H8sb5sb”,1,b“test.jpg”,0,b“octet”,0) 
如何保证操作码(1/2/3/4/5)占两个字节?如何保证 0 占一个字节? 
#!H8sb5sb: ! 表示按照网络传输数据要求的形式来组织数据(占位的格式) 
H 表示将后面的 1 替换成占两个字节
8s 相当于 8 个 s(ssssssss)占 8 个字节
b 占一个字节

【示例】TFTP 文件下载客户端

import struct
from socket import *

filename = 'face.jpg'
server_ip = '127.0.0.1'

send_data = struct.pack('!H%dsb5sb' % len(filename), 1, filename.encode(), 0, 'octet'.encode(), 0)
s = socket(AF_INET, SOCK_DGRAM)
s.sendto(send_data, (server_ip, 69)) # 第一次发送,连接服务器 69 端口

f = open(filename, 'ab') # a:以追加模式打开(必要时可以创建)append;b:表示二进制

while True:
    recv_data = s.recvfrom(1024) # 接收数据
    caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4]) # 获取数据块编号

    rand_port = recv_data[1][1] # 获取服务器的随机端口
    
    if int(caozuoma) == 5: 
        print('文件不存在...')
        break
        
    print("操作码:%d,ACK:%d,服务器随机端口:%d,数据长度:%d" % (caozuoma, ack_num, rand_port, len(recv_data[0])))
    
    f.write(recv_data[0][4:]) # 将数据写入
    
    if len(recv_data[0]) < 516:
        break
        
    ack_data = struct.pack("!HH", 4, ack_num)  
    s.sendto(ack_data, (server_ip, rand_port)) # 回复ACK确认包

TCP 编程

面向连接的 Socket 使用的主要协议是传输控制协议,也就是常说的 TCP,TCP 的 Socket 名称是 SOCK_STREAM。创建套接字 TCP/IP 套接字,可以调用 socket.socket()。示例代码 如下:

tcpSocket=socket.socket(AF_INET,SOCK_STREAM)

在这里插入图片描述

三次握手

  • 第一步,客户端发送一个包含 SYN 即同步(Synchronize)标志的 TCP 报文,SYN 同步报文会指明客户端使用的端口以及 TCP 连接的初始序号。
  • 第二步,服务器在收到客户端的 SYN 报文后,将返回一个 SYN+ACK 的报文,表 示客户端的请求被接受,同时 TCP 序号被加一,ACK 即确认(Acknowledgement)。
  • 第三步,客户端也返回一个确认报文 ACK 给服务器端,同样 TCP 序列号被加一, 到此一个 TCP 连接完成。然后才开始通信的第二步:数据处理。
  • 这就是所说的 TCP 的三次握手(Three-way Handshake)。

在这里插入图片描述

​ 在 Python语言中创建 Socket服务端程序,需要使用 socket模块中的 socket类。创建 Socket 服务器程序的步骤如下:

(1) 创建 Socket 对象。

(2) 绑定端口号。

(3) 监听端口号。

(4) 等待客户端 Socket 的连接。

(5) 读取客户端发送过来的数据。

(6) 向客户端发送数据。

(7) 关闭客户端 Socket 连接。

(8) 关闭服务端 Socket 连接。

【示例】TCP 服务器端接收数据

from socket import *

serverSocket = socket(AF\_INET, SOCK\_STREAM)
serverSocket.bind(("", 8899))
serverSocket.listen(5)
clientSocket,clientInfo = serverSocket.accept()
#clientSocket 表示这个新的客户端
#clientInfo 表示这个新的客户端的 ip 以及 port
recvData = clientSocket.recv(1024)
print("%s:%s"%(str(clientInfo), recvData.decode('gb2312')))
clientSocket.close()
serverSocket.close() 

执行代码,服务器端会等待客户端连接,打开网络助手,选择网络协议、服务器的 IP、端 口如下图所示:

在这里插入图片描述

填写好消息,点击发送,在服务器控制台就会输出接收到的消息,如下图:

在这里插入图片描述

【示例】TCP 客户端

from socket import *

clientSocket = socket(AF\_INET, SOCK\_STREAM) 
clientSocket.connect(("192.168.121.1", 8899))

#注意:
	# 1.tcp 客户端已经链接好了服务器,所以在以后的数据发送中,不需要填写对方的 iph 和 port----->打电话
	# 2.udp 在发送数据的时候,因为没有之前的链接,所依需要 在每次的发送中 都要填写接收方的 ip 和 port----->写信

clientSocket.send("haha".encode("gb2312"))
recvData = clientSocket.recv(1024) 
print("recvData:%s"%recvData.decode('gb2312'))
clientSocket.close()

先配置网络助手,选择网络协议为 TCP Server,本地 IP、本地端口,点击连接,执行编写的 TCP 客户端代码,执行结果如下图所示:

在这里插入图片描述
在这里插入图片描述

【示例】TCP:双向通信 Socket 之服务器端

'''
双向通信 Socket 之服务器端
读取客户端发送的数据,将内容输出到控制台 将控制台输入的信息发送给客户器端
'''

#导入 socket 模块
from socket import *

#创建 Socket 对象 tcpServerSocket=socket(AF_INET,SOCK_STREAM) #绑定端口
tcpServerSocket.bind(('',8888))
#监听客户端的连接
tcpServerSocket.listen()
#接收客户端连接 
tcpClientSocket,host=tcpServerSocket.accept()

while True:
    #读取客户端的消息 re_data=tcpClientSocket.recv(1024).decode('utf-8') #将消息输出到控制台
    print('客户端说:',re_data)
    if re_data=='end':
        break

    #获取控制台信息
    msg=input('>')
    tcpClientSocket.send(msg.encode('utf-8')) 

tcpClientSocket.close()
tcpServerSocket.close()

【示例】TCP:双向通信 Socket 之客户端

'''
双向通信Socket 之客户端
将控制台输入的信息发送给服务器端
读取服务器端的数据,将内容输出到控制台
'''
# 导入 socket 模块
from socket import *
# 创建客户端 Socket 对象
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
# 连接服务器端
tcpClientSocket.connect(('127.0.0.1', 8888))
while True:
    msg = input('>')
    # 向服务器发送数据
    tcpClientSocket.send(msg.encode('utf-8'))
    if msg == 'end':
        break
    # 接收服务器端数据    
    re_data = tcpClientSocket.recv(1024)
    print('服务器端说:', re_data.decode('utf-8'))

tcpClientSocket.close()

首先运行示例启动服务器端程序,然后运行示例客户端程序。执行结果如下图所示:

运行效果图—服务器端

在这里插入图片描述

运行效果图—客户端

在这里插入图片描述

注意事项

  • 运行时,要先启动服务器端,再启动客户端,才能得到正常的运行效果。

​ 上面这个程序,必须按照安排好的顺序,服务器和客户端一问一答!不够灵活!!可以 使用多线程实现更加灵活的双向通讯!!

​ 服务器端:一个线程专门发送消息,一个线程专门接收消息。

​ 客户端:一个线程专门发送消息,一个线程专门接收消息。

欢迎扫描微信添加,技术交流+资源分享

ID: Txtechcom

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值