您现在的位置是:首页 >技术杂谈 >Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)网站首页技术杂谈
Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)
简介Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)
目录
因为,TCP面向字节流,所以TCP有粘包问题,故我们需要应用层协议来区分每一个数据包。防止读取到半个,一个半数据包的情况。
Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#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;
};
TcpServer.hpp
#ifndef _TCP_SERVER_HPP_
#define _TCP_SERVER_HPP_
#include "Sock.hpp"
#include <vector>
#include <functional>
// 说实话,这个TcpServer类实现的非常棒,真的很棒,网络和服务进行了解耦。
// 使用者直接BindServer, 然后start即可
namespace ns_tcpserver
{
using func_t = std::function<void(int socket)>; // 服务器提供的服务方法类型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<func_t> _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
Protocol.hpp
#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <jsoncpp/json/json.h>
// important and new
namespace ns_protocol
{
// #define MYSELF 1 // 自己实现序列化反序列化还是使用json库
#define SPACE " "
#define SPACE_LENGTH strlen(SPACE)
#define SEP "
"
#define SEP_LENGTH strlen(SEP)
// 请求和回复,都需要序列化和反序列化的成员函数
// 序列化和反序列化双方都不同。但是添加报头和去报头是相同的,"Length
xxxxx
";
// 客户端生成请求,序列化之后发送给服务端
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
x op y
" 成功返回有效载荷,失败返回空串
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
1+1
"
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] = '