目录
在TCP通信中,TCP的特点是有连接,可靠,面向字节流:
有连接在TCP套接字编写的时候,也可以体会到,我们在编写的时候需要进程设置监听套接字,还有 accept, 并且客户端也需要connect。
可靠目前我们是体会不到的,但是我们会在说TCP协议的时候会说到可靠性。
还有就是字节流,字节流这里先简单的说一下概念,就是发送的次数与接收的次数不一致。
在前面学习UDP的时候,UDP是面向数据报的,而数据报就是发一个报文,如果拿的话,就需要拿一个报文,而TCP面向字节流的话,就是一次性可以拿一部分,也可以全部拿走。
所以说UDP就是发送一次们就需要接收一次,而TCP是可以多次发送,但是只接收一次!
那么就是说如果我们使用TCP通信的话,那么就是需要有问题需要注意的:
我们怎么知道,我们收到了一个数据,是否是一个完整的报文?
因为我们可能会收到一半数据,也可能收到一个多的数据,所以我们并不能保证我们收到的是完整的报文!
所以下面我们的代码主要就是为了解决这个问题。
而今天需要做的工作就是序列化与反序列化。
我们就先介绍一下,我们今天打算如何写序列化和反序列化:
我们打算写一个网络版本的计算器,客户端发送数据,服务器处理好后返回答案即可。
所以我们需要一个客户端,还有服务器。
客户端只需要做的就是生产数据,然后发送给服务器。
服务器就需要接收数据,接收数据后,处理完成后,将处理结果返回。
而在客户端发送之前,由于我们直接发送我们的请求的话,也就是类似于结构体,但是由于主机和主机不同,所以结构体在不同的环境里面恐怕有区别,所以不建议直接发送结构体,所以我们在发送的时候,做好将数据给序列化为字符串,然后发送出去,就可以理解为序列化就是转成字符串。
所以服务器在收到数据后,是需要将字符串转换成结构体的也就是反序列化,将客户端的请求反序列化为请求的结构体,然后进行计算,计算后将答案封装成结构体,发送给客户端,但是服务器发送的时候也是同样的问题,需要序列化。
那么当客户端接收到服务器的响应时候,接收到的还是一个字符串,而客户端就需要将响应的字符串反序列化为结构体。
上面就是我们要写的大概逻辑,我们还回对服务器进行封装,同时我们对网络套接字也进行一定的封装。
套接字的封装,我相信不需要仔细的说明,因为在这之前,已经写了好几次套接字了:
#include "log.hpp"
?
class Sock
{
public:
? ?Sock(){};
?
? ?// socket 函数封装
? ?int Socket()
? {
? ? ? ?// TCP套接字
? ? ? ?int listensock = socket(AF_INET, SOCK_STREAM, 0);
? ? ? ?if(listensock < 0)
? ? ? {
? ? ? ? ? ?log(FATAL, "创建套接字失败! errno: %d %s", errno, strerror(errno));
? ? ? ? ? ?exit(errno);
? ? ? }
? ? ? ?return listensock;
? }
?
? ?void Bind(const int listensock, const uint16_t port, const std::string& ip = "0.0.0.0")
? {
? ? ? ?struct sockaddr_in local;
? ? ? ?local.sin_family = AF_INET;
? ? ? ?local.sin_port = htons(port);
? ? ? ?inet_aton(ip.c_str(), &local.sin_addr);
? ? ? ?
? ? ? ?int r = bind(listensock, (struct sockaddr*)&local, (socklen_t)sizeof(local));
? ? ? ?if(r < 0)
? ? ? {
? ? ? ? ? ?log(FATAL, "bind 失败! errno: %d %s", errno, strerror(errno));
? ? ? ? ? ?exit(errno);
? ? ? }
? }
?
? ?void Listen(int listensock)
? {
? ? ? ?int r = listen(listensock, 0);
? ? ? ?if(r < 0)
? ? ? {
? ? ? ? ? ?log(FATAL, "设置监听套接字失败! errno: %d %s", errno, strerror(errno));
? ? ? ? ? ?exit(errno);
? ? ? }
? }
?
? ?int Accept(int listensock, struct sockaddr* out_sockaddr, socklen_t* out_len)
? {
? ? ? ?int sockfd = accept(listensock, out_sockaddr, out_len);
? ? ? ?if(sockfd < 0)
? ? ? {
? ? ? ? ? ?log(ERROR, "accept 获取新套接字失败!");
? ? ? ? ? ?return -1;
? ? ? }
? ? ? ?return sockfd;
? }
?
? ?void Connect(int sock, const std::string& ip, const uint16_t port)
? {
? ? ? ?struct sockaddr_in peer;
? ? ? ?peer.sin_family = AF_INET;
? ? ? ?peer.sin_port = htons(port);
? ? ? ?inet_aton(ip.c_str(), &peer.sin_addr);
?
? ? ? ?int r = connect(sock, (struct sockaddr*)&peer, (socklen_t)sizeof(peer));
? ? ? ?if(r < 0)
? ? ? {
? ? ? ? ? ?log(FATAL, "连接失败! errno: %d %s", errno, strerror(errno));
? ? ? ? ? ?exit(errno);
? ? ? }
? }
private:
? ?static Log log;
};
?
Log Sock::log;
这里的封装就不介绍了,下面我们将服务器也封装一下:
那么服务器如何封装呢?其实我们在前面也封装过好多次,但是这次我们还是在介绍一下。
成员变量
服务器需要什么?首先是IP和端口,还有监听套接字,还有就是需要我们想要执行的回调函数,以及我们前面封装的 sock对象,为了方便套接字编写!
成员函数
我们可以在服务器的构造函数里面将套接字等准备好,所以我们不需要在写一个init函数。
那么我们还需要一个启动的函数,就是让服务器一直在 accept 如果有连接到达,那么就创建一个新的线程,去执行回调函数。
以及还可以写一个send和recv 方便我们调用。
上面的服务器函数的轮廓就是这样,而我们在服务器里面只需要创建一个服务器的对象。
下面设置回调函数。
然后再启动服务器即可。
所以我们再看一下服务器怎么写?
服务器里面,我们就调用服务器对象的构造函数
设置服务器里面的回调函数
启动服务器
int main(int argc, char *argv[])
{
? ?if (argc != 2)
? {
? ? ? ?usage(argv[0]);
? ? ? ?exit(0);
? }
? ?uint16_t port = atoi(argv[1]);
? ?std::unique_ptr<TcpServer> sev(new TcpServer(port));
? ?sev->setCallBack(calculator);
? ?sev->start();
? ?return 0;
}
而我们的 calculator 函数,就是我们想要执行的函数。
那么这个函数我们需要怎么写呢?
其中 calculator 函数里面就是需要从套接字里面读取数据,然后对读取到的数据进行解码,还要进行反序列化,反序列化后就可以计算了,计算出结果后将结果序列化,序列化后为字符串添加应用层报头,然后进行返回数据。
而这就是我们的应用岑协议,所以我们特需要一个协议的头文件,为了处理序列化以及反序列化,还有编码也解码。
下面我们再看一下服务器的头文件的编写:
typedef std::function<void(int)> threadCall_t;
?
class TcpServer
{
private:
? ?struct threadDate
? {
? ?public:
? ? ? ?threadDate()
? ? ? {
? ? ? }
?
? ? ? ?threadDate(int sock, TcpServer *self)
? ? ? ? ? : _sock(sock), _self(self)
? ? ? {
? ? ? }
?
? ?public:
? ? ? ?int _sock;
? ? ? ?TcpServer *_self;
? };
?
public:
? ?// 构造函数里面,我们进行套接字的初始化,以及绑定等动作
? ?TcpServer(const uint16_t port, const std::string ip = "0.0.0.0")
? ? ? : _port(port), _ip(ip)
? {
? ? ? ?_listensock = _sock.Socket();
? ? ? ?log(INFO, "创建监听套接字成功~ listensock: %d", _listensock);
? ? ? ?_sock.Bind(_listensock, _port, _ip);
? ? ? ?log(INFO, "绑定成功~");
? ? ? ?_sock.Listen(_listensock);
? ? ? ?log(INFO, "设置监听套接字成功~");
? }
?
? ?~TcpServer()
? {
? ? ? ?if (_listensock > 0)
? ? ? ? ? ?close(_listensock);
? }
? ?// 启动函数就是我们进行 accept 然后去执行回调函数
? ?void start()
? {
? ? ? ?while (true)
? ? ? {
? ? ? ? ? ?struct sockaddr_in peer;
? ? ? ? ? ?socklen_t len;
? ? ? ? ? ?int sock = _sock.Accept(_listensock, (struct sockaddr *)&peer, &len);
? ? ? ? ? ?log(INFO, "接收到一个新的连接 sock: %d", sock);
? ? ? ? ? ?// 下面创建线程去执行任务
? ? ? ? ? ?pthread_t tid;
? ? ? ? ? ?// 这是线程的数据,里面有一个新的套接字,还有一个就是服务器类型的指针
? ? ? ? ? ?// 传入这个指针是为了让 routine 函数可以找到回调函数,因为 routine 函数是 static 的
? ? ? ? ? ?threadDate *td = new threadDate(sock, this);
? ? ? ? ? ?pthread_create(&tid, nullptr, routine, (void *)td);
? ? ? }
? }
?
? ?void setCallBack(threadCall_t callBack)
? {
? ? ? ?_callBack = callBack;
? }
? ?// recv 函数就是对 recv 系统调用的封装,而将接收到的数据追加到 str 上
? ?static ssize_t Recv(int sock, std::string& str)
? {
? ? ? ?char buffer[1024];
? ? ? ?// 0 表示阻塞读取
? ? ? ?ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);
? ? ? ?if (s < 0)
? ? ? {
? ? ? ? ? ?log(DEBUG, "服务器读取数据失败!");
? ? ? }
? ? ? ?else if(s == 0)
? ? ? {
? ? ? ? ? ?log(INFO, "对端关闭连接!");
? ? ? ? ? ?return 0;
? ? ? }
? ? ? ?buffer[s] = 0;
? ? ? ?log(DEBUG, "接收到一条数据: %s", buffer);
? ? ? ?str += buffer;
? ? ? ?return s;
? }
?
? ?// Send 函数就是对 send 系统调用的封装,而这就是直接发送就可以
? ?static ssize_t Send(int sock, const std::string& resp)
? {
? ? ? ?ssize_t s = send(sock, resp.c_str(), resp.size(), 0);
? ? ? ?if(s < 0)
? ? ? {
? ? ? ? ? ?log(ERROR, "发送失败!");
? ? ? ? ? ?return -1;
? ? ? }
? ? ? ?return s;
? }
?
private:
? ?// 创建的线程就会执行该函数,而该函数就是对回调函数的封装调用
? ?static void *routine(void *args)
? {
? ? ? ?pthread_detach(pthread_self());
? ? ? ?threadDate *td = static_cast<threadDate *>(args);
? ? ? ?TcpServer *self = td->_self;
? ? ? ?self->_callBack(td->_sock);
? ? ? ?close(td->_sock);
? ? ? ?// 因为线程的参数是 new 出来的所以是需要 delete 的
? ? ? ?delete td;
? }
?
private:
? ?int _listensock;
? ?std::string _ip;
? ?uint16_t _port;
? ?threadCall_t _callBack;
? ?Sock _sock;
? ?static Log log;
};
?
Log TcpServer::log;
那么我们打算怎么写协议的头文件呢?
我们先把服务器里面的 calculator 函数写一下,将 calculator 里面对于协议的序列化反序列化的轮廓写出来:
我们打算如何写这个 calculator 函数呢?
首先是从套接字里面读取数据。
读取到数据后,我们进行 decode 也就是将应用层的协议解析出来。
解析出一个后,我们对这个数据进行反序列化,得到一个请求的结构化数据。
有了结构化数据就可以计算了,计算后,我们得到一个响应的结构化数据。
有了响应后,我们需要对响应进行反序列化,得到一个字符串。
有了这个字符串,我们继续添加应用层报头。
添加完成后,我们就发送数据给客户端。
response computer(request req)
{
? ?response resp;
? ?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 = 1;
? ? ? ?else
? ? ? ? ? ?resp._result = req._x % req._y;
? }
? ?break;
? ?default:
? {
? ? ? ?log(ERROR, "没有此种运算!");
? }
? ?break;
? }
? ?return resp;
}
?
void calculator(int sock)
{
? ?std::string buffer;
? ?while (true)
? {
? ? ? ?// 1. 读取数据
? ? ? ?// 读取到的数据都会添加到 buffer 里面,buffer 里面可能不只有一个数据
? ? ? ?ssize_t s = TcpServer::Recv(sock, buffer);
? ? ? ?if (s < 0)
? ? ? {
? ? ? ? ? ?continue;
? ? ? }
? ? ? ?else if (s == 0)
? ? ? {
? ? ? ? ? ?break;
? ? ? }
? ? ? ?log(DEBUG, "服务器读取数据成功 buffer: %s", buffer.c_str());
? ? ? ?// 2. 读取到的数据是序列化的,现在需要反序列化
? ? ? ?std::string message;
? ? ? ?request req;
? ? ? ?// 我们循环的处理 buffer 里面的数据,知道 buffer 里面的数据被处理完了
? ? ? ?while (true)
? ? ? {
? ? ? ? ? ?// 我们 decode 就是解码,每一次解析出来一个数据,如果没有数据,那么就返回空串
? ? ? ? ? ?message = req.decode(buffer);
? ? ? ? ? ?log(DEBUG, "decode 结果: %s", message.c_str());
? ? ? ? ? ?// 如果是空串,那么说明 buffer 里面现在没有一个完整的数据,所以需要继续读取,就 break 出去,继续读取
? ? ? ? ? ?if (message.empty())
? ? ? ? ? ? ? ?break;
? ? ? ? ? ?// 到了这里,说明是有一个完整的报文的,而这个数据就被保存到 message 里面,然后我们对这个数据进行但序列化,也就是反序列化成一个结构体
? ? ? ? ? ?if (!req.deserialize(message))
? ? ? ? ? {
? ? ? ? ? ? ? ?// 反序列化失败了,重新读取数据
? ? ? ? ? ? ? ?std::cerr << "buffer: " << buffer << std::endl;
? ? ? ? ? ? ? ?break;
? ? ? ? ? }
? ? ? ? ? ?std::cerr << "获取了一个任务: " << req._x << req._op << req._y << "=?" << std::endl;
? ? ? ? ? ?// 3. 计算然后构建响应
? ? ? ? ? ?// 反序列化成功后,我们就可以进行计算,然后返回一个响应的结构化数据
? ? ? ? ? ?response resp = computer(req);
? ? ? ? ? ?// 4. 方便响应的发送,将需要发送的数据序列化
? ? ? ? ? ?std::string respMessage = resp.serialize(resp);
? ? ? ? ? ?// 4.5 还需要再加上长度报头
? ? ? ? ? ?respMessage = resp.encode(respMessage);
? ? ? ? ? ?// 5. 发送数据
? ? ? ? ? ?s = TcpServer::Send(sock, respMessage);
? ? ? }
? }
}
上面就是 calculator 函数编写的逻辑,但是我们目前还没有协议的头文件,所以我们需要将该函数里面用到的协议的函数写好!
那么我们现在就开始看一下我们的协议的定制,以及应用层协议的添加等...
我们先想清楚,我们打算如何序列化,反序列化,以及应用层报头里面有哪些字段?
我们的服务就是一个在线的计算器。
所以我们再序列化的时候,将每一个数字和字符使用空格隔开即可。
而我们的报头也是很简单,我们只需要添加正文的长度,但是由于都是字符,所以我们需要将报头以及有效载荷使用标记隔开,我们这里使用\r\n来充分隔符!
#include <iostream>
#include "log.hpp"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
上面就是我们下面使用的分隔符等,还有使用到的头文件。
下面我们看一下协议的编写:
对于客户端发过来的数据,我们称之为请求,对于服务器发送回去的数据,我们称之为响应,而请求和响应的结构体是不同的,这里我们简单的认为请求里面的成员变量就是下面这个样子:
struct request
{
int _x;
int _y;
char _op;
};
我们这个就是简单的计算,_x 表示第一个数字, _y 表示第二个数据, _op 表示操作符,而操作符我们认为只有 “+,-,*,/,%” 这几种。
序列化就是将这个结构体里面的这三个变量序列化为下面这样:
_x(SPACE)_op(SPACE)_y // 括号只是为了区分,协议里面是没有括号的
序列化后还需要添加报头,我们认为我们的报头很简单就是有效载荷的长度,而上面序列化后的数据就是有效载荷的长度。
而为了添加报头后方便拆分,我们将长度后面加一个 SEP ,再加上上面的有效载荷,然后为了每一个报文的可读性,我们为每一个报文后面再加上 SEP
所以就是下面这个样子:
length(SEP)_x(SPACE)_op(SPACE)_y(SEP)
而这里我们认为 SPACE 就是空格,而 SEP 就是 \r\n 所以下面我们用 1+1 这个算式举例:
5\r\n1 + 1\r\n// 1+1 序列化后就是 1空格+空格1,而这个就是有效载荷,而这个的长度就是5
现在基本概念说清楚了,下面我们就看一下如何编写,实际上 request 和 response 的序列化和反序列化基本一样,只有一点差别。
所以下面我就只介绍一个:
class request
{
public:
request(){};
request(int x, int y, char op)
: _x(x), _y(y), _op(op){};
private:
public:
// x(SPACE)+(SPACE)y
// length\r\nx(SPACE)+(SPACE)y\r\n
// 序列化函数,就是将结构化的数据变成字符串,而每一个成员变量之间使用空格(SPACE)隔开
// 序列化的函数很简单
std::string serialize()
{
std::string ret;
ret += std::to_string(_x);
ret += SPACE;
ret += _op;
ret += SPACE;
ret += std::to_string(_y);
return ret;
}
// 序列化好后,得到的字符串就是有效载荷,所以我们还需要添加报头,报头就是有效载荷的长度
std::string encode(std::string &s)
{
std::string ret = std::to_string(s.size());
ret += SEP;
ret += s;
ret += SEP;
return ret;
}
// 当服务器收到请求时候,是有报头的,所以我们需要去掉报头并分析,有效载荷的长度
// 然后将有效载荷分离出来,这就是解码
std::string decode(std::string &s)
{
// length\r\n1 + 2\r\n
// 因为当我们收到数据后,我们知道需要一个这样的数据,这样才算一个完整的报文
// 那么我们想要拿到长度报头,我们需要找到第一个 \r\n 所以我们直接 find
int pos1 = s.find(SEP);
if (pos1 == std::string::npos)
return "";
// 当找到后,我们需要对这个长度进行解析,我们需要计算当前缓冲区里是否有一个完整的报文
int size = atoi(s.substr(0, pos1).c_str());
int need = pos1 + SEP_LEN + size + SEP_LEN;
if (s.size() < need)
return "";
// 这里表示是有完整的报文的,所以我们将有效载荷提取出来
std::string ret = s.substr(pos1 + SEP_LEN, size);
// 提取结束后,我们对缓冲区里面解析过的数据进行删除即可。
s.erase(0, need);
return ret;
}
// 对有效载荷进行分序列化
bool deserialize(const std::string &str)
{
// 反序列化需要找到两个空格,其中第一个空格前面是数字,后面是字符
log(DEBUG, "请求反序列化开始...");
int pos = str.find(SPACE, 0);
if (pos == std::string::npos)
{
// 有问题
log(ERROR, "请求反序列化失败!");
return false;
}
log(DEBUG, "找到了第一个位置 pos: %d", pos);
// 第一个没问题的话,就找第二个空格,第二个空格前面是字符,后面是数字
int pos1 = str.find(SPACE, pos + SPACE_LEN);
if (pos == std::string::npos)
{
// 有问题
log(ERROR, "请求反序列化失败!");
return false;
}
log(DEBUG, "找到了第二个位置 pos: %d", pos1);
// 发现都没有问题之后,就开始对请求的对象进行赋值
_x = std::atoi(str.substr(0, pos).c_str());
log(DEBUG, "x: %s", str.substr(0, pos).c_str());
_y = std::atoi(str.substr(pos1 + SPACE_LEN).c_str());
log(DEBUG, "y: %s", str.substr(pos1 + SPACE_LEN, str.size()).c_str());
_op = str[pos1 - 1];
log(DEBUG, "算式为: %d%c%d", _x, _op, _y);
log(DEBUG, "请求反序列化结束...");
return true;
}
public:
int _x;
int _y;
char _op;
static Log log;
};
这就是协议的定制。
下面的响应的协议的定制也基本是一样的:
class response
{
public:
response(){};
response(int result, int code = 0)
: _result(result), _code(code){};
private:
public:
// length\r\nXXX YYY\r\n
std::string encode(std::string &s)
{
std::string ret = std::to_string(s.size());
ret += SEP;
ret += s;
ret += SEP;
return ret;
}
std::string decode(std::string &s)
{
int pos1 = s.find(SEP);
if (pos1 == std::string::npos)
return "";
int size = atoi(s.substr(0, pos1).c_str());
int need = pos1 + SEP_LEN + size + SEP_LEN;
if (s.size() < need)
return "";
std::string ret = s.substr(pos1 + SEP_LEN, size);
s.erase(0, need);
return ret;
}
// _result(SPACE)_code
std::string serialize(const response &resp)
{
std::string ret;
ret += std::to_string(resp._result);
ret += SPACE;
ret += std::to_string(resp._code);
return ret;
}
bool deserialize(const std::string &str)
{
int pos = str.find(SPACE);
if (pos == std::string::npos)
{
log(ERROR, "响应反序列化失败");
return false;
}
_result = atoi(str.substr(0, pos).c_str());
_code = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
}
public:
int _result;
int _code = 0;
static Log log;
};
客户端的编写是比服务器的要简单的,客户端这里我们就不详细说了。
客户端也需要套接字,所以需要创建套接字,还有连接服务器。
通过用户输入获取数据。
对获取的数据进行分析,将数据转化为 _x(SPACE)_op(SPACE)_y 这样的字符串
然后对上面的有效载荷进行添加报头,也就是 length,length后面还需要加分隔\r\n 然后加上有效载荷在加上\r\n
然后发送给服务器,等服务器计算完毕后,然后数据
然后客户端读取数据,将读取到的数据同样放到缓冲区中,因为缓冲区里面可能不止一个响应
然后对缓冲区的数据进行解码,获取到一个响应的有效载荷
然后对响应的有效载荷进行反序列化
然后将计算的结果打印出来
Sock sock;
Log log;
void usage(char *proc)
{
cout << "\nuse: " << proc << " server_ip server_port\n"
<< endl;
}
request getCal()
{
request req;
std::cerr << "please Enter _X> ";
std::cin >> req._x;
std::cerr << "please Enter _OP> ";
std::cin >> req._op;
std::cerr << "please Enter _Y> ";
std::cin >> req._y;
return req;
}
bool isOperartor(char c)
{
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%')
return true;
else
return false;
}
// 1+1
std::string analyze(const std::string &equat)
{
std::string ret;
int i = 0;
// 找第一个数字
for (; i < equat.size(); ++i)
{
if (!(equat[i] >= '0' && equat[i] <= '9'))
{
if (equat[i] == ' ' || isOperartor(equat[i]))
{
ret += equat.substr(0, i + 1);
break;
}
else
{
return "";
}
}
}
log(DEBUG, "i 的位置: %d", i);
ret += SPACE;
// 找字符,并且去掉空格
for (; i < equat.size(); ++i)
{
if (isOperartor(equat[i]))
ret += equat[i];
else if ((equat[i] >= '0' && equat[i] <= '9'))
break;
else if (equat[i] == ' ')
continue;
else
return "";
}
log(DEBUG, "i 的位置: %d", i);
ret += SPACE;
ret += equat.substr(i);
return ret;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(0);
}
// 1. 创建套接字
int sockfd = sock.Socket();
cout << "创建套接字成功 sock: " << sockfd << endl;
// 2. 连接
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
sock.Connect(sockfd, ip, port);
cout << "客户端连接成功~" << endl;
char buffer[1024];
std::string client_buffer;
while (true)
{
std::cerr << "please Enter> ";
std::string equation;
std::getline(std::cin, equation);
// 0. 分析字符串
std::string message = analyze(equation);
if (message.empty())
continue;
log(DEBUG, "message: %s", message.c_str());
// 1. 获取算式
request req;
// 1.5 将获取到的消息,反序列化
req.deserialize(message);
// std::cin >> req._x >> req._op >> req._y;
// 2. 序列化
std::string reqMessage = req.serialize();
// 2.5 增加长度报头
reqMessage = req.encode(reqMessage);
log(DEBUG, "客户端请求序列化成功 req: %s", reqMessage.c_str());
// 3. 发送
ssize_t s = send(sockfd, reqMessage.c_str(), reqMessage.size(), 0);
if (s < 0)
{
log(ERROR, "客户端发送数据失败!");
continue;
}
log(DEBUG, "客户端数据发送成功~");
// 4. 接收数据
s = recv(sockfd, buffer, 1024, 0);
if (s < 0)
{
log(ERROR, "客户端接收数据失败!");
continue;
}
log(DEBUG, "客户端数据接收成功~");
// 接收成功就反序列化
buffer[s] = 0;
client_buffer += buffer;
std::string ret;
response resp;
while (true)
{
ret = resp.decode(client_buffer);
if (ret.empty())
break;
if (!resp.deserialize(ret))
{
continue;
break;
}
log(DEBUG, "客户端数据反序列化成功~");
// 计算成功
if (resp._code == 0)
cout << req._x << req._op << req._y << "=" << resp._result << endl;
else
{
cout << req._x << req._op << req._y << "=?" << endl;
log(INFO, "计算出错了 _code: %d", resp._code);
}
}
}
return 0;
}
客户端这里就不详细介绍了,基本和服务器也是相同的。
同时也可以继续扩招,这个服务只能进行 x op y 的操作,也可以对这个进行扩展,还有就是在输入的时候也是可以扩展的,可以扩展为直接输入字符串,然后对字符串进行解析,解析为有效载荷,然后进行反序列化,在进行序列化,在添加报头,和前面基本一样。