通过打包的方式,将结构体message发送给对方
对方收到后就会报告给上层QQ客户端
结构化的数据 是由 多个 string 构成的
而以前在网络套接字 发送时,都是按照一个字符串的方式来发送和接收的
所以想办法 ,把多个字符串 转化为 一个大"字符串",对方在接收时也是一个长的字符串,
再想办法把这个字符串转回结构化的数据,就可以让上层使用
把一个结构化的数据 转化为 一个长的字符串 的 过程 称之为 序列化
把一个长的字符串 转化为 一个结构化的数据的 过程 称之为 反序列化
实现一个服务器版的加法器,把客户端把要计算的两个加数发过去,由服务器计算,最后把结果返回给客户端
Sock.hpp 表示 对Tcp套接字的封装
设置一个私有变量 监听套接字 (与accept返回的文件描述符 进行区分)
输入 man socket,创建套接字
第一个参数 domain ,用于区分 进行网络通信还是 本地通信
若想为网络通信,则使用 AF_INET
若想为本地通信,则使用 AF_UNIX
第二个参数 type, 套接字对应的服务类型
SOCK_STREAM 流式套接
SOCK_DGRAM 无连接不可靠的通信(用户数据报)
第三个参数 protocol ,表示想用那种协议,协议默认为0
若为 流式套接,则系统会认为是TCP协议 ,若为用户数据报,则系统会认为是UDP协议
套接字的返回值:若成功则返回文件描述符,若失败则返回 -1
使用socket 创建一个TCP的网络通信,并返回文件描述符到 _listensock中
把上篇博客的 日志(log.hpp)与错误信息枚举(err.hpp)拷贝过来
若套接字创建失败,则通过日志将错误信息打印处来,并借助 错误信息枚举 终止程序
输入 man 2 bind ,查看绑定
给一个套接字绑定一个名字
第一个参数 sockfd 为 套接字
第二个参数 addr 为 通用结构体类型
第三个参数 addrlen 为 第二个参数的实际长度大小
bind返回值:若成功,则返回0,若失败,返回 -1
想要使用bind函数,就需要先创建一个网络通信类型的变量,通过该变量存储端口号 IP地址 16位地址类型
所以要先定义一个 struct sockaddr_in(网络通信) 类型的 变量 local
htons 主机序列转化为 网络序列
需要借助 htons 将传进来的参数 port端口号进行转化
INADDR_ANY 表示 本机的所有IP
若小于0,则绑定失败
依旧使用日志打印处错误码和错误原因,再终止程序
输入 man 2 listen
设置当前套接字状态为 监听状态
第一个参数 sockfd 为 套接字
第二个参数 暂不做解释,一般设为整数
若成功则返回0,若失败返回-1
若小于0,则监听失败
依旧使用日志打印处错误码和错误原因,再终止程序
输入 man 2 accept
需要知道谁连的你,所以要获取到客户端的相关信息
第一个参数 sockfd 为套接字
第二个参数 addr 为通用结构体类型的 结构体 这个结构体是用来记录客户端内的port号以及IP地址 、16位地址类型等信息
第三个参数 addrlen 为 结构体的大小
返回值:
若成功,则返回一个合法的整数 即文件描述符
若失败,返回-1并且设置错误码
sock 这个文件描述符 是真正给用户提供IO服务的
若连接失败,则返回-1,使用日志将错误信息打印出来
若连接成功,则需获取到对应的客户端的 端口号 与客户端的IP地址
使用 inet_ntoa 4字节风格IP转化为字符串风格IP
使用 ntohs 网络序列转主机序列
connect 函数功能为客户端主动连接服务器
成功返回0,失败返回-1
使用Sock这个类,实例化对象_listensock
在初始化中,使用_listensock这个对象 去访问 Scok类中实现过的 Socket Bind Listen 等函数
作为一款服务器,就需要一直运行 作数据的分析
通过_listensock对象访问Accept函数获取客户端的IP地址和端口号
在类中的函数如果不加static修饰,就会导致存在隐藏的this指针
所以 回调函数 需加 static 修饰
使用 pthread_join 默认是阻塞的 ,即主线程等待 新线程退出
在这个过程中,主线程会直接卡住,就没办法继续向后运行,也就什么都干不了
若主线程 想做其他事情 ,所以就提出了线程分离的概念
创建一个结构体ThreadData内部包含sock套接字以及一个指向服务器的指针 ip地址 port端口号
在初始化 多线程部分,new对象,将sock clientip client port 与this指针传递过去作为参数 完成构造
再将td传过去作为回调函数的参数
在回调函数内部调用 serviceIO函数 来完成协议
在命名空间Protocol_n中,定义两个类,分别为Request类和Reponse类
若读到 字符串风格的Request ,就需要通过 序列化 转成 结构化的数据
自己定义 将结构化的数据 转化为 字符串
假设空格作为分割符
使用to_string 将任意类型转化为string
使用 宏, 将SEP表示为空格
将_x _y _op 使用空格连接起来
提供一个函数StringSplit ,去掉字符串中的空格,分别填入vector数组中,作为vetcor数组中的元素
下标为0开始的位置 填入_x ,下标为1开始的位置 填入 _op
下标为2开始的位置 填入 _y
借助函数 toInt,将string类型的元素 转化为 整数
_op在 vector数组的1号下标中,对应其中的一个字符
寻找SEP分割符所在位置,即可分割出区间
使用find函数,从start位置开始寻找分隔符sep,找到分割符sep后,将区间内的子串插入vector数组中
当sep为空格时,只占用一个位置,pos处于空格位置 ,只需加1即可跳出空格
故start的位置 只需 从pos 位置 加上 sep长度即可得到
若出了循环str中依旧有子串没有被插入vector中,则全部当做一个整体放入vector中
使用 atoi 函数 将字符串转化为 整形
使用to_string 将任意类型转化为string
将 res_string SEP 和 code_string 连接起来
同样取调用 StringSplit函数 将字符串 转换为 vector数组中的元素
分别将结果和错误码提取出来
定义一个string类型的package,从套接字sock读取,将结果添加到package中
若有完整报文就交给package,没有完整报文,则一直读取
inbuffer 用于记录报文的所有数据
输入 man recv
第一个参数为 套接字
第二个参数为缓冲区
第三个参数 为缓冲区长度
第四个参数为 读取方式 ,一般默认为0
返回值为读取到的字节数,若字节数小于0,则表示读取出错
先使用recv,将sock中的数据读取到buffer中,再将数据传入inbuffer中
通过find 查找inbuffer中的\r\n的位置,在使用substr将提取到的头部字符串(报头) ,
使用 toInt 将字符串转化为数字 ,即获取到字符串长度
最终将有效载荷数据传入 package中
若返回值为-1,则表示读取失败,若返回值为0,则表示继续读取
若返回值为1,则表示读取成功,即可进入下面步骤
从后面先减去一个分隔符,再减去有效载荷的长度
从有效载荷位置开始 取 有效载荷的长度个字符 即 取到有效载荷
构建一个Request 对象
通过该对象去访问请求的 反序列化 ,将字符串str转化为结构化的数据
定义一个包装器,其返回值类型为Response ,参数为 Request ,并重命名为 func_t
使用func_t类型 定义 一个func的私有成员变量
将Request处理完 变为 Response
在Calculatorserver.cc中,进行请求处理
先将结果与错误码默认都设置为0,表示成功
使用 switch case 把request变量的req 中的 _x _y 通过 加 减 乘 除 取模 等进行运算
若期间错误码 出现 1 2 3,则表示错误
最终 将执行后的结果 返回resp中
对response结构进行序列化,将其转化为字符串
将send_string字符串 中 添加字符串长度 分隔符 \r\n
输入 man send
第一个参数为 套接字
第二个参数为特定字符串数据
第三个参数为 数据长度
第四个参数为 默认为0
#pragma once
#include
#include
#include
#include
using namespace std;
class Util
{
public:
//将字符串str 按照sep分隔符 把结果放入 result中
//分割成功 则为true 分割失败,则为false
static bool StringSplit(const string &str,const string &sep,std::vector<string>*result)
{
size_t start=0;
// 10 + 20
while(start<str.size())
{
auto pos=str.find(sep,start);//从start位置开始寻找sep
if(pos==string::npos)//找不到分隔符了
{
break;
}
//从start位置开始 寻找pos-start个字符,并将其放入vector数组中
result->push_back(str.substr(start,pos-start));
//位置的重新加载
start=pos+sep.size();
}
//若出了循环str中依旧有子串没有被插入vector中,则全部当做一个整体放入
if(start<str.size())
{
result->push_back(str.substr(start));
}
return true;
}
//字符串转整数
static int toInt(string &s)
{
return atoi(s.c_str());
}
};
.PHONY:all
all:calserver calclient
calclient:CalculatorClient.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
calserver:CalculatorServer.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -f calclient calserver
#pragma once
#include
#include
#include
#include"Sock.hpp"
#include"Protocol.hpp"
//服务器
namespace tcpserver_ns
{
using namespace protocol_ns;
class TcpServer;
//定义包装器
using func_t =std::function<Response(const Request&)>;
class ThreadData
{
public:
ThreadData(int sock,std::string ip,uint16_t port,TcpServer*tsvrp)//构造
:_sock(sock),_ip(ip),_port(port),_tsvrp(tsvrp)
{}
~ThreadData()
{}
public:
int _sock;//套接字
TcpServer *_tsvrp;//指针指向Tcp服务器
std::string _ip;
uint16_t _port;
};
class TcpServer
{
public:
TcpServer(func_t func,uint16_t port ):_func(func),_port(port)
{}
void InitServer()//初始化
{
//1.初始化服务器
_listensock.Socket();//创建套接字
_listensock.Bind(_port);//绑定
_listensock.Listen();//监听
logmessage(INFO,"init server done,listensock:%d",_listensock.Fd());
}
//该函数被多线程调用
void serviceIO(int sock,const std::string ip,const uint16_t port)//提供服务
{
std::string inbuffer;//用于记录报文的所有数据
while(true)
{
//1.如何保证读到完整的字符串报文? ----7\r\n""10 + 20"\r\n
//不能保证
//所以要一直循环读取,边读取 边检测 测试
std::string package;
//从sock中读,将结果添加到package
int n=ReadPackage(sock,inbuffer,&package);
if(n==-1) //-1表示读取失败
{
break;;
}
else if(n==0)//0表示继续读
{
continue;
}
else //读取成功,返回报头长度
{
//2.需要的只是有效载荷的部分 "10 + 20"
package=RemoveHeader(package,n);//将package中的有效载荷提取出来
//3.假设已经读到一个完整的string
Request req; //构建一个Request对象
std::string str;
req.Deserialize(str);//对读到的request字符串进行反序列化
//4.提取用户的请求数据
Response resp= _func(req);
//5.给用户返回响应
std::string send_string;
resp.Serialize(&send_string);//对计算完毕的response结构进行序列化,形成可发送字符串
//6. 添加报头
send_string =AddHeader(send_string);//添加报头
//7. 发送
send(sock,send_string.c_str(),send_string.size(),0);
}
}
close(sock);
}
static void*ThreadRoutine(void*args)
{
pthread_detach(pthread_self());//线程分离
ThreadData* td=(ThreadData*)args;
td->_tsvrp->serviceIO(td->_sock,td->_ip,td->_port);
delete td;
return nullptr;
}
void Start()//启动
{
for(; ;)
{
std::string clientip;
uint16_t clientport;
int sock=_listensock.Accept(&clientip,&clientport);//获取连接
if(sock<0)//连接失败
{
continue;
}
logmessage(DEBUG,"get a new client,client info:[%s:%d]",clientip.c_str(),clientport);
//多线程
pthread_t tid;
ThreadData*td=new ThreadData(sock,clientip,clientport,this);
pthread_create(&tid,nullptr, ThreadRoutine,td);
}
}
~TcpServer()
{
_listensock.Close();
}
private:
func_t _func;
uint16_t _port;//端口号
Sock _listensock;
};
}
#pragma once
#include
#include
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include"log.hpp"
#include"err.hpp"
static const int gbacklog=32;
static const int defaultfd=-1;
class Sock
{
public:
Sock() //构造
:_sock(defaultfd)
{
}
void Socket()//创建套接字
{
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)//套接字创建失败
{
logmessage(FATAL,"socket error,code:%s,errstring:%s",errno,strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(uint16_t port)//绑定
{
struct sockaddr_in local;
memset(&local,0,sizeof(local));//清空
local.sin_family=AF_INET;//16位地址类型
local.sin_port= htons(port); //端口号
local.sin_addr.s_addr= INADDR_ANY;//IP地址
//若小于0,则绑定失败
if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
logmessage(FATAL,"bind error,code:%s,errstring:%s",errno,strerror(errno));
exit(BIND_ERR);
}
}
void Listen()//将套接字设置为监听状态
{
//小于0则监听失败
if(listen(_sock,gbacklog)<0)
{
logmessage(FATAL,"listen error,code:%s,errstring:%s",errno,strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string *clientip,uint16_t * clientport)//获取连接
{
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
int sock=accept(_sock,(struct sockaddr*)&temp,&len);
if(sock<0)
{
logmessage(WARNING,"accept error,code:%s,errstring:%s",errno,strerror(errno));
}
else
{
//inet_ntoa 4字节风格IP转化为字符串风格IP
*clientip = inet_ntoa(temp.sin_addr) ; //客户端IP地址
//ntohs 网络序列转主机序列
*clientport= ntohs(temp.sin_port);//客户端的端口号
}
return sock;//返回新获取的套接字
}
int Connect(const std::string&serverip,const uint16_t &serverport )//发起链接
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));//清空
server.sin_family=AF_INET;//16位地址类型
server.sin_port=htons(serverport);//端口号
//inet_addr 字符串风格IP转化为4字节风格IP
server.sin_addr.s_addr=inet_addr(serverip.c_str());//IP地址
//成功返回0,失败返回-1
return connect(_sock, (struct sockaddr*)&server,sizeof(server));
}
int Fd()
{
return _sock;
}
void Close()
{
if(_sock!=defaultfd)
{
close(_sock);
}
}
~Sock()//析构
{
}
private:
int _sock;
};
#pragma once
#include
#include
#include
#include
#include
#include"Util.hpp"
//#define MYSELF 1 //用于条件编译
//给网络版本计算器定制协议
namespace protocol_ns
{
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEADER_SEP "\r\n"
#define HEADER_SEP_LEN strlen("\r\n")
//"长度"\r\n "_x _op _y"\r\n
//Request与 Response 都要提供序列化和反序列化功能
// "10 + 20" -> "7\r\n""10 + 20"\r\n
std::string AddHeader(const std::string&str)//添加报头
{
std::string s=std::to_string(str.size());//字符串的长度
s+= HEADER_SEP;//加上分隔符
s+= str;//加上正文
s+= HEADER_SEP;
}
// "7\r\n""10 + 20"\r\n" -> "10 + 20"
std::string RemoveHeader(const std::string &str,int len)//提取数据
{
return str.substr(str.size()-HEADER_SEP_LEN-len,len);//获取有效载荷
}
// 0表示继续读 1表示读取成功 -1表示读取失败
int ReadPackage(int sock,std::string& inbuffer,std::string* package)
{
//边读取
char buffer[1024];
ssize_t s=recv(sock,&buffer,sizeof(buffer-1),0);//将sock中的数据读取到buffer中
if(s<=0)//读取出错
{
return -1;
}
buffer[s]=0;
inbuffer += buffer;
//边分析 7\r\n""10 + 20"\r\n
auto pos=inbuffer.find( HEADER_SEP);//查询\r\n的位置
if(pos==std::string::npos)//没有找到\r\n,则说明报文不完整
{
return 0;
}
std::string lenstr=inbuffer.substr(0,pos);//获取头部字符串
int len =Util::toInt(lenstr);//有效载荷长度 "123"-> 123
int targetPackageLen=lenstr.size() + len+2* HEADER_SEP_LEN;//总报文=报头长度+有效载荷长度+分隔符长度
if(inbuffer.size()<targetPackageLen)
//说明缓冲区不存在完整报文
{
return 0;
}
*package= inbuffer.substr(0,targetPackageLen); //提取有效载荷数据
inbuffer.erase(0,targetPackageLen);//从inbuffer中移除整个报文
return len;//返回有效载荷长度
}
class Request//请求
{
public:
Request()
{}
Request(int x,int y,char op)
:_x(x),_y(y),_op(op)
{}
//序列化 结构化的数据 转为字符串
bool Serialize( std::string* outstr)
{
*outstr="";//清空
#ifdef MYSELF
// _x _op _y
*outstr="";//清空
std::string x_string =std::to_string(_x);
std::string y_string =std::to_string(_y);
//手动序列化
*outstr=x_string + SEP + _op + SEP + y_string;
#else
Json::Value root;//value:一种万能对象,接收任意的kv类型
root["x"]=_x;
root["y"]=_y;
root["op"]=_op;
Json::FastWriter writer; // write 用于进行序列化 将结构化字段转化为字符串
//Json::StyledWriter
*outstr =writer.write(root);
#endif
return true;
}
//反序列化 字符串 转为 结构化的数据
bool Deserialize(const std::string &instr)
{
#ifdef MYSELF
std::vector<std::string> result;
Util::StringSplit(instr,SEP,&result);
//根据协议规定必须等于3 _x _op _y
if(result.size()!=3)
{
return false;
}
_x=Util::toInt(result[0]);
_y=Util::toInt(result[2]);
if(result[1].size()!=1)
{
return false;
}
_op= result[1][0];
#else
Json::Value root;
Json::Reader reader;//Reader 用于进行反序列化
reader.parse(instr,root);//将结果放入root中
_x=root["x"].asInt();//将字符串类型转换为整形
_y=root["y"].asInt();
_op=root["op"].asInt();//转化为数字 放入char中,最后会被解释为字符
#endif
}
~Request()
{}
public:
// _x op _y
//x y为操作数 op为操作符
int _x;
int _y;
char _op;
};
class Response//响应
{
public:
Response()
{}
Response(int result,int code)
:_result(result),_code(code)
{}
//序列化 结构化的数据 转为字符串
bool Serialize( std::string* outstr)
{
#ifdef MYSELF
*outstr="";//清空
std::string res_string =std::to_string(_result);
std::string code_string =std::to_string(_code);
*outstr=res_string + SEP + code_string;
#else
Json::Value root;
root["reslut"]=_result;
root["code"] =_code;
Json::FastWriter writer;//用于反序列化
*outstr=writer.write(root);
#endif
return true;
}
//反序列化 字符串 转为 结构化的数据
bool Deserialize(const std::string &instr)
{
#ifdef MYSELF
// 10 0 / 10 1 / 10 2
std::vector<std::string> result;
Util::StringSplit(instr,SEP,&result);
//当前只存在 结果和错误码
if(result.size()!=2)
{
return false;
}
_result=Util::toInt(result[0]);
_code=Util::toInt(result[1]);
#else
Json::Value root;
Json::Reader reader;//用于反序列化
reader.parse(instr,root);//将结果传给root
_result=root["result"].asInt();//将字符串结果转化为整数
_code=root["code"].asInt();
#endif
return true;
}
~Response()
{}
public:
int _result;//结果
int _code;//默认为0 表示成功 1 2 3 4 不同的数字表示不同的错误码
};
}
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
const std::string filename="tecpserver.log";
//日志等级
enum{
DEBUG=0, // 用于调试
INFO , //1 常规
WARNING, //2 告警
ERROR , //3 一般错误
FATAL , //4 致命错误
UKNOWN//未知错误
};
static std::string tolevelstring(int level)//将数字转化为字符串
{
switch(level)
{
case DEBUG : return "DEBUG";
case INFO : return "INFO";
case WARNING : return "WARNING";
case ERROR : return "ERROR";
case FATAL : return "TATAL";
default: return "UKNOWN";
}
}
std::string gettime()//获取时间
{
time_t curr=time(nullptr);//获取time_t
struct tm *tmp=localtime(&curr);//将time_t 转换为 struct tm结构体
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
void logmessage(int level, const char*format,...)
{
//日志左边部分的实现
char logLeft[1024];
std::string level_string=tolevelstring(level);
std::string curr_time=gettime();
snprintf(logLeft,sizeof(logLeft),"%s %s %d",level_string.c_str(),curr_time.c_str());
//日志右边部分的实现
char logRight[1024];
va_list p;//p可以看作是1字节的指针
va_start(p,format);//将p指向最开始
vsnprintf(logRight,sizeof(logRight),format,p);
va_end(p);//将指针置空
//打印日志
printf("%s%s\n",logLeft,logRight);
}
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,//2
BIND_ERR,//3
LISTEN_ERR,//4
SETSID_ERR,//5
OPEN_ERR//6
};
#include"TcpServer.hpp"
#include
using namespace tcpserver_ns;
// ./calserver 8888
Response calculate(const Request& req)
{
//一定保证req 是有具体数据的
//默认将结果和错误码设置 为0
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=1;
}
else
{
resp._result= req._x / req._y;
}
break;
case '%':
if(req._y==0)
{
resp._code=2;
}
else
{
resp._result= req._x % req._y;
}
break;
default:
resp._code=3;
break;
}
return resp;
}
int main()
{
uint16_t port=8888;
std::unique_ptr<tcpserver_ns::TcpServer> tsvr(new tcpserver_ns::TcpServer(calculate,port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
#include"TcpClient.hpp"
#include
#include
#include "Sock.hpp"
#include"Protocol.hpp"
using namespace protocol_ns;
static void usage(std::string proc)
{
std::cout<<"usage:\n\t"<< proc<<" serverip serverport\n"<<std::endl;
}
//./tcpclient serverip serverport
int main(int argc,char*argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(USAGE_ERR);//终止程序
}
std::string serverip= argv[1];
uint16_t serverport=atoi(argv[2]);//将字符串转化为整形
Sock sock;
sock.Socket();//创建套接字
int n=sock.Connect(serverip,serverport);//发起连接
if(n!=0)//连接失败
{
return 1;
}
std::string buffer;
while(true)
{
std::cout<<" enter# "<<std::endl;//1+1
std::string line;
std::getline(std::cin,line);
Request req;
std::cout<<"data#1"<<std::endl;
std::cin>>req._x;
std::cout<<"data#2"<<std::endl;
std::cin>>req._y;
std::cout<<"op#3"<<std::endl;
std::cin>>req._op;
std::cout<<req._x<<req._op<<req._y<<std::endl;
//1.序列化
std::string sendstring;
req.Serialize(&sendstring);
//2.添加报头
AddHeader(sendstring);
//3.发送
send(sock.Fd(),sendstring.c_str(),sendstring.size(),0);
//4.获得响应
std::string package;
//返回有效载荷长度 若大于0则继续执行
int n=0;
START:
n=ReadPackage(sock.Fd(),buffer,&package);
if(n==0)
{
goto START;
}
else if(n<0)
{
break;
}
else
{}
//5. 去掉报头
package=RemoveHeader(package,n);//获取有效载荷
//6.反序列化
Response resp;
resp.Deserialize(package);//反序列化
std::cout<<"result:"<<resp._result<<" "<<"code:"<<resp._code<<std::endl;
}
// sock.Close();
return 0;
}