CalculatorClient.cc
//telnet是一个命令,和接口要区分,可以向服务其发起tcp登录入telnet 127.0.0.1 8888
#include "TcpClient.hpp"
int main()
{
std::cout << "cal client" << std::endl;
return 0;
}
CalculatorServer.cc
#include "TcpServer.hpp"
#include <memory>
using namespace tcpserver_ns;
Response calculate(const Request &req)//服务器对于客户端发送的request的处理逻辑,也就是服务器对于客户端提供的一个具体的服务,以后只要将该函数以及Protocol.h文件一改,就当与是一个替换协议的过程。
{
// 走到这里,一定保证req是有具体数据的!
// _result(result), _code(code)
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;
}
// ./calserver 8888,服务器运行的时候给端口号参数,这边省略了直接在函数内部定义了。
int main()
{
uint16_t port = 8888;
std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port)); //服务器创建起来,该服务器只进行网络的io读取,并不进行数据的处理,数据处理逻辑时外部通过回调函数操作的。
tsvr->InitServer();//初始化服务器
tsvr->Start();//让服务器跑起来
return 0;
}
Err.hpp
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
SETSID_ERR,
OPEN_ERR
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
// 日志是有日志等级的
const std::string filename = "log/tcpserver.log";
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal,
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 "Fatal";
default:
return "Uknown";
}
}
static std::string getTime()
{
time_t curr = time(nullptr);
struct tm *tmp = localtime(&curr);
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;
}
// 日志格式: 日志等级 时间 pid 消息体
// logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); // DEBUG hello:12, world
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(), getpid());
char logRight[1024];
va_list p;
va_start(p, format);
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);
// 打印
printf("%s%s\n", logLeft, logRight);
// 保存到文件中
// FILE *fp = fopen(filename.c_str(), "a");
// if(fp == nullptr)return;
// fprintf(fp,"%s%s\n", logLeft, logRight);
// fflush(fp); //可写也可以不写
// fclose(fp);
// 预备
// va_list p; // char *
// int a = va_arg(p, int); // 根据类型提取参数
// va_start(p, format); //p指向可变参数部分的起始地址
// va_end(p); // p = NULL;
}
Sock.hpp
//对tcpsocket做一下封装
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#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: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(const uint16_t &port)//绑定套接字,只有服务器需要主动绑定,所以这个是专门给服务器写的接口。之所以参数里面没有ip地址是因为
{
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;
if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)//开始绑定
{
logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()//将套接字设置成为listen状态
{
if (listen(_sock, gbacklog) < 0)
{
logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string *clientip, uint16_t *clientport)//为tcp服务端准备的接口,这两个参数是输出型参数,获得客户端的信息。
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int sock = accept(_sock, (struct sockaddr *)&temp, &len);//tcp既然面向连接,就一定有人连我,现在就先把连接获取上来。
if (sock < 0)
{
logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
}
else
{
*clientip = inet_ntoa(temp.sin_addr);//网络转主机序列,成为点分十进制的ip地址
*clientport = ntohs(temp.sin_port);//网络转主机序列
}
return sock;
}
int Connect(const std::string &serverip, const uint16_t &serverport)//为tcp客户端准备的接口,传进来的参数告诉我们应该像谁进行连接,也就是服务端的ip和端口号。
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
return connect(_sock, (struct sockaddr *)&server, sizeof(server));//发起请求
}
int Fd()//用来获取套接字
{
return _sock;
}
~Sock()
{
if (_sock != defaultfd)
close(_sock);
}
private:
int _sock;//这个套接字是用来监听、connect、还是获取连接等等由用户自己来决定
};
TcpClient.hpp
#pragma once
#include <iostream>
TcpServer.hpp
//tcpserver和tcpclient这两个hpp文件是用来存放实现网络应用的函数接口
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include "Sock.hpp"
#include "Protocol.hpp"
namespace tcpserver_ns
{
using namespace protocol_ns;
class TcpServer;
using func_t = std::function<Response(const Request&)>;//func_t是一个函数类型,函数返回类型是Response,参数是Request
class ThreadData//pthread_create传参时需要很多的数据,比如说客户端的ip地址端口号,服务器创建的listen套接字等等,以及服务器对象,里面有服务器的业务逻辑
{
public:
ThreadData(int sock, std::string ip, uint16_t port, TcpServer *tsvrp)
: _sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp)
{
}
~ThreadData() {}
public:
int _sock;
std::string _ip;
uint16_t _port;
TcpServer *_tsvrp;
};
class TcpServer
{
public:
TcpServer(func_t func, uint16_t port) : _func(func), _port(port)//func是对于Request的处理逻辑函数,也就是Response
{
}
void InitServer()
{
// 1. 初始化服务器,首先得有套接字
_listensock.Socket();//创建套接字
_listensock.Bind(_port);//绑定套接字
_listensock.Listen();//将套接字设置成为listen状态
logMessage(Info, "init server done, listensock: %d", _listensock.Fd());
}
void Start()
{
for (;;)//服务器启动之后就需要周而复始的循环
{
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);//获得连接,并得到客户端的ip和地址
if (sock < 0)
continue;
logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);//走到这边说明accept成功了
//采用多线程的版本
pthread_t tid;
ThreadData *td = new ThreadData(sock, clientip, clientport, this);//
pthread_create(&tid, nullptr, ThreadRoutine, td);//td做为线程执行函数的参数,里面由很多信息,比如说this指针、套接字sock、客户端的ip和端口号等等,this指针里面还有ServiceIO函数用来处理来自客户端的数据
}
}
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());//省略了主线程的join操作
ThreadData *td = static_cast<ThreadData *>(args);
td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);//该服务器的线程要开始执行处理客户端的任务了
// logMessage(Debug, "thread running ...");
delete td;
return nullptr;
}
// 我们这个函数是被多线程调用的,负责提供具体的服务内容。将sock文件描述符的创建工作和sock的读取工作让不同的线程进行。,
void ServiceIO(int sock, const std::string &ip, const uint16_t &port)//负责从sock读客户端的数据以及将处理号的数据write到sock,第二三个参数是让函数知道我是在和哪个客户端进行通信
{
// 1. read/recv - 如何正确的读,继续设计协议,该协议的目的是为了保证用户正确的获得一个完整的报文string!
// 你怎么保证你读到的string 就是一个完整的string风格的请求呢??不能!
// 我们进行一直循环读取,边读取,边检测,测试
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer), 0); // ssize_t recv(int sockfd, void *buf, size_t len, int flags)第四个参数flag代表读取方式,默认为零即可。
// 2. 假设已经读到了一个完整的string
Request req;
req.Deserialize(str); // 对读到的request字符串要进行反序列化。将字符串str反序列化成为结构化的数据存储到req的x、y、op成员变量里面。
// 3. 直接提取用户的请求数据x、y、op啦,处理逻辑是客户端发个我一个request,服务器返回一个response
Response resp = _func(req); // 业务逻辑!这里是计算,未来可以是登录、注册等等服务
// 4. 给用户返回响应 - 序列化
std::string send_string;
resp.Serialize(&send_string); // 对计算完毕的response结构要进行序列化,形成可发送字符串
// 5. 发送到网络
}
~TcpServer()
{
}
private:
uint16_t _port;//任何一个服务器都需要一个端口号
Sock _listensock;//创建listen套接字,虽然Class Sock里面并没有就sock进行具体的类型划分,但是在服务器里面我们要的就是listrn套接字
func_t _func;
};
}
Util.hpp
//存放工具方法
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
using namespace std;
class Util
{
public:
// 纯输入型参数,在函数内部不会做修改: const &
// 输出型参数: *
// 输入输出型参数: &
static bool StringSplit(const string &str, const string &sep, vector<std::string> *result)//sep是分隔符,将str按照分隔符sep分割并且将结果放到result里面
{
size_t start = 0;
// 10 + 20
while (start < str.size())//循环切割
{
auto pos = str.find(sep, start);
if (pos == string::npos) break;
result->push_back(str.substr(start, pos-start));
// 位置的重新reload
start = pos + sep.size();
}
if(start < str.size()) result->push_back(str.substr(start));//将最后一个结构化的变量放入vector当中
return true;
}
static int toInt(const std::string &s)//将字符串转整数
{
return atoi(s.c_str());//字符串转整数
}
};