1.TCP的数据传输

当客户端要给服务器发数据,TCP毕竟是面向字节流的,所以步骤上肯定是先把用户缓冲区里的数据通过 write 这类拷贝函数,写到 sockfd 对应的内核发送缓冲区里。
但这时候,数据真的发出去了吗?
其实没有。因为它还停留在发送缓冲区里。那数据到底什么时候发、一次发多少、如果网络不好出问题了怎么办?这些其实不是我们应用程序能决定的,而是 TCP 自己来控制。
所以 TCP 才叫"传输控制协议"------它决定了发送时机、发送数量、以及出现异常时的处理策略。
有时候网络不稳定,TCP 可能只发了一半,于是服务器那边只收到一半,导致服务器用 read 读取数据时根本无法保证完整性。这就是为什么我们不能太依赖 TCP 本身的保证。
那我们把数据交给 TCP 到底算不算安全?其实背后真正的执行者是操作系统。
因为 TCP 本来就在操作系统内核里,属于网络协议栈的一部分。所以我们可以理解为:数据是交给了 OS 去处理。这点相对可靠,因为操作系统会负责底层的网络控制。
在TCP中使用的write 和 read 调用,本质上和读写文件差不多。
比如写磁盘时,数据也是先进内核缓冲区,再由操作系统在合适的时机写回磁盘。read 也是一样。
但同样的机制放在 TCP 这里,却并不保证应用层的数据完整性,我们并不知道是不是发送了一个完整的报文,也不能保证收到的就是一条完整的数据。
我们需要要自己在应用层做协议设计,比如定义清楚数据包的格式、做序列化与反序列化,这样才能确保数据接收是完整的。
2.协议定制,序列化和反序列化
这次要做一个网络版的计算器,核心是靠 TCP 自定义协议,再加上序列化和反序列化来完成客户端与服务端的通信。既然是网络程序,自然要分客户端和服务端两端:客户端发请求,服务端算结果再回应。
客户端这边,我们可以定义一个请求结构,里面放左操作数 x、右操作数 y,还有运算符 op。服务端那边也要准备一个响应结构,存计算结果 result 和错误码 code。这样两端的数据结构对齐了,通信才不会乱。
但正常用 socket 收发时,底层只能传字符串或字节流,而我们需要传的是结构化数据------这就涉及到序列化和协议。
协议其实就是客户端和服务端之间的一种"约定格式",它对应到代码里,就是我们定义的那个 request 和 response 结构体。两端都要看见同样的结构体,解析逻辑才统一。
那序列化到底怎么做?简单说,就是把结构体里的字段按规则拆开、变成可传输的字节流。
比如要发送 1 + 1 ,如果直接拼成 "11+" ,服务端根本看不懂哪个是操作数,哪个是运算符。所以我们要约定字段之间用分隔符,比如空格。这样一来就变成了 "1 1 +" 。
但光有字段分隔符还不够。
因为 TCP 在网络不好时可能会拆包或粘包,客户端连续写几次请求,TCP 可能一次性打包发出去。结果服务端收到一堆黏在一起的字符串,比如 "1 + 11 + 11 + 1" ,根本分不清哪个是一个请求。
所以我们还需要给每个完整报文加一个结束标记,比如用 \n 。
这样一个请求就是 "1 + 1\n" ,多个请求连在一起也能切分得清清楚楚。服务端只要按 \n 切割,就能把一个个报文还原出来。如果半路只收到了半个报文、没有 \n ,那就说明还没读完,继续等待。
另外一种常见的方案是在报文头部加长度字段,比如固定 4 字节表示后面正文的长度。不过这次为了调试和打印方便,我们换一种更直观的方式:用 \n 隔开长度和正文。
例如报文长这样: 5\n1 + 1\n 。
前面的 5 就是正文 "1 + 1" 的长度(空格也算字符),后面一个 \n 标记长度结束,再后面就是真正数据。
最后那个 \n 可以作为报文结束,方便切割。因为有了长度,我们甚至可以不用它,但加上之后打印更清晰,解析也容易。

理清了协议格式、序列化、粘包拆包方案,接下来就可以正式动手写客户端和服务端了。
3.TCP服务器
为了方便我们使用关于套接字的操作,将套接字的操作封装为一个类,方便后续的使用。
socket.hpp
cpp
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include<string>
#include<netinet/in.h>
#include <arpa/inet.h>//sockaddr_in 头文件
#include"log.hpp"
using namespace std;
enum{
SocketErr=2,
BindErr,
ListenErr,
};
const int backlog=10;
class sock
{
public:
sock()
{}
~sock()
{}
public:
void Socket()
{
sockfd_=socket(AF_INET,SOCK_STREAM,0);
if(sockfd_<0)
{
lg(Fatal,"socket error,%s: %d",strerror(errno),errno);
exit(SocketErr);
}
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_port=htons(port);
local.sin_family=AF_INET;
local.sin_addr.s_addr=INADDR_ANY;
if(bind(sockfd_,(struct sockaddr*)(&local),sizeof(local))<0)
{
lg(Fatal,"bind error,%s: %d",strerror(errno),errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_,backlog)<0)
{
lg(Fatal,"listen error,%s: %d",strerror(errno),errno);
exit(ListenErr);
}
}
int Accept(string*clientip,uint16_t*clientport)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int newfd=accept(sockfd_,(struct sockaddr*)(&client),&len);
if(newfd<0)
{
lg(Fatal,"accept error,%s: %d",strerror(errno),errno);
return -1;
}
char ipstr[32];
inet_ntop(AF_INET,&(client.sin_addr),ipstr,sizeof(ipstr));//字节序转字符串
*clientip=ipstr;
*clientport=ntohs(client.sin_port);
return newfd;
}
bool Connect(const string&ip,const uint16_t&port)
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=ntohs(port);
inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));
int n=connect(sockfd_,(struct sockaddr*)(&server),sizeof(server));
if(n==-1)
{
cerr<<"connect to"<<ip<<":"<<port<<" error "<<endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
TcpServer.hpp
在这个类中,我们主要负责搭建起一个服务器模块,及那个客户端发送过来的数据进行处理,之后将处理后的数据发送回给客户端。
成员变量和成员函数的设计
成员变量中需要有一个sock类对象,一个端口号,原来创建套接字,绑定,监听。一个回调函数,原来处理客户端发送过来的数据。
成员函数中需要有一个InitServer来对网络编程套接字的初始化。
成员函数中需要有一个Strat来接收客户端的数据,进行处理返回客户端。
cpp
#pragma once
#include<functional>
#include<string>
#include<signal.h>
#include"log.hpp"
#include"socket.hpp"
using func_t=function<string(string&package)>;
class TcpServer
{
public:
TcpServer(uint16_t port,func_t callback)
:port_(port)
,callback_(callback)
{}
~TcpServer()
{}
bool InitServer()
{}
void Strat()
{}
private:
int port_;
sock listensock_;
func_t callback_;
};
InitServer()
cpp
bool InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
lg(Info,"Init server......done");
return true;
}
Start()
服务器一旦启动,它的核心任务只有一个:不停地接收新的客户端连接。
至于连接之后的数据处理,我们完全丢给子进程去做,父进程只负责监听,不负责等待子进程。
所以为了避免子进程退出后父进程被一堆退出信号轰炸,也为了防止子进程因为客户端断开连接而崩溃,我们上来就把两个信号都忽略掉:
SIGCHLD:父进程忽略子进程的退出信号,这样子进程退出后系统会自动回收,不用父进程等待。
SIGPIPE:忽略管道断裂信号。因为和客户端的通信通道如果断了,只会影响子进程,不该把服务器也崩掉。
而且因为信号会被继承,子进程也会跟着忽略 SIGPIPE,所以子进程在连接断开时也不会莫名其妙崩溃。
服务器的主逻辑自然就是一个 while(true) 的死循环,因为服务端一旦启动就不会随便停止。
循环里第一步就是调用 Accept 等待客户端连接,如果连接成功,就拿到客户端的 IP 和端口,还有通信的文件描述符 sockfd ,然后打一条日志记录一下有人连上了。
接下来就是 fork 出子进程。
父进程和子进程从此分开走:
父进程:只关心新连接,不关心通信,所以直接把 sockfd 关掉即可,然后继续回到 Accept 等下一个客户。
子进程:拿到了和客户端通信的 sockfd ,负责所有的读写交互。子进程不需要监听套接字,所以也把 listensock_ 关掉。
子进程接下来要做的就是循环读取客户端请求。
我们定义一个流缓冲区 inbuffer_stream ,它的作用就是把从网络读到的数据一点点拼接起来。
因为 TCP 是字节流,一次 read 可能读到半个报文、一个完整报文、甚至好几个报文,所以必须靠缓冲区来黏合。
子进程内部也有一个 while(true) 循环,不断调用 read 把数据从内核缓冲区读到本地的 buffer 里。
然后对 read 的返回值 n 做判断:
n == 0:客户端主动关闭了连接,子进程也该结束。
n < 0:读取出错,同样退出子进程。
n > 0:读取成功,把数据追加到 inbuffer_stream 里。
接下来就是最核心的一步:调用回调函数 callback_ 处理请求。
回调函数会做三件事:
判断 inbuffer_stream 里是否有完整报文。
如果只有半个报文 → 返回空串 "" ,告诉子进程继续收数据。
如果有完整报文 → 解析、计算、序列化结果,并且把已处理的报文从缓冲区里擦掉(因为参数是引用,缓冲区会被修改)。
所以我们要判断回调的返回值:如果返回空串就continue ,继续 read 。
如果返回非空,说明已经处理完一个完整报文,把结果 info 打日志,然后用 write 发回给客户端。
注意, write 只是把数据送到内核发送缓冲区,真正的发送由 TCP 控制,不用我们管。
一旦子进程的循环退出了,说明通信结束了。
直接关闭 sockfd ,然后 exit(0) 让子进程干净退出,不要继续跑父进程的逻辑。
这样,整个服务器的架构就完整了:
父进程只管接连接,子进程只管处理请求,信号处理干净,缓冲区机制完善,不会出现粘包、断连、僵尸进程这些问题。
cpp
void Strat()
{
signal(SIGCHLD,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
while(true)
{
string clientip;
uint16_t clientport;
int sockfd=listensock_.Accept(&clientip,&clientport);
if(sockfd<0)
{
continue;
}
lg(Info,"accept a new link,sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip.c_str(),clientport);
if(fork()==0)
{
//child
//子进程不关心监听套接字
listensock_.Close();
string inbuffer_stream;
while(true)
{
char buffer[1280];
size_t n=read(sockfd,buffer,sizeof(buffer));
if(n>0)
{
buffer[n]=0;
inbuffer_stream+=buffer;
lg(Debug,"Debug:request\n%s",inbuffer_stream.c_str());
string info=callback_(inbuffer_stream);
if(info.empty())
{
continue;
}
lg(Debug,"Debug:request\n%s",info.c_str());
write(sockfd,info.c_str(),info.size());
}
else if(n==0)
{
break;
}
else
{
break;
}
}
close(sockfd);
exit(0);
}
close(sockfd);
}
}
4.Protocol.hpp自定义协议
这个模块提供两个类,类包含对请求和响应的处理,两个函数,对于数据的转码和解码来完成对请求和响应党的构建

Encode转码和Dncode解码
所以我们首先就要定义分隔符,那么协议报文和协议报文之间的分隔符我们采用\n,有效载荷内的各个字段的分隔符我们采用空格,报头和有效载荷的分隔符是\n,协议报文"5"\n"1 + 1"\n",这里的有效载荷仅仅是指的"1 + 1",不包含报头"5",以及报头和有效载荷的分隔符\n"以及末尾协议之间的分隔符\n
而序列化和反序列化操作的对象仅仅是有效载荷,将有效载荷封装成报文是转码Encode的过程,将报文中的有效载荷提取出来是解码的过程
Encode转码
先获取输入的业务有效载荷(例如 "1 + 1" ),然后计算有效载荷的长度,并转换为字符串作为报头。按照协议格式,依次拼接:报头 + 分隔符 + 有效载荷 + 结束分隔符,最终生成完整报文。返回拼接好的报文字符串。
Dncode解码
std::string& package :输入输出型参数,代表当前的接收缓冲区(流缓冲区)。函数内部会在成功解析后, erase 掉已处理的报文,保留剩余未处理数据。
std::string& content :输出型参数,用于存放提取出的有效载荷(Payload)。
在缓冲区中查找第一个换行符 \n 。若查找失败,说明报头不完整,直接返回 false 。从缓冲区开头到第一个 \n 的子串即为报头长度字符串 len_str 。将其转换为整数 len ,即有效载荷的长度。完整报文的总长度 total_len
计算公式为: total_len = len_str.size() (报头长度) + 1 (报头与载荷分隔符\n) + len (载荷长度) + 1 (载荷结束分隔符\n)
校验报文完整性:比较当前缓冲区 package 的长度与 total_len 。
如果package.size() < total_len ,说明缓冲区数据不足,报文不完整,返回 false 。
如果package.size() >= total_len ,说明数据完整,继续处理。
根据计算出的长度,从缓冲区中截取有效载荷 content 。将缓冲区 package 从头开始 erase 掉 total_len 个字符,即移除已处理的首个报文,为处理下一个报文做好准备。
假设缓冲区 inbuffer_stream 中累积了多个完整报文:
"5\n1 + 1\n5\n2 + 2\n"
第一次调用 Decode,解析出第一个完整报文 5\n1 + 1\n ,提取 content = "1 + 1" ,并将缓冲区清理为 "5\n2 + 2\n" 。
若再次调用 Decode,缓冲区数据长度足够,将继续解析第二个报文,提取 content = "2 + 2" ,缓冲区变为空。
若缓冲区中只有半截数据如 "5\n1 + 1" ,Decode 会返回 false ,缓冲区保持不变,等待后续数据接收完整后再解析。
cpp
//有效载荷中的有效数据的分隔符
const string blank_space_sep=" ";
//完整报文的分隔符
const string protlcol_sep="\n";
//转码
//12+4->"len\n12 + 4\n"
string Encode(const string&content)
{
string package=to_string(content.size());
package+=protlcol_sep;
package+=content;
package+=protlcol_sep;
return package;
}
//解码
//"len\n12 + 4\n"->12+4
bool Dncode(string package,string*content)
{
size_t pos=package.find(protlcol_sep);
if(pos==string::npos)
{
return false;
}
string len_str=package.substr(0,pos);
size_t len=stoi(len_str);//有效载荷的长度
size_t total_len=len_str.size()+1+len+1;//报文的长度
if(package.size()<total_len)
{
return false;
}
*content+=package.substr(pos+1,len);
package.erase(0,total_len);
return true;
}
Request请求类
进行对请求的序列化和反序列化,序列化也就是将类里面的数据转为化为一个字符串,反序列化也就是将字符串转为一个类的成员变量。
cpp
//发送的报头格式"protocol""\n""x op y""\n"
class Request
{
public:
Request(int data1,char oper,int data2)
:x(data1)
,op(oper)
,y(data2)
{}
Request()
{}
public:
bool Serialize(string*out)//序列化"x op y"
{
string s=to_string(x);
s+=blank_space_sep;
s+=op;
s+=blank_space_sep;
s+=to_string(y);
*out=s;
return true;
}
bool Deserialize(const string&in)//反序列化 (x op y)
{
size_t left=in.find(blank_space_sep);
if(left==string::npos) return false;
string part_x=in.substr(0,left);//part_x表示第一个数字
size_t right=in.rfind(blank_space_sep);
if(right==string::npos) return false;
string part_y=in.substr(right+1);//part_y表示第二个数字
if(left+2!=right) return false;
op=in[left+1];
x=stoi(part_x);
y=stoi(part_y);
return true;
}
void DebugPrint()
{
cout<<"新请求构建完成: "<<x<<op<<y<<"=?"<<endl;
}
public:
int x;
char op;
int y;
};
Response响应类
cpp
class Response
{
public:
Response(int res,int c)
:result(res)
,code(c)
{}
Response()
{}
public:
bool Serialize(string*out)
{
string s=to_string(result);
s+=blank_space_sep;
s+=to_string(code);
*out=s;
return true;
}
bool Deserialize(const string&in)
{
size_t pos=in.find(blank_space_sep);
if(pos==string::npos) return false;
string part_result=in.substr(0,pos);
string part_code=in.substr(pos+1);
result=stoi(part_result);
code=stoi(part_result);
return true;
}
void DebugPrint()
{
cout<<"结果响应完成,result : "<<result<<",code"<<code<<endl;
}
public:
int result;
int code;
};
所以其实数据的发送数据先序列化转码,之后对方收到了数据先进行解码,在进行。反序列化
5.服务端
服务端作为服务器的上层要提供回调函数Calculator传给服务器,然后服务器就可以使用这个回调函数Calculator去执行计算任务了,所以为了规范性,我们将这个回调函数Calculator封装到一个类ServerCal中,所以我们就需要一个文件ServerCal.hpp去进行这个类ServerCal的实现,那么对于服务端的main函数主调用服务器进行传参回调等逻辑我们就放在ServerCal.cc这个文件中
ServerCal.hpp
这个类中我们需要对客户端收到的数据进行解码,然后反序列化,进行数据的处理,之后对数据进行转码,序列化,返回给服务端。
Calculator接收到客户端发来的数据,也就是转码之后的数据,接收到数据之后,先进行解码,然后反序列化,得到请求,对数据进行计算,返回响应,对响应类中的数据进行序列化,转码,返回给上层。
cpp
#pragma once
#include<iostream>
#include"Protocol.hpp"
using namespace std;
enum
{
Dive_Zero=1,
Mod_Zero,
Other_Oper
};
class ServerCal
{
public:
ServerCal()
{}
Response CalculateHelper(const Request&req)
{
Response res(0,0);
switch(req.op)
{
case '+':
res.result=req.x+req.y;
break;
case '-':
res.result=req.x-req.y;
break;
case '*':
res.result=req.x*req.y;
break;
case '/':
{
if(req.y==0)
res.code=Dive_Zero;
else
res.result=req.x/req.y;
}
break;
case '%':
{
if(req.y==0)
res.code=Mod_Zero;
else
res.result=req.x%req.y;
}
break;
default:
res.code=Other_Oper;
break;
}
return res;
}
string Calculator(string&package)
{
string content;
bool r=Dncode(package,&content);
if(!r)
{
return "";
}
Request req;
req.Deserialize(content);
if(!r)
{
return "";
}
content="";
Response req=CalculateHelper(req);
req.Serialize(&content);
Encode(content);
return content;
}
};
ServerCal.cpp
服务器的启动。
当用户启动程序未输入端口号时,打印帮助信息,./ServerCal 8080 。如果运行时只输入了 ./ServerCal ,就会触发此函数,提示正确用法。
获取输入的端口号,绑定Calculator给TcpServer.hpp中的回调方法,后续用到回调函数会自动执行绑定的方法。初始化服务器,启动服务器。
cpp
#include"ServerCal.hpp"
#include"TcpServer.hpp"
#include<unistd.h>
static void Usage(const string &proc)
{
cout<<"\nUsage"<<proc<<"port\n"<<endl;
}
int main(int argc,char *argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port=stoi(argv[1]);
ServerCal cal;
TcpServer *ts=new TcpServer(port,bind(&ServerCal::Calculator,&cal,placeholders::_1));
ts->InitServer();
ts->Start();
return 0;
}
6.客户端
如果 argc 不等于 3,那肯定是用户输错了。这时候就得打印个使用说明告诉人家"正确用法是啥",然后直接退出程序,别让程序瞎跑。参数没问题的话,就从 argv[1] 里拿到服务器的 IP 地址,再从 argv[2] 里把端口号字符串转成整数。客户端要跟服务器通信,肯定得有一个网络套接字。这部分功能我们已经封装好,在 Socket.hpp 里的 Sock 类里。所以直接实例化一个 sockfd 对象,调用 Socket 方法创建出套接字就行。
同时,客户端也要准备一个接收数据的缓冲区 inbuffer_stream ,用来存服务器返回的完整报文。
客户端一启动,就自动生成随机数据去请求,然后收结果,我们进行10次的测试,将随机生成的计算式进行反序列化,转码,write发送给服务端。
使用read来读取服务端放回的数据,进行解码,序列化,得到最后的结果。
cpp
#include<iostream>
#include<string>
#include<ctime>
#include<cassert>
#include"socket.hpp"
#include"Protocol.hpp"
static void Usage(const string &proc)
{
cout<<"\nUsage"<<proc<<"serverip serverport\n"<<endl;
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
sock sockfd;
sockfd.Socket();
bool r=sockfd.Connect(serverip,serverport);
if(!r)
{
return 1;
}
srand(time(nullptr)^getpid());
int cnt=1;
const string opers="+-*/%=-&^";
string inbuffer_stream;
while(cnt<=10)
{
cout<<"--------------第"<<cnt<<"次测试----------------"<<endl;
int x=rand()%100+1;
int y=rand()%100;
char oper=opers[rand()%opers.size()];
string package;
Request req(x,oper,y);
req.DebugPrint();
req.Serialize(&package);
package=Encode(package);
write(sockfd.Fd(),package.c_str(),sizeof(package));
char buffer[128];
int n=read(sockfd.Fd(),buffer,sizeof(buffer));
if(n>0)
{
buffer[n]=0;
inbuffer_stream+=buffer;
cout<<inbuffer_stream<<endl;//len\nresult code\n
string content;
bool r=Decode(inbuffer_stream,&content);
assert(r);
Response res;
r=res.Deserialize(content);
assert(r);
res.DebugPrint();
}
cout<<"--------------------------------------------------"<<endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
测试:

源代码:
client.cpp
cpp
#include<iostream>
#include<string>
#include<ctime>
#include<cassert>
#include"socket.hpp"
#include"Protocol.hpp"
static void Usage(const string &proc)
{
cout<<"\nUsage"<<proc<<"serverip serverport\n"<<endl;
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
sock sockfd;
sockfd.Socket();
bool r=sockfd.Connect(serverip,serverport);
if(!r)
{
return 1;
}
srand(time(nullptr)^getpid());
int cnt=1;
const string opers="+-*/%-&^";
string inbuffer_stream;
while(cnt<=10)
{
cout<<"--------------第"<<cnt<<"次测试----------------"<<endl;
int x=rand()%100+1;
int y=rand()%100;
char oper=opers[rand()%opers.size()];
string package;
Request req(x,oper,y);
req.DebugPrint();
req.Serialize(&package);
package=Encode(package);
write(sockfd.Fd(),package.c_str(),sizeof(package));
char buffer[128];
int n=read(sockfd.Fd(),buffer,sizeof(buffer));
if(n>0)
{
buffer[n]=0;
inbuffer_stream+=buffer;
cout<<inbuffer_stream<<endl;//len\nresult code\n
string content;
bool r=Decode(inbuffer_stream,&content);
assert(r);
Response res;
r=res.Deserialize(content);
assert(r);
res.DebugPrint();
}
cout<<"--------------------------------------------------"<<endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
makefile
cpp
.PHONY:all
all:servercal clientcal
servercal:ServerCal.cpp
g++ -o $@ $^ -std=c++11 -g
clientcal:Client.cpp
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f servercal clientcal
Protocol.hpp
cpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<stdio.h>
using namespace std;
const string blank_space_sep=" ";
const string protlcol_sep="\n";
string Encode(string &content)//封装有效载荷
{
string package=to_string(content.size());
package+=protlcol_sep;
package+=content;
package+=protlcol_sep;
return package;
}
bool Decode(string&package,string*content)//从package中提取完整的报文放入content,解报
{
size_t pos=package.find(protlcol_sep);
if(pos==string::npos) return false;
string len_str=package.substr(0,pos);//protocol的长度 substr[pos,pos+len)
size_t len=stoi(len_str);//有效载荷的长度
size_t total_len=len_str.size()+len+2;//报头的总长度
if(package.size()<total_len) return false;//未收到一个完整的报头
*content=package.substr(pos+1,len);
//移除报文
package.erase(0,total_len);
return true;
}
//发送的报头格式"protocol""\n""x op y""\n"
class Request
{
public:
Request(int data1,char oper,int data2)
:x(data1)
,op(oper)
,y(data2)
{}
Request()
{}
public:
bool Serialize(string*out)//序列化"x op y"
{
string s=to_string(x);
s+=blank_space_sep;
s+=op;
s+=blank_space_sep;
s+=to_string(y);
*out=s;
return true;
}
bool Deserialize(const string&in)//反序列化 (x op y)
{
size_t left=in.find(blank_space_sep);
if(left==string::npos) return false;
string part_x=in.substr(0,left);//part_x表示第一个数字
size_t right=in.rfind(blank_space_sep);
if(right==string::npos) return false;
string part_y=in.substr(right+1);//part_y表示第二个数字
if(left+2!=right) return false;
op=in[left+1];
x=stoi(part_x);
y=stoi(part_y);
return true;
}
void DebugPrint()
{
cout<<"新请求构建完成: "<<x<<op<<y<<"=?"<<endl;
}
public:
int x;
char op;
int y;
};
class Response
{
public:
Response(int res,int c)
:result(res)
,code(c)
{}
Response()
{}
public:
bool Serialize(string*out)
{
string s=to_string(result);
s+=blank_space_sep;
s+=to_string(code);
*out=s;
return true;
}
bool Deserialize(const string&in)
{
size_t pos=in.find(blank_space_sep);
if(pos==string::npos) return false;
string part_result=in.substr(0,pos);
string part_code=in.substr(pos+1);
result=stoi(part_result);
code=stoi(part_code);
return true;
}
void DebugPrint()
{
cout<<"结果响应完成,result : "<<result<<",code"<<code<<endl;
}
public:
int result;
int code;
};
ServerCal.cpp
cpp
#include"ServerCal.hpp"
#include"TcpServer.hpp"
#include<unistd.h>
static void Usage(const string &proc)
{
cout<<"\nUsage"<<proc<<"port\n"<<endl;
}
int main(int argc,char *argv[])
{
// if(argc!=2)
// {
// Usage(argv[0]);
// exit(0);
// }
// uint16_t port=stoi(argv[1]);
uint16_t port=8888;
ServerCal cal;
TcpServer *ts=new TcpServer(port,bind(&ServerCal::Calculator,&cal,placeholders::_1));
ts->InitServer();
ts->Start();
return 0;
}
ServerCal.hpp
cpp
#pragma once
#include<iostream>
#include"Protocol.hpp"
using namespace std;
enum
{
Dive_Zero=1,
Mod_Zero,
Other_Oper
};
class ServerCal
{
public:
ServerCal()
{}
Response CalculateHelper(const Request&req)
{
Response res(0,0);
switch(req.op)
{
case '+':
res.result=req.x+req.y;
break;
case '-':
res.result=req.x-req.y;
break;
case '*':
res.result=req.x*req.y;
break;
case '/':
{
if(req.y==0)
res.code=Dive_Zero;
else
res.result=req.x/req.y;
}
break;
case '%':
{
if(req.y==0)
res.code=Mod_Zero;
else
res.result=req.x%req.y;
}
break;
default:
res.code=Other_Oper;
break;
}
return res;
}
//"len"\n""10 + 20""\n"
string Calculator(string&package)
{
string content;
bool r=Decode(package,&content); //content:"10 + 20"
if(!r)
{
return "";
}
Request req;
r=req.Deserialize(content);//服务器进行反序列化,提取数字和符号
if(!r)
{
return "";
}
content="";//清空content,方便下面使用
Response res=CalculateHelper(req);//在服务器进行计算
//计算完成
res.Serialize(&content);//序列化
content=Encode(content);//封装有效载荷
return content;
}
};
socket.hpp
cpp
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include<string>
#include<netinet/in.h>
#include <arpa/inet.h>//sockaddr_in 头文件
#include"log.hpp"
using namespace std;
enum{
SocketErr=2,
BindErr,
ListenErr,
};
const int backlog=10;
class sock
{
public:
sock()
{}
~sock()
{}
public:
void Socket()
{
sockfd_=socket(AF_INET,SOCK_STREAM,0);
if(sockfd_<0)
{
lg(Fatal,"socket error,%s: %d",strerror(errno),errno);
exit(SocketErr);
}
int opt=1;
setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_port=htons(port);
local.sin_family=AF_INET;
local.sin_addr.s_addr=INADDR_ANY;
if(bind(sockfd_,(struct sockaddr*)(&local),sizeof(local))<0)
{
lg(Fatal,"bind error,%s: %d",strerror(errno),errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_,backlog)<0)
{
lg(Fatal,"listen error,%s: %d",strerror(errno),errno);
exit(ListenErr);
}
}
int Accept(string*clientip,uint16_t*clientport)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int newfd=accept(sockfd_,(struct sockaddr*)(&client),&len);
if(newfd<0)
{
lg(Fatal,"accept error,%s: %d",strerror(errno),errno);
return -1;
}
char ipstr[32];
inet_ntop(AF_INET,&(client.sin_addr),ipstr,sizeof(ipstr));//字节序转字符串
*clientip=ipstr;
*clientport=ntohs(client.sin_port);
return newfd;
}
bool Connect(const string&ip,const uint16_t&port)
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=ntohs(port);
inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));
int n=connect(sockfd_,(struct sockaddr*)(&server),sizeof(server));
if(n==-1)
{
cerr<<"connect to"<<ip<<":"<<port<<" error "<<endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
TcpServer.hpp
cpp
#pragma once
#include<functional>
#include<string>
#include<signal.h>
#include"log.hpp"
#include"socket.hpp"
using func_t =function<string(string&package)>;
class TcpServer
{
public:
TcpServer(uint16_t port,func_t callback)
:port_(port)
,callback_(callback)
{}
bool InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
lg(Info,"Init server......done");
return true;
}
void Start()
{
signal(SIGCHLD,SIG_IGN);//忽略子进程退出时的信号
signal(SIGPIPE,SIG_IGN);
for(;;)
{
string clientip;
uint16_t clientport;
int sockfd=listensock_.Accept(&clientip,&clientport);
if(sockfd<0)
{
continue;
}
lg(Info,"accept a new link,sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip.c_str(),clientport);
if(fork()==0)
{
listensock_.Close();//关闭子进程的文件描述符,不影响父进程
string inbuffer_stream;
while(true)
{
char buffer[1280];//处理未收到完整报头的情况
ssize_t n=read(sockfd,buffer,sizeof(buffer));
if(n>0)
{
buffer[n]=0;
inbuffer_stream+=buffer;
lg(Debug,"debug:\n%s",inbuffer_stream.c_str());
while(true)
{
string info=callback_(inbuffer_stream);//回调,调用"计算机函数(Calculator),"要是存在不是完整的报文就返回""
if(info.empty())
{
break;//1.报头全被处理完2.未收到完整的报头
}
lg(Debug,"debug response:\n%s",info.c_str());
lg(Debug,"debug:\n%s",inbuffer_stream.c_str());
write(sockfd,info.c_str(),info.size());
}
}
else if(n==0)
{
break;
}
else
{
break;
}
}
exit(0);
}
close(sockfd);
}
}
~TcpServer()
{}
private:
uint16_t port_;
sock listensock_;
func_t callback_;
};