目录
Sock.hpp
TcpServer.hpp
Protocol.hpp
CalServer.cc
CalClient.cc
分析
因为,TCP面向字节流,所以TCP有粘包问题,故我们需要应用层协议来区分每一个数据包。防止读取到半个,一个半数据包的情况。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
// 对于一些TCP相关调用的封装
class Sock
{
private:
const static int gback_log = 20;
public:
int Socket()
{
// 1. 创建套接字,成功返回对应套接字,失败直接进程exit
int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 网络套接字, 面向字节流(tcp)
if (listen_sock < 0)
{
logMessage(FATAL, "create listen socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create listen socket success: %d", listen_sock); // 1111Log
return listen_sock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
// 2. bind,注意云服务器不能绑定公网IP,不允许。
// 成功bind则成功bind,失败进程exit(bind不在循环语句内,故失败直接进程退出。)
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
}
void Listen(int sock)
{
// 3. listen监听: 因为TCP是面向连接的,在我们正式通信之前,需要先建立连接
// listen: 将套接字状态设置为监听状态。服务器要一直处于等待状态,这样客户端才能随时随地发起连接。
// 成功则成功,失败则exit
if (listen(sock, gback_log) < 0) // gback_log后面讲,全连接队列的长度。我猜测就是这个服务器同一时刻允许连接的客户端的数量最大值?也不太对呀,这么少么?
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "listen success");
}
// 一般经验
// const std::string &: 输入型参数
// std::string *: 输出型参数
// std::string &: 输入输出型参数
int Accept(int sock, uint16_t *port, std::string *ip)
{
// accept失败进程不退出,返回-1
// 成功则返回对应的通信套接字
struct sockaddr_in client;
socklen_t len = sizeof client;
// 其实accept是获取已经建立好的TCP连接。建立好的连接在一个内核队列中存放,最大数量的第二个参数+1
int service_sock = accept(sock, (struct sockaddr *)&client, &len); // 返回一个用于与客户端进行网络IO的套接字,不同于listen_sock
// On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
if (service_sock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
return -1; // accept失败不直接exit,而是返回-1。因为在循环语句内部。
}
if (port)
*port = ntohs(client.sin_port);
if (ip)
*ip = inet_ntoa(client.sin_addr);
logMessage(NORMAL, "link(accept) success, service socket: %d | %s:%d", service_sock,
(*ip).c_str(), *port);
return service_sock;
}
int Connect(int sock, const std::string &ip, const uint16_t &port)
{
// 惯例写一下:失败返回-1,成功则客户端与服务端连接成功,返回0
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
{
return -1;
}
return 0;
}
public:
Sock() = default;
~Sock() = default;
};
#ifndef _TCP_SERVER_HPP_
#define _TCP_SERVER_HPP_
#include "Sock.hpp"
#include
#include
// 说实话,这个TcpServer类实现的非常棒,真的很棒,网络和服务进行了解耦。
// 使用者直接BindServer, 然后start即可
namespace ns_tcpserver
{
using func_t = std::function; // 服务器提供的服务方法类型void(int),可变
class TcpServer;
class ThreadData
{
public:
ThreadData(int sock, TcpServer *server)
: _sock(sock), _server(server)
{}
~ThreadData() {}
public:
int _sock;
TcpServer *_server; // 因为静态成员函数呀
};
class TcpServer
{
// 不关心bind的ip和port,因为用不到啊,保留一个listen_sock用于accept就够了。
private:
int _listen_sock;
Sock _sock;
std::vector _funcs; // 服务器提供的服务
private:
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 线程分离(避免类似于僵尸进程状态)
ThreadData *td = (ThreadData *)args;
td->_server->excute(td->_sock); // 提供服务
close(td->_sock); // 保证四次挥手正常结束
delete td;
return nullptr;
}
public:
TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
{
// 创建监听套接字,bind,listen
_listen_sock = _sock.Socket();
_sock.Bind(_listen_sock, port, ip);
_sock.Listen(_listen_sock);
}
void start()
{
for (;;)
{
// 开始accept,然后执行任务
std::string ip;
uint16_t port; // 这两个东西,也并没有传给线程。
int sock = _sock.Accept(_listen_sock, &port, &ip); // 后面是输出型参数
if (sock == -1)
continue; // 本次accept失败,循环再次accept。目前来看几乎不会
// 连接客户端成功,ip port已有。但是这里没用...
pthread_t tid;
ThreadData *td = new ThreadData(sock, this);
pthread_create(&tid, nullptr, threadRoutine, (void *)td); // 新线程去提供service,主线程继续accept
}
}
void bindService(func_t service) // 暴露出去的接口,用于设置该服务器的服务方法
{
_funcs.push_back(service);
}
void excute(int sock)
{
for (auto &func : _funcs)
{
func(sock);
}
}
~TcpServer()
{
if (_listen_sock >= 0)
close(_listen_sock);
}
};
}
#endif
#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_
#include
#include
#include
#include
#include
#include
#include
#include
// important and new
namespace ns_protocol
{
// #define MYSELF 1 // 自己实现序列化反序列化还是使用json库
#define SPACE " "
#define SPACE_LENGTH strlen(SPACE)
#define SEP "\r\n"
#define SEP_LENGTH strlen(SEP)
// 请求和回复,都需要序列化和反序列化的成员函数
// 序列化和反序列化双方都不同。但是添加报头和去报头是相同的,"Length\r\nxxxxx\r\n";
// 客户端生成请求,序列化之后发送给服务端
class Request
{
public:
Request() = default;
Request(int x, int y, char op)
: _x(x), _y(y), _op(op)
{
}
~Request() {}
public:
int _x;
int _y;
char _op;
public:
std::string serialize()
{
// 序列化为"_x _op _y" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
#ifdef MYSELF
std::string s = std::to_string(_x);
s += SPACE;
s += _op;
s += SPACE;
s += std::to_string(_y);
return s;
#else
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool deserialize(const std::string &s)
{
#ifdef MYSELF
// "_x _op _y"
std::size_t left = s.find(SPACE);
if (left == std::string::npos)
return false;
std::size_t right = s.rfind(SPACE);
if (right == left)
return false;
_x = atoi(s.substr(0, left).c_str());
_op = s[left + SPACE_LENGTH];
_y = atoi(s.substr(right + SPACE_LENGTH).c_str());
#else
Json::Value root;
Json::Reader reader;
reader.parse(s, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asInt();
#endif
return true;
}
};
// 服务端收到请求,反序列化,业务处理生成response,序列化后发送给客户端
class Response
{
public:
Response(int result = 0, int code = 0)
: _result(result), _code(code)
{
}
~Response() {}
public:
std::string serialize()
{
// 序列化为"_code _result" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
#ifdef MYSELF
std::string s = std::to_string(_code);
s += SPACE;
s += std::to_string(_result);
return s;
#else
Json::Value root;
root["code"] = _code;
root["result"] = _result;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool deserialize(const std::string &s)
{
#ifdef MYSELF
// "_code _result"
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
_code = atoi(s.substr(0, pos).c_str());
_result = atoi(s.substr(pos + SPACE_LENGTH).c_str());
#else
Json::Value root;
Json::Reader reader;
reader.parse(s, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
#endif
return true;
}
public:
int _result;
int _code; // 状态码, 防止除零,模零,和其他错误(比如非法运算符运算符)。code == 0时,result有效。
};
// 进行去报头,报文完整则去报头,并返回有效载荷,不完整则代表失败返回空字符串。
std::string deCode(std::string &s) // 输入型输出型参数
{
// "Length\r\nx op y\r\n" 成功返回有效载荷,失败返回空串
std::size_t left = s.find(SEP);
if (left == std::string::npos)
return "";
std::size_t right = s.rfind(SEP);
if (right == left)
return "";
int length = atoi(s.substr(0, left).c_str());
if (length > s.size() - left - 2 * SEP_LENGTH)
return ""; // 有效载荷长度不足,不是一个完整报文,其实经过上面两次的if判断已经够了可能。
// 是一个完整报文,进行提取
std::string ret;
s.erase(0, left + SEP_LENGTH);
ret = s.substr(0, length);
s.erase(0, length + SEP_LENGTH);
return ret;
}
std::string enCode(const std::string &s)
{
// "Length\r\n1+1\r\n"
std::string retStr = std::to_string(s.size());
retStr += SEP;
retStr += s;
retStr += SEP;
return retStr;
}
// 我真的很想用引用,但是好像传统规则是输出型参数用指针...
// 其实这个Recv就是一个单纯的读数据的函数,将接收缓冲区数据读到应用层缓冲区中,也就是*s中。存储的是对端发来的应用层报文。
bool Recv(int sock, std::string *s)
{
// 仅仅读取数据到*s中
char buff[1024];
ssize_t sz = recv(sock, buff, sizeof buff, 0);
if (sz > 0)
{
buff[sz] = '\0';
*s += buff;
return true;
}
else if (sz == 0)
{
std::cout << "peer quit" << std::endl;
return false;
}
else
{
std::cout << "recv error" << std::endl;
return false;
}
}
bool Send(int sock, const std::string &s)
{
ssize_t sz = send(sock, s.c_str(), s.size(), 0);
if (sz > 0)
{
return true;
}
else
{
std::cout << "send error!" << std::endl;
return false;
}
}
}
#endif
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include
using namespace ns_tcpserver;
using namespace ns_protocol;
Response calculatorHelp(const Request &req)
{
// "1+1"???
Response resp;
int x = req._x;
int y = req._y;
switch (req._op)
{
case '+':
resp._result = x + y;
break;
case '-':
resp._result = x - y;
break;
case '*':
resp._result = x * y;
break;
case '/':
if (y == 0)
resp._code = 1;
else
resp._result = x / y;
break;
case '%':
if (y == 0)
resp._code = 2;
else
resp._result = x % y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
void calculator(int sock)
{
std::string s;
for (;;)
{
if (Recv(sock, &s) <= 0) // 输出型参数
break; // 大概率对端退出,则服务结束。一般不会读取失败recv error
std::string package = deCode(s);
if (package.empty())
continue; // 不是一个完整报文,继续读取(因为TCP面向字节流!!!)
// 读取到一个完整报文,且已经去了应用层报头,有效载荷在package中。如"1 + 2"
Request req;
req.deserialize(package);
Response resp = calculatorHelp(req);
std::string backStr = resp.serialize();
backStr = enCode(backStr);
if (!Send(sock, backStr)) // 发送失败就退出
break;
}
}
// ./cal_server port
int main(int argc, char **argv)
{
// std::cout << "test remake" << std::endl; // success
if (argc != 2)
{
std::cout << "\nUsage: " << argv[0] << " port\n"
<< std::endl;
exit(1);
}
std::unique_ptr server(new TcpServer(atoi(argv[1])));
server->bindService(calculator); // 给服务器设置服务方法,将网络服务和业务逻辑进行解耦
server->start(); // 服务器开始进行accept,连接一个client之后就提供上方bind的服务
return 0;
}
#include "Protocol.hpp"
#include "Sock.hpp"
#include
using namespace ns_protocol;
// ./client serverIp serverPort
int main(int argc, char **argv)
{
if (argc != 3)
{
std::cout << "\nUsage: " << argv[0] << " serverIp serverPort\n"
<< std::endl;
exit(1);
}
Sock sock;
int sockfd = sock.Socket();
// 客户端不需要显式bind, 老生常谈了。
if (sock.Connect(sockfd, argv[1], atoi(argv[2])) == -1)
{
std::cout << "connect error" << std::endl;
exit(3);
}
std::string backStr; //
bool quit = false;
while (!quit)
{
Request req;
std::cout << "Please enter# ";
std::cin >> req._x >> req._op >> req._y;
std::string reqStr = req.serialize();
reqStr = enCode(reqStr); // 添加应用层报头,此处添加报头(制定协议)是为了解决TCP粘包问题,因为TCP是面向字节流的。
if (!Send(sockfd, reqStr))
break;
while (true)
{
if (!Recv(sockfd, &backStr))
{
quit = true;
break;
}
std::string package = deCode(backStr);
if(package.empty())
continue; // 这次不是一个完整的应用层报文,继续读取
// 读取到一个完整的应用层报文,且已经去报头,获取有效载荷成功,在package中。(这个有效载荷是server发来的,计算结果)
Response resp;
resp.deserialize(package);
switch (resp._code)
{
case 1:
std::cout << "除零错误" << std::endl;
break;
case 2:
std::cout << "模零错误" << std::endl;
break;
case 3:
std::cout << "其他错误" << std::endl;
break;
default:
std::cout << req._x << " " << req._op << " " << req._y << " = " << resp._result << std::endl;
break;
}
break; // 退出防止TCP粘包问题的循环。
}
// 进行下一次获取用户输入,进行计算。
}
close(sockfd);
return 0;
}
可以分为两个模块:网络通信模块,应用层模块(包括应用层协议,以及应用层计算器逻辑)。
网络模块中,Sock.hpp就是一个简单的对于系统调用的封装,TcpServer.hpp的设计很优雅,内部有一个std::vector
应用层协议:一个Request,一个Response。分别是客户端的请求(x,y,运算符)和服务端的响应(计算结果)。这两个类,都有序列化和反序列化的方法,便于网络传输。还有一个Encode添加报头和Decode去报头的方法,这个其实就是应用层协议的报头,大体格式为 Length\r\nxxxx\r\n。目的就是解决TCP面向字节流所引起的粘包问题。
Recv内部就是一个recv调用,将读取的网络数据添加到一个输出型参数string*指向string的结尾。因为TCP粘包问题,所以可能读取的不是一个完整报文(半个?),故,在Decode方法内部,也就是去报头时,会检测此时是否有至少一个完整应用层报文。若有,则去报头,获取有效载荷。若没有则返回一个空串。上层可以通过判断是否为空串。判断是否读到了一个完整应用层报文,若没有,则再次Recv,直到读到一个完整应用层报文为止。所以,server和client在读取网络数据并去报头时,都是在while循环内部进行的。