【Linux】认识协议

🎇Linux:


  • 博客主页:一起去看日落吗
  • 分享博主的在Linux中学习到的知识和遇到的问题
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

在这里插入图片描述

目录

  • 🍁1. 协议的概念
  • 🍁2. 结构化数据的传输
  • 🍁3. 序列化和反序列化
  • 🍁4. 网络版计算器
    • 🍂4.1 服务端
    • 🍂4.2 协议定制
    • 🍂4.3 客户端
    • 🍂4.4 服务线程执行历程
    • 🍂4.6 代码测试

🍁1. 协议的概念

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

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


🍁2. 结构化数据的传输

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

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

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

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

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

约定方案一:

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

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

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

约定方案二:

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

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


🍁3. 序列化和反序列化

序列化和反序列化:

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

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

序列化和反序列化的目的

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

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

在这里插入图片描述


🍁4. 网络版计算器

🍂4.1 服务端

#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获取到套接字作为参数传递给该线程,为了避免该套接字被下一次获取到的套接字覆盖,最好在堆区开辟空间存储该文件描述符的值。

🍂4.2 协议定制

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

在实现时可以采用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;

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


🍂4.3 客户端

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

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

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

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

send函数

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

参数说明:

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

返回值说明:

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

recv函数

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;
}

🍂4.4 服务线程执行历程

当服务端调用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;
}

🍂4.6 代码测试

运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。

而如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。

在这里插入图片描述


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT程序退出还占进程

问题情况 程序运行时的样子&#xff1a; 程序退出时的样子&#xff1a; 其跑到了后台进程里面&#xff1a; 程序退出了&#xff0c;但在任务管理器里查看&#xff0c;其从进程里面转移到后台进程了。 这种问题&#xff0c;怎么办&#xff0c;代码里&#xff0c;应该释放的也都…

【我的创作纪念日】恒川的创作一周年

机缘 大家好&#xff0c;我是热爱跑步的恒川&#xff0c;今天是个特殊的日子&#xff08;我的创作纪念日&#xff09;&#xff0c;在去年的今天&#xff0c;我发了第一篇博文。去年的时候&#xff0c;刚接触到CSDN&#xff0c;只想把他当作一个学习的工具&#xff0c;后来&…

浅析时间复杂度与空间复杂度

时间复杂度 何为时间复杂度 算法的时间复杂度&#xff0c;是一个用于度量一个算法的运算时间的一个描述&#xff0c;本质是一个函数&#xff0c;根据这个函数能在不用具体的测试数据来测试的情况下&#xff0c;粗略地估计算法的执行效率&#xff0c;换句话讲时间复杂度表示的…

UNIX高级编程--管道

管道 管道是 UNIX 系统 IPC 的最高老形式&#xff0c;所有的 UNIX 系统都提供此种通信机制。管道有以下两种局限性。 历史上&#xff0c;它们是半双工&#xff08;既数据只能在一个方向上流动&#xff09;。现在&#xff0c;某些系统提供全双工管道&#xff0c;但是为了最佳的…

代码随想录算法训练营第五十七天 | 647. 回文子串、516. 最长回文子序列、动态规划总结

647. 回文子串 动规五部曲 1、确定dp数组&#xff08;dp table&#xff09;以及下标的含义 在判断字符串S是否为回文时&#xff0c;如果知道 s[1]&#xff0c;s[2]&#xff0c;s[3] 这个子串是回文的&#xff0c;那么只需要比较 s[0]和s[4]这两个元素是否相同&#xff0c;如果…

Motion Planning学习笔记一:配置空间、图、图搜索、图遍历

学习高飞博士的路径规划课程所总结的学习笔记。 目录 1、配置空间&#xff08;Configuration Space, C-space&#xff09; 2、图&#xff08;Graphs&#xff09; 3、图搜索&#xff08;Graph Search Basis&#xff09; 3.1、总体框架 3.2、两种基本的图遍历算法 3.3、启…

【Python】【进阶篇】十七、Python爬虫实现实时翻译

目录十七、Python爬虫实现实时翻译17.1 JS代码slat与sign17.2 Python代码表示参数17.3 完整程序实现十七、Python爬虫实现实时翻译 YD翻译是以异步方式实现数据加载的&#xff0c;要实现数据抓取&#xff0c;其过程极其繁琐。 上一节《Python爬虫的浏览器实现抓包》&#xff…

【hello Linux】Linux开发工具

目录 1. vim&#xff1a;文本编辑器 1.1 各种模式的切换 补充&#xff1a;ctrl r命令 1.2 命令模式的操作 1.3 插入模式的操作 1.4 底行模式的操作 1.5 配置vim环境 1.6 配置亲属关系 2. gcc/g&#xff1a;编译器 2.1 预处理&#xff1a; 2.2 编译&#xff1a; 2.3 汇编&#x…

如何利用ChatGPT辅助优化刷题性能

根据土著刷题共建群里的一个小伙伴反馈&#xff0c;刷题会出现切题卡顿的情况&#xff0c;有时会出现滑不动的情况。 定位问题 为了定位切题卡顿问题的具体原因&#xff0c;测试了高低端手机&#x1f4f1;、切换2G、3G、4G低网络状态等各种影响切题的现实情况&#xff0c;经过借…

STM32F4_定时器精讲(TIM)

目录 1. 什么是定时器&#xff1f; 2. STM32定时器简介 2.1 高级控制定时器 TIM1和TIM8 2.1.1 TIM1和TIM8简介 2.1.2 时基单元 2.1.3 计数器模式 2.1.4 重复计数器 2.1.5 时钟选择 2.1.6 捕获/比较通道 2.1.7 输入捕获模式 2.1.8 其他功能 2.2 通用定时器 TIM2到TI…

Mysql 你还在一个字段一个索引吗

今天看到某系统的mysql在某时段存在thread_running线程数飙高触发告警&#xff0c;挤时间分析了该异常时间段的慢日志记录&#xff0c;并进行了sql优化 慢日志记录主要归为3个慢sql (编号1&#xff0c;2&#xff0c;3) 一、 1号sql原文 select * from feeds where topics_id &…

【MySQL数据库原理】MySQL Community安装与配置

目录 安装成功之后查看版本验证1、介绍、安装与配置数据库2、操作MySQL数据库3、MySQL数据库原理安装成功之后查看版本验证 SELECT VERSION();查看mysql版本号 1、介绍、安装与配置数据库 下载安装包:https://download.csdn.net/download/weixin_41194129/87672588 MySQL…

Visual studio C#中通过nuget安装sqlite库及C#中sliqte的用法

以前在Visual studio 的2017版中讲过如何使用sqlite&#xff0c;这里我们再次说说如何使用sqlite&#xff0c;以前Nuget使用还不是很流行很普及&#xff0c;大多数人不知道&#xff0c;但随着VS的升级&#xff0c;Nuget成为安装插件或者引用库文件标准的获取手段&#xff0c;所…

Qt Quick - TabBar

Qt Quick - TabBar使用总结一、概述二、调整选项卡三、Flickable标签三、定制化一、概述 TabBar其实就是选项卡&#xff0c;TabBar是由TabButton控件填充&#xff0c;TabBar可以与任何提供currentIndex属性的布局或容器控件一起使用&#xff0c;如StackLayout或SwipeView。Tab…

Vector - CAPL - CAN x 总线信息获取

在CAN&CANFD测试中&#xff0c;我们经常需要获取到CAN总线的负载、错误帧、过载帧、发送错误等等CAN总线上面的信息&#xff0c;这些信息如此重要&#xff0c;但是如果真的要写代码去实现也是相当不易的&#xff0c;那我们该如何去获取到的呢&#xff1f;下面我们就来一起看…

Object方法

私人博客 许小墨のBlog —— 菜鸡博客直通车 系列文章完整版&#xff0c;配图更多&#xff0c;CSDN博文图片需要手动上传&#xff0c;因此文章配图较少&#xff0c;看不懂的可以去菜鸡博客参考一下配图&#xff01; 系列文章目录 前端系列文章——传送门 JavaScript系列文章—…

柔性数组【结构体和动态内存的结合】

全文目录前言柔性数组的定义语法柔性数组的特点柔性数组的使用柔性数组的优势前言 很多人可能没有听过柔性数组这个概念&#xff0c;但是在C99中柔性数组是确实存在的。我个人感觉有点像动态内存和结构体的结合。 柔性数组的定义语法 结构中的最后一个元素允许是未知大小的数…

NumPy 秘籍中文第二版:三、掌握常用函数

原文&#xff1a;NumPy Cookbook - Second Edition 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 在本章中&#xff0c;我们将介绍许多常用函数&#xff1a; sqrt()&#xff0c;log()&#xff0c;arange()&#xff0c;astype()和sum()ceil()&#xff0c;modf()&…

《Java8实战》第1章 Java 8、9、10 以及 11 的变化

如想了解 Oracle 公司对 JDK 的最新支持情况&#xff0c;请访问https://www.oracle.com/technetwork/java/java-se-supportroadmap.html。所有的示例代码均可见于图灵社区本书主页 http://ituring.com.cn/book/2659“随书下载”处。 1.1 为什么要关心 Java 的变化 Java8做的…

[MAUI 项目实战] 手势控制音乐播放器(三): 动画

文章目录吸附动画确定位置平移动画回弹动画使用自定义缓动函数多重动画点击动画项目地址上一章节我们创建了手势容器控件PanContainer&#xff0c;它对拖拽物进行包装并响应了平移手势和点击手势。拖拽物现在虽然可以响应手势操作&#xff0c;但视觉效果较生硬&#xff0c;一个…