c语言自定义tcp协议实现socket通信

2023-05-16

    一般的tcp协议示例,大家给出的demo都是类似一个helloworld的示例,简单罗列了socket建立,创建连接,发送数据,关闭连接的过程,实际上tcp通信确实也就这么多内容。但是,在实际的开发中,我们用tcp通信,肯定不会只是发送一句简单的“你好”。

    实际应用中,我们需要自定义一个协议,也就是protocol,然后与服务端约定网络字节序,最后双方都能根据协议实现数据编码与解码即可。

    自定义协议,没有固定的格式,没有严格的数据类型限制,只要双方都认可就行了。因为通信的双方都需要编解码,不存在只有一方需要编码或者解码,或者说我用协议发数据,你回复数据就用一个"ok"或者200就搞定了,既然是协议,也就是双方都要遵守,tcp是双向通信,两边都需要编解码。

    这里给出一个简单的demo,说明tcp通信如何通过自定义协议来实现。

    自定义协议一般重点在于发送方,因为这是数据源头,所以这里只给出一个client,服务端我用一个netcat的工具来模拟,这里主要看数据的定义,以及最终形成的网络字节序。

    c语言默认采用的是小端序表示的网络字节序。这个会跟解码有关,一般而言,我们数据类型都遵守大端序,尤其是在java语言中,字节序默认就是大端序,这个顺序跟我们的认知是相符合的,也就是高位在前,低位在后,比如0x0001,大端序就表示的是1,这也符合我们的认知习惯,尤其是对于十进制非常熟悉的人来说。

    而小端序恰好相反,比如0x0001,实际上他表示却是0x0100,高位在后,低位在前,不符合我们的认知习惯。

    当然,虽然说大端序,小端序是相反的,但是并不是说我们在表示的时候就是相反的,刚才举的示例都是在转换之后的表现,而且只有数字类型才会表现这种情况,我们通常表示数字都不会直接使用这种十六进制来表示,但是在协议的表示中,为了与位数对齐,会使用0x0001来表示short类型的1。

    定义协议格式如下所示:

字段数据类型备注
flagshort标识
versionshort版本
typeshort类型
reservedshort保留位
lengthint数据长度
payloadchar[]

数据体

checksumchar校验位

 

 

 

    

    

 

 

 

 

    前面说过,协议没有固定的格式,也没有严格的数据类型限制,根据需要定义。但是我是基于以下几点来做的考虑:

    1、协议最好有个明显的起始标志

    2、一般有个操作类型,就是表示这条报文是干什么的,不可能什么东西都在数据体中描述。

    3、报文定义可能为了将来的考虑,设置一个保留位,以免考虑不周,将来需要新增字段整个协议就废了。

    4、报文中一个长度来表示数据体的长度,这个可以使用定长,但是定长多长合适也是一个问题,将来可能有更大的数据进来也说不定,设置太大了,很多时候都是默认值,增加了网络传输的压力,设置太小了,将来扩展没法玩。

    5、数据体是真实的报文内容,加密或者不加密都可以,使用json还是其他格式也可以。

    6、校验位,用来校验报文的正确性,一般有意义,但是如果所有报文都有问题,那只能说传输太不可靠了,网络有问题或者程序哪里有bug。虽说是校验位,但是不做强制要求,本示例就没有赋值,默认0x00。

    因为在c语言中定义结构体,存在一个位对齐的问题,所以本协议前面的几个字段都使用short,统一起来,最后一位使用char。

    上面说了这么多,都是个人在实际中的总结,下面直接show me the code:

  main.cpp

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef struct _pktdata{
    uint16_t flag;
    uint16_t version;
    uint16_t type;
    uint16_t reserved;
    uint32_t length;
    uint16_t payload[1024];
    uint8_t checksum;
}pktdata;
void build(pktdata *data){
    data->flag = (0x5aa5);
    data->version = (0x0001);
    data->type = (0x0001);
    data->reserved = (0xffff);
    const char* payload = "{\"name\":\"buejee\",\"age\":18}";
    memcpy(data->payload,payload,strlen(payload));
    data->length = strlen(payload);
    data->checksum = (0x00);
}
int main()
{
    int sockfd,ret;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd){
        printf("create socket error : (%d)\n",errno);
        return 0;
    }
    printf("socket start.\n");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(6666);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
    if(-1 == ret){
        printf("connect error : (%d)\n",errno);
        return 0;
    }
    for(int i=0;i<10;i++){
        char sendData[1024];
        pktdata pd;
        build(&pd);
        int len = pd.length+12;
        memcpy(sendData,(pktdata *)&pd,len);
        send(sockfd,sendData,len+1,0);
        sleep(3);
    }
    close(sockfd);
    printf("done.\n");
    return 0;
}

    这段代码是在linux或者mac下运行的,扩展名是cpp,其实都是c的东西,这个不重要。因为平台之间的差别太大,windows下很多api都发生了改变,包括引用的头文件都有很大差别。这里千万不要在windows下去试。

    这段代码只是表示了client如何自定义协议发送给服务端的,所以我们要运行,需要先开启一个服务端。

    我这里在mac下,可以使用netcat这个指令,也是brew install netcat安装的,linux下面也有这个指令。

    启动一个tcp server并开启输出,这里使用输出可以很明显的看到最终接收的十六进制数。

netcat -lp 6666 -o bin.out

    接着在ide中运行上面的程序,其实这个单文件程序可以直接g++编译,然后运行可执行程序。

    我在程序中设置默认发送10次消息,然后停止。所以我们看服务端的输出文件:

Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.         
Received 39 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00                               e":18}.       

    这里模拟了一个真实的自定义协议发送数据,并最终接收到数据。

    我们在实际的开发中,看到的tcp协议报文数据大部分都是这个样子的,涉及到很多字节转数字,字节转字符串,字节转十六进制。 

    需要注意的是小端序,大端序。上面的程序默认传输的网络字节序数字类型就是小端序。比如flag=(0x5aa5),最终传输变成了A55A,version=(0x0001),传过来就是0100,这里特别要注意的是length这个字段,1A 00 00 00,这个很明显就是小端序,十进制的26在正常的字节序中,肯定是00 00 00 1A。这个数字还关系到后面取数据体的内容,所以如果端序弄反了,取到的就是一个天文数字,最终影响到协议解析。

    这里看到的服务端接收的报文里面正好是10条分开的报文,是因为我在send调用之后,sleep让程序睡眠了3秒,所以接收端数据是断断续续的,没有连在一起,如果不睡眠,客户端一下子发送完成,服务端接收的就是10条全部合在一起的报文。

Received 390 bytes from the socket
00000000  A5 5A 01 00  01 00 FF FF  1A 00 00 00  7B 22 6E 61  .Z..........{"na
00000010  6D 65 22 3A  22 62 75 65  6A 65 65 22  2C 22 61 67  me":"buejee","ag
00000020  65 22 3A 31  38 7D 00 A5  5A 01 00 01  00 FF FF 1A  e":18}..Z.......
00000030  00 00 00 7B  22 6E 61 6D  65 22 3A 22  62 75 65 6A  ...{"name":"buej
00000040  65 65 22 2C  22 61 67 65  22 3A 31 38  7D 00 A5 5A  ee","age":18}..Z
00000050  01 00 01 00  FF FF 1A 00  00 00 7B 22  6E 61 6D 65  ..........{"name
00000060  22 3A 22 62  75 65 6A 65  65 22 2C 22  61 67 65 22  ":"buejee","age"
00000070  3A 31 38 7D  00 A5 5A 01  00 01 00 FF  FF 1A 00 00  :18}..Z.........
00000080  00 7B 22 6E  61 6D 65 22  3A 22 62 75  65 6A 65 65  .{"name":"buejee
00000090  22 2C 22 61  67 65 22 3A  31 38 7D 00  A5 5A 01 00  ","age":18}..Z..
000000A0  01 00 FF FF  1A 00 00 00  7B 22 6E 61  6D 65 22 3A  ........{"name":
000000B0  22 62 75 65  6A 65 65 22  2C 22 61 67  65 22 3A 31  "buejee","age":1
000000C0  38 7D 00 A5  5A 01 00 01  00 FF FF 1A  00 00 00 7B  8}..Z..........{
000000D0  22 6E 61 6D  65 22 3A 22  62 75 65 6A  65 65 22 2C  "name":"buejee",
000000E0  22 61 67 65  22 3A 31 38  7D 00 A5 5A  01 00 01 00  "age":18}..Z....
000000F0  FF FF 1A 00  00 00 7B 22  6E 61 6D 65  22 3A 22 62  ......{"name":"b
00000100  75 65 6A 65  65 22 2C 22  61 67 65 22  3A 31 38 7D  uejee","age":18}
00000110  00 A5 5A 01  00 01 00 FF  FF 1A 00 00  00 7B 22 6E  ..Z..........{"n
00000120  61 6D 65 22  3A 22 62 75  65 6A 65 65  22 2C 22 61  ame":"buejee","a
00000130  67 65 22 3A  31 38 7D 00  A5 5A 01 00  01 00 FF FF  ge":18}..Z......
00000140  1A 00 00 00  7B 22 6E 61  6D 65 22 3A  22 62 75 65  ....{"name":"bue
00000150  6A 65 65 22  2C 22 61 67  65 22 3A 31  38 7D 00 A5  jee","age":18}..
00000160  5A 01 00 01  00 FF FF 1A  00 00 00 7B  22 6E 61 6D  Z..........{"nam
00000170  65 22 3A 22  62 75 65 6A  65 65 22 2C  22 61 67 65  e":"buejee","age
00000180  22 3A 31 38  7D 00                                  ":18}.          

    这里也引出了一个问题,就是数据传输 接收端存在的拆包问题。简单粗暴的解决办法就是间隔时间段发送,但是实际中这个办法不管用,因为会有很多客户端不同时间发送大量的数据,所以间隔发送不能从根本上解决拆包问题。 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c语言自定义tcp协议实现socket通信 的相关文章

随机推荐

  • c/c++中的回调函数

    c c 43 43 中的回调函数是一个很奇怪的东西 xff0c 在java中 xff0c 方法调用的时候 xff0c 参数最多可以传入另一个对象实例 xff0c 然后在方法体内 xff0c 调用实例的方法 xff0c 做不到方法调用的时候
  • 在Windows下配置多个git账号

    本文记录在Windows下配置两个github账号的过程 1 生成并部署SSH key 安装好Git客户端后 xff0c 打开git bash xff0c 输入以下命令生成user1的SSH Key xff1a ssh keygen t r
  • windows命令行下通过cl命令编译动态链接库示例

    一般在windows下写一个c c 43 43 的动态链接库 xff0c 我们都是在visual studio或着visual c 43 43 这些ide里面进行编译和生成的 xff0c 今天介绍 xff0c 如何通过命令行来实现手动编译和
  • gdb命令调试c程序

    一般开发c语言程序 xff0c 都是在ide中编码 xff0c 调试也是使用集成环境 xff0c 有时候 xff0c 我们的程序是在文本编辑器中编写的 xff0c 这时候可能使用gcc编译 xff0c 然后运行可执行程序 遇到需要调试的场景
  • 选择排序算法与示例详解(c语言)

    选择排序是排序算法的一种 xff0c 思想就是 xff0c 每一轮寻找数组中最大的值或者最小的值 xff0c 放在头部或者放入一个新的数组 这样经历一轮遍历 xff0c 数组或者新数组就是排好序的 xff0c 他的目的很明确 xff0c 每
  • 2020年csdn盘点

    十年前就注册了csdn账号 xff0c 之后一直没有写过博客 xff0c 都是看别人的博客 xff0c 等到2015年左右发表了第一篇自己的博客 xff0c 直到2016年底觉着做技术的就需要记录自己的博客 xff0c 不仅是自己学习的过程
  • chrome浏览器安装react-devtools

    react devtools是react开发时的一个浏览器插件 xff0c 对于各大主流高级浏览器都有扩展程序可以安装 xff0c 官方的地址默认是https github com facebook react devtools xff0c
  • react+typescript项目构建

    react项目构建可以很简单 xff0c 但是如果是结合typescript xff0c 其实也不是很麻烦 xff0c 官网也有很明确的说明 有两种办法 xff1a 1 直接构建带有typescript的react项目 xff0c 我们需要
  • react项目启动报错:Uncaught TypeError: Cannot read property ‘forEach‘ of undefined

    如题 xff0c react项目启动报错 xff0c 具体信息 xff0c 如下所示 xff1a 这个问题是因为浏览器安装了react devtools扩展程序导致的 xff0c 很多人的解决办法就是直接禁用react devtools x
  • react组件之间传值

    看过一些文章介绍react组件之间传值 xff0c 无外乎以下几种情况 xff1a 父子组件之间相互传值 xff0c 兄弟节点之间传值 最常见的就是父子组件 xff0c 做法也很简单 xff1a 就是在父组件中直接通过props属性的方式将
  • 利用mocha进行以太坊智能合约编译部署测试

    使用智能合约编程语言solidity编写的智能合约 xff0c 除了可以直接通过以太坊的工具链truffle ganache cli进行测试之外 xff0c 还可以结合mocha进行单元测试 mocha单元测试本质上 xff0c 还是需要对
  • electron报错:Uncaught Error: A dynamic link library (DLL) initialization routine failed

    如题所示 xff0c 我们在进行node 43 electron开发桌面应用的时候 xff0c 经常会遇到这样的问题 xff1a 根据提示是因为ref依赖模块没有合适的编译版本 xff0c 但是我们进行npm install的时候没有报错啊
  • C语言 利用冒泡排序法对10个字符由小到大排序

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • truffle+ganache-cli构建简单以太坊智能合约并编译部署

    以前接触过以太坊工具链来做区块链练手项目 xff0c 后来荒废了 xff0c 如今再次捡起来 xff0c 算是回忆和加深 之前可能因为网络的原因 xff0c 在truffle各种初始化编译合成智能合约都遇到了好多问题 xff0c 如今再次尝
  • docker-compose搭建mongo+elasticsearch+graylog运行环境

    graylog是一个小型的开源日志收集 分析 展示框架 主要graylog运行需要mongo elasticsearch xff0c 所以他们就组成了一个小的整体 一般在虚拟机上搭建 xff0c 可以分开安装部署 xff0c 但是既然是一个
  • word文档中插入图片显示不全解决办法

    在windows下写word文档 xff0c 正常情况下 xff0c 我们应该不会遇到插入图片显示不全的问题 xff0c 好像是如果在已有的文档中插入图片 xff0c 比如文档中间插入 xff0c 图片没办法自动扩展空间 xff0c 这就导
  • quasar构建linux版本electron项目以及如何让electron程序在linux下运行

    quasar构建 xff0c 默认命令是 xff1a quasar build m electron 如果要支持linux xff0c 通常是直接加参数 target linux或者简写 T linux quasar build m ele
  • docker容器改变时区

    默认情况下的docker容器启动之后 xff0c 系统时间是UTC时间 xff0c 这导致和我们的北京时间相差8小时 xff0c 会引出很多问题 有一些容器是和项目一起构建的 xff0c 我们可以在构建的时候 xff0c 将系统时区修改为A
  • elasticsearch加入中文分词器elasticsearch-analysis-ik插件

    前言 elasticsearch作为一个分布式弹性存储与检索系统 xff0c 默认是不支持中文分词的 xff0c 但是呢 xff0c 这个工作有人做 xff0c 估计都是中国人做的吧 什么是中文分词呢 xff0c 简单来说 xff0c 就是
  • c语言自定义tcp协议实现socket通信

    一般的tcp协议示例 xff0c 大家给出的demo都是类似一个helloworld的示例 xff0c 简单罗列了socket建立 xff0c 创建连接 xff0c 发送数据 xff0c 关闭连接的过程 xff0c 实际上tcp通信确实也就