Python网络编程
- 什么是C/S架构、
- 计算机基础
- socket介绍
C/S架构
C指的就是Client(客户端软件),S指的是Server(服务端软件),本章的重点就是教大家写一个C/S架构的软件,实现服务软件与客户端软件基于网络通信。
计算机基础
作为一名应用开发程序员,我们开发的都是应用软件,而应用软件必须运行在操作系统之上,操作系统则运行在一堆硬件之上,应用软件是无法直接操作硬件的,应用程序对硬件的操作必须调用操作系统的接口,由操作系统操控硬件。
如客户端软件想要基于网络发送一条消息给服务端软件,流程是https://www.processon.com/view/5b0f5d79e4b068c2520a6497)
1.客户端软件产生数据,存放于客户端软件的内存中,然后调用接口将自己内存中的数据发送/拷贝给操作系统的内存中
2.客户端操作系统收到数据后,按照客户端软件指定的规则(协议),调用网卡硬件设备发送数据
3.网络传输数据
4.服务端软件调用系统接口,想要将数据从操作系统拷贝到自己的内存里
5.服务端操作系统收到指令后,使用与客户端相同的规则(协议)从网卡接收到数据,然后拷贝到服务端软件的内存中
socket介绍
引子
我们已经知道,假设我要写一个程序,给另一台计算机发送数据,必须通过TCP/IP协议,但是具体的实现过程是什么呢?我应该怎么操作才能把数据封装成TCP/IP的包呢?又执行什么指令才能把数据发送到对端机器上呢?此时,socket隆重登场,简而言之,socket这个东西就是干这个事情的,就是帮你把TCP/IP协议层的各种数据封装、数据发送、接受等通过代码已经给你封装好了,你只需要调用几行代码,就可以给别的机器发消息了。
socket介绍
什么是socket?
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部。
socket抽象层(https://www.processon.com/view/5b0f6b9de4b02e4b26ec3521)
socket起源于Unix,而Unix/Linux基本哲学之一就是'一切皆文件',都可以用“打开open-->读写write/read-->关闭close"模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO,打开,关闭)
你想给另一台计算机发消息,你知道它的IP地址,他的服务器上同事运行着QQ、迅雷、word、浏览器等程序,你想给他的QQ发消息,那么想一下,你现在只能通过IP找到他的机器,但如果让这台计算机知道把消息发送给qq程序呢?大难就是通过port,一个机器上可以有0-65535个端口,你的程序想从网络上收发数据,就必须绑定一个端口,这样,远程发到这个端口上的数据,就全都会转给这个程序了
过程(https://www.processon.com/view/5b0f81c5e4b0a838a077af78)
socket通信套路
当通过socket建立起两台机器的连接后,本质上socket只干两件事,一个是收数据,一个是发数据,没数据时就等着。
socket建立连接的过程和我们现实中打电话比较像,打电话必须是打电话方和接电话方共同完成的事情,我们分别来看看他们是如何建立通话的:
接电话方
1.首先要有一个电话
2.你的电话要有号码
3.你的电话必须连上电话线
4.开始在家等电话
5.电话铃响了,听到对方的声音
打电话方
1.首先要有一个电话
2.输入想拨打的电话号码
3.等待对方接听
4.say 'hi ,我有七天酒店的打折卡奥~'
5.等待回应 --> 响应回应 --> 等待回应.......
把它翻译成socket通信
接电话方(socket服务器端)
1.首先要有一个电话(生成一个socket对象)
2.你的电话要有号码(绑定本机的IP+PORT)
3.你的电话必须连上电话线(联网)
4.开始在家等电话(开始监听电话)
5.电话铃响了,听到对方的声音(接收一个新连接)
打电话方(socket客户端)
1.首先要有一个电话(生成一个socket对象)
2.输入想拨打的电话号码(connect 远程主机的IP和PORT)
3.等待对方接听
4.say 'hi ,我有七天酒店的打折卡奥~'(send发送小心)
5.等待回应 --> 响应回应 --> 等待回应.......
socket服务端和客户端通信过程(https://www.processon.com/view/5b0f850de4b07596cf3588cb)
socket套接字方法
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
family(socket家族)
- socket.AF_UNIX:用于本机间的通讯,为了保证程序安全,两个独立的程序(进程)是不能互相访问的彼此的内存的,但为了实现进程间的通讯,可以创建一个本地的socket来完成
- socket.AF_INET:(还有AF_INET6被用于ipv6, 还有一些其他的地址家族,不过吗,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现),所有的地址家族中,AF_INET是使用最广泛的一个,Python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分我们只使用AF_INET
socket.type类型
- socket.SOCK_STREAM:for TCP
- socket.SOCK_DGRAM:for UDP
- socket.SOCK_RAW:原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头部
- socket.SOCK_RDM:是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用
- socket.SOCK_SEQPACKET:已经被废弃了
proto = 0 请忽略,特殊用途
fileno = None 请忽略,特殊用途
服务端套接字函数
- s.bind():绑定(主机,端口号)到套接字
- s.listen():开始TCP监听
- s.accpet():被动接受TCP客户端的连接,(阻塞式)等待连接的到来
客户端套接字函数
- s.connect():主动初始化TCP服务器的连接
- s.connect_ex():connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
套接字对象(内置)方法
- s.recv():接受数据
- s.send():发送数据(send在待发送数据量大于已端缓存剩余空间时,数据丢失,不会发完)
- s.sendall():发送完整的TCP数据(本质上就是循环调用send,sendall在待发送数据量大于已端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
- s.recvfrom():接收UDP消息
- s.getpeername():连接到套接字(TCP)的远程地址
- s.close():关闭套接字
- socket.setblocking(flag):True or False,设置socket为非阻塞模式
- socket.getaddrinfo(host,port,family=0,type=0,proto=0,flags=0):返回远程主机的地址信息
- socket.getfqdn():拿到本机的主机名
- socket.gethostbyname():通过域名解析IP地址
socket代码实例
我们以打电话的例子,来讲一下这个例子该如何写?
# 服务端
1.首先要有一部手机(生成一个socket对象)
2.绑定手机卡,没有手机卡怎么接电话呢?(绑定本机的IP地址和PORT端口)
3.开机,等待电话打进来(listen监听连接)
4.等电话连接(accpet接受客户端的连接)
5.收发消息(recv和send,因为你们之间总要对话吧)
6.挂电话(这个电话打完了,就要把电话挂了close)
7.关机(可选的,关闭服务器套接字)
# 客户端
1.首先要有一部手机(生成一个socket对象)
2.打电话(connect,拨打服务器端的IP和PORT)
3.发、收数据(send,recv)
4.挂电话(close)
服务端代码
import socket
# 1.先创建出来一个socket对象
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2.绑定本机的IP和PORT
phone.bind(('127.0.0.1',8082))
# 3.对连接进行监听,开始监听,5代表在允许有五个连接排队,更多的新连接连进来时就会被拒绝
phone.listen(5)
print('starting.......')
# 4.接受客户端的连接,让我们来看一下客户端是怎么连接过来的?
res = phone.accept()
print(res) # (<socket.socket fd=132, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8082), raddr=('127.0.0.1', 52690)>, ('127.0.0.1', 52690)) 前面部分代表的是套接字对象(连接对象),后面代表的是客户端的IP地址和PORT
# 所以我们使用conn和addr分别接受套接字对象和客户端IP和PORT
conn,addr = phone.accept()
print(addr)
# 5.接收客户端发过来的数据,recv(1024)代表最大接收1024个字节,如只发送一个字节,那么久接收1个字节;如果发送的是1025个字节,那么还是接收1024个,这个等下会专门讲的
data = conn.recv(1024)
print('客户端数据:',data)
# 6. 处理了客户端发送过来的数据,然后再send给客户端
conn.send(data.upper())
# 7.本次连接关闭
conn.close()
# 8.服务器关闭
phone.close()
# 因为接收客户端的连接会收到套接字对象和客户端的IP及PORT,所以要把以下两行注释掉,用参数分别来接收
# res = phone.accept()
# print(res)
客户端代码
import socket
# 1.生成一个socket对象
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2.连接服务端的IP和PORT(打电话)
phone.connect(('127.0.0.1',8082))
# 3.send发送消息,
msg = input('>>>>>').strip()
phone.send(msg.encode('utf-8'))
# 4.recv从客户端收到的消息,缓冲区大小设置为1KB
data = phone.recv(1024)
print(data)
# 5.结束本次
phone.close()
需要注意一点的是,Python3中发送的数据只能是bytes类型
那么我们来运行一个这两段代码:
# 服务器端
starting.......
('127.0.0.1', 52885)
客户端数据: b'admin'
# 客户端
>>>>>admin 输入了一个小写,返回了一个大写
b'ADMIN'
循环收发数据
刚刚的这段代码中,我们发现了一个弊端,就是收发数据只能一次,设想在打电话上,我打通了电话,我说了一句hello,对方说了一句hello,然后就结束了吗?所以,这样子肯定是不行的,我们需要修改代码让 服务端和客户端能够循环收发数据:
服务端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,0) # 表示如果下面绑定的端口被占用,则重用此端口
phone.bind(('127.0.0.1',8083))
phone.listen(5)
print('starting.......')
conn,addr = phone.accept()
print(addr)
while True:
data = conn.recv(1024)
print('客户端数据:',data)
conn.send(data.upper())
conn.close()
phone.close()
有几点需要说明下:
# 1.对于一台对外提供服务的服务器来说,应该是一直对外提供服务,有一个客户端连接请求,就给他回数据,如果没有的话,就在这里一直等着
# 2.在服务端中,有两种套接字,第一个是phone,这个就是负责绑定、监听和接受连接的,另一个套接字叫做conn,这个套接字就是负责收发消息的。你可以把phone套接字看成酒店的前台,把conn看成酒店的服务员
客户端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8083))
while True:
msg = input('>>>>>').strip()
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data)
phone.close()
# 在客户端中就只有个套接字phone,连接、发收消息都是这个套接字完成的
在这次代码修改中,只是在客户端和服务端各添加了一个while True,所以这个程序就可以一直运行了。
当不小心碰到回车键
那么我在玩的时候,不小心输入快了,只输入一个回车键,这下会怎么样呢?两边都会等待,客户端在等待着服务端回复数据,服务端在等待客户端发过来数据,为了解决这个问题,我们不妨不允许用户输入空
# 客户端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8083))
while True:
msg = input('>>>>>').strip()
if not msg:continue # 如果为空,则跳出本次循环重新输入
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data)
phone.close()
让我们想想到底到底是不是这么回事,客户端输入回车,然后服务端就会报错,那么究竟是客户端报错还是服务器端的报错呢?让我们来更深入的找一下原因
# 服务端的代码不变
# 客户端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8085))
while True:
msg = input('>>>>>').strip()
print('send msg...') # 在输入msg之后打印
phone.send(msg.encode('utf-8'))
print('before recv msg') # 收到数据之前打印
data = phone.recv(1024)
print('before print data') # 在打印收到的数据之前打印
print(data)
phone.close()
# 此时服务端是这样的
starting.......
('127.0.0.1', 53353)
客户端数据: b'admin'
# 此时客户端是这样的
>>>>>admin
send msg...
before recv msg
before print data
b'ADMIN'
>>>>>
send msg...
总结:数据发出去了,但是没有收到回复的数据包
before recv msg
好的,让我们现在自己想想:数据发出去了,但是没有数据返回,那么数据发送的时候在哪一步出现了问题?
算了不用猜了,我直接说吧,数据包在发送到操作系统的时候就已经没了。
为什么?我们在章节前面就已经说了,数据包发送是把应用软件里的数据的这一块内存拷贝到操作系统的内存里,因为应用软件无法直接调用硬件设备,但是操作系统拷贝过来后,怎么发这个数据包和以什么协议发数据包是应用软件干涉不了的,操作系统收到这个数据的内存后,发现是空,那么对于操作系统来说空就是没有,所以,就没有把这个包发出去,但是对于应用程序来说,我这个包是已经发出去了的。
好了上面的一个问题解决了,还有一个问题,有没有一种可能,客户端和我通信的过程中,突然断开了连接,(断网等),那么这样会发生什么情况?
Traceback (most recent call last):
File "D:/py_study/day20-网络编程/1.py", line 20, in <module>
data = conn.recv(1024)
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
会报错的,因为客户端和服务端是基于TCP协议建立的连接,TCP协议有一句话“三次握手,四次分手”,客户端不正常的断开了连接,都会报错,那么解决这个问题的办法是:
# 服务端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,0) # 表示如果下面绑定的端口被占用,则重用此端口
phone.bind(('127.0.0.1',8085))
phone.listen(5)
print('starting.......')
conn,addr = phone.accept()
print(addr)
while True:
try:
data = conn.recv(1024)
print('客户端数据:',data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
phone.close()
# 客户端
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8085))
while True:
msg = input('>>>>>').strip()
if not msg:continue
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data)
phone.close()
模拟SSH登录
首先我们想一下如何在系统上执行命令并且拿到命令的结果,我们之前讲过了os和subprocess模块,其中os.system是运行系统命令的,subprocess模块是和系统交互的,那么:
In [1]: import os
In [2]: res = os.system('ls') # 我们想把命令执行的结果存放到一个变量里,然后返回给客户端
anaconda-ks.cfg apr-1.4.6 apr-1.4.6.tar.gz apr-util-1.4.1 apr-util-1.4.1.tar.gz httpd-2.4.7 httpd-2.4.7.tar.gz 网络编程
In [3]: print(res) # 但是我们打印的时候却是返回的状态码
0
import subprocess
obj = subprocess.Popen('ls /tmp',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
# 如果命令执行正确,则
print(obj.stdout.read().decode('utf-8'))
# 如果命令执行错误,则
print(obj.stderr.read().decode('utf-8'))
注意:如果是windows系统,需要将输出转换成为gbk编码
好了,现在让我们写一个模拟SSH登陆拿到命令的执行结果的代码吧
# 服务端
import socket
import subprocess
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.bind(('127.0.0.1',8880))
socket.listen(5)
print('starting...')
conn,addr = socket.accept()
while True:
cmd = conn.recv(1024) # 收到命令
obj = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # subprocess模块交互,decode解码
# obj.stdout.read()读出的就是GBK编码,在接收端需要用GBK编码,且只能从管道里读一次结果
stdout = obj.stdout.read() # 标准正确输出用一个变量来接收
stderr = obj.stderr.read() # 标准错误输入用一个变量来接收
print(len(stdout) + len(stderr)) # 打印命令执行结果的长度
conn.send(stdout+stderr) # 发送给客户端
conn.close()
socket.close()
# 客户端
import socket
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.connect(('127.0.0.1',8880))
while True:
msg = input('>>>').strip() # 输入要执行的系统命令
if not msg:continue # 验证
socket.send(msg.encode('utf-8')) # 以UTF-8编码发送
data = socket.recv(1024)
print(data.decode('gbk')) # 转码成gbk,这里用windows环境测试的
socket.close()
运行结果为:
# 服务端打印结果
starting...
1009 # 表示发送的字节为1009个
# 客户端打印结果
>>>ipconfig
Windows IP 配置
以太网适配器 以太网:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
无线局域网适配器 本地连接* 1:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
无线局域网适配器 WLAN:
连接特定的 DNS 后缀 . . . . . . . : hzdomain1.com
本地链接 IPv6 地址. . . . . . . . : fe80::fca6:24d8:1890:c8a4%4
IPv4 地址 . . . . . . . . . . . . : 10.202.102.80
子网掩码 . . . . . . . . . . . . : 255.255.254.0
默认网关. . . . . . . . . . . . . : 10.202.102.1
无线局域网适配器 本地连接* 2:
连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::6560:ca5a:830a:ce86%20
IPv4 地址 . . . . . . . . . . . . : 192.168.137.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :
以太网适配器 蓝牙网络连接 2:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
继续来测试
# 服务端
starting...
1009 # 这个是ipconfig命令发送的字节数
16226 # tasklist
# 客户端tasklist,当前进程和ps aux一样
>>>tasklist
映像名称 PID 会话名 会话# 内存使用
========================= ======== ================ =========== ============
System Idle Process 0 Services 0 8 K
System 4 Services 0 24 K
Registry 96 Services 0 18,044 K
smss.exe 360 Services 0 488 K
csrss.exe 532 Services 0 1,720 K
wininit.exe 640 Services 0 1,252 K
csrss.exe 648 Console 1 2,568 K
services.exe 712 Services 0 5,864 K
lsass.exe 728 Services 0 9,596 K
svchost.exe 844 Services 0 396 K
fontdrvhost.exe 864 Services 0 88 K
svchost.
我们可以看到,tasklist返回的结果,好像不是那么全,那让我们在运行一个ipconfig一下试试,在服务端发送的字节码还是1009:
>>>ipconfig
exe 872 Services 0 17,556 K
WUDFHost.exe 920 Services 0 5,180 K
svchost.exe 1004 Services 0 10,260 K
svchost.exe 292 Services 0 6,140 K
winlogon.exe 896 Console 1 3,728 K
fontdrvhost.exe 1068 Console 1 8,024 K
dwm.exe 1144 Console 1 75,552 K
svchost.exe 1192 Services 0 14,476 K
svchost.exe 1240 Services 0 2,984 K
svchost.exe 1248 Services 0 1,832 K
svchost.exe 1324 Services 0 4,492 K
svchost.exe 1420 Services 0 4,368 K
svchost.exe 1488 Services 0 11,732 K
那么为什么我们输入的是ipconfig命令,收到的结果还是tasklist的呢?如果你运行个15-16次,那个时候你就能得到网络配置信息,我为啥会这么肯定的说呢?因为这是一个粘包现象
因为tasklist命令的结果比较长,但是客户端只能接收1024个字节,可结果比1024长呀,那怎么办呢?只好在服务器端的IO缓冲区里把客户端还没收走的暂时存下来,等到客户端下次再来收,所以当客户端第二次调用recv(1024)就会把上次没有收完的数据先收下来,再收取ifconfig的命令结果。
那这个应该怎么结局呢?有人会说吧recv(1024)改大点就好了,可是这么干的话,并不能解决实际问题,因为你不可能提前知道对方返回的结果数据不大,无论你改成多大,对方的结果都有可能比你设置的大,另外这个recv并不是真的可以随便改特别大的,有关部门建议不要超过8192,再大反而会出现影响收发速度和不稳定的现象。
这个现象就是粘包,就是把两次结果粘到一起了。它的发生主要是因为socket缓冲区导致的,来看一下
粘包现象图解(https://www.processon.com/view/5b10d1f5e4b06350d44ad070)
你的应用程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你就要给远程发数据时,首先是先把数据从用户态copy到内核态,这样的按操作是耗资源和时间的,频繁的在内核态和用户态之间交换数据势必会导致发送效率降低,因此,socket为了提高传输效率,发送方往往要手机到足够多的数据后才发送一次数据给对方,若连续几次需要send的数据都很少,通常TCP socket会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样就接收方就收到了粘包数据。
粘包问题只存在于TCP中
发送端可以一K一K的发送数据,而接收端的应用程序可以两K两K的提走数据,当然可以一次性的提走3K,或者一次只提走几个字节的数据,也就是说,应用程序所看到的的数据是一个整体,或者说是一个流(STREAM),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
例如基于TCP的套接字客户端忘服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看,根本不知道该文件的字节流是从何开始,在何处结束的
举个例子看一看就知道了
# 服务端
import socket
import subprocess
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.bind(('127.0.0.1',8801))
socket.listen(5)
print('starting...')
conn,addr = socket.accept()
print(addr)
data = conn.recv(1024)
print('data:',data) # 打印
conn.send(data.upper()) # 变成大写发过去
conn.close()
socket.close()
# 客户端
import socket
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.connect(('127.0.0.1',8801))
# 客户端连续发送两个数据包
socket.send('hello'.encode('utf-8'))
socket.send('world'.encode('utf-8'))
data = socket.recv(1024)
print(data.decode('gbk'))
socket.close()
运行结果为:
# 服务端
starting...
('127.0.0.1', 50506)
data: b'helloworld'
# 客户端
HELLOWORLD
粘包总结
- 1.TCP(transport control protocol,传输控制协议) 是面向连接的,面向流的,提供高可靠性服务。收发两端(服务端和客户端)都要有成对的socket,因此,发送端为了将多个发送接收端的包,更有效的发送给对方,使用了优化算法(Nagle算法),将多次间隔比较小且数据量较小的数据,合并成一个大的数据块,然后进行封包,这样,接收端就很难分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
- 2.TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住。
备注:优化算法即上面演示客户端连续发送两个数据包代码