Linux 自定义协议完成网络版本计算器

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_ 处理请求。

回调函数会做三件事:

  1. 判断 inbuffer_stream 里是否有完整报文。

  2. 如果只有半个报文 → 返回空串 "" ,告诉子进程继续收数据。

  3. 如果有完整报文 → 解析、计算、序列化结果,并且把已处理的报文从缓冲区里擦掉(因为参数是引用,缓冲区会被修改)。
    所以我们要判断回调的返回值:

如果返回空串就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"

  1. 第一次调用 Decode,解析出第一个完整报文 5\n1 + 1\n ,提取 content = "1 + 1" ,并将缓冲区清理为 "5\n2 + 2\n" 。

  2. 若再次调用 Decode,缓冲区数据长度足够,将继续解析第二个报文,提取 content = "2 + 2" ,缓冲区变为空。

  3. 若缓冲区中只有半截数据如 "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_;
};
相关推荐
小龙在慢慢变强..7 小时前
目录结构(FHS 标准)
linux·运维·服务器
2035去旅行7 小时前
嵌入式开发,如何选择C标准库
linux·arm开发
刘延林.7 小时前
win11系统下通过 WSL2 安装Ubuntu 24.04 使用RTX 5080 GPU
linux·运维·ubuntu
星恒讯工业路由器7 小时前
星恒讯工业生产自动化解决方案
运维·物联网·自动化·智能路由器·信息与通信
Diros1g7 小时前
如何通过普通网线给另一个设备供网
网络·网络协议
a8a3027 小时前
Laravel9.x新特性全解析
运维·spring boot·nginx
beyond阿亮7 小时前
IEC104 Client Simulator - IEC104 主站/客户端模拟器 仿真器免费使用教程
运维·服务器·网络
(Charon)8 小时前
【C++/Qt】Qt 封装 TCP 客户端底层 Network 类:连接、收发、自动测试与错误处理
服务器·网络·qt·tcp/ip
KKKlucifer8 小时前
日志审计与行为分析在安全服务中的应用实践
网络·人工智能·安全
Agent产品评测局8 小时前
生产排期与MES/ERP系统打通,实操方法详解:2026企业级智能体与超自动化集成实战指南
运维·人工智能·ai·chatgpt·自动化