目录
编辑
一,问题引入:
二,协议
三,自定义协议
1,协议
2, 序列化和反序列化
四,网络版本的计算器
1,协议的定制
2,计算逻辑
3,服务端
4,客户端
5,main函数
一,问题引入:
当我们使用TCP协议来进行网络通信时,因为TCP通信的特点:
面向字节流,没有数据边界。
所以,当我们在读取数据时就会遇到一个问题:在使用TCP通信时到底怎样才算将一段数据读取完成呢?
二,协议
为了解决上面的问题,在网络中便引入了协议。比较著名的协议就有HTTP协议,TCP协议等等。
这些协议通俗的讲其实就是一个约定,收发数据两方的约定。两方约定好如何发数据,如何收数据。
三,自定义协议
虽然在平时使用网络通信时并不需要我们来写协议。但是,我们还是有自己定制协议的能力的。 所以,为了更好的了解协议这个东西。我们可以自己尝试来写一个协议,然后让双方互相通信。
1,协议
现在,我们就来实现一个网络版的计算器。但是我们要做如下约定:数据要以如下格式发送:
len: 代表内容content的长度
\n:代表一个分割符
content:代表数据的正文内容
\n:代表一个分割符
2, 序列化和反序列化
在QQ接收消息时,我们不仅仅会收到信息,我们还会收到头像和昵称。这些消息就是一个结构化的消息。这些消息经过打包后会形成一段报文(一个整体),打包的过程就是序列化的过程。在将这段报文解开的过程就是一个反序列化的过程。序列化和反序列化的过程中要使用的就是协议。
四,网络版本的计算器
1,协议的定制
在写这个计算器时首先确定的便是协议的定制。协议定制如下:
#include<iostream>
#include"log.hpp"const std::string blank_sep = " ";
const std::string protocol_sep = "\n";std::string Encode(std::string& content)//加密:"len\ncontent\n"
{std::string package = std::to_string(content.size());package += protocol_sep;package+=content;package += protocol_sep;return package;//返回打包后的数据
}bool Decode(std::string &package,std::string* content) // 解密一段打包后数据:"len\ncontent\n"
{//先找第一个protocol_sepint pos = package.find(protocol_sep);if(pos == std::string::npos){// lg(Debug,"Decode err1");return false;}//找到了第一个protocol_sep,找到这段数据的长度std::string len_str = package.substr(0, pos);//std::cout << len_str << std::endl;int len = std::atoi(len_str.c_str());int total_len = len_str.size()+2+len;if(package.size()<total_len){// lg(Debug, "Decode err2");return false;}*content = package.substr(pos+1,len);package.erase(0,total_len);//解码后将package消掉return true;
}//定制协议
class Request//
{
public:
Request()
:x(0),y(0),op('+')
{}Request(int data1,int data2,char oper)
:x(data1),y(data2),op(oper)
{}//序列化:将结构化的数据变成字符串(x op y),并带出到外面
void Serialize(std::string* content)
{std::string str = std::to_string(x);str += blank_sep;str += op;str+= blank_sep;str += std::to_string(y);*content = str;
}bool Deserialize(std::string &content) // 将字符串转化为结构化的数据
{int pos = content.find(blank_sep);if(pos == std::string :: npos){//lg(Debug,"Request Deserilization err1");return false;}x = std::stoi(content.substr(0, pos));op = content[pos + 1];pos = content.rfind(blank_sep);if (pos == std::string ::npos){// lg(Debug, "Request Deserilization err2");return false;}y = std::stoi(content.substr(pos+1));return true;
}public:int x;int y;char op;
};class Response
{public:Response():result(0),code(0){}Response(int res,int c): result(res), code(c){}void Serialize(std::string* content)//序列化:str:"result code"{std::string str;str += std::to_string(result);str += blank_sep;str+=std::to_string(code);*content = str;}bool Deserialize(std::string &content){int pos = content.find(blank_sep);if(pos == std::string::npos){//lg(Debug, "Response Deserilization err");return false;}result = std::stoi(content.substr(0, pos));code = std::stoi(content.substr(pos + 1));return false;}void Debugprint(){std::cout <<"result: "<< result << " "<<"code: " << code << std::endl;}public:int result;//结果int code;//0表示结果正确,!0表示结果错误
};
在这段代码中,我定义了两个类:
Request:代表一个请求。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:x y op,代表着左右操作数和操作符。
Response:代表一个响应。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:result code,代表着结果和结果码(显示结果可不可信)。
在这段代码中还有两个公共的方法:
Encode:对序列化后的内容进行加码,变成如下形式:
Decode:对加码后的内容进行解码,变成一个简单的反序列化的代码。
2,计算逻辑
在制定好协议以后,便可以开始根据这些协议来对客户发来的数据进行处理了。但是如何处理呢?因为我们这里的处理逻辑就是一个计算。所以,写出计算逻辑如下:
#pragma once
#include "Protocol.hpp"class Calculator // 计算逻辑
{
public://开始计算Response CalHelper(Request& req)//开始计算{Response resp(0, 0);switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':if(req.y!=0)resp.result = req.x / req.y;elseresp.code = 3;break;case '%':if (req.y != 0)resp.result = req.x % req.y;elseresp.code = 3;break;default:resp.code = 4;break;}return resp;}// 对加包的数据进行计算,并且要返回序列化并打包的结果std::string Calculate(std::string &package)//要保证我的数据是一个package{std::string content;bool r = Decode(package, &content);//将package解包并且带出内容if(!r)return "";Request req;r = req.Deserialize(content);//反序列化后得到了一个结果,然后去计算if(!r)return "";Response resp;resp = CalHelper(req);//对req进行计算content = "";//序列化并打包resp.Serialize(&content);//std::string package;content = Encode(content);return content;//返回打包后的结果}
};
Calculate:对服务端接收到的消息进行处理。处理过程便是先对数据进行解包,然后再对数据进行反序列化。
CalHelper:对解包并且反序列化后的数据进行计算。并将结果存于Response对象中返回。
3,服务端
在写完如上代码后,我们的服务端便可以开始处理数据了。现在就让我们来搭建一个基于TCP协议的服务端。代码如下:
#pragma once
#include "log.hpp"
#include"Protocol.hpp"
#include"Socket.hpp"
#include<signal.h>
#include"Calculator.hpp"Calculator Cal;//定义一个计算器class CalServer
{
public:CalServer(uint16_t port) : port_(port){}bool Init(){listensock_.Sock();listensock_.Bind(port_);listensock_.Listen();lg(Info, "init server .... done");return true;}void Start(){signal(SIGCHLD, SIG_IGN);//父进程忽略子进程的信号while (true){std::string clientip;int clientport;int sockfd = listensock_.Accept(&clientip, &clientport);if (sockfd < 0)continue;lg(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);// 提供服务if (fork() == 0){listensock_.Close();std::string inbuffer_stream;// 数据计算while (true)//为什么是两个循环?因为我要保证读取到的数据拼接到inbuffer_stream中时是一个完整的报文。{char buffer[1280];ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer;lg(Debug, "debug:\n%s", inbuffer_stream.c_str());while (true){std::string info = Cal.Calculate(inbuffer_stream);if (info.empty())break;lg(Debug, "debug, response:\n%s", info.c_str());lg(Debug, "debug:\n%s", inbuffer_stream.c_str());write(sockfd, info.c_str(), info.size());}}else if (n == 0)break;elsebreak;}exit(0);}close(sockfd);}}~CalServer(){}private:uint16_t port_;Socket listensock_;
};
这里的listensock_ 对象是一个Socket类对象。这个Socket类定义如下:
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>//定义一些变量
#define blog 10
#define defaultport 8080class Socket
{public://构造函数Socket(): sockfd_(0){}public://创建套接字bool Sock(){sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字if (sockfd_ < 0){std::cerr << "创建套接字失败" << std::endl;return false;}return true; // 将创建好的套接字返回}//bind,服务端只要绑定端口号bool Bind(int16_t port = defaultport){sockaddr_in server_addr;memset(&server_addr, 0, sizeof (server_addr));//清空数据server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);server_addr.sin_addr.s_addr = INADDR_ANY;int r1 = bind(sockfd_,(sockaddr*)&server_addr,sizeof server_addr);if(r1<0){std::cerr << "bind err" << std::endl;return false;}return true;}//监听bool Listen(){int r2 = listen(sockfd_, blog);if(r2<0){std::cerr << "listen err" << std::endl;return 0;}return true;}//接收int Accept(std::string* ip,int* port){sockaddr_in cli_addr;socklen_t len = sizeof(cli_addr);int sockfd = accept(sockfd_, (sockaddr *)&cli_addr, &len);if(sockfd<0){std::cerr << "accept err" << std::endl;return -1;}char buff[64]={0};inet_ntop(AF_INET, &cli_addr, buff, sizeof(buff));*ip = buff;*port = ntohs(cli_addr.sin_port);return sockfd;}//连接bool Connect(std::string& ip,int16_t port){sockaddr_in addr_;addr_.sin_family = AF_INET;addr_.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &(addr_.sin_addr));int r = connect(sockfd_, (sockaddr *)&addr_, sizeof (addr_));if(r<0){std::cerr << "connect err" << std::endl;return false;}return true;}//关闭void Close(){close(sockfd_);}public://成员int sockfd_;
};
CalServer内的函数作用:
Init:创建套接字,bind套接字,监听套接字。
Start:循环接收客户端套接字,并且创建子进程对客户端的Request进行服务。在服务过程中父进程要对子进程的信号进行忽略,所以在创建子进程之前加上 signal(SIGCHLD, SIG_IGN)对子进程信号进行忽略。
4,客户端
客户端的创建代码比较简单,代码如下:
#include "Socket.hpp"
#include "Protocol.hpp"
#include "log.hpp"class CalClient
{
public:void Init(std::string ip, int port){Sock.Sock(); // 创建套接字Sock.Connect(ip, port);srand(time(0));lg(Info, "Connect sucess");}void Start(){// 创建100以内的数据const std::string opers = "+-*/%";const int len = opers.size();int cnt = 10;while (cnt--){std::cout <<"------------"<< "第" << cnt << "次测试"<< "------------" << std::endl;int data1 = rand() % 100;int data2 = rand() % 100;char op = opers[rand() % len];// 建立需求Request req(data1, data2, op);// 序列化std::string content;req.Serialize(&content);// encodecontent = Encode(content);// 送数据到Serverstd::cout << "请求构建完成:" << req.x << req.op << req.y << "="<< "?" << std::endl;write(Sock.sockfd_, content.c_str(), content.size());// 接收数据char buff[1280];read(Sock.sockfd_, buff, sizeof(buff));// 解码std::string package = buff;content = "";Decode(package, &content);// 反序列化Response resp;resp.Deserialize(content);resp.Debugprint();std::cout << "结果相应完成"<< "-----------------" << std::endl;sleep(1);}}private:Socket Sock;
};
Init:创建套接字 向客户端建立连接,生成随机数种子。
Start:随机数的方式构建请求,将请求序列化和加码后使用write发送给服务端,然后再使用read将服务端发送回来的结果读取显示。
5,main函数
在是实现完如上代码后,我们可以来实现两个main函数来调用一下如上代码。
Server.cc:
#include "CalServer.hpp"void usage(std::string proc)
{std::cout << proc << "port[1024+]" << std::endl;
}int main(int argc, char *argv[])
{if(argc!=2){usage(argv[0]);}int port = std::stoi(argv[1]);CalServer *sev = new CalServer(port);sev->Init();sev->Start();return 0;
}
Client.cc:
#include"CalClient.hpp"
void usage(std::string proc)
{std::cout << proc << "port[1024+]" << std::endl;
}int main(int argc,char* argv[])
{if(argc!=3){usage(argv[0]);}std::string serip = argv[1];int serport = std::stoi(argv[2]);CalClient *cli = new CalClient;cli->Init(serip, serport);cli->Start();}
调用以后结果如下: