跳到主要内容C++ 网络编程入门:TCP 协议下的简易计算器项目 | 极客日志C++
C++ 网络编程入门:TCP 协议下的简易计算器项目
本项目基于 C++ 与 TCP 协议实现了一个简易的网络计算器。采用 Fork 多进程模型处理并发连接,封装了 Socket 基础操作类以简化网络编程。通过自定义协议头解决粘包问题,支持序列化与反序列化,实现了加减乘除及取余运算,包含除零错误处理。代码结构清晰,适合初学者理解网络编程核心流程。
云间漫步1 浏览 C++ 网络编程入门:TCP 协议下的简易计算器项目
基于之前的经验,结合网络套接字、序列化与反序列化以及守护进程等知识,我们来实现一个小型的 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) {}
void Init() {
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
lg(INFO, "Server Init");
}
void Start() {
while (true) {
std::string ip;
uint16_t 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_;
};
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
- 构造函数:接收端口号和回调函数。回调函数是处理数据的核心入口。
- Init 方法:完成 Socket 创建、绑定和监听的标准流程。
- Start 方法:这是主循环。每次有新连接时,通过
fork() 创建子进程处理该客户端。父进程继续等待下一个连接,实现了简单的并发模型。子进程中读取数据,调用回调处理,然后写回响应。
TcpServer.cc 启动入口
#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Protocal.hpp"
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;
}
这里主要做了参数校验,实例化计算类对象,并使用 std::bind 将成员函数绑定为回调函数传递给服务器。
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];
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;
}
- 使用
srand(time(nullptr)) 确保每次运行生成不同的随机数。
- 在循环中不断生成随机请求,序列化后发送,接收后解码并打印结果。
- 注意
sleep(1) 是为了模拟真实场景中的请求间隔,避免瞬间流量过大。
计算器业务逻辑
ServerCal 类
#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() {}
};
这里处理了基本的四则运算及取余,特别增加了除零检查,返回相应的错误码。
协议与数据封装
编码与解码
为了处理粘包问题,我们在应用层设计了简单的长度前缀协议。
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;
}
请求与响应结构
支持两种模式:自定义字符串格式或 JSON 格式(通过宏控制)。
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_;
};
网络组件封装
为了简化 Socket 操作,我们封装了一个 Sock 类。
#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_;
};
该类封装了标准的 Socket 生命周期管理,包括创建、绑定、监听、接受连接和关闭,并在出错时输出详细日志。
效果展示
源码地址
相关免费在线工具
- 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