协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。
为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
通信双方在进行网络通信时:
比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。
如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。
将结构化的数据组合成一个字符串
约定方案一:
客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中,此时服务端每次从网络当中获取到的就是这样一个字符串,然后服务端再以相同的方式对这个字符串进行解析,此时服务端就能够从这个字符串当中提取出这些结构化的数据。
定制结构体+序列化和反序列化
约定方案二:
客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。
序列化和反序列化:
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
序列化和反序列化的目的
我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。
下面实现一个网络版的计算器,主要目的是感受一下什么是协议。
首先我们需要对服务器进行初始化:
初始化完服务器后就可以启动服务器了,服务器启动后要做的就是不断调用accept函数,从监听套接字当中获取新连接,每当获取到一个新连接后就创建一个新线程,让这个新线程为该客户端提供计算服务。
#include
#include
#include
#include
#include
#include
#include
#include
#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;
}
说明一下:
要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。
在实现时可以采用C++当中的类来实现,也可以直接采用结构体来实现,这里就使用结构体来实现,此时就需要一个请求结构体和一个响应结构体。
规定状态字段对应的含义:
此时我们就完成了协议的设计,但需要注意,只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。
#pragma once
//请求
typedef struct request{
int x; //左操作数
int y; //右操作数
char op; //操作符
}request_t;
//响应
typedef struct response{
int code; //计算状态
int result; //计算结果
}response_t;
注意: 协议定制好后必须要被客户端和服务端同时看到,这样它们才能遵守这个约定,如果我们将这份代码写到一个头文件中,那么客户端和服务端都应该包含这个头文件。
客户端首先也需要进行初始化:
客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。这里可以让用户输入两个操作数和一个操作符构建一个计算请求,然后将该请求发送给服务端。而当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。
客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收。
send函数
send函数的函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
返回值说明:
recv函数
recv函数的函数原型如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
返回值说明:
#include
#include
#include
#include
#include
#include
#include
#include
#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;
}
现在代码已经编写完毕了,但实际这份代码存在很多问题:
虽然当前代码存在很多潜在的问题,但这个代码能够很直观的告诉我们什么是约定,这里将其当作一份示意性代码就行了。
运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。
而如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。
此时我们就以这样一种方式约定出了一套应用层的简单的网络计算器,这就叫做协议。