跳到主要内容C++ 网络编程入门:TCP 协议下的简易计算器项目 | 极客日志C++
C++ 网络编程入门:TCP 协议下的简易计算器项目
综述由AI生成本项目实现了一个基于 TCP 协议的简易计算器,涵盖服务端与客户端开发。服务端采用 fork 多进程模型处理并发连接,通过回调机制解耦业务逻辑。通信层实现了自定义协议,包含数据编码、解码及序列化反序列化流程。客户端生成随机计算请求发送至服务器,服务器解析后执行加减乘除及取余运算,并处理除零异常,最终将结果返回。项目展示了 C++ 网络编程中 Socket 基础操作、进程间通信及协议设计的基本实践。
极客零度26 浏览 C++ 网络编程入门:TCP 协议下的简易计算器项目
前言
根据前面的经验:网络套接字,序列化与反序列化,守护进程等等,写出一个小项目。
文件组成
.vscode/
log.hpp
Makefile
Protocol.hpp
ServerCal.hpp
Socket.hpp
TcpClient/
TcpClient.cc
TcpClient.hpp
TcpServer/
TcpServer.cc
TcpServer.hpp
TCP 服务端
TcpServer.hpp
#include <functional>
#include "Socket.hpp"
#include "ServerCal.hpp"
using func_t = std::function<std::string(std::string& package)>;
class TcpServer {
public:
TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback) {}
{
listensock_.();
listensock_.(port_);
listensock_.();
(INFO, );
}
{
() {
std::string ip;
port;
fd = listensock_.(&ip, &port);
(INFO, );
(fork() == ) {
(INFO, );
listensock_.();
std::string inbuffer_stream;
() {
buffer[];
n = (fd, &buffer, (buffer));
(n > ) {
(INFO, );
buffer[n] = ;
inbuffer_stream += buffer;
() {
std::string info = (inbuffer_stream);
(info.()) ;
(fd, info.(), info.());
}
} (n == )
;
;
}
();
}
(fd);
}
}
:
port_;
Sock listensock_;
callback_;
};
void Init()
Socket
Bind
Listen
lg
"Server Init"
void Start()
while
true
uint16_t
int
Accept
lg
"Server Accept"
if
0
lg
"fork success"
Close
while
true
char
1024
int
read
sizeof
if
0
lg
"read success"
0
while
true
callback_
if
empty
break
write
c_str
size
else
if
0
break
else
break
exit
0
close
private
uint16_t
func_t
代码说明:
- 构造函数
TcpServer(uint16_t port, func_t callback):
- 初始化服务器的端口和回调函数。回调函数
callback_ 将在收到客户端请求时被调用,用于处理数据。
Init():
- 该方法用于初始化服务器,创建套接字、绑定端口并开始监听客户端连接。
Start():
- 该方法是服务器的主循环,用于接受客户端的连接请求。
- 每当有新的客户端连接时,服务器会通过
fork() 创建子进程来处理该客户端的请求。
- 在子进程中,读取客户端发送的数据,并通过回调函数处理数据。处理结果将通过套接字返回给客户端。
- 子进程处理完毕后会退出,而父进程继续等待新的客户端连接。
Sock listensock_:
Sock 类的对象,用于处理底层套接字操作,包括创建、绑定、监听、接收连接等。
- 回调函数
func_t callback_:
- 回调函数类型,负责处理客户端发来的数据包,并返回处理后的结果。
TcpServer.cc
#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Protocal.hpp"
void ResquestTest();
void ResponseTest();
void Usage() {
std::cout << "\n\r" << "[Usage]:prot" << "\n" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage();
return -1;
}
lg(INFO, "Server start");
ServerCal cal;
std::string port_1 = argv[1];
uint16_t port = std::stoi(port_1);
TcpServer* tsur = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
tsur->Init();
tsur->Start();
return 0;
}
代码说明:
- 头文件引入:
#include "log.hpp":用于记录日志,lg(INFO, "message") 可以在程序中输出日志信息。
#include "TcpServer.hpp":引入定义 TcpServer 类的头文件,用于创建 TCP 服务器。
#include "Protocal.hpp":引入协议相关的头文件,可能包含协议的定义和数据格式的处理方法。
Usage() 函数:
- 该函数在命令行参数不正确时,输出程序的使用方式,提示用户如何正确传递命令行参数。
main() 函数:
- 程序的入口点,首先检查命令行参数的数量。如果参数不正确,则调用
Usage() 输出帮助信息,并返回错误。
- 如果参数正确,程序继续执行:
- 创建
ServerCal 对象 cal,该对象负责计算接收到的数据。
- 通过
argv[1] 获取并转换端口号。
- 创建
TcpServer 对象 tsur,并将 ServerCal::Calculator 函数作为回调函数绑定到 TcpServer 中。此回调函数会处理客户端请求的数据。
- 调用
tsur->Init() 初始化服务器,tsur->Start() 启动服务器,开始监听和处理客户端连接。
TCP 客户端
#include <iostream>
#include <unistd.h>
#include <time.h>
#include <string>
#include "log.hpp"
#include "Socket.hpp"
#include "Protocal.hpp"
void Usage() {
std::cout << "\n\r" << "[Usage]:port" << "\n" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage();
return -1;
}
srand(time(nullptr));
Sock sock;
sock.Socket();
std::string ip = argv[1];
std::string port_1 = argv[2];
std::cout << "port: " << port_1 << std::endl;
uint16_t port = std::stoi(port_1);
sock.Connect(ip, port);
lg(INFO, "connect successful");
std::string op = "+-*/%";
int cnt = 1;
while (true) {
cnt++;
Requset req;
req.x_ = rand() % 100 + 1;
req.y_ = rand() % 100;
req.op_ = op[rand() % op.size()];
std::string con;
std::string out_stream;
bool r = req.Serialize(&con);
if (!r) lg(WARNING, "Serialize failure");
out_stream = Encode(con);
std::cout << "============" << " The " << cnt << " Request " << "============" << std::endl;
std::cout << "x: " << req.x_ << std::endl;
std::cout << "op: " << req.op_ << std::endl;
std::cout << "y: " << req.y_ << std::endl;
write(sock.GetFd(), out_stream.c_str(), out_stream.size());
std::string in_stream;
char inbuffer[1024];
int n = read(sock.GetFd(), &inbuffer, sizeof(inbuffer));
if (n > 0) {
inbuffer[n] = 0;
}
in_stream += inbuffer;
std::string bon;
Response resp;
Decode(in_stream, &bon);
resp.Deserialize(bon);
resp.Print();
sleep(1);
std::cout << "========================================" << std::endl;
}
return 0;
}
代码说明:
Usage():
- 用于输出程序的使用说明,提示用户需要传递端口号作为参数。
main():
- 主程序入口,首先检查命令行参数是否正确。如果参数不正确,调用
Usage() 输出帮助信息。
- 使用
srand(time(nullptr)) 设置随机数种子,确保每次运行时生成不同的随机数。
- 创建一个
Sock 对象,初始化并连接到指定的 IP 地址和端口号。
- 在
while 循环中,程序持续进行请求:
- 生成随机的请求数据,包括
x_、y_ 和运算符 op_。
- 使用
Serialize() 方法将请求数据序列化,调用 Encode() 方法进行编码。
- 发送请求数据到服务器。
- 接收来自服务器的响应数据,使用
Decode() 进行解码,并使用 Deserialize() 方法将响应数据反序列化。
- 最后,输出计算结果。
- 日志记录:
- 使用
lg(INFO, "message") 输出程序的日志信息,帮助调试和跟踪程序的状态。
计算器
#pragma once
#include <iostream>
#include "log.hpp"
#include "Protocal.hpp"
enum err_symbol {
div_zero = 1,
mod_zero
};
class ServerCal {
public:
ServerCal() {}
Response Calculatate(Requset &req) {
Response resp(0, 0);
switch (req.op_) {
case '+':
resp.result_ = req.Getx() + req.Gety();
break;
case '-':
resp.result_ = req.Getx() - req.Gety();
break;
case '*':
resp.result_ = req.Getx() * req.Gety();
break;
case '/':
if (req.Gety() == 0)
{
resp.code_ = div_zero;
break;
}
resp.result_ = req.Getx() / req.Gety();
break;
case '%':
if (req.Gety() == 0)
{
resp.code_ = mod_zero;
break;
}
resp.result_ = req.Getx() % req.Gety();
break;
default:
break;
}
return resp;
}
std::string Calculator(std::string &package) {
std::string context;
bool rDecode = Decode(package, &context);
if (!rDecode) return "";
Requset req;
req.Deserialize(context);
Response resp;
resp = Calculatate(req);
sleep(3);
std::string in;
bool r = resp.Serialize(&in);
if (!r) return "";
context = "";
context = Encode(in);
return context;
}
~ServerCal() {}
};
代码说明:
1. ServerCal 类:
ServerCal 类包含了处理数学计算的逻辑,通过接收客户端发送的请求并计算结果,然后返回计算结果给客户端。
2. Calculatate(Requset &req) 函数:
- 该函数接收一个
Requset 对象,表示客户端发送的请求。
- 根据请求的操作符(
op_),执行相应的数学运算(加、减、乘、除、取余)。如果请求中存在除数为零的情况(除法和取余),则返回相应的错误代码(div_zero 或 mod_zero)。
- 最终返回一个
Response 对象,包含计算结果。
3. Calculator(std::string &package) 函数:
- 该函数接收客户端发送的包含请求数据的字符串
package。
- 首先解码数据包头部,去除冗余信息并获取有效数据。
- 然后将解码后的数据反序列化为
Requset 对象。
- 使用
Calculatate() 方法进行实际的计算。
- 计算结果通过
Serialize() 序列化,之后进行编码。
- 返回最终的结果给客户端。
请求和响应服务
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>
#include "log.hpp"
const std::string space_sep = " ";
const std::string protocal_sep = "\n";
std::string Encode(std::string &context) {
std::string s = std::to_string(context.size());
s += protocal_sep;
s += context;
s += protocal_sep;
return s;
}
bool Decode(std::string &package, std::string *context) {
auto pos = package.find(protocal_sep);
if (pos == std::string::npos) return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
int total_len = len + len_str.size() + 2;
if (package.size() < total_len) return false;
*context = package.substr(pos + 1, len);
package.erase(0, total_len);
return true;
}
class Requset {
public:
Requset(int data1, char op, int data2) : x_(data1), op_(op), y_(data2) {}
Requset() {}
~Requset() {}
void Print()
{
std::cout << x_ << op_ << y_ << std::endl;
}
bool Serialize(std::string *out) {
#ifdef Myself
std::string s = std::to_string(x_);
s += space_sep;
s += op_;
s += space_sep;
s += std::to_string(y_);
*out = s;
return true;
#else
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter w;
*out = w.write(root);
return true;
#endif
}
bool Deserialize(const std::string &in) {
#ifdef Myself
auto pos1 = in.find(space_sep);
if (pos1 == std::string::npos) return false;
std::string part_x = in.substr(0, pos1);
auto pos2 = in.rfind(space_sep);
std::string oper = in.substr(pos1 + 1, pos2);
std::string part_y = in.substr(pos2 + 1);
if (pos2 != pos1 + 2)
return false;
op_ = in[pos1 + 1];
x_ = std::stoi(part_x);
y_ = std::stoi(part_y);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asString()[0];
return true;
#endif
}
int Getx() { return x_; }
int Gety() { return y_; }
char Getop() { return op_; }
private:
int x_;
char op_;
int y_;
};
class Response {
public:
Response(int ret, int code) : result_(ret), code_(code) {}
Response() {}
~Response() {}
bool Serialize(std::string *out) {
#ifdef Myself
std::string s = std::to_string(result_);
s += space_sep;
s += std::to_string(code_);
*out = s;
return true;
#else
Json::Value root;
root["result"] = result_;
root["code"] = code_;
Json::FastWriter w;
*out = w.write(root);
return true;
#endif
}
bool Deserialize(const std::string &in) {
#ifdef Myself
auto pos = in.find(space_sep);
std::string res = in.substr(0, pos);
std::string code = in.substr(pos + 1);
if (pos != in.rfind(space_sep))
return false;
result_ = std::stoi(res);
code_ = std::stoi(code);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
result_ = root["result"].asInt();
code_ = root["code"].asInt();
return true;
#endif
}
void Print()
{
std::cout << "result_: " << result_ << " code_: " << code_ << std::endl;
}
private:
int result_;
int code_;
};
代码总结:
1. Encode 和 Decode:
Encode:将字符串的大小和内容打包为带有协议头的格式,便于传输。
Decode:从带有协议头的包中提取有效数据,移除头部信息并返回有效部分。
2. Requset 类:
- 表示客户端的计算请求,包括
x_、op_、y_ 三个字段(操作数和运算符)。
Serialize 和 Deserialize 用于数据的序列化和反序列化。
- 提供
Getx、Gety 和 Getop 获取请求参数的函数。
3. Response 类:
- 表示服务器的响应,包括计算结果
result_ 和错误代码 code_。
Serialize 和 Deserialize 用于数据的序列化和反序列化。
- 提供
Print 方法输出响应内容。
网络组件
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
int backlog = 10;
enum err {
Socketerr = 1,
Bindeterr,
Listeneterr,
Accepteterr
};
class Sock {
public:
Sock() {}
~Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
lg(FATAL, "Socket error: %d,%s", errno, strerror(errno));
exit(Socketerr);
}
}
void Bind(uint16_t port) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
bzero(&peer, len);
peer.sin_port = htons(port);
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd_, (struct sockaddr*)&(peer), len) < 0) {
lg(FATAL, "Bind error: %d,%s", errno, strerror(errno));
exit(Bindeterr);
}
}
void Listen() {
if (listen(sockfd_, backlog) < 0)
{
lg(FATAL, "Listen error: %d,%s", errno, strerror(errno));
exit(Listeneterr);
}
}
int Accept(std::string *clientip, uint16_t *clientport) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
bzero(&peer, len);
int newfd = accept(sockfd_, (struct sockaddr*)&(peer), &len);
if (newfd < 0) {
lg(FATAL, "Accept error: %d,%s", errno, strerror(errno));
exit(Accepteterr);
}
char ip[64];
inet_ntop(AF_INET, &peer.sin_addr.s_addr, ip, sizeof(ip));
*clientip = ip;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
bzero(&peer, len);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
peer.sin_port = htons(port);
peer.sin_family = AF_INET;
int n = connect(sockfd_, (struct sockaddr*)&(peer), len);
if (n < 0) {
lg(WARNING, "Connect error: %d,%s", errno, strerror(errno));
return false;
}
return true;
}
void Close() {
close(sockfd_);
}
int GetFd() {
return sockfd_;
}
private:
int sockfd_;
};
代码说明:
1. Socket():
- 创建一个 IPv4 TCP 套接字,并检查是否成功创建。如果创建失败,输出日志并退出程序。
2. Bind(uint16_t port):
- 将套接字绑定到指定的端口,并设置为监听来自所有网络接口的请求。如果绑定失败,输出日志并退出程序。
3. Listen():
- 开始监听客户端连接,
backlog 变量定义了最大等待连接队列。如果监听失败,输出日志并退出程序。
4. Accept(std::string *clientip, uint16_t *clientport):
- 接受一个客户端的连接请求,并返回一个新的文件描述符,用于与客户端进行通信。还会返回客户端的 IP 地址和端口号。
5. Connect(const std::string &ip, const uint16_t &port):
- 连接到指定的服务器 IP 和端口。如果连接失败,输出日志并返回
false;否则返回 true。
6. Close():
7. GetFd():
- 返回套接字的文件描述符,这个文件描述符可用于读取或写入数据。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online