前言:
首先对于Tcp的socket的通信,我们已经大概了解了,但是其中其实是由一个小问题,我们目前是不得而知得,在我们客户端与服务端连接成功后,服务端读取来自客户端得消息,我们使用的是函数read,通过来读取accept之后的文件,从而获取信息,可是我们怎么去知道每次读取都是一个完成的报文呢?
在用户端与服务端通信的时候,用户首先写信息到发送缓冲区当中,经过网络推送到接收缓冲区,之后服务端从接收缓冲区中读取数据。在这个过程中,其实都是有Tcp自主控制的,这也就是他为什么叫传输控制协议,tcp会处理在发送过程中所有遇到的问题:发什么,什么时候发,出错了怎么办?这里我们用到的接口,如write,read,accept等都是实现用户到内核的数据拷贝。
如何保证,数据的发送是准确无误的,这就取决于tcp,而tcp怎么保证?这就需要协议的定制。
目录
前言:
协议的定制
套接字文件
protocol.hpp
Calcultor.hpp
客户端:
服务端:
主运行函数:
协议的定制
协议是一种约定,在进行socket通信时,读写都是用字符串,那么如果要传输的数据是结构化数据呢?
我们以接受发消息为例:
我们平常发的消息,不仅仅只有我们的消息的内容,其实还有时间,名字,和消息。
一般,用户发出消息,系统会将该消息写到一个固定的结构体里,在将该结构化数据转化为字符串,再通过网络发送出去,接受的时候,还是需要将字符串先转化为结构数据从,之后再访问其中成员获取消息给用户。
数据的结构化的过程我们可以简单的理解为协议的封装。
现在我们就通过编写一个网络计算机为例,将所有的知识结合起来:
套接字文件
首先是套接字文件,里面包含了客户端与服务端需要直接调用的方法,例如创建套接字,绑定,监听,连接,接受等。
//网络接口
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include <string.h>
#include<strings.h>
const int backlog=10;
const int defaultfd=-1;
enum
{
CREATEERROR=1,
BINDERROR=2,
LISTENERROR=3,
ACCEPTERROR=4
};
class Sock
{
public:
Sock(const int socket=defaultfd):_sockfd()
{}
void Createsockfd()
{
//1.创建套接字文件描述符
_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(_sockfd<0)
{
std::cout<<"创建失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
exit(CREATEERROR);
}
std::cout<<"创建成功"<<std::endl;
}
void Bind(uint16_t port)
{
//初始化sockaddr_in
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
//2.绑定端口号
socklen_t len=sizeof(local);
int n=bind(_sockfd,(const struct sockaddr*)&local,len);
if(n<0)
{
std::cout<<"绑定失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
exit(BINDERROR);
}
std::cout<<"端口号绑定成功"<<std::endl;
}
void Listen()
{
//3.将套接字设置为监听状态
int l=listen(_sockfd,backlog);
if(l<0)
{
std::cout<<"监听失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
exit(LISTENERROR);
}
std::cout<<"设置监听成功"<<std::endl;
}
void Connect(std::string &ip,uint16_t &port)
{
//我去连接哪一个服务端
//获取服务端信息
struct sockaddr_in remote;
bzero(&remote, sizeof(remote));
remote.sin_family=AF_INET;
remote.sin_port=htons(port);
inet_pton(AF_INET, ip.c_str(),&(remote.sin_addr));
//建立连接
socklen_t len=sizeof(remote);
int n=connect(_sockfd,(const struct sockaddr*)&remote,len);
if(n<0)
{
std::cout<<"连接失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
exit(ACCEPTERROR);
}
std::cout<<"连接成功"<<std::endl;
}
int Accept(std::string *clientip,uint16_t* clientport)
{
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
std::cout<<"测试1。。。。。。"<<std::endl;
int newfd=accept(_sockfd,(struct sockaddr*)&client,&clientlen);
if(newfd<0)
{
std::cout<<"接收失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
return -1;
}
std::cout<<"测试2。。。。。。"<<std::endl;
//获取ip地址与端口号
char _ptr[32];
inet_ntop(AF_INET, &client.sin_addr,_ptr,sizeof(_ptr));
*clientip=std::string(_ptr);
*clientport=ntohs(client.sin_port);
std::cout<<"接收成功"<<std::endl;
return newfd;
}
int Fd()
{
return _sockfd;
}
~Sock()
{
}
void Close()
{
//孙子进程提供服务
close(_sockfd);
}
private:
int _sockfd;
};
我们来看看协议封装的头文件:
protocol.hpp
主要是序列化与反序列化
#pragma once
#include <iostream>
#include <string>
using namespace std;
const std::string blankspace = " ";
const string protocol_str = "/n";
//这里我们计算的字符串 10 * 10 会被Encode为 5/n10 * 10/n
std::string Encode(std::string &content)//编码格式为 len/nx op y/n
{
std::string package = std::to_string(content.size());
package += protocol_str;
package += content;
package += protocol_str;
return package;
}
//反之去掉/n 与长度 变为 x op y格式
bool Decode(std::string &package, std::string *content)
{
std::size_t pos = package.find(protocol_str);
if(pos == std::string::npos) return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
// package = len_str + content_str + 2
std::size_t total_len = len_str.size() + len + 2;
if(package.size() < total_len) return false;
*content = package.substr(pos+1, len);
// earse 移除报文 package.erase(0, total_len);
package.erase(0, total_len);
return true;
}
// 定制协议,这里我们打算搞得是网络计算器
// 请求
class Request
{
public:
Request(int data1, int data2,char c) : _x(data1), _y(data2),_op(c)
{
}
// 报文的读格式为 len/n a op b
bool Serialize(string *out) // 序列化
{
// 转化为字符串
// 构建报文的有效载荷
std::string s = std::to_string(_x);
s += blankspace;
s += _op;
s += blankspace;
s += std::to_string(_y);
*out = s;
return true;
//序列化 将已知的x,op,y进行第一层封装为 x op y 格式的字符串
}
bool Deserialize(const string &package) // 反序列化 已经定义好了是 len\nx op y
{
size_t pos=package.find(blankspace);
if(pos==std::string::npos)
{
return false;
}
std::string x=package.substr(0,pos);
size_t pot=package.rfind(blankspace);
if(pot==std::string::npos)
{
return false;
}
std::string y=package.substr(pot+1);
if(pos+2!=pot)
{
return false;
}
_op=package[pos+1];
_x=std::stoi(x.c_str());
_y=std::stoi(y.c_str());
//通过反序列化取得 字符 x,y,op 并转化相应类型
}
~Request()
{
}
public:
int _x;
int _y;
char _op; // + = * / %
};
// 应答
class Response
{
public:
Response(int result, int code) : _result(result), _code(code)
{
}
~Response()
{
}
// 序列化
bool Serialize(string *out)
{
//"len"\n"result code"
// 构建有效的报文载荷
std::string s = std::to_string(_result);
s += blankspace;
s += std::to_string(_code);
*out = s;
return true;
}
// 反序列化
bool Deserialize(const std::string &in) // len\n result code
{
std::size_t pos = in.find(blankspace);
if (pos == std::string::npos)
return false;
std::string part_left = in.substr(0, pos);
std::string part_right = in.substr(pos+1);
_result = std::stoi(part_left);
_code = std::stoi(part_right);
return true;
}
public:
int _result;
int _code; // 错误码,非0具体实际表明错误原因
};
之后根据我们初始化构造传入的参数,进行编码,传入到定制对象Request中,进行序列化,之后完成序列化,取得定制对象并传入 函数Calculator中进行计算,再根据返回的结果在进行Response对象的定制,序列化,再编码,作为报文字符串可以进行发了,服务端之后接受到到再解码,反序列化,即可。
计算转化头文件
Calcultor.hpp
主要是将传入的字符串,编码,序列化,之后调用计算,再解码,反序列化。实现计算过程。
#pragma once
#include<iostream>
#include"protocol.hpp"
class ServerCal{
public:
ServerCal()
{}
Response CalculatorOperator(const 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.code = Div_Zero;
else
resp.result = req.x / req.y;
}
break;
case '%':
{
if (req.y == 0)
resp.code = Mod_Zero;
else
resp.result = req.x % req.y;
}
break;
default:
resp.code = Other_Oper;
break;
}
return resp;
}
std::string Calculator(std::string &package)
{
//首先把内容转成报文格式
std::string content;
bool can=Decode(package,&content);//把传进来的字符解码 len/n x op y
if(!can)
return ;
Request req;
can=req.Deserialize(content);//反序列化 x op y
if(!can)
return ;
content="";
Response resp=CalculatorOperator(req);//进行计算req.x,req.y,req.op
resp.Serialize(&content);//变为 result code
content=Encode(content);//变为 len/n result code
return content;
}
private:
};
协议的定制就是约定,无论客户端还是服务端都要以这种方式来接受发报文,不符合该要求的是不会接收的。
可以看到序列与反序列化基本上都是字符串操作,每次这样写有点麻烦,实际上,再开发过程中,有现成的序列与反序列化的方法供我们使用,一般就是json,protobuf.
//json
bool Serialize(std::string *out)
{
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
// Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
}
bool Deserialize(const std::string &in) // "x op y"
{
Json::Value root;
Json::Reader r;
r.parse(in, root);
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asInt();
return true;
}
bool Serialize(std::string *out)
{
Json::Value root;
root["result"] = result;
root["code"] = code;
// Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
}
bool Deserialize(const std::string &in) // "result code"
{
Json::Value root;
Json::Reader r;
r.parse(in, root);
result = root["result"].asInt();
code = root["code"].asInt();
return true;
}
客户端:
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<assert.h>
#include"Protocol.hpp"
#include"Socket.hpp"
void USage(char *s)
{
std::cout<<"Usage:"<<s<<"<ip> <port>"<<std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
USage(argv[0]);
exit(0);
}
uint16_t port=stoi(argv[2]);
std::string ip =argv[1];
Sock sockfd;
sockfd.Createsockfd();
sockfd.Connect(ip, port);
int cnt=0;
srand(time(NULL)^getpid());
const std::string opers = "+-*/%=-=&^";
while(cnt <= 10)
{
std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 100;
usleep(4321);
char oper = opers[rand()%opers.size()];
Request req(x, y, oper);
std::string package;
req.Serialize(&package);
package = Encode(package);
write(sockfd.Fd(), package.c_str(), package.size());
cout<<"发送字符串:"<<package<<endl;
char buffer[128];
std::string inbuffer_stream;
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer; // "len"\n"result code"\n
std::cout << inbuffer_stream << std::endl;
std::string content;
bool r = Decode(inbuffer_stream, &content); // "result code"
assert(r);
Response resp;
r = resp.Deserialize(content);
assert(r);
cout<<"接受字符串:"<<content<<endl;
}
std::cout << "=================================================" << std::endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
服务端:
#pragma once
#include<iostream>
#include<functional>
#include<strings.h>
#include <signal.h>
#include"Socket.hpp"
using namespace std;
const uint16_t defaultport=8080;
const string defaultip="127.0.0.1";
const int defaultsockfd=-1;
using func_t = std::function<std::string(std::string &package)>;
class Tcpserver
{
public:
Tcpserver(uint16_t port ,func_t callback) : _port(port), _callback(callback)
{}
void InitServer()
{
_listensock.Createsockfd();
_listensock.Bind(_port);
_listensock.Listen();
}
void Start()
{
signal(SIGCHLD,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
while(true)
{
string clientip;
uint16_t clientport;
int sockfd=_listensock.Accept(&clientip,&clientport);//回调初始化地址与端口号
if(sockfd<0)
{
continue;
}
//孙子进程提供服务,可并发访问。
if(fork()==0)
{
//关闭文件描述符不影响文件缓冲区,防止文件描述符不够用
_listensock.Close();
//service
std::string buffoutput;
while(true)
{
char buff[1024];
ssize_t n=read(sockfd, buff, sizeof(buff));
if(n==0)
{
std::cout<<"读取消息失败"<<"错误码"<<errno<<"错误原因"<<strerror(errno)<<std::endl;
break;
}else if(n>0)
{
std::cout<<"读取消息成功:"<<buff<<",进行报文解读.........."<<std::endl;
buff[n]=0;
buffoutput+=std::string(buff);
std::cout<<buffoutput<<endl;
//可能有一堆请求
while(true)
{
std::string info=_callback(buffoutput);//回调给函数DoCalculator
//为空就继续读
if(info.empty())
{
break;
}
std::cout<< "debug, response:"<< info.c_str()<<std::endl;
std::cout<<"debug:"<<buffoutput.c_str()<<std::endl;
//向客户写回信息,向缓冲区里写
write(sockfd,info.c_str(),info.size());
}
}else
{
break;
}
}
exit(0);
}
close(sockfd);
}
}
~Tcpserver()
{
}
private:
Sock _listensock;
uint16_t _port;
func_t _callback;
};
主运行函数:
#include"Calculator.hpp"
#include"ServerCalculator.hpp"
#include <unistd.h>
void Hlper(char *s)
{
std::cout<<"please enter correct command in '"<<s<<" port[1024+]'"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=2)
{
Hlper(argv[0]);
exit(1);
}
uint16_t port=stoi(argv[1]);
ServerCal Calculator;
Tcpserver *Cal=new Tcpserver(port,std::bind(&ServerCal::DoCalculator,&Calculator,std::placeholders::_1));//这里用的是包装器bind
Cal->InitServer();
Cal->Start();
return 0;
}