目录
自定义协议
应用层
其实我们程序员平常写的程序代码去解决问题的网络程序,都是在应用层。
什么是协议?
协议是一种“约定”,我们使用的socket api的接口,都是按字符串形式发送和接收的,如果我们要传输一些“结构化的数据”,怎么办呢?
其实协议就是一种双方约定好的一种结构化数据。
网络版计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
约定方案一:
- 客户端发送一个形如“1+1”的字符串。
- 这个字符串中有两个操作数,都为整形。
- …
约定方案二: - 定义结构体表示我们所需要交互的信息。
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构。
- 这个过程叫做序列化和反序列化。
什么是序列化和反序列化
看这个图就能够知道什么是序列化和反序列化,其实就是把结构化数据转换为字符串,然后再转换回来的一个过程。
重新理解read、write、recv、send和tcp为什么支持全双工
在tcp传输层我们可以看到,一个fd代表一个连接,一个连接,两个缓冲区!!!
write、read等函数本质不都是往缓冲区拷贝数据然后由tcp自己或者说是OS来把缓冲区的数据刷新到网络中,以前我们是刷新到磁盘,现在变成了网络!!!
数据发送什么时候发?发多少?出错了怎么办?都由tcp决定,OS完成!!!所以tcp叫做传输控制协议。
- read、write、recv、send本质都是拷贝函数。
- 发数据的本质:是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方的接收缓冲区。
- 这不就是tcp支持全双工通信的原因。
- 这也是tcp叫做传输控制协议的原因(OS)。
- 生产消费模型。
- 为什么IO函数要阻塞啊!本质就是在维护同步关系!
tcp我们已经知道是面向字节流的,那就出现一个问题,客户端发的,服务端不一定就能全部收到啊,可能收一半呢?
所以我们就需要进行相应的解析,下面我们自己写一个网络计算器,自定义一个协议,来理解这部分内容!!!
网络版计数器
Socket.hpp
这个文件主要是使用模版设计模式来封装一个Socket套接字。
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
namespace socket_ns
{
class Socket;
using SockSPtr = std::shared_ptr<Socket>;
using namespace log_ns;
const static int gbacklog = 8;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
LISTEN_ERROR
};
// 模版方法模式
class Socket
{
public:
virtual void CreateSocketOrDie() = 0;
virtual void CreateBindOrDie(uint16_t port) = 0;
virtual void CreateListenOrDie(int backlog = gbacklog) = 0;
virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;
virtual bool Connector(const std::string &peerip, uint16_t peerport) = 0;
virtual int Sockfd() = 0;
virtual void Close() = 0;
virtual ssize_t Recv(std::string *out) = 0;
virtual ssize_t Send(const std::string &in) = 0;
public:
void BuildListenSocket(uint16_t port)
{
CreateSocketOrDie();
CreateBindOrDie(port);
CreateListenOrDie();
}
bool BuildClientSocket(const std::string &peerip, uint16_t peerport)
{
CreateSocketOrDie();
return Connector(peerip, peerport);
}
// void BuildUdpSocket()
//{}
};
class TcpSocket : public Socket
{
public:
TcpSocket()
{
}
TcpSocket(int sockfd) : _sockfd(sockfd)
{
}
void CreateSocketOrDie() override
{
// 1.创建socket
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "socket create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);
}
void CreateBindOrDie(uint16_t port) override
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
// 2.bind sockfd 和 Socket addr
if (::bind(_sockfd, (struct sockaddr *)&local, sizeof local) < 0)
{
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
LOG(INFO, "bind success\n");
}
void CreateListenOrDie(int backlog) override
{
// 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
if (::listen(_sockfd, gbacklog) < 0)
{
LOG(FATAL, "listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO, "listen success\n");
}
SockSPtr Accepter(InetAddr *cliaddr) override
{
struct sockaddr_in client;
socklen_t len = sizeof client;
// 4.获取新连接
int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
return nullptr;
}
*cliaddr = InetAddr(client);
LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", cliaddr->AddrStr().c_str(), _sockfd);
return std::make_shared<TcpSocket>(sockfd); // C++14
}
bool Connector(const std::string &peerip, uint16_t peerport) override
{
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(peerport);
// server.sin_addr.s_addr =
::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr.s_addr);
int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect sockfd error" << std::endl;
return false;
}
return true;
}
int Sockfd()
{
return _sockfd;
}
void Close()
{
if (_sockfd > 0)
{
::close(_sockfd);
}
}
ssize_t Recv(std::string *out) override
{
char inbuffer[4096]; // 当做字符串
ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
if (n > 0)
{
inbuffer[n] = 0;
*out += inbuffer;
}
return n;
}
ssize_t Send(const std::string &in) override
{
return ::send(_sockfd, in.c_str(), in.size(), 0);
}
private:
int _sockfd;
};
// class UdpSocket : public Socket
//{};
};
TcpSocket.hpp
这个文件建立出具体的TcpServer服务器端的实现逻辑。
#pragma once
#include<functional>
#include"Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace log_ns;
using namespace socket_ns;
static const uint16_t gport = 8888;
using service_io_t = std::function<void(SockSPtr, InetAddr &)>;
class TcpServer
{
public:
TcpServer(service_io_t service, uint16_t port = gport)
: _port(port),
_listensock(std::make_shared<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensock->BuildListenSocket(_port);
}
class ThreadData
{
public:
SockSPtr _sockfd;
TcpServer *_self;
InetAddr _addr;
public:
ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr):_sockfd(sockfd), _self(self), _addr(addr)
{}
};
void Loop()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
SockSPtr newsock = _listensock->Accepter(&client);
if(newsock == nullptr)
continue;
LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->Sockfd());
// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
pthread_t tid;
ThreadData *td = new ThreadData(newsock, this, client);
pthread_create(&tid, nullptr, Execute, td);//新线程进行分离
}
_isrunning = false;
}
static void *Execute(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
td->_self->_service(td->_sockfd, td->_addr);
td->_sockfd->Close();
delete td;
return nullptr;
}
~TcpServer()
{
}
private:
uint16_t _port;
SockSPtr _listensock;
bool _isrunning;
service_io_t _service;
};
service.hpp
这个文件内用来设计完成IOServer逻辑实现。
#pragma once
#include <iostream>
#include<functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include"Protocol.hpp"
using namespace socket_ns;
using namespace log_ns;
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;
class IOService
{
public:
IOService(process_t process) :_process(process)
{
}
void IOExcute(SockSPtr sock, InetAddr &addr)
{
std::string packagestreamqueue;
while (true)
{
//1.负责读取
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
break;
}
std::cout << "----------------------------------------------" << std::endl;
std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;
//我们能保证我们读到的是一个完整的报文吗?不能!
//2.报文解析
std::string package = Decode(packagestreamqueue);
if(package.empty()) continue;
// 我们能保证我们读到的是一个完整的报文吗?能!!!
auto req = Factory::BuildRequestDefault();
std::cout << "package: \n" << package << std::endl;
//3.反序列化
req->Deserialize(package);
//4.业务处理
auto resp = _process(req);// 通过一个请求,得到应答
//5.序列化应答
std::string respjson;
resp->Serialize(&respjson);
std::cout << "respjson: \n" << respjson << std::endl;
//6.添加len长度报头
respjson = Encode(respjson);
std::cout << "respjson add header done: \n" << respjson << std::endl;
sock->Send(respjson);
}
}
~IOService()
{
}
private:
process_t _process;
};
Protocol.hpp
这个文件实现了自定义协议逻辑。
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
static const std::string sep = "\r\n";
// 设计一下协议的报头和报文的完整格式
//"len"\r\n"{json}"\r\n --- 完整的报文, len 有效载荷的长度!
//\r\n: 区分len 和json串
//\r\n: 暂时没有其他用,打印方便,debug
// 添加报头
std::string Encode(const std::string &jsonstr)
{
int len = jsonstr.size();
std::string lenstr = std::to_string(len);
return lenstr + sep + jsonstr + sep;
}
// 不能带const
//"le
//"len"
//"len"\r\n
//"len"\r\n"{
//"len"\r\n"{json}"
//"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r
//"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n
std::string Decode(std::string &packagestream)
{
// 分析
auto pos = packagestream.find(sep);
if (pos == std::string::npos)
return std::string();
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr);
// 计算一个完整的报文应该是多长
int total = lenstr.size() + len + 2 * sep.size();
if (packagestream.size() < total)
return std::string();
// 提取
std::string jsonstr = packagestream.substr(pos + sep.size(), len);
packagestream.erase(0, total);
return jsonstr;
}
class Request
{
public:
Request()
{
}
bool Serialize(std::string *out)
{
// 1.使用现成的库,xml,json(jsoncpp),protobuf
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
// Json::StyledWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
~Request()
{
}
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
void SetValue(int x, int y, char oper)
{
_x = x;
_y = y;
_oper = oper;
}
private:
int _x;
int _y;
char _oper; //+ - * / % (x oper y)
};
class Response
{
public:
Response() : _result(0), _code(0), _desc("success")
{
}
bool Serialize(std::string *out)
{
// 1.使用现成的库,xml,json(jsoncpp),protobuf
Json::Value root;
root["result"] = _result;
root["code"] = _code;
root["desc"] = _desc;
Json::FastWriter writer;
// Json::StyledWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
bool Deserialize(const std::string &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();
_desc = root["desc"].asString();
return true;
}
void PrintResult()
{
std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl;
}
~Response()
{
}
public:
int _result;
int _code; // 0:success, 1:div zero 2. 非法操作
std::string _desc;
};
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
static std::shared_ptr<Response> BuildResponseDefault()
{
return std::make_shared<Response>();
}
};
NetCal.hpp
具体的网络计算器服务的计算实现逻辑。
#pragma once
#include"Protocol.hpp"
#include<memory>
class NetCal
{
public:
NetCal()
{}
~NetCal()
{}
std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
{
auto resp = Factory::BuildResponseDefault();
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;
resp->_desc = "div zero";
}
else
{
resp->_result = req->X() / req->Y();
}
}
break;
case '%':
{
if(req->Y() == 0)
{
resp->_code = 2;
resp->_desc = "mod zero";
}
else
{
resp->_result = req->X() % req->Y();
}
}
break;
default:
{
resp->_code = 3;
resp->_desc = "illegal operation";
}
break;
}
return resp;
}
};
ServerMain.cc
服务端启动逻辑
#include "TcpServer.hpp"
#include "Service.hpp"
#include"NetCal.hpp"
//./calserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage:" << argv[0] << "local-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
//我们的软件代码,我们手动的划分了三层
NetCal cal;
IOService service(std::bind(&NetCal::Calculator, &cal, std::placeholders::_1));
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
std::bind(&IOService::IOExcute,
&service, std::placeholders::_1,
std::placeholders::_2),
port);
tsvr->Loop();
return 0;
}
ClientMain.cc
客户端启动逻辑
#include <iostream>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace socket_ns;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage:" << argv[0] << "server-ip" << "server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
SockSPtr sock = std::make_shared<TcpSocket>();
if (!sock->BuildClientSocket(serverip, serverport))
{
std::cerr << "connect error" << std::endl;
exit(1);
}
srand(time(nullptr) ^ getpid());
const std::string opers = "+-*/%&^!";
std::string packagestreamqueue;
while (true)
{
// 构建数据
int x = rand() % 10;
usleep(x * 1000);
int y = rand() % 10;
usleep(x * y * 100);
char oper = opers[y % opers.size()];
std::cout << 1 << std::endl;
// 构建请求
auto req = Factory::BuildRequestDefault();
req->SetValue(x, y, oper);
// 1.序列化
std::string reqstr;
req->Serialize(&reqstr);
// 2.添加长度报头字段
reqstr = Encode(reqstr);
std::cout << "#####################################" <<std::endl;
std::cout << "request string: \n" << reqstr << std::endl;
// 3.发送数据
sock->Send(reqstr);
while (true)
{
// 4.读取应答,response
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
break;
}
// 我们能保证我们读到的是一个完整的报文吗?不能!
// 5.报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if (package.empty())
continue;
std::cout << "package: \n" << package << std::endl;
//6.反序列化
auto resp = Factory::BuildResponseDefault();
resp->Deserialize(package);
//7.打印结果
resp->PrintResult();
break;
}
sleep(1);
}
sock->Close();
return 0;
}
Makefile
自动化处理工具
.PHONY:all
all:calserver calclient
calclient:ClientMain.cc
@g++ -o $@ $^ -std=c++14 -lpthread -ljsoncpp
calserver:ServerMain.cc
@g++ -o $@ $^ -std=c++14 -ljsoncpp
.PHONY:clean
clean:
@rm -rf calserver
总结
这个网络版计算器使用了三层结构,分别对应了ISO七层模型中的应用层(NetCal.hpp)、会话层(Service.hpp)、表示层(TcpSocket.hpp),进行了解耦。