【网络篇】第十三篇——认识“协议“与实现网络版计算器

简介: 【网络篇】第十三篇——认识“协议“与实现网络版计算器

认识"协议"


协议的概念


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

为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

结构化数据的传输


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

  • 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
  • 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。

比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。


如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。

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

约定方案一:

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

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

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

约定方案二:

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

客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。

序列化和反序列化


序列化和反序列化:

  • 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
  • 反序列化是把字节序列恢复为对象的过程。

OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。

序列化和反序列化的目的

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

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

image.png

网络版计算器


服务端代码


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

  • 调用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>
#include "protocol.hpp"
using namespace std;
int main(int argc, char* argv[])
{
  if (argc != 2){
    cerr << "Usage: " << argv[0] << " port" << endl;
    exit(1);
  }
  int port = atoi(argv[1]);
  //创建套接字
  int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
  if (listen_sock < 0){
    cerr << "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){
    cerr << "bind error!" << endl;
    exit(3);
  }
  //监听
  if (listen(listen_sock, 5) < 0){
    cerr << "listen error!" << endl;
    exit(4);
  }
  //启动服务器
  struct sockaddr peer;
  memset(&peer, 0, sizeof(peer));
  for (;;){
    socklen_t len = sizeof(peer);
    int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    if (sock < 0){
      cerr << "accept error!" << endl;
      continue;
    }
    pthread_t tid = 0;
    int* p = new int(sock);
    pthread_create(&tid, nullptr, Routine, p);
  }
  return 0;
}

说明一下:

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

协议定制


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

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

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

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

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

此时我们就完成了协议的设计,但需要注意,只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。

#pragma once
//请求
typedef struct request{
  int x; //左操作数
  int y; //右操作数
  char op; //操作符
}request_t;
//响应
typedef struct response{
  int code; //计算状态
  int result; //计算结果
}response_t;

注意: 协议定制好后必须要被客户端和服务端同时看到,这样它们才能遵守这个约定,如果我们将这份代码写到一个头文件中,那么客户端和服务端都应该包含这个头文件。

客户端代码


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

  • 调用socket函数,创建套接字。

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

客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收。

send函数

send函数的函数原型如下

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

recv函数

ecv函数的函数原型如下:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • len:数据的个数,表示从该文件描述符中读取数据的字节数。
  • flags:读取的方式,一般设置为0,表示阻塞式读取。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。
#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>
#include "protocol.hpp"
using namespace std;
int main(int argc, char* argv[])
{
  if (argc != 3){
    cerr << "Usage: " << argv[0] << " server_ip server_port" << endl;
    exit(1);
  }
  string server_ip = argv[1];
  int server_port = atoi(argv[2]);
  //创建套接字
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 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(server_port);
  peer.sin_addr.s_addr = inet_addr(server_ip.c_str());
  if (connect(sock, (struct sockaddr*)&peer, sizeof(peer)) < 0){
    cerr << "connect failed!" << endl;
    exit(3);
  }
  //发起请求
  while (true){
    //构建请求
    request_t rq;
    cout << "请输入左操作数# ";
    cin >> rq.x;
    cout << "请输入右操作数# ";
    cin >> rq.y;
    cout << "请输入需要进行的操作[+-*/%]# ";
    cin >> rq.op;
    send(sock, &rq, sizeof(rq), 0);
    //接收请求响应
    response_t rp;
    recv(sock, &rp, sizeof(rp), 0);
    cout << "status: " << rp.code << endl;
    cout << rq.x << rq.op << rq.y << "=" << rp.result << endl;
  }
  return 0;
}

服务线程执行例程


当服务端调用accept函数获取到新连接并创建新线程后,该线程就需要为该客户端提供计算服务,此时该线程需要先读取客户端发来的计算请求,然后进行对应的计算操作,如果客户端发来的计算请求存在除0、模0、非法运算等问题,就将响应结构体当中的状态字段对应设置为1、2、3即可。

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.x + rq.y;
        break;
      case '-':
        rp.result = rq.x - rq.y;
        break;
      case '*':
        rp.result = rq.x * rq.y;
        break;
      case '/':
        if (rq.y == 0){
          rp.code = 1; //除0错误
        }
        else{
          rp.result = rq.x / rq.y;
        }
        break;
      case '%':
        if (rq.y == 0){
          rp.code = 2; //模0错误
        }
        else{
          rp.result = rq.x % rq.y;
        }
        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;
}
相关文章
|
7月前
|
数据采集 算法 数据挖掘
模块化控制协议(MCP)在网络中增强智能体执行效率的研究
随着Web3技术的迅速发展,去中心化应用和智能体在各种领域的应用逐渐增多。MCP(Modularized Control Protocol,模块化控制协议)作为一种增强智能体执行能力的关键技术,为Web3场景中的智能体提供了更强的灵活性和可扩展性。本文将探讨如何利用MCP技术提升智能体在Web3场景中的执行能力,并通过实例代码展示其实现路径。
597 22
|
4月前
|
监控 负载均衡 安全
WebSocket网络编程深度实践:从协议原理到生产级应用
蒋星熠Jaxonic,技术宇宙中的星际旅人,以代码为舟、算法为帆,探索实时通信的无限可能。本文深入解析WebSocket协议原理、工程实践与架构设计,涵盖握手机制、心跳保活、集群部署、安全防护等核心内容,结合代码示例与架构图,助你构建稳定高效的实时应用,在二进制星河中谱写极客诗篇。
WebSocket网络编程深度实践:从协议原理到生产级应用
|
5月前
|
运维 架构师 安全
二层协议透明传输:让跨域二层协议“无感穿越”多服务商网络
简介:本文详解二层协议透明传输技术,适用于企业网工、运营商及架构师,解决LLDP/LACP/BPDU跨运营商传输难题,实现端到端协议透传,提升网络韧性与运维效率。
|
负载均衡 网络协议 算法
|
9月前
|
安全 网络协议 Linux
Linux网络应用层协议展示:HTTP与HTTPS
此外,必须注意,从HTTP迁移到HTTPS是一项重要且必要的任务,因为这不仅关乎用户信息的安全,也有利于你的网站评级和粉丝的信心。在网络世界中,信息的安全就是一切,选择HTTPS,让您的网站更加安全,使您的用户满意,也使您感到满意。
250 18
|
10月前
|
安全 网络安全 定位技术
网络通讯技术:HTTP POST协议用于发送本地压缩数据到服务器的方案。
总的来说,无论你是一名网络开发者,还是普通的IT工作人员,理解并掌握POST方法的运用是非常有价值的。它就像一艘快速,稳定,安全的大船,始终为我们在网络海洋中的冒险提供了可靠的支持。
292 22
|
10月前
|
网络协议 数据安全/隐私保护 网络架构
|
11月前
|
缓存 网络协议 API
掌握网络通信协议和技术:开发者指南
本文探讨了常见的网络通信协议和技术,如HTTP、SSE、GraphQL、TCP、WebSocket和Socket.IO,分析了它们的功能、优劣势及适用场景。开发者需根据应用需求选择合适的协议,以构建高效、可扩展的应用程序。同时,测试与调试工具(如Apipost)能助力开发者在不同网络环境下优化性能,提升用户体验。掌握这些协议是现代软件开发者的必备技能,对项目成功至关重要。
|
12月前
|
人工智能 自然语言处理 决策智能
智能体竟能自行组建通信网络,还能自创协议提升通信效率
《一种适用于大型语言模型网络的可扩展通信协议》提出创新协议Agora,解决多智能体系统中的“通信三难困境”,即异构性、通用性和成本问题。Agora通过标准协议、结构化数据和自然语言三种通信格式,实现高效协作,支持复杂任务自动化。演示场景显示其在预订服务和天气预报等应用中的优越性能。论文地址:https://arxiv.org/pdf/2410.11905。
448 6
|
前端开发 网络协议 安全
【网络原理】——HTTP协议、fiddler抓包
HTTP超文本传输,HTML,fiddler抓包,URL,urlencode,HTTP首行方法,GET方法,POST方法

热门文章

最新文章