专栏首页原创分享实现自己的应用层协议和解析器

实现自己的应用层协议和解析器

我们每天都在使用应用层协议,http协议,smtp协议,dns协议等。但是我们有没有想过实现自己的应用层协议呢?有没有想过应用层协议的实现和解析呢?本文就着这些问题,分享一下如何实现定义一个简单的应用层协议,并实现对应的解析器。

1 定义协议

协议,顾名思义,就是一种通信的约定,通信双方都能理解就行,我们可以定义复杂的协议,也可以定义简单的协议。本文在于讲解原理,定义一个几乎最简单的协议。格式如下

开始字符一字节|整个报文长度四字节|序列号四字节|数据部分n字节|结束字符一字节

// 开始标识符
const PACKET_START = 0x3;
// 结尾标识符
const PACKET_END = 0x4;
// 整个数据包长度
const TOTAL_LENGTH = 4;
// 序列号长度
const SEQ_LEN = 4;
// 数据包头部长度
const HEADER_LEN = TOTAL_LENGTH + SEQ_LEN;

2 解析协议

本文是基于tcp的应用层协议,我们知道tcp是面向字节流的协议,他只负责透明传输字节,不负责解释字节,但是我们的数据包是有固定格式的,如果tcp把我们两个包放在一起传输,那我们如何识别呢?所以我们协议格式里,定义了开头和结束字符,一般定义一些特殊字符或者特殊格式为结束字符,这里我们定义的开始和结束字符是0x3和0x4。所以我们要做的事情就是,判断tcp交给我们的字节流,根据开始和结束字符,逐个解析出我们的协议报文。

3 实现

下面开始通过代码实现这个协议的解析。首先实现一个有限状态机。

/**
 * 
 * @param {*} state 状态和处理函数的集合
 * @param {*} initState 初始化状态
 * @param {*} endState 结束状态
 */
function getMachine(state, initState, endState) {
    // 保存初始化状态
    let ret = initState;
    let buffer;
    return function(data) {
        if (ret === endState) {
            return;
        }
        if (data) {
            buffer = buffer ? Buffer.concat([buffer, data]) : data;
        }
        // 还没结束,继续执行
        while(ret !== endState) {
            if (!state[ret]) {
                return;
            }
            /*
                执行状态处理函数,返回[下一个状态, 剩下的数据],
            */
            const result = state[ret](buffer);
            // 如果下一个状态是-1或者返回的数据是空说明需要更多的数据才能继续解析
            if (result[0] === -1) {
                return;
            }
            // 记录下一个状态和数据
            [ret, buffer] = result;
            if (!buffer.length) {
                return;
            }
        }
    }
}

因为解析的过程就是各种状态的转移和处理,所以用有限状态机来实现会清晰很多。上面的代码是一个小型的有限状态机框架。我们通过定义状态和对应的处理函数、开始状态、结束状态。然后得到一个状态机,就可以对输入的数据进行处理了。接下来我们定义一些数据结构。 定义一个表示数据包的类。

// 表示一个协议包
class Packet {
    constructor() {
        this.length = 0;
        this.seq = 0;
        this.data = null;
    }
    set(field, value) {
        this[field] = value;
    }
    get(field) {
        return this[field];
    }
}

定义状态机状态和函数集

// 解析器状态
const PARSE_STATE = {
  PARSE_INIT: 0,
  PARSE_HEADER: 1,
  PARSE_DATA: 2,
  PARSE_END: 3,
};
// 保存当前正在解析的数据包
var packet;
const STATE_TRANSITION = {
    [PARSE_STATE.PARSE_INIT](data) {
        if (!data || !data[0]) {
            return [-1, data];
        }
        if (data[0] !== PACKET_START) {
            return [-1, data ? data.slice(1) : data];
        }
        packet = new Packet();
        // 跳过开始标记符
        return [PARSE_STATE.PARSE_HEADER, data.slice(Buffer.from([PACKET_START]).length)];
    },
    [PARSE_STATE.PARSE_HEADER](data) {
        if (data.length < HEADER_LEN) {
          return [-1, data];
        }
        // 有效数据包的长度 = 整个数据包长度 - 头部长度
        packet.set('length', data.readUInt32BE() - HEADER_LEN);
        // 序列号
        packet.set('seq', data.readUInt32BE(TOTAL_LENGTH));
        // 解析完头部了,跳过去
        data = data.slice(HEADER_LEN);
        return [PARSE_STATE.PARSE_DATA, data];
    },
    [PARSE_STATE.PARSE_DATA](data) {
        const len = packet.get('length');
        if (data.length < len) {
            return [-1, data];
        }
        packet.set('data', data.slice(0, len));
        // 解析完数据了,完成一个包的解析,跳过数据部分和结束符
        data = data.slice(len);
        // 解析完一个数据包,输出
        return [PARSE_STATE.PARSE_END, data];
    },
    [PARSE_STATE.PARSE_END](data) {
        if (!data || !data[0]) {
            return [-1, data];
        }
        if (data[0] !== PACKET_END) {
            return [-1, data ? data.slice(1) : data];
        }
        console.log('parse success: ', packet);
        // 跳过开始标记符
        return [PARSE_STATE.PARSE_INIT, data.slice(Buffer.from([PACKET_START]).length)];
    },
};

这就是所有的代码。最后我们写两个测试用例玩一下。首先写一个服务器。

const net = require('net');
const parse = require('./machine');
net.createServer(function(socket) {
  socket.on('data', function() {
    console.log('receiver: ', ...arguments)
    parse(...arguments);
  });
  socket.on('error', function() {
    console.log(...arguments)
  })
}).listen(10001);

然后写两个测试用例 用例一:正常发送

const net = require('net');

async function test() {
  const socket = net.connect({port: 10001});
  socket.on('error', function() {
    console.log(...arguments);
  });
  let i = 0;
  const a = setInterval(() => {
    socket.write(Buffer.from([0x3,0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x1, i+1, 0x4]));
    i++ > 5 && (socket.end(), clearInterval(a));
  },1000)
}
test()

用例二:延迟发送

const net = require('net');

async function test() {
  const socket = net.connect({port: 10001});
  socket.on('error', function() {
    console.log(...arguments);
  });
  let data = Buffer.from([0x3,0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x1, 0x1, 0x4]);
  let i = 0;
  const id = setInterval(() => {
      if (!data.length) {
        socket.end();
        return clearInterval(id);
      }
    const packet = data.slice(0, 1);
    console.log(packet)
    socket.write(packet);
    data = data.slice(1);
  }, 500);
}
test()

这就完成了一个应用层协议的实现和解析。源码地址(https://github.com/theanarkh/tiny-application-layer-protocol)

本文分享自微信公众号 - 编程杂技(theanarkh),作者:theanarkh

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • uninx域基础数据结构及操作

    unix_datas变量维护个数组,每个元素是unix_proto_data 结构。

    theanarkh
  • nginx0.1.0之event模块初始化源码分析(4)

    event的配置解析相关的代码已经分析完毕。下面分析一下另一个流程中event模块的实现。即在nginx创建进程,并且开始执行进程里的代码的时候。入口函数是ng...

    theanarkh
  • 实现nodejs进程间通信

    对于有继承关系的进程,nodejs本身为我们提供了进程间通信的方式,但是对于没有继承关系的进程,比如兄弟进程,想要通信最简单的方式就是通过主进程中转,类似前端框...

    theanarkh
  • 算法学习笔记(三):冒泡排序和归并排序

    free赖权华
  • 数据科学家常犯的十大编程错误

    数据科学家是“比任何软件工程师都更擅长统计,比任何软件工程师都更擅长软件工程的的统计学家”。许多数据科学家都有统计学背景却缺乏在软件工程方面的经验。我是资深的数...

    AiTechYun
  • 数据科学家易犯的十大编码错误,你中招了吗?

    我是一名高级数据科学家,在 Stackoverflow 的 python 编码中排前 1%,而且还与众多(初级)数据科学家一起工作。下文列出了我常见到的 10 ...

    统计学家
  • 数据科学家易犯的十大编码错误,你中招了吗?

    我是一名高级数据科学家,在 Stackoverflow 的 python 编码中排前 1%,而且还与众多(初级)数据科学家一起工作。下文列出了我常见到的 10 ...

    机器之心
  • 数据科学家常遇到的10个错误

    数据科学家是“在统计方面比任何软件工程师都要出色,在软件工程方面比任何统计学家都出色的人”。许多数据科学家都有统计学背景,但很少有软件工程经验。我是一位高级数据...

    磐创AI
  • 最长公共子串- LCS 算法

    已知字符串str1="网站高并发解决方案",str2="如何解决网站高并发",如何字符串最长公共子串?

    仙士可
  • python实现在线翻译

    砸漏

扫码关注云+社区

领取腾讯云代金券