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_;
};
相关推荐
色空大师2 小时前
【Linux ln 命令详解】
linux·运维·服务器·链接·ln
星辰_mya2 小时前
jvm——时不我待
运维·服务器·jvm·面试
Xzq2105092 小时前
IP协议——网络层协议
服务器·网络·tcp/ip
..过云雨2 小时前
【负载均衡oj项目】04. oj_server题目信息获取、界面渲染、负载均衡、后台交互功能
运维·c++·html·负载均衡·交互
( ⩌ - ⩌ )2 小时前
wmware中相机打开失败的解决
linux·ubuntu·相机·wmware
一水鉴天2 小时前
整体设计自动化部署方案定稿(部分):统一工程共生坊三层架构设计 20260315(豆包助手)
运维·架构·自动化
wait a minutes2 小时前
【大模型】本地怎么通过kilo code调用Qwen免费模型
linux·运维·服务器
江畔何人初2 小时前
Argo CD 的核心架构组件与作用
linux·服务器·云原生·kubernetes
LuL_Vegetable2 小时前
写一个Linux服务器自动tcpdum抓包脚本
linux·wireshark·bash·tcpdump