手把手教你实现自定义的应用层协议 今日观点

2023-06-22 08:35:02 来源:面包芯语

打印 放大 缩小

扫描关注一起学嵌入式,一起学习,一起成长

1.简述


(资料图片仅供参考)

互联网上充斥着各种各样的网络服务,在对外提供网络服务时,服务端和客户端需要遵循同一套数据通讯协议,才能正常的进行通讯;就好像你跟台湾人沟通用闽南语,跟广东人沟通就用粤语一样。

实现自己的应用功能时,已知的知名协议(http,smtp,ftp等)在安全性、可扩展性等方面不能满足需求,从而需要设计并实现自己的应用层协议。

2.1按编码方式

2.2按协议边界

固定边界协议,能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。

模糊边界协议,无法明确得知一个协议报文的长度,这样的协议解析较为复杂,通常需要通过某些特定的字节来界定报文是否结束,比如http协议。

3.协议优劣的基本评判标准

高效的,快速的打包解包减少对cpu的占用,高数据压缩率降低对网络带宽的占用。

简单的,易于人的理解、程序的解析。

易于扩展的, 对可预知的变更,有足够的弹性用于扩展。

向后兼容,对于新协议发出的报文,能使用旧协议进行解析,只是新协议支持的新功能不能使用。

4.自定义应用层协议的优缺点

扩展性更好,可以根据业务需求和发展扩展自己的协议,而已知的知名协议不好扩展。

设计难度高,协议需要易扩展,最好能向后向前兼容。

实现繁琐,需要自己实现序列化和反序列化。

5.动手前的预备知识

5.1大小端

计算机系统在存储数据时起始地址是高地址还是低地址。大端,从高地址开始存储。小端,从低地址开始存储。

图解

判断方法,这里以c/c++语言代码为例,使用了c语言中联合体的特性。

#include #include using namespace std;bool bigCheck(){    union Check    {        char a;        uint32_t data;    };        Check c;    c.data = 1;        if (1 == c.a)    {        return false;    }        return true;}int main(){    if (bigCheck())    {        cout << "big" << endl;    }    else    {        cout << "small" << endl;    }    return 0;}

struct Test{    char a;    char b;    int32_t c;};

5.5 序列化与反序列化

将计算机语言中的内存对象转换为网络字节流,例如把c语言中的结构体Test转化成uint8_t data[6]字节流。

6.一个例子

6.1 协议设计

本协议采用固定边界+混合编码策略。

6.2 协议实现

talk is easy,just code it,使用c/c++语言来实现。

6.2.1 c/c++语言实现

使用结构体 MyProtoHead 来存储协议头

/*    协议头 */struct MyProtoHead{    uint8_t version;    //协议版本号    uint8_t magic;      //协议魔数    uint16_t server;    //协议复用的服务号,标识协议之上的不同服务    uint32_t len;       //协议长度(协议头长度+变长json协议体长度)};

使用开源的Jsoncpp类来存储协议体

协议消息体

/*    协议消息体 */struct MyProtoMsg{    MyProtoHead head;   //协议头    Json::Value body;   //协议体};
/*    MyProto打包类 */class MyProtoEnCode{public:    //协议消息体打包函数    uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);private:    //协议头打包函数    void headEncode(uint8_t * pData, MyProtoMsg * pMsg);};

解包类

typedef enum MyProtoParserStatus{    ON_PARSER_INIT = 0,    ON_PARSER_HAED = 1,    ON_PARSER_BODY = 2,}MyProtoParserStatus;/*    MyProto解包类 */class MyProtoDeCode{public:    void init();    void clear();    bool parser(void * data, size_t len);    bool empty();    MyProtoMsg * front();    void pop();private:    bool parserHead(uint8_t ** curData, uint32_t & curLen,         uint32_t & parserLen, bool & parserBreak);    bool parserBody(uint8_t ** curData, uint32_t & curLen,         uint32_t & parserLen, bool & parserBreak);    private:    MyProtoMsg mCurMsg;                     //当前解析中的协议消息体    queuemMsgQ;              //解析好的协议消息队列    vectormCurReserved;           //未解析的网络字节流    MyProtoParserStatus mCurParserStatus;   //当前解析状态};

void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg){    //设置协议头版本号为1    *pData = 1;     ++pData;    //设置协议头魔数    *pData = MY_PROTO_MAGIC;    ++pData;    //设置协议服务号,把head.server本地字节序转换为网络字节序    *(uint16_t *)pData = htons(pMsg->head.server);    pData += 2;    //设置协议总长度,把head.len本地字节序转换为网络字节序    *(uint32_t *)pData = htonl(pMsg->head.len);}uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len){    uint8_t * pData = NULL;    Json::FastWriter fWriter;        //协议json体序列化    string bodyStr = fWriter.write(pMsg->body);    //计算协议消息序列化后的总长度    len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();    pMsg->head.len = len;    //申请协议消息序列化需要的空间    pData = new uint8_t[len];    //打包协议头    headEncode(pData, pMsg);    //打包协议体    memcpy(pData + MY_PROTO_HEAD_SIZE, bodyStr.data(), bodyStr.size());        return pData;}

6.2.3 解包(反序列化)

bool MyProtoDeCode::parserHead(uint8_t ** curData, uint32_t & curLen,     uint32_t & parserLen, bool & parserBreak){    parserBreak = false;    if (curLen < MY_PROTO_HEAD_SIZE)    {        parserBreak = true; //终止解析        return true;    }    uint8_t * pData = *curData;    //解析版本号    mCurMsg.head.version = *pData;    pData++;    //解析魔数    mCurMsg.head.magic = *pData;    pData++;    //魔数不一致,则返回解析失败    if (MY_PROTO_MAGIC != mCurMsg.head.magic)    {        return false;    }    //解析服务号    mCurMsg.head.server = ntohs(*(uint16_t*)pData);    pData+=2;    //解析协议消息体的长度    mCurMsg.head.len = ntohl(*(uint32_t*)pData);    //异常大包,则返回解析失败    if (mCurMsg.head.len > MY_PROTO_MAX_SIZE)    {        return false;    }        //解析指针向前移动MY_PROTO_HEAD_SIZE字节    (*curData) += MY_PROTO_HEAD_SIZE;    curLen -= MY_PROTO_HEAD_SIZE;    parserLen += MY_PROTO_HEAD_SIZE;    mCurParserStatus = ON_PARSER_HAED;    return true;}bool MyProtoDeCode::parserBody(uint8_t ** curData, uint32_t & curLen,     uint32_t & parserLen, bool & parserBreak){    parserBreak = false;    uint32_t jsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE;    if (curLen < jsonSize)    {        parserBreak = true; //终止解析        return true;    }    Json::Reader reader;    //json解析类    if (!reader.parse((char *)(*curData),         (char *)((*curData) + jsonSize), mCurMsg.body, false))    {        return false;    }    //解析指针向前移动jsonSize字节    (*curData) += jsonSize;    curLen -= jsonSize;    parserLen += jsonSize;    mCurParserStatus = ON_PARSER_BODY;    return true;}bool MyProtoDeCode::parser(void * data, size_t len){    if (len <= 0)    {        return false;    }    uint32_t curLen = 0;    uint32_t parserLen = 0;    uint8_t * curData = NULL;        curData = (uint8_t *)data;    //把当前要解析的网络字节流写入未解析完字节流之后    while (len--)    {        mCurReserved.push_back(*curData);        ++curData;    }    curLen = mCurReserved.size();    curData = (uint8_t *)&mCurReserved[0];    //只要还有未解析的网络字节流,就持续解析    while (curLen > 0)    {        bool parserBreak = false;        //解析协议头        if (ON_PARSER_INIT == mCurParserStatus ||            ON_PARSER_BODY == mCurParserStatus)        {            if (!parserHead(&curData, curLen, parserLen, parserBreak))            {                return false;            }            if (parserBreak) break;        }        //解析完协议头,解析协议体        if (ON_PARSER_HAED == mCurParserStatus)        {            if (!parserBody(&curData, curLen, parserLen, parserBreak))            {                return false;            }            if (parserBreak) break;        }        if (ON_PARSER_BODY == mCurParserStatus)        {            //拷贝解析完的消息体放入队列中            MyProtoMsg * pMsg = NULL;            pMsg = new MyProtoMsg;            *pMsg = mCurMsg;            mMsgQ.push(pMsg);        }    }    if (parserLen > 0)    {        //删除已经被解析的网络字节流        mCurReserved.erase(mCurReserved.begin(), mCurReserved.begin() + parserLen);    }    return true;}

7. 完整源码与测试

code is easy,just run it.

7.1 源码

#include #include #include #include #include #include #include #include using namespace std;const uint8_t MY_PROTO_MAGIC = 88;const uint32_t MY_PROTO_MAX_SIZE = 10 * 1024 * 1024; //10Mconst uint32_t MY_PROTO_HEAD_SIZE = 8;typedef enum MyProtoParserStatus{    ON_PARSER_INIT = 0,    ON_PARSER_HAED = 1,    ON_PARSER_BODY = 2,}MyProtoParserStatus;/*    协议头 */struct MyProtoHead{    uint8_t version;    //协议版本号    uint8_t magic;      //协议魔数    uint16_t server;    //协议复用的服务号,标识协议之上的不同服务    uint32_t len;       //协议长度(协议头长度+变长json协议体长度)};/*    协议消息体 */struct MyProtoMsg{    MyProtoHead head;   //协议头    Json::Value body;   //协议体};void myProtoMsgPrint(MyProtoMsg & msg){    string jsonStr = "";    Json::FastWriter fWriter;    jsonStr = fWriter.write(msg.body);        printf("Head[version=%d,magic=%d,server=%d,len=%d]\n"        "Body:%s", msg.head.version, msg.head.magic,         msg.head.server, msg.head.len, jsonStr.c_str());}/*    MyProto打包类 */class MyProtoEnCode{public:    //协议消息体打包函数    uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);private:    //协议头打包函数    void headEncode(uint8_t * pData, MyProtoMsg * pMsg);};void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg){    //设置协议头版本号为1    *pData = 1;     ++pData;    //设置协议头魔数    *pData = MY_PROTO_MAGIC;    ++pData;    //设置协议服务号,把head.server本地字节序转换为网络字节序    *(uint16_t *)pData = htons(pMsg->head.server);    pData += 2;    //设置协议总长度,把head.len本地字节序转换为网络字节序    *(uint32_t *)pData = htonl(pMsg->head.len);}uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len){    uint8_t * pData = NULL;    Json::FastWriter fWriter;        //协议json体序列化    string bodyStr = fWriter.write(pMsg->body);    //计算协议消息序列化后的总长度    len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();    pMsg->head.len = len;    //申请协议消息序列化需要的空间    pData = new uint8_t[len];    //打包协议头    headEncode(pData, pMsg);    //打包协议体    memcpy(pData + MY_PROTO_HEAD_SIZE, bodyStr.data(), bodyStr.size());        return pData;}/*    MyProto解包类 */class MyProtoDeCode{public:    void init();    void clear();    bool parser(void * data, size_t len);    bool empty();    MyProtoMsg * front();    void pop();private:    bool parserHead(uint8_t ** curData, uint32_t & curLen,         uint32_t & parserLen, bool & parserBreak);    bool parserBody(uint8_t ** curData, uint32_t & curLen,         uint32_t & parserLen, bool & parserBreak);    private:    MyProtoMsg mCurMsg;                     //当前解析中的协议消息体    queuemMsgQ;              //解析好的协议消息队列    vectormCurReserved;           //未解析的网络字节流    MyProtoParserStatus mCurParserStatus;   //当前解析状态};void MyProtoDeCode::init(){    mCurParserStatus = ON_PARSER_INIT;}void MyProtoDeCode::clear(){    MyProtoMsg * pMsg = NULL;        while (!mMsgQ.empty())    {        pMsg = mMsgQ.front();        delete pMsg;        mMsgQ.pop();    }}bool MyProtoDeCode::parserHead(uint8_t ** curData, uint32_t & curLen,     uint32_t & parserLen, bool & parserBreak){    parserBreak = false;    if (curLen < MY_PROTO_HEAD_SIZE)    {        parserBreak = true; //终止解析        return true;    }    uint8_t * pData = *curData;    //解析版本号    mCurMsg.head.version = *pData;    pData++;    //解析魔数    mCurMsg.head.magic = *pData;    pData++;    //魔数不一致,则返回解析失败    if (MY_PROTO_MAGIC != mCurMsg.head.magic)    {        return false;    }    //解析服务号    mCurMsg.head.server = ntohs(*(uint16_t*)pData);    pData+=2;    //解析协议消息体的长度    mCurMsg.head.len = ntohl(*(uint32_t*)pData);    //异常大包,则返回解析失败    if (mCurMsg.head.len > MY_PROTO_MAX_SIZE)    {        return false;    }        //解析指针向前移动MY_PROTO_HEAD_SIZE字节    (*curData) += MY_PROTO_HEAD_SIZE;    curLen -= MY_PROTO_HEAD_SIZE;    parserLen += MY_PROTO_HEAD_SIZE;    mCurParserStatus = ON_PARSER_HAED;    return true;}bool MyProtoDeCode::parserBody(uint8_t ** curData, uint32_t & curLen,     uint32_t & parserLen, bool & parserBreak){    parserBreak = false;    uint32_t jsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE;    if (curLen < jsonSize)    {        parserBreak = true; //终止解析        return true;    }    Json::Reader reader;    //json解析类    if (!reader.parse((char *)(*curData),         (char *)((*curData) + jsonSize), mCurMsg.body, false))    {        return false;    }    //解析指针向前移动jsonSize字节    (*curData) += jsonSize;    curLen -= jsonSize;    parserLen += jsonSize;    mCurParserStatus = ON_PARSER_BODY;    return true;}bool MyProtoDeCode::parser(void * data, size_t len){    if (len <= 0)    {        return false;    }    uint32_t curLen = 0;    uint32_t parserLen = 0;    uint8_t * curData = NULL;        curData = (uint8_t *)data;    //把当前要解析的网络字节流写入未解析完字节流之后    while (len--)    {        mCurReserved.push_back(*curData);        ++curData;    }    curLen = mCurReserved.size();    curData = (uint8_t *)&mCurReserved[0];    //只要还有未解析的网络字节流,就持续解析    while (curLen > 0)    {        bool parserBreak = false;        //解析协议头        if (ON_PARSER_INIT == mCurParserStatus ||            ON_PARSER_BODY == mCurParserStatus)        {            if (!parserHead(&curData, curLen, parserLen, parserBreak))            {                return false;            }            if (parserBreak) break;        }        //解析完协议头,解析协议体        if (ON_PARSER_HAED == mCurParserStatus)        {            if (!parserBody(&curData, curLen, parserLen, parserBreak))            {                return false;            }            if (parserBreak) break;        }        if (ON_PARSER_BODY == mCurParserStatus)        {            //拷贝解析完的消息体放入队列中            MyProtoMsg * pMsg = NULL;            pMsg = new MyProtoMsg;            *pMsg = mCurMsg;            mMsgQ.push(pMsg);        }    }    if (parserLen > 0)    {        //删除已经被解析的网络字节流        mCurReserved.erase(mCurReserved.begin(), mCurReserved.begin() + parserLen);    }    return true;}bool MyProtoDeCode::empty(){    return mMsgQ.empty();}MyProtoMsg * MyProtoDeCode::front(){    MyProtoMsg * pMsg = NULL;    pMsg = mMsgQ.front();    return pMsg;}void MyProtoDeCode::pop(){    mMsgQ.pop();}int main(){    uint32_t len = 0;    uint8_t * pData = NULL;    MyProtoMsg msg1;    MyProtoMsg msg2;    MyProtoDeCode myDecode;    MyProtoEnCode myEncode;    msg1.head.server = 1;    msg1.body["op"] = "set";    msg1.body["key"] = "id";    msg1.body["value"] = "9856";    msg2.head.server = 2;    msg2.body["op"] = "get";    msg2.body["key"] = "id";    myDecode.init();    pData = myEncode.encode(&msg1, len);    if (!myDecode.parser(pData, len))    {        cout << "parser falied!" << endl;    }    else    {        cout << "msg1 parser successful!" << endl;    }    pData = myEncode.encode(&msg2, len);    if (!myDecode.parser(pData, len))    {        cout << "parser falied!" << endl;    }    else    {        cout << "msg2 parser successful!" << endl;    }    MyProtoMsg * pMsg = NULL;    while (!myDecode.empty())    {        pMsg = myDecode.front();        myProtoMsgPrint(*pMsg);        myDecode.pop();    }        return 0;}

8.总结

原文:https://my.oschina.net/u/2245781/blog/1622414

觉得文章不错,点击“分享”、“赞”、“在看” 呗!

关键词:

责任编辑:ERM523

相关阅读

精彩推送

手把手教你实现自定义的应用层协议 今日观点 财政收入30强城市排行榜:苏杭宁甬惊艳亮相 当前头条
悦康药业:融资余额8950.47万元,创近一年新低(06-21)-焦点讯息 现在还有星空台吗_为什么现在没有星空台|全球头条
天天新动态:孔雀蓝色值cmyk(1寸照片蓝色背景的CMYK值是多少) 三聚氰胺板和密度板哪个好 三聚氰胺板 新动态
凯尔达:融资净偿还201.76万元,融资余额5316.15万元(06-21) A股人工智能主线高位“熄火” 新能源车板块逆势走强
冷链基地抢占“鲜”机 世界看点 多家猪企探索全产业链布局 能否对冲猪价走低风险? 环球观察
调研:连锁经济型酒店卫生管理不到位是共性问题 对扫码消费“强制关注”勇敢说不
新品量产渐行渐近 “果链”公司招兵买马 新视野 世界热讯:证监会公布最新一期证券公司“白名单”
“油电同价”概念火热 业内称或有炒作之嫌 预计1亿人次出游 燃情“端午经济”-全球热讯
我国自主培育种鸡首次实现出口 天天快播报 世界今热点:前5月新能源汽车出口45.7万辆
我市大力推进普通公路隐患排查整治工作 数码视讯06月21日被深股通减持331.5万股_世界热消息
全球快消息!延链补链强链,中牟汽车产业链展览会开幕 长盈通:6月21日融券净卖出1.06万股,连续3日累计净卖出2.01万股|环球消息
端午节当天,河北省血液中心所有献血点停采一天 每日视讯:我的世界怎么联机 我的世界怎么联机两个人一起玩
如何管理班级纪律呢_如何管理班级纪律20条 世界视点 国家粮食局 国家粮食局网上直报系统
幸福大街嫁衣照片_幸福大街嫁衣|天天聚看点 宁夏银川一烧烤店发生燃气爆炸,现场搜救出38人
当前滚动:周启豪夺冠后排名大涨,国际乒联第25周世界排名公布 成都猎人队退出OWL,联盟本身也已陷入困境-全球即时看
热点在线丨市盈率ttm和lyr(市盈率ttm什么意思) 高尔夫广东巡回赛广州站出现三人加洞,范诗宇捧杯,女将苏文露屈居亚军_天天快资讯
在这个“神秘”组织里,10多位专家正为苹果等大厂制定气候行动时间表 环球聚看点 新股申购时间几点到几点
凯尔特人奇才快船三方交易达成!波尔津吉斯去绿军 快船得到最佳第六人 牡丹江市人才工作网(牡丹江市民心网首页)
请关闭拦截弹出窗口工具怎么操作?_请关闭拦截弹出窗口工具支付页面在弹出窗口中 每日焦点 每日聚焦:四川富豪王仁果以“新身份”开卖千元级白酒背后:其本人、旗下地产公司诉讼缠身
世界热点评!7月份企业即享研发费用加计扣除红利,或达数千亿 低营收、无利润、无产品,智翔金泰上市即破发
新式茶饮融资“久旱逢甘霖”,头部品牌开店、布局供应链“两手抓” 百万医疗险痛点待解,0免赔额会是下一个风口吗?_天天微资讯
200万粉丝大V称车“烂”、“差”,还说买车用户“有病”!知名车企“怒怼”:永久删除并道歉 618复盘|销量大增的这些快消企业靠什么|今日热门
昆明市西山区的邮编_昆明市西山区邮编 德赛电池(000049):6月21日北向资金减持17.33万股_观察
最资讯丨深圳能源(000027):6月21日北向资金增持251.48万股 全球报道:旅游英语怎么样 旅游英语就业前景
今日讯!华数传媒(000156):6月21日北向资金增持6.9万股 深化合作繁荣消费市场 2023年萍“湘”美食文化节开幕 今日最新
天天新消息丨美国经济措施向富人倾斜 贫富鸿沟越来越深 天天即时看!白敬亭宋轶主演,古装剧《长风渡》平台热度破万
f1排位赛规则_lol排位赛规则 焦点!奥秘杂志电子版全集(奥秘杂志)
观点:海贼王香克斯结局到底怎么样了_海贼王里的香克斯有什么能力 焦点报道:燕窝等级排行_燕窝等级
多地新增地方政府债务预算调整 呈现差异化、精准化特征 每日速讯 形容冬天温暖的成语
12306列车运行图调整要多久_列车运行图调整要多久 迎宾曲进行曲 开幕式音乐喜洋洋_迎宾曲进行曲 环球即时
iphonexr换电池(iphonex停产)-环球关注 世界观天下!海底两万理阿龙纳斯_什么的阿龙纳斯 什么的尼摩 什么的尼德兰 什么的康赛尔
讯息:全面质量管理概念与内涵(全面质量管理概念) OPPORenoFlip5G曝光采用内折方案
信息:喷码器如何使用(喷码器) 全球微动态丨新课标中考活题训练·数学_关于新课标中考活题训练·数学简介
actionscript30下载安装_actionscript3 0|全球快看点 世界即时看!洛克人x8完整攻略(洛克人x8完美存档)
怎样查询考研参考书目数_怎样查询考研参考书目 天天要闻 焦点消息!宜春明月山温泉
股票行情快报:通程控股(000419)6月21日主力资金净卖出120.19万元 corum软件下载_corum_资讯推荐
天天热门:让学生拥有成才的N种可能!南开大学大力推进“招生-培养-就业一体化”改革 枸杞人参泡酒配方_人参泡酒补肾壮阳配方|环球资讯
突尼斯一辆列车出轨30余人死伤 柠檬糖_关于柠檬糖概略|视讯
天空体育:错误记录不败赛季赛果,阿森纳下架新赛季正品主场球衣 全球球精选!百分数的认识练习题(百分数的认识练习题)
chmod文件夹下文件权限_chmod文件夹-全球新动态 环球观热点:cdc是什么缩写说唱_cdc是什么缩写
税率错误跨月如何申报_发票税率开错了还跨月怎么办 每日报道 【天天聚看点】粽子迎来销售小高峰 海东20余个品类粽子未发现违规
临颍县繁城镇:专项检查端午食品,为群众饮食筑牢"防护盾"! 跑腿哪个城市赚钱(PTN光传输技术)
归真堂活熊取胆事件辩论(归真堂活熊取胆事件)_世界快播 忍不住心动_忍不住 天天简讯
贞观之治的泡沫“谷贱伤农”问题为何被有意掩盖? 尚书网注册_尚书网|新资讯
万花筒怎么换东西_开通万花筒后怎么把万花筒去掉而只留下10元GPRS专享会员_热点在线 本周盘点(6.12-6.16):上能电气周涨7.60%,主力资金合计净流出1873.94万元
天天观察:“药茅”片仔癀多事之秋,分拆上市关键时刻,化妆品公司原副董事长被双开 Keep通过港交所聆讯:2023年Q1营收4.47亿元,连续3年增长
贾跃亭发长文致歉!回应FF 91推迟交付等质疑 端午节书单 | 粽子香、龙舟快,端午好书趁现在!
事假工资扣除标准按小时算 请事假工资扣除标准 天天动态 星巴克被约谈! 焦点速递
女人网名陌上花开什么意思_陌上花开什么意思 世界动态 长龄液压:公司的减速器目前主要用于光伏跟踪支架和工程机械 世界微速讯
深圳三九药业有限公司(关于深圳三九药业有限公司介绍)|每日观察 直径多大管子用氩弧焊和氩电联焊_氩弧焊与氩电联焊区别 环球时讯