基于TCP的网络计算器实现

小灵蛇 2024-09-19 16:37:10 阅读 66

目录

一.   重新理解协议

二.   序列化与反序列化

2.1   概念

2.2   重新理解 read、write、recv、send 和 tcp 为什么支持全双工 

2.3   理解TCP面向字节流

三.   请求应答模块实现

3.1   添加与解析报头

3.2   定制协议:

3.3   Json

四.   计算模块实现

五.   Socket自主实现封装

六.   服务端模块实现

七.   服务端调用模块实现

八.   客户端模块实现

九.   效果展示


此篇博客我们来讲解基于TCP的网络计算器的实现,其实重点是讲解序列化和反序列化。话不多说,开始今日份学习吧。

一.   重新理解协议

        前面说过, 在网络层面,协议 是一组规则、标准或约定,它们定义了在网络环境中,计算机、服务器、路由器、交换机等网络设备之间如何相互通信和交换信息。这些规则涵盖了数据格式、数据交换的顺序、速度、以及数据同步方式等各个方面,以确保数据在网络中的传输是可靠、有效和安全的。

        但在此节,我们要加深对协议的理解:在语言层面,协议就是通信双方定制一样的结构化的数据。

二.   序列化与反序列化

2.1   概念

        在网络发送消息过程中,我们不方便直接传输结构化数据。而是将结构化数据转化为字符串,进行网络传输,将字符串发送给对方。到达目的地后再转化为结构化数据。这就是序列化与反序列化。

序列化:将数据结构或对象状态转换成可以存储或传输的格式的过程。

反序列化:序列化的逆过程,将之前序列化后的中间格式转换回原始的数据结构或对象状态。

那为什么要这么做呢?

为什么需要序列化

数据持久化:将对象的状态保存到文件或数据库中,以便程序可以在未来的某个时间点重新创建或恢复对象。网络通信:在网络中传输对象时,由于网络协议基于字节流,所以需要将对象序列化为字节流才能在网络上传输。对象传输:在进程间通信(IPC)或远程过程调用(RPC)中,对象需要被序列化后才能在不同的内存地址空间中传输。安全传输:在发送敏感数据前,序列化后可以进行加密处理,增加数据传输的安全性。


 为什么需要反序列化

数据恢复:从文件或数据库中读取序列化的数据,并将其恢复为原始的对象或数据结构,以便在程序中进一步使用。接收网络数据:在网络通信中,接收到的序列化数据需要被反序列化为对象,以便程序能够理解和处理这些数据。对象重构:在进程间通信或远程过程调用中,接收到的序列化数据需要被反序列化为对象,以便在当前进程中使用。

总的来说:

跨平台兼容性:不同的编程语言和平台可能有不同的内存布局、字节序(endianess)和数据类型表示方式。直接发送结构化数据可能会导致接收方无法正确解析这些数据,因为它们可能不遵循接收方平台上的数据表示规则。序列化将结构化数据转换为一种平台无关的格式(如JSON、XML、Protobuf等),从而确保了数据可以在不同的平台之间无缝传输。网络传输效率:直接发送结构化数据可能包含大量的冗余信息(如对齐填充、指针等),这些信息对于网络传输来说是不必要的,并且会增加传输的数据量。序列化过程可以去除这些冗余信息,只保留必要的数据,从而提高网络传输的效率。

2.2   重新理解 read、write、recv、send 和 tcp 为什么支持全双工 

       何为全双工,即可以发送数据也能接收数据。在之前TCP的相关代码实现中,我们发现也确实是这样的。客户端可以给服务端发送消息,而客户端也能接收来自服务端发送的消息。 

       其实read、write、recv和send函数本质是拷贝函数。就拿read函数来讲。

<code>ssize_t read(int fd, void *buf, size_t count);

        read函数的作用就是将文件描述符 fd 对应的数据拷贝到对应的缓冲区 buf 中。也就是将数据从内核空间拷贝到了用户空间。那这和全双工有什么关系呢?

        我们知道tcp套接字的本质也是一个文件描述符,但是tcp套接字在设计的时候其实设计了两个缓冲区,一个用来发送数据一个用来接受数据,在通信时客户端和服务端会各自创建一个套接字,此时其实一共存在四个缓冲区。当客户端发送数据时其实是将客户端发送缓冲区的数据拷贝到服务端的接收缓冲区,而客户端接收数据时其实是将服务端的发送缓冲区的数据拷贝到客户端的接收缓冲区,这两个缓冲区相对独立互不干扰。

所以:

在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工。这就是为什么一个 tcp sockfd 读写都是它的原因。实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议。

2.3   理解TCP面向字节流

        由于TCP是面向字节流,所以他不像UDP一样发送与接收的都是一个完整的数据报,我们TCP在接收时,可能接收到的是半个请求,也有可能是一个半个请求。这就是TCP的粘报问题。类似于我们平时蒸包子,蒸好后你去拿,可能拿到的是半个包子,也有可能是多个包子粘在一起。

为了解决这个问题,我们采用在报文前面添加报头的方式,来检测我们是否收到一个完整的请求。当检测到不足一个时,可以继续往下读,读到下一个报头就读到完整的报文。具体实现我们通过代码实现。

三.   请求应答模块实现

3.1   添加与解析报头

此处为了与如今网络上一致,我们采用 \r\n 作为行与行之间的分隔符。

首先解决粘报问题,在发送数据之前添加报头:

<code>const string SEP="\r\n";code>

string Encode(const string& json_str)

{

int json_str_len=json_str.size();

string proto_str=to_string(json_str_len);

proto_str+=SEP;

proto_str+=json_str;

proto_str+=SEP;

return proto_str;

}

 可以看到我们添加的报头是字符串的长度,添加报头之后得到的报文就像这样:

"len"\r\n"{ }"\r\n;//大括号里面是正文部分。

在接收数据之后解析报头:

const string SEP="\r\n";code>

string Decode(string& inbuffer)

{

auto pos=inbuffer.find(SEP);

if(pos==string::npos)//报文没有\r\n,报文不完整

{

return string();

}

//计算报文的总长度

string len_str=inbuffer.substr(0,pos);

if(len_str.empty())

{

return string();

}

int packlen=stoi(len_str);

int total=packlen+len_str.size()+2*SEP.size();

if(inbuffer.size()<total)

{

return string();

}

//到这说明收到的报文流一定存在一个完整的报文

string package=inbuffer.substr(pos+SEP.size(),packlen);

inbuffer.erase(0,total);

return package;

}

3.2   定制协议:

        定制协议就是定制一个双方需要的结构化字段,对于网络计算器来说,我们需要设计一个类包含操作数和操作符,而结果的返回我们也需要设计一个类,包含计算结果、返回码及相关描述等信息。

class Request

{

public:

Request()

{}

Request(int x, int y, char oper)

: _x(x), _y(y), _oper(oper)

{}

bool Serialize(string *out) // 序列化

{}

bool Deserialize(string &in) // 反序列化

{}

~Request()

{}

public:

int _x;

int _y;

char _oper; // "+-*/%" _x _oper _y

};

class Response

{

public:

Response()

{}

Response(int result, int code)

: _result(result), _code(code)

{}

bool Serialize(string *out) // 序列化

{}

bool Deserialize(string &in) // 反序列化

{}

public:

int _result;//结果

int _code;//0:success 1:除0 2:非法操作 3. 4. 5

};

有了结构化数据大体框架之后,我们来看看如何序列化与反序列化,此处我们采用Json来实现。

3.3   Json

要能使用Json,必须包含 jsoncpp 这个库。

#include<jsoncpp/json/json.h>

        Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。

安装:

C++

ubuntu:sudo apt-get install libjsoncpp-dev

Centos: sudo yum install jsoncpp-devel

序列化

 首先我们需要创建一个Json::Value对象,如代码所示,我们将p对象的成员赋值给了Json::Value 的root对象,于此同时我们还给这些成员各自起了一个名字,这样我们就可以通过键值对的方式来找到对应的数据了。

#include <iostream>

#include <string>

#include <jsoncpp/json/json.h>

int main()

{

people p("joe","男");

Json::Value root;

root["name"] = p.name;

root["sex"] = p.sex;

return 0;

}

我们可以创建一个Json::FastWriter对象,调用它的write方法:

#include <iostream>

#include <string>

#include <jsoncpp/json/json.h>

int main()

{

people p("joe","男");

Json::Value root;

root["name"] = p.name;

root["sex"] = p.sex;

Json::FastWriter writer;

std::string s = writer.write(root);

return 0;

}

序列化出来的字符串就是这样的:

{"name":"joe","sex":"男"}

反序列化:

 使用Json::Reader

int main()

{

std::string str={"name":"joe","sex":"男"}

Json::Value root;

Json::Reader read;

//将Json字符串写到Json对象中

bool ret = read.parse(str, root);

//将Json对象中的数据写到数据化结构中

People p;

p.name= root["x"].asString();

p.sex= root["y"].asString();

}


那我们该如何去实现请求与应答的序列化与反序列化呢?仿照上面的就可写出:

class Request

{

public:

Request()

{}

Request(int x, int y, char oper)

: _x(x), _y(y), _oper(oper)

{}

bool Serialize(string *out) // 序列化

{

// 转化成为字符串

Json::Value root;

root["x"] = _x;

root["y"] = _y;

root["oper"] = _oper;

Json::FastWriter writer;

*out = writer.write(root);

return true;

}

bool Deserialize(string &in) // 反序列化

{

Json::Value root;

Json::Reader reader;

bool res = reader.parse(in, root);

if(!res) return false;

_x = root["x"].asInt();

_y = root["y"].asInt();

_oper = root["oper"].asInt();

return res;

}

~Request()

{}

public:

int _x;

int _y;

char _oper; // "+-*/%" _x _oper _y

};

class Response

{

public:

Response()

{}

Response(int result, int code)

: _result(result), _code(code)

{}

bool Serialize(string *out) // 序列化

{

// 转化成为字符串

Json::Value root;

root["result"] = _result;

root["code"] = _code;

Json::FastWriter writer;

*out = writer.write(root);

return true;

}

bool Deserialize(string &in) // 反序列化

//你怎么保证读到的in是完整的呢

{

Json::Value root;

Json::Reader reader;

bool res = reader.parse(in, root);

if(!res) return false;

_result = root["result"].asInt();

_code = root["code"].asInt();

return true;

}

public:

int _result;//结果

int _code;//0:success 1:除0 2:非法操作 3. 4. 5

};

最后我们再封装一个类,用来实现创建出请求与应答:

class Factory

{

public:

Factory()

{

srand(time(nullptr)^getpid());

opers="+/*/%^&|";code>

}

shared_ptr<Request> BuildRequest()

{

int x=rand()%10+1;

usleep(x*10);

int y=rand()%5;

usleep(y*x*5);

char oper=opers[rand()%opers.size()];

shared_ptr<Request> req=make_shared<Request>(x,y,oper);

return req;

}

shared_ptr<Response> BuildResponse()

{

return make_shared<Response>();

}

~Factory()

{}

private:

string opers;

};

由此我们的请求应答模块就实现完整了,代码总体如下:

#pragma once

#include <iostream>

#include <string>

#include<jsoncpp/json/json.h>

#include<sys/types.h>

#include<unistd.h>

using namespace std;

namespace protocol_ns

{

const string SEP="\r\n";code>

//我们把tcp中读到的报文,可能读到半个,也可能读到1个半等-->TCP 粘报问题

//解决TCP粘报问题

string Encode(const string& json_str)

{

int json_str_len=json_str.size();

string proto_str=to_string(json_str_len);

proto_str+=SEP;

proto_str+=json_str;

proto_str+=SEP;

return proto_str;

}

// "len"\r\n"{

// "len"\r\n"{ }"

// "len"\r\n"{ }"\r\n;

// "len"\r\n"{ }"\r\n"len";

// "len"\r\n"{ }"\r\n"len"\r\n"{ }";

// "len"\r\n"{ }"\r\n"len"\r\n"{ }"\r\n

string Decode(string& inbuffer)

{

auto pos=inbuffer.find(SEP);

if(pos==string::npos)//不完整

{

return string();

}

string len_str=inbuffer.substr(0,pos);

if(len_str.empty())

{

return string();

}

int packlen=stoi(len_str);

int total=packlen+len_str.size()+2*SEP.size();

if(inbuffer.size()<total)

{

return string();

}

string package=inbuffer.substr(pos+SEP.size(),packlen);

inbuffer.erase(0,total);

return package;

}

// 我们协议的样子

// 报文 = 报头 + 有效载荷

//"有效载荷的长度""\r\n""有效载荷""\r\n"

//"len""\r\n"_x _op _y""\r\n" ->len:有效载荷的长度,约定\r\n是分隔符,不参与统计

// 结构化数据,不能向网络中直接发送,需转化为字符串

//其他问题:struct Request req(10,20,"*"),为什么不直接发送req对象?可以但不建议,跨平台性差

class Request

{

public:

Request()

{}

Request(int x, int y, char oper)

: _x(x), _y(y), _oper(oper)

{}

bool Serialize(string *out) // 序列化

{

// 转化成为字符串

Json::Value root;

root["x"] = _x;

root["y"] = _y;

root["oper"] = _oper;

Json::FastWriter writer;

*out = writer.write(root);

return true;

}

bool Deserialize(string &in) // 反序列化

{

Json::Value root;

Json::Reader reader;

bool res = reader.parse(in, root);

if(!res) return false;

_x = root["x"].asInt();

_y = root["y"].asInt();

_oper = root["oper"].asInt();

return res;

}

~Request()

{}

public:

int _x;

int _y;

char _oper; // "+-*/%" _x _oper _y

};

class Response

{

public:

Response()

{}

Response(int result, int code)

: _result(result), _code(code)

{}

bool Serialize(string *out) // 序列化

{

// 转化成为字符串

Json::Value root;

root["result"] = _result;

root["code"] = _code;

Json::FastWriter writer;

*out = writer.write(root);

return true;

}

bool Deserialize(string &in) // 反序列化

//你怎么保证读到的in是完整的呢

{

Json::Value root;

Json::Reader reader;

bool res = reader.parse(in, root);

if(!res) return false;

_result = root["result"].asInt();

_code = root["code"].asInt();

return true;

}

public:

int _result;//结果

int _code;//0:success 1:除0 2:非法操作 3. 4. 5

};

class Factory

{

public:

Factory()

{

srand(time(nullptr)^getpid());

opers="+/*/%^&|";code>

}

shared_ptr<Request> BuildRequest()

{

int x=rand()%10+1;

usleep(x*10);

int y=rand()%5;

usleep(y*x*5);

char oper=opers[rand()%opers.size()];

shared_ptr<Request> req=make_shared<Request>(x,y,oper);

return req;

}

shared_ptr<Response> BuildResponse()

{

return make_shared<Response>();

}

~Factory()

{}

private:

string opers;

};

}

四.   计算模块实现

我们产生了请求之后,需要根据请求里面的操作数以及操作符,计算出结果,如果操作数与操作符不符合计算习惯,还需要填写应答里面的状态。

仍然封装成一个类:

class Calculate

{

public:

Calculate()

{

}

Response Excute(const Request &req)

{

Response resp{0, 0};

switch (req._oper)

{

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;

}

~Calculate()

{}

private:

};

五.   Socket自主实现封装

为了更好的代码独立性,我们自己封装一下Socket的相关操作。

我们设计了一个基类Socket,并在基类里面设计一些虚函数。让TcpSocket继承这个基类,只需在TcpSocket里面重写虚函数,实现具体操作即可。此处由于只涉及TCP,所以我们之封装了TcpSocket。读者可以自行封装UdpSocket。

#include "InetAddr.hpp"

#include "Log.hpp"

namespace socket_ns

{

class Socket;

const static int gbacklog = 8;

using socket_sptr = shared_ptr<Socket>;

enum

{

SOCKET_ERROR = 1,

BIND_ERROR,

LISTEN_ERROR,

USAGE_ERROR

};

class Socket

{

public:

virtual void CreateSocketOrDie() = 0;

virtual void BindSocketOrDie(InetAddr &addr) = 0;

virtual void ListenSocketOrDie() = 0;

virtual socket_sptr Accepter(InetAddr *addr) = 0;

virtual bool Connectcor(InetAddr &addr) = 0;

virtual int Sockfd() = 0;

virtual int Recv(string *out) = 0;

virtual int Send(const string &in)=0;

public:

void BuildListenSocket(InetAddr &addr)

{

CreateSocketOrDie();

BindSocketOrDie(addr);

ListenSocketOrDie();

}

bool BuildClientSocket(InetAddr &addr)

{

CreateSocketOrDie();

return Connectcor(addr);

}

};

class TcpSocket : public Socket

{

public:

TcpSocket(int fd=-1)

:_sockfd(fd)

{}

void CreateSocketOrDie() override

{

// 1.创建流式套接字

_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);

if (_sockfd < 0)

{

LOG(FATAL, "socket error\n");

exit(SOCKET_ERROR);

}

LOG(DEBUG, "socket create success, sockfd is: %d\n", _sockfd);

}

void BindSocketOrDie(InetAddr &addr)

{

// 2.bind

struct sockaddr_in local;

memset(&local, 0, sizeof(local));

local.sin_family = AF_INET;

local.sin_port = htons(addr.Port());

local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

if (n < 0)

{

LOG(FATAL, "bind error\n");

exit(BIND_ERROR);

}

LOG(DEBUG, "bind success,sockfd is: %d\n", _sockfd);

}

void ListenSocketOrDie() override

{

int n = ::listen(_sockfd, gbacklog);

if (n < 0)

{

LOG(FATAL, "listen error\n");

exit(LISTEN_ERROR);

}

LOG(DEBUG, "listen success,sockfd is: %d\n", _sockfd);

}

socket_sptr Accepter(InetAddr *addr)

{

struct sockaddr_in peer;

socklen_t len = sizeof(peer);

int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);

if (sockfd < 0)

{

LOG(WARNING, "accept error\n");

return nullptr;

}

*addr = peer;

socket_sptr sock = make_shared<TcpSocket>(sockfd);

return sock;

}

bool Connectcor(InetAddr &addr)

{

// 构建目标主机的socket信息

struct sockaddr_in server;

memset(&server, 0, sizeof(server));

server.sin_family = AF_INET;

server.sin_port = htons(addr.Port());

server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));

if (n < 0)

{

cerr << "connect error"<< endl;

return false;

}

return true;

}

int Sockfd() override

{

return _sockfd;

}

int Recv(string *out) override

{

char inbuffer[1024];

ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1,0);

if (n > 0)

{

inbuffer[n] = 0;

*out+=inbuffer;//??? +=

}

return n;

}

int Send(const string& in) override

{

int n=::send(_sockfd,in.c_str(),in.size(),0);

return n;

}

private:

int _sockfd;

};

}

对于,InetAddr.hpp与Log.hpp两个文件,前面几篇博客均提到了,可以去看看(点此查看)。

六.   服务端模块实现

服务端我设计了一个类TcpServer来实现具体的代码实现。

并且我们为了保证代码的低耦合,服务端我们只需调用服务即可,并不需要执行具体服务,采用回调函数的方式让上层实现,这也是我们上面将计算服务封装起来的原因。

using namespace std;

using namespace socket_ns;

class TcpServer;

using io_service_t=function<void(socket_sptr sockfd,InetAddr client)>;//处理方法

struct ThreadData

{

public:

ThreadData(socket_sptr fd,InetAddr addr,TcpServer* s)

:sockfd(fd)

,clientaddr(addr)

,self(s)

{}

public:

socket_sptr sockfd;

InetAddr clientaddr;

TcpServer* self;

};

class TcpServer

{

public:

TcpServer(int port,io_service_t service)

:_localaddr("0",port)

,_listensock(make_unique<TcpSocket>())

,_service(service)

,_isrunning(false)

{

_listensock->BuildListenSocket(_localaddr);

}

static void* HandlerSock(void* args)

{

pthread_detach(pthread_self());

ThreadData* td=static_cast<ThreadData*>(args);

td->self->_service(td->sockfd,td->clientaddr);

::close(td->sockfd->Sockfd());

delete td;

return nullptr;

}

void Loop()

{

_isrunning=true;

//4.不能直接接收数据,应该先获取连接

while(_isrunning)

{

InetAddr peeraddr;

socket_sptr normalsock=_listensock->Accepter(&peeraddr);

if(normalsock==nullptr) continue;

//Version 2:采用多线程

//此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的

pthread_t t;

ThreadData* td=new ThreadData(normalsock,peeraddr,this);

pthread_create(&t,nullptr,HandlerSock,td);//将线程分离

}

_isrunning=false;

}

~TcpServer()

{

}

private:

InetAddr _localaddr;

unique_ptr<Socket> _listensock;

bool _isrunning;

io_service_t _service;

};

七.   服务端调用模块实现

服务端调用就要实现结构化数据与字符串的转换。

具体思路就是:

读取数据分析数据,确认完整报文反序列化业务处理数据响应,序列化,添加报头发送数据

void Usage(string proc)

{

cout << "Usage:\n\t" << proc << " serverport\n"

<< endl;

}

using callback_t = function<Response(const Request &req)>;

class Service

{

public:

Service(callback_t cb)

: _cb(cb)

{

}

void ServiceHelper(socket_sptr sockptr, InetAddr client)

{

int sockfd = sockptr->Sockfd();

LOG(DEBUG, "get a new link,info %s:%d,fd: %d\n", client.Ip().c_str(), client.Port(), sockfd);

string clientaddr = "[" + client.Ip() + ":" + to_string(client.Port()) + "]# ";

string inbuffer;

while (true)

{

Request req;

// 1.读取数据

// 你怎么保证一个大的字符串里面有没有完整的请求

int n = sockptr->Recv(&inbuffer); // 你怎么保证读到的是完整的request->有效载荷的长度

if (n <= 0)

{

LOG(DEBUG, "client %s quit\n", clientaddr.c_str());

break;

}

// 2.分析数据,确认完整报文

string package;

while(true)

{

cout<<"inbuffer: "<<inbuffer<<endl;

package = Decode(inbuffer);

if (package.empty())

break;

cout<<"------------------begin--------------------"<<endl;

cout<<"resq string:\n"<<package<<endl;

// 到这,可以保证一定读到了一个完整的json串

// 3.反序列化

req.Deserialize(package);

// 4.业务处理

Response resp = _cb(req);

// 5.对应答做序列化

string send_str;

resp.Serialize(&send_str);

cout<<"resp Serialize:"<<endl;

cout<<send_str<<endl;

// 6.添加长度报头

send_str = Encode(send_str); //"len"\r\n""{ }""\r\n"

cout<<"resp Encode:"<<endl;

cout<<send_str<<endl;

//"len"\r\n""{ }""\r\n"

sockptr->Send(send_str);

}

}

}

private:

callback_t _cb;

};

// ./tcpserver serverport

int main(int argc, char *argv[])

{

if (argc != 2)

{

Usage(argv[0]);

return 1;

}

uint16_t port = stoi(argv[1]);

//daemon(0,0);

//EnableScreen();

Calculate cal;

Service calservice(bind(&Calculate::Excute, &cal, placeholders::_1));

io_service_t service = bind(&Service::ServiceHelper, &calservice, placeholders::_1, placeholders::_2);

unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port, service);

tsvr->Loop();

return 0;

}

八.   客户端模块实现

客户端就需要实现具体请求的关创建,并向服务端发送请求,然后接收服务端返回的应答。

具体思路是:

创建请求将数据序列化添加报头发送数据读取响应检测报头反序列化

using namespace std;

using namespace socket_ns;

using namespace protocol_ns;

void Usage(string proc)

{

cout<<"Usage:\n\t"<<proc<<" serverip serverport\n"<<endl;

}

// ./tcpclient serverip serverport

int main(int argc,char *argv[])

{

if(argc!=3)

{

Usage(argv[0]);

exit(1);

}

string serverip=argv[1];

uint16_t serverport=stoi(argv[2]);

InetAddr serveraddr(serverip,serverport);

Factory factory;

unique_ptr<Socket> cli=make_unique<TcpSocket>();

bool res=cli->BuildClientSocket(serveraddr);

string inbuffer;

while(res)

{

sleep(1);

//1.构建一个请求

auto req=factory.BuildRequest();

//2.对请求进行序列化

string send_str;

req->Serialize(&send_str);

cout<<"Serialize:\n"<<send_str<<endl;

//3.添加长度报头

send_str =Encode(send_str);

cout<<"Encode:\n"<<send_str<<endl;

//4."len"\r\n""{}"\r\n"

cli->Send(send_str);

//5.读取应答

int n=cli->Recv(&inbuffer);

if(n<=0) break;

string package=Decode(inbuffer);

if(package.empty()) continue;

//6.我能保证package一定是一个完整的应答

auto resp=factory.BuildResponse();

//6.1 反序列化

resp->Deserialize(package);

//7.拿到了结构化的应答吗

cout<<resp->_result<<"["<<resp->_code<<"]"<<endl;

}

return 0;

}

九.   效果展示


总结:

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。