【Hello Network】协议

news2024/9/22 3:43:08

作者:@小萌新
专栏:@网络
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:简单介绍下协议并且设计一个简单的网络服务器

协议

    • 协议的概念
    • 结构化数据传输
    • 序列化和反序列化
    • 网络版计算机
      • 服务端代码
      • 协议定制
      • 客户端代码
      • 服务线程执行例程
      • 存在的问题
      • 代码测试

协议的概念

协议 网络协议的简称 网络协议是通信计算机双方必须共同遵从的一组约定 比如怎么建立连接、怎么互相识别等

就像我们在之前的博客 网络基础 里面写的一样 协议的本质其实就是一种约定

结构化数据传输

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

  • 如果要传输的数据是一个字符串 那么通信双方直接发送即可
  • 如果要传输的数据是一些结构体 此时就不能将这些数码一个个发送到网络中

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

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

所以说最好的方案是客户端将这些结构化的数据统一打包发送到网络中 此时服务端接受的就是一个完整的请求了 客户端常见的打包方式有下面两种

将结构化的数据组合成一个字符串

约定方案一:

  • 客户端发送一个形如“1+1”的字符串
  • 这个字符串中有两个操作数 都是整型
  • 两个数字之间会有一个字符是运算符
  • 数字和运算符之间没有空格

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

定制结构体+序列化和反序列化

约定方案二:

  • 定制结构体来表示我们想要传递的信息
  • 发送数据时将这个结构体按照一个规则转换成网络标准数据格式 接收数据时再按照相同的规则把接收到的数据转化为结构体
  • 这个过程我们就叫做序列化和反序列化

客户端可以定制一个结构体 将需要交互的信息定义到这个结构体当中

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

序列化和反序列化

序列化和反序列化

  • 序列化就是将对象的状态信息转化为字节序的过程
  • 反序列化就是将字节序恢复为对象的过程

OSI七层模型中表示层的作用就是 实现设备固有数据格式和网络标准数据格式的转换

其中设备固有的数据格式指的是数据在应用层上的格式 而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式

序列化和反序列化的目的

  • 在网络传输时 序列化目的是为了方便网络数据的发送和接收 无论是何种类型的数据 经过序列化后都变成了二进制序列 此时底层在进行网络数据传输时看到的统一都是二进制序列
  • 序列化后的二进制序列只有在网络传输时能够被底层识别 上层应用是无法识别序列化后的二进制序列的 因此需要将从网络中获取到的数据进行反序列化 将二进制序列的数据转换成应用层能够识别的数据格式

我们可以认为网络通信和业务处理处于不同的层级 在进行网络通信时底层看到的都是二进制序列的数据 而在进行业务处理时看得到则是可被上层识别的数据 如果数据需要在业务处理和网络通信之间进行转换 则需要对数据进行对应的序列化或反序列化操作

在这里插入图片描述

网络版计算机

服务端代码

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

  • 调用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;   

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

客户端代码

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

  • 调用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操作 该服务器就会返回我们一个错误码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/453330.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Netty] HashWheelTimer时间轮 (十六)

文章目录 1.常见定时任务实现2.时间轮算法3.HashedWheelTimer源码分析3.1 内部结构分析3.2 构造方法3.3 添加任务3.4 工作线程Worker3.5 停止时间轮 4.HashWheelTimer总结 1.常见定时任务实现 定时器的使用场景包括&#xff1a;成月统计报表、财务对账、会员积分结算、邮件推送…

Linux Podman容器介绍

目录 Podman讲解 Container 和 Container Images 的关系 安装Podman 配置root的容器管理 国内镜像源 配置Podman的镜像源 创建容器相关命令 配置rootless的容器管理 配置Podman镜像源 管理容器镜像 管理容器 将容器作为systemd服务运行 配置普通用户来创建systemd…

Jetpack Compose之线性布局和帧布局

概述 Compose 中的线性布局对应的是Android传统视图中的LinearLayout,不一样的地方是&#xff0c;Compose根据Orientation的不同又将布局分为Column和Row, Column对应传统视图LinearLayout中orientation “vertical”的情况&#xff0c;Row对应传统视图LinearLayout中orienta…

Redis入门学习笔记【二】Redis缓存

目录 一、Redis缓存 二、Redis使用缓存遇到的问题 2.1 数据一致性 2.2缓存雪崩 2.3 缓存穿透 2.4 缓存击穿 一、Redis缓存 数据缓存是Redis最重要的一个场景&#xff0c;为缓存而生&#xff0c;在springboot中&#xff0c;一般有两种使用方式&#xff1a; 直接通过RedisT…

helm部署相关服务过程中问题记录

在学习helm部署相关服务过程中出现一些相关问题&#xff0c;自己记录并供大家一起学习&#xff01;&#xff01;&#xff01; 【问题1】部署helm 获取软件包失败 在通过wget https://storage.googleapis.com/kubernetes-helm/helm-v2.13.1-linux-amd64.tar.gz文件过程发现无法…

消息中间件的定义

中间件(middleware)是基础软件的一大类&#xff0c;属于可复用的软件范畴。中间件在操作系统软件&#xff0c;网络和数据库之上&#xff0c;应用软件之下&#xff0c;总的作用是为处于自己上层的应用软件提供运行于开发的环境&#xff0c;帮助用户灵活、高效的开发和集成复杂的…

【软考数据库】第二章 程序语言基础知识

目录 2.1 程序设计语言的基本概念2.2 程序设计语言的基本成分2.3 编译程序基本原理 前言&#xff1a; 笔记来自《文老师软考数据库》教材精讲&#xff0c;精讲视频在b站&#xff0c;某宝都可以找到&#xff0c;个人感觉通俗易懂。 2.1 程序设计语言的基本概念 程序设计语言是…

Nginx中的location规则与rewrite重写

location与rewrite的区别 rewrite &#xff1a;对访问的域名或者域名内的URL路径地址重写 location&#xff1a;对访问的路径做访问控制或者代理转发 从功能看 rewrite 和 location 似乎有点像&#xff0c;都能实现跳转&#xff0c;主要区别在于 rewrite 是在同一域名内更改获…

常见的3d bounding box标注工具

0. 简介 对于3d bounding box而言&#xff0c;近几年随着自动驾驶的火热&#xff0c;其标注工具也日渐多了起来&#xff0c;本篇文章不讲具体的算法&#xff0c;这里主要聚焦于这些开源的3d bounding box标注工具&#xff0c;以及他们是怎么使用的。这里借鉴了我想静静&#x…

牛客前端编程语言错题2

【语法】 名为“ctx”的变量是某个HTML5画布对象的上下文。以下代码绘制的是什么&#xff08;&#xff09; Ctx.arc(x,y,r,0,Math.PI,true); 在给定点绘制一个矩形 从一个点到另一个点绘制一条直线 在给定点绘制一个半圆 在给定点绘制一个圆 链接&#xff1a;https://www.now…

分布式系统反向代理设计与正向代理

反向代理与正向代理分析 代理服务器&#xff1a;位于发起请求的客户端与原始服务器端之间的一台跳板服务器&#xff0c;代理服务器分为正向代理服务器和反向代理服务器 正向代理 &#xff1a;代理客户端&#xff0c;隐藏了真实的请求客户端&#xff0c;服务端不知道真实的客户…

安全响应中心 — 垃圾邮件事件报告(4.18)

天空卫士安全响应中心邮件安全小组是成都研发中心的核心部门之一。在日常工作中&#xff0c;对大量样本进行分析并提取规则&#xff0c;实现对包含垃圾内容、钓鱼内容的邮件进行检测和隔离&#xff0c;从而抵御对业务电子邮件的入侵&#xff0c;防止钓鱼邮件等隐蔽邮件威胁。其…

9. 树的进阶

9. 树的进阶 ​ 之前我们学习过二叉查找树&#xff0c;发现它的查询效率比单纯的链表和数组的查询效率要高很多&#xff0c;大部分情况下&#xff0c;确实是这样的&#xff0c;但不幸的是&#xff0c;在最坏情况下&#xff0c;二叉查找树的性能还是很糟糕。 例如我们依次往二叉…

基于tensorflow2.x的多GPU并行训练

由于最近训练transformer&#xff0c;在单卡上显存不够&#xff0c;另外一块卡上也无法加载&#xff0c;故尝试使用双卡并行的策略。将基本的流程、遇见的难题汇总在这里。分布策略解释 使用官方给出的tf.distribute.MirroredStrategy作为分布策略。这个策略通过如下的方式运行…

Echarts渲染行政区划,实现聚焦高亮交互

首先需要准备行政区划的JSON数据&#xff0c;可以在DataV获取省市区的JSON数据。 最终效果图 渲染地图 建立一个地图容器&#xff0c;注意要给宽高 <!-- 地图容器 --> <div id"map"></div>请求JSON数据&#xff0c;渲染地图 $(function() {var …

Ubuntu 20版本将动态ip修改为静态ip时,ping 不通网络

问题描述&#xff1a; 在对Ubuntu 20版本将动态ip修改为静态ip时&#xff0c;ping www.baidu.com ping不通了 火狐浏览器没有了网路&#xff0c;下载不了东西 一直卡在这里不动 问题出在哪里还是配置ip dns 网关的问题 如果我们在当初安装ubuntu 时&#xff0c;将网络设置成…

24年专转本想要成功我们一个怎样做

23年转本报名人数创造高峰&#xff0c; 24年转本的同学们 如何脱颖而出&#xff0c;成功转本呢&#xff1f;一、明确转本目的 转本是一场重要的考试&#xff0c;有人把转本比喻为第二次高考。面对这唯一的进入本科院校学习的机会&#xff0c;考还是不考&#xff1f; 很多同…

小六壬学习笔记

小六壬学习笔记 简介前置知识:十二地支和十二时辰适用范围起课&#xff1a;月令日时卦象 疑问&#xff1a;遇到闰月怎么办&#xff1f;禁忌数字起课法手机计算器取余数 简单解卦 简介 马前课&#xff0c;又名&#xff1a;小六壬。 小六壬历史渊源&#xff1a;https://m.sohu.c…

RXJava2的基本概念与常见操作符使用实例解析

RXJava2是什么&#xff1f;可以简单介绍一下其特点和应用场景吗&#xff1f; RXJava2是基于观察者模式和链式编程思想的异步编程库&#xff0c;它可以用来优雅地处理异步操作&#xff0c;比如网络请求、数据库查询、文件I/O等操作&#xff0c;减少了回调嵌套&#xff0c;提高了…

【LeetCode】剑指 Offer 68. 二叉树中两个节点的最低公共祖先 p326 -- Java Version

1. 题目介绍&#xff08;68. 二叉树中两个节点的最低公共祖先&#xff09; 面试题68&#xff1a;二叉树中两个节点的最低公共祖先&#xff0c; 一共分为两小题&#xff1a; 题目一&#xff1a;二叉搜索树的最近公共祖先题目二&#xff1a;二叉树的最近公共祖先 2. 题目1&#x…