通用技术 用 Python 写网络编程(三)

陈子昂 · 2021年02月14日 · 最后由 skottZy 回复于 2021年04月20日 · 3569 次阅读

前言

今天是一个特别的节日,1946 年情人节,世界上第一台计算机 ENIAC 在米国的宾夕法尼亚大学被 new 了,标志着新的时代到来。
计算机陪伴人类已经走过了 75 个年头,所以今天,没啥特别的事情,请多去陪一陪自己家的电脑,手动狗头,手动狗头。
网络编程会是一个比较庞大的知识体系,第三篇会开始讲如何 encode 和 decode。
第一篇的数据结构是提供给第三篇使用的,然后通过第二篇的通道进行传输。
第一篇地址
第二篇地址

Tcp pickle

Tcp 网络流,加工过程也可以理解成是字节流到二进制流,字节就是第一篇提到的 bytes。
encode 压包就是把字节流转换到二进制流,那么 Python 是通过怎么做到的呢。

主要分为二种,pickle(python 独有序列化字节数组),struct(从 C 那边继承来的序列化字节数组的)库。
我们看看 pickle 是如何序列化数据的。

def encode_pickle(packet:bytes):
    """
    压入pickle
    :param packet: 
    :return: 
    """
    return pickle.pack(">i",len(packet))+packet

注意这里并没有发包出去,回顾前面的,需要先建立 socket 链接,确认地址,才能进行发包。
pickle.pack(fmt,*args) 的第一个参数 fmt 是个很重要的知识点。

先来了解下最重要的概念之一 fmt,众所周知,Python 是不需要先声明内存宽度,才能编程的。
内存宽度是指内存里面占位的长度,在声明对象前会标注数据类型(动态语言没这个,静态语言很多有自动推断声明的 比如 golang var 和:=)

网络编程需要学习这块,是因为操作二进制,细致的话会标记为有符号和无符号。有符号和无符号做一些原子计算,做减法的时候,部分语言需要做一些特殊处理,还好 Python 不用担心这些,有符号一般是指包含负数。
short 是有符号的 2 个字节的,unsigned short 是无符号的 2 个字节,负数这个上面图需要背下来。

网络自定义字节和字节序

上面代码里面是 int 和 unsigned int,fmt 前面是主机字节序,">i"代表大端有符号的 4 个字节,"<"代表小端,"!"代表不用匹配。
暂时不考虑具体学字节序转换的问题,目前网络编程是模拟客户端往服务器发。
字节序分别有大端(big endian)和小端(little endian),程序的内容都是以字节为单位的,一个字节 8 位,每个地址对应一个字节。
大端模式就是将数据的高位放在低位地址,低位地址就是左侧。转 16 进制只是给人读的,int 类型 4 个字节,ff 6c 5a 4s。
小端就是大端反过来,是 4s 5a 6c ff。从这里可以发现大小端只是对内存寻址的顺序有关,但是内存地址里面的 6c,5a 是不会变成 c6 和 a5 的
这块问题,前期不熟悉的话,可以通过问需求来确认如何写。

数据类型后面数字除以 8 就是字节数,数字是 bit。
拿 JS 举个例子:buffer 做视图处理的,比如 Uint32Array,U 代表无符号,。int32 是 4 个字节,Array 可以理解为数组。
字节数组就是多个字节,Python 数据结构就是 bytes 或者 bytearray(这个和前者差别是内存不可变)

Uint64Array 那就是 8 个字节?这个是不合法的。这里有个定长和变长的概念,int 属于定长,int 是不能超过宽度 4 个字节的,所以不会有 Uint64Array,但是可以有 Uint16Array,16/8=2,在内存里面占位 2 个字节。

定长在具体实例内阐述,不定长是比较核心的部分,看下下面 2 个问题:
问题 1:引用类型,比如指针指向一个对象,如果操作这个对象,这个对象宽度是不会改变的。
问题 2:long 类型,网络编程也是为了模拟客户端向服务器进行发包,long 类型也是不定长的,比如不是 8 的倍数,是 9 个字节。
看 9 个字节是怎么写的。

def encode_pack(packet:str or bytes,size:int,endian:str="big"):
    """
    压包 
    :param packet:
    :param size:定长
    :param endian:
    :return:
    """
    return int(packet).to_bytes(length=size, byteorder=endian)

print(encode_pack("1256478912".encode("utf-8"),9))

当然这个模式并不是推荐的,更合适用 fmt 里面去拼接,比如大端 9 个字节- -,直接用>9s 就行了。
struct 和 pickle 本质是一样的,本质一样就好比 Json 和 bjson 和其他 json 一样,方法都是一样的。现在可以把前面的给串起来。

def encode_9_buffer(packet: bytes):
    """
    压入9个字节的缓存区
    :param packet:
    :return:
    """
    return struct.pack(">9s", len(packet).to_bytes(length=9, byteorder="big")) + packet

pg = "hello world".encode()
print(struct.calcsize(">9s"))  #长度9
print(encode_9_buffer(pg)) #b'\x00\x00\x00\x00\x00\x00\x00\x00\x0bhello world'

pickle 和 struct 混合

前面 9s 是一整个包的例子,这里预告下,后面不定长,也是游戏产业最常用的,第 4 篇会提到。当前章节其实可以满足互联网的一些 TCP 前置的网络编程了。
使用 pickle 压入缓存区 4 个字节大端 hello world,然后使用 struct 去解包还原,来验证正反序列化可兼容。
所以 python 写的后端如果是用 pickle 的,用 struct 也是可以的。

def encode_pickle(packet: bytes):
    """
    压入pickle
    :param packet:
    :return:
    """
    return pickle.pack(">i", len(packet)) + packet

def decode_unpack(packet: bytes):
    """
    unpack包 这里不包含struct.unpack
    :param packet: 
    :return: 
    """
    # 因为包头是4个字节,合法性验证是先切出4个字节。
    if len(packet) >= 4:
        return packet[4:].decode()

if __name__ == '__main__':
    pg = "hello world".encode()
    packet = encode_pickle(pg)
    print(decode_unpack(packet))

回顾之前说的知识点。一个数据包由包头和包体组成,包头是 4 个字节(里面存放着包体长度)。
简单判断合法性就是先看是否包完整,包完整由 2 个部分组成,第一个部分是 4 个字节,那么就是先判断大于等于 4 个字节,然后解包先去掉头部 4 个字节,那里面的就是包体内容。
包体内容还原就是 decode(),下面看看如何取出包头里面的包体长度

struct.unpack(">i",packet[:4])

其他内容可以看第四篇文章。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 14 条回复 时间 点赞
仅楼主可见
陈子昂 来自入职半年多游戏测试新人的迷茫 中提及了此贴 02月27日 15:54
仅楼主可见

大佬什么时候出四啊 受益匪浅

膜拜大佬,顺便问下 struct.unpack(">i",packet[:4]) 类似这种 如果服务端返还的内容是不定长的字符串该怎么去解包?比如现在返回的格式是 int str int str ,第一个 str 怎么确定它的结尾?

会有格式的,前面的 int 是 string 长度,比如说是 int 是 2,后面就是跟着 2 个 str

苏立轩 回复

本周会出,稍等。最近实在工作上事情比较多。

这个是一个处理分包的问题,后面会讲的。
TCP 是一个包里面包含包头 + 包体部分。当然这个中间有可能还有别的,假定是目前只有这 2 个,包头 4 个字节,里面存放包体,一个完整的包就是包头 4 个字节加里面存放的长度。
你收包其实是收到多个包,如果把一整段包拆成正确的包,才能进行 unpack(),判断合法就是判断收到的包起码等于一个包头的长度。
struct.unpack(">i",packet[:4]) 首先如果这个是解包头的话,可以不用那么复杂。通过这个直接可以取里面值(包体长度 比如是 57)。然后切出整个包的 total 总收到长度 - 4-57 后面就是第二个包,然后再切 4 个字节,依次往下一定会到 0 的

没问题,有问题随时问,争取本周出个第 4 章

skottZy 回复

嗯嗯 感谢大佬! 一开始是我自己想岔了,解包的时候按照类似 int 这种去解了,然后陷入误区了,后面找服务端对格式 说前面有 2 个字节是表示字符串长度的 。😔

仅楼主可见

{
unsigned int com : 1;
unsigned int com : 31;
}

这种结构体是 4 个字节的,但是我如果用 struct.unpack('>II',XX) 解析 需要 8 个字节,这种情况要怎么搞。

1.一列特别长是把传输的协议变成 Json 了吗,如果用 excel 做,这样是比较长的。
以前考虑过做一些压缩,就是把必填的不传入,根据场景名传入,后面发现很容易忘记和不好维护。excel 表里面可以用 Json 格式化来稍微好读一点。
2.参数还需要某个协议的某个数据取值 这个需要存上下文,上文(上面 case)对象存到 redis 定义 key 为 string,value 就是具体对象,比如 guild_key 公会的 key,是用 {{guild_key}}做模板的形式,遇到解析{{}}之前的就从 redis 那边读取。
3.培训起来是都有成本的,毕竟一开始学代码时解析多层 Json 数据也是难点,怎么填和修改 json 对象才能达成好的效果,建议是多写文档。

苏立轩 回复

其实感觉你已经写出来了啊 unpack 那段。也可能是我没理解你的问题,要不再补充点我再给答复啊

陈子昂 用 Python 写网络编程(四) 中提及了此贴 04月12日 01:14

现在一般都是 protbuf+ 压缩 + 加密,比较少那种纯粹自己定义的了。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册