序列化与反序列化

1.结构化数据传输

通信双方在进行网络通信的时候:

  • 如果要传输的数据是一个字符串 那么通信双方直接发送即可

  • 如果要传输的数据是一些结构体 此时就不能将这些数码一个个发送到网络中

    如果要实现一个网络版本的计算器,那么客户端每次发送的请求就需要包括左操作数, 右操作数, 和对应的操作 。那么此时客户端要发送的就不是一个简单的字符串 ,而是一个结构体。

    如果客户端将这些结构化的数据单独一个个发送到网络中, 那么服务端也只能一个个的接受, 但是这样子传输容易导致数据错乱。

    所以客户端往往会将这些结构化的数据统一打包发送到网络中 ,此时服务端接受的就是一个完整的请求了。

2.客户端常见的两种打包方式

我们还是以我们要设计的计算器为例子。

2.1将结构化的数据组合成一个字符串
  • 客户端发送一个形如“1+1”的字符串
  • 这个字符串中有两个操作数 都是整型

  • 两个数字之间会有一个字符是运算符

  • 数字和运算符之间没有空格

    客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中 。此时服务端每次从网络当中获取到的就是这样一个字符串 ,然后服务端再以相同的方式对这个字符串进行解析, 此时服务端就能够从这个字符串当中提取出这些结构化的数据。

2.2序列化和反序列化
  • 定制结构体来表示我们想要传递的信息

  • 发送数据时将这个结构体按照一个规则转换成网络标准数据格式 接收数据时再按照相同的规则把接收到的数据转化为结构体

  • 这个过程我们就叫做序列化和反序列化

    客户端发送数据时先对数据进行序列化 ,服务端接收到数据后再对其进行反序列化 ,此时服务端就能得到客户端发送过来的结构体 ,进而从该结构体当中提取出对应的信息。

3.网络版计算器的实现

服务端代码

首先我们需要对服务器进行初始化:

  • 调用socket函数,创建套接字
  • 调用bind函数,为服务端绑定一个端口号
  • 调用listen函数,将套接字设置为监听状态

初始化完服务器后就可以启动服务器了 服务器启动后要做的就是不断调用accept函数 从监听套接字当中获取新连接 每当获取到一个新连接后就创建一个新线程 让这个新线程为该客户端提供计算服务

  #include <iostream>    
  #include <string>    
  #include <cstring>    
  #include <sys/socket.h>    
  #include <sys/types.h>    
  #include <netinet/in.h>    
  #include <arpa/inet.h>    
  #include <unistd.h>    
  using namespace std;    
      
  int main(int argc , char* argv[])    
  {    
    if (argc != 2)    
    {    
      cout << "usage error" << endl;    
      exit(1);    
    }    
      
    // port socket                     
    int port = atoi(argv[1]);    
    int listen_sock = socket(AF_INET , SOCK_STREAM , 0);    
    if (listen_sock < 0)    
    {    
      cout << "socket error" << endl ;    
      exit(2);                                                                                                                                                                                                                                                                                                                                                      
    }    
    struct sockaddr_in local;    
    memset(&local , 0 , sizeof(local));    
    local.sin_family = AF_INET;    
    local.sin_port = htons(port);    
    local.sin_addr.s_addr = htonl(INADDR_ANY);    
      
      
    if (bind(listen_sock , (struct sockaddr*)&local , sizeof(local)) < 0)    
    {    
      cout << "bind error" << endl;    
      exit(3);    
    }    
      
    if (listen(listen_sock , 5) < 0)    
    {    
      cout << "listen error" << endl;    
      exit(4);    
    }    
      
      
    struct sockaddr_in peer;    
    memset(&peer , '\0' , sizeof(peer));    
    socklen_t len;    
    for(;;)    
    {    
      int sock = accept(listen_sock , (struct sockaddr*)&peer , &len);    
      if (sock < 0)    
      {    
        cout << "accept error" << endl;    
        continue; // do not stop     
      }    
      
      pthread_t tid;    
      int* p = new int(sock);    
E>    pthread_create(&tid , nullptr , Rontinue , (void*) p)    
    }    
      
    return 0;    
  }  

说明一下:

  • 当前服务器采用的是多线程的方案 你也可以选择采用多进程的方案或是将线程池接入到多线程当中
  • 服务端创建新线程时 需要将调用accept获取到套接字作为参数传递给该线程 为了避免该套接字被下一次获取到的套接字覆盖 最好在堆区开辟空间存储该文件描述符的值
协议定制

要实现一个网络版的计算器 就必须保证通信双方能够遵守某种协议约定 因此我们需要设计一套简单的约定 数据可以分为请求数据和响应数据 因此我们分别需要对请求数据和响应数据进行约定

在实现时可以采用C++当中的类来实现 也可以直接采用结构体来实现 这里就使用结构体来实现 此时就需要一个请求结构体和一个响应结构体

  • 请求结构体中需要包括两个操作数 以及对应需要进行的操作
  • 响应结构体中需要包括一个计算结果 除此之外 响应结构体中还需要包括一个状态字段 表示本次计算的状态 因为客户端发来的计算请求可能是无意义的 比如说除0操作等

规定状态字段对应的含义:

  • 状态字段为0 表示计算成功
  • 状态字段为1 表示非法计算
  typedef struct request    
  {    
    int left;    
    int right;    
    char op;    
  }request_t;    
      
  typedef struct response    
  {    
    int code;    
    int result;                                                                                                                          
  }response_t;   
123456789101112

要注意的是作为一种约定 它必须要被通信的双方所知晓 也就是说 要么我们将这个协议写在一个头文件中并同时包含在客户端和服务端中 要么在客户端和服务端都写上这么一段相同的代码

客户端代码

客户端首先也需要进行初始化:

  • 调用socket函数 创建套接字

客户端初始化完毕后需要调用connect函数连接服务端 当连接服务端成功后 客户端就可以向服务端发起计算请求了 这里可以让用户输入两个操作数和一个操作符构建一个计算请求 然后将该请求发送给服务端 而当服务端处理完该计算请求后 会对客户端进行响应 因此客户端发送完请求后还需要读取服务端发来的响应数据

int main(int argc , char* argv[])    
{    
  if (argc != 3)    
  {    
    cerr << "usage error" << endl;    
    exit(1);    
  }    
    
  string ip = argv[1];    
  int port = atoi(argv[2]);    
    
    
  int sockfd = socket(AF_INET , SOCK_STREAM , 0);    
  if (sockfd < 0)    
  {    
    cerr << "socket error" << endl;    
    exit(2);    
  }    
    
    
  struct sockaddr_in peer;    
  memset(&peer , '\0' , sizeof(peer));    
    
  peer.sin_family = AF_INET;    
  peer.sin_port = htons(port);    
  peer.sin_addr.s_addr = inet_addr(ip.c_str());    
    
  // connect     
  if (connect(sockfd , (struct sockaddr*)&peer , sizeof(peer)) < 0)    
  {    
    cerr << "connect error" << endl;    
    exit(3);    
  }    
    
  while(true)    
  {    
    request_t rq;    
    cout << "请输入左操作数#" ;    
    cin >> rq.left;    
    cout << "请输出右操作数#" ;    
    cin >> rq.right;    
    cout << "请输出操作符#" ;    
    cin >> rq.op;    
    write(sockfd , &rq , sizeof(rq));    
    
    
    response_t rp;    
    read(sockfd , &rp , sizeof(rp)) ;    
    cout << "code: " << rp.code << endl;    
    cout << "result:" << rp.result << endl;    
  }    
  return 0;    
}    
服务线程执行例程

当服务端调用accept函数获取到新连接并创建新线程后 该线程就需要为该客户端提供计算服务 此时该线程需要先读取客户端发来的计算请求 然后进行对应的计算操作

void* Routine(void* arg)    
{                      
  pthread_detach(pthread_self()); //分离线程    
  int sock = *(int*)arg;    
  delete (int*)arg;    
                                                      
  while (true){       
    request_t rq;                  
    ssize_t size = recv(sock, &rq, sizeof(rq), 0);    
    if (size > 0){    
      response_t rp = { 0, 0 };            
      switch (rq.op){    
      case '+':    
        rp.result = rq.left + rq.right;    
        break;    
      case '-':    
        rp.result = rq.left - rq.right;    
        break;    
      case '*':    
        rp.result = rq.left * rq.right;    
        break;                      
      case '/':    
        if (rq.right == 0){    
          rp.code = 1; //除0错误             
        }    
        else{     
          rp.result = rq.left / rq.right;    
        }                      
        break;                      
      case '%':    
        if (rq.right == 0){    
          rp.code = 2; //模0错误             
        }    
        else{     
          rp.result = rq.left % rq.right;    
        }                          
        break;    
      default:    
        rp.code = 3; //非法运算          
        break;    
      }                     
      send(sock, &rp, sizeof(rp), 0);    
    }           
    else if (size == 0){    
      cout << "service done" << endl;    
      break;                           
    }           
    else{    
      cerr << "read error" << endl;                                                                                         
      break;      
    }                
  }    
  close(sock);    
  return nullptr;    
}  
存在的问题
  • 如果客户端和服务器分别在不同的平台下运行 在这两个平台下计算出请求结构体和响应结构体的大小可能会不同 此时就可能会出现一些问题
  • 在发送和接收数据时没有进行对应的序列化和反序列化操作 正常情况下是需要进行的

虽然当前代码存在很多潜在的问题 但这个代码能够很直观的告诉我们什么是约定 这里将其当作一份示意性代码

代码测试

我们开始运行代码
在这里插入图片描述

如果是正常的计算 我们的计算器就能正常运行

如果涉及到除0 模0操作 该服务器就会返回我们一个错误码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值