作者:@小萌新
专栏:@网络
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:简单介绍下协议并且设计一个简单的网络服务器
协议的概念
协议 网络协议的简称 网络协议是通信计算机双方必须共同遵从的一组约定 比如怎么建立连接、怎么互相识别等
就像我们在之前的博客 网络基础 里面写的一样 协议的本质其实就是一种约定
结构化数据传输
通信双方在进行网络通信的时候:
- 如果要传输的数据是一个字符串 那么通信双方直接发送即可
- 如果要传输的数据是一些结构体 此时就不能将这些数码一个个发送到网络中
比如说我们现在要实现一个网络版本的计算器 那么客户端每次发送的请求就需要包括左操作数 右操作数 和对应的操作 那么此时客户端要发送的就不是一个简单的字符串 而是一个结构体
如果客户端将这些结构化的数据单独一个个发送到网络中 那么服务端也只能一个个的接受 但是这样子传输容易导致数据错乱
所以说最好的方案是客户端将这些结构化的数据统一打包发送到网络中 此时服务端接受的就是一个完整的请求了 客户端常见的打包方式有下面两种
将结构化的数据组合成一个字符串
约定方案一:
- 客户端发送一个形如“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;
}
存在的问题
- 如果客户端和服务器分别在不同的平台下运行 在这两个平台下计算出请求结构体和响应结构体的大小可能会不同 此时就可能会出现一些问题
- 在发送和接收数据时没有进行对应的序列化和反序列化操作 正常情况下是需要进行的
虽然当前代码存在很多潜在的问题 但这个代码能够很直观的告诉我们什么是约定 这里将其当作一份示意性代码
代码测试
我们开始运行代码
如果是正常的计算 我们的计算器就能正常运行文章来源:https://www.toymoban.com/news/detail-426665.html
如果涉及到除0 模0操作 该服务器就会返回我们一个错误码文章来源地址https://www.toymoban.com/news/detail-426665.html
到了这里,关于【Hello Network】协议的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!