基于TCP协议的网络计算器实现

本文介绍了TCP协议的通信流程,包括三次握手和四次挥手,并详细阐述了服务端TCP的建立和断开连接过程。文章通过示例代码展示了如何实现简单的应用层自定义协议,包括序列化和反序列化对象,以及处理网络通信中的数据完整性问题。此外,还提到了使用现成的JSON格式作为应用层协议的选项,以及处理SIGPIPE信号和创建守护进程的重要性。
摘要由CSDN通过智能技术生成

本文代码仓库地址2023_04_22_Calculator_TCPSocket · hepburn0504_yyq/LinuxClass - 码云 - 开源中国 (gitee.com)

TCP协议的通信流程

之前写的客户端和应用端的代码是基于应用层的代码。TCP是基于确认应答来保证单向可靠性的

TCP是面向连接的通信协议,在双方通信之前,要先进行3次握手(SYN、SYN/ACK、ACK)才能建立连接,谁调用connect就是谁先发起连接,3次握手完成后,才能把这个连接从内核拿到应用层;断开连接,需要4次挥手(FIN、ACK、FIN、ACK)

服务端TCP:服务端应用层:

listenfd = sock(),【让系统分配一个文件描述符】;

bind(listenfd, 服务器地址端口),【即绑定listenfd的源地址,目标地址任意】;

listen(listenfd, 连接队列长度),【使listenfd 成为监听描述符(揽客的)】;

connfd = accept(listenfd, 客户端地址端口),【阻塞等待客户端连接】;

当客户端来连接的时候,OS底层经过3次握手,把连接建立好之后,给了应用层,accept才会返回,系统会分配新的描述符connfd和客户端通信;

read(connfd, buf, size),【阻塞等待客户端数据请求】

再谈传输控制协议

TCP和UDP都是传输控制协议。调用send/write函数是把数据拷贝到发送缓冲区里,i调用recv/read函数是把数据从接收缓冲区中拷贝到字符串里。这一类IO接口,本质都是拷贝函数。

那数据什么时候发送到网络中呢?由传输层的协议TCP/UDP协议决定,什么时候发送、一次发多少、出错了怎么办都是由传输协议来决定的。

应用层协议

序列化和反序列化

序列化:就是将对象转化成字节序列的过程。

反序列化:就是将字节序列转化成对象的过程。

网络可以直接传输数据,但是无法直接传输对象,故需要在传输前序列化,传输完成后反序列化成对象。所以所有可在网络上传输的对象都必须是可序列化的。

对象(如结构体等)是为了方便上层用户使用。

只要保证一端发送时构造的数据,在另一端能够正确的进行解析,就是ok的。这种约定, 就是 应用层协议protocol。

应用层自定协议
//Protocol.hpp
#pragma once

#include <string>
#include <cstring>

namespace ns_protocol
{
// 分隔符为空格
// 分隔符长度
#define SPACE " "
#define SPACELEN strlen(SPACE)//要用strlen,不能用sizeof

    class Request
    {
    public:
        // 序列化
        std::string Serialize()
        {
            // 1.我们自己写的协议 "num1+空格+op+空格+num2"
            std::string pkg;
            pkg = std::to_string(_num1);
            pkg += SPACE;
            pkg += _op;
            pkg += SPACE;
            pkg += std::to_string(_num2);
            return pkg;
        }
        // 反序列化
        bool Deserialize(const std::string &pkg)
        {
            // 从字节流中提取数据,初始化结构体
            // 根据分隔符提取数据
            // 找第1个空格
            std::size_t left = pkg.find(SPACE);
            if (std::string::npos == left)
            {
                // 非法字符串,不认识这个格式的字节序列
                return false;
            }
            // 找第2个空格
            std::size_t right = pkg.rfind(SPACE);
            if (std::string::npos == right)
            {
                // 非法字符串,不认识这个格式的字节序列
                return false;
            }
            // 正常提取数据 "123+空格+456"
            _num1 = atoi(pkg.substr(0, left).c_str());
            _num2 = atoi(pkg.substr(right + SPACELEN).c_str());
            if (left + SPACELEN > pkg.size())
                return false;
            else
                _op = pkg[left + SPACELEN];
            return true;
        }

    public:
        Request() {}
        Request(int x, int y, char op)
            : _num1(x), _num2(y), _op(op)
        {
        }
        ~Request() {}

    public:
        int _num1; // 第一个操作数
        int _num2; // 第二个操作数
        char _op;  // 运算符 '+' '-' '*' '/' '%'
    };

    class Response
    {
    public:
        // 序列化 "code+空格+result"
        std::string Serialize()
        {
            std::string pkg;
            pkg = std::to_string(_code);
            pkg += SPACE;
            pkg += std::to_string(_result);
            return pkg;
        }
        // 反序列化
        bool Deserialize(const std::string &pkg)
        {
            // 根据分隔符提取数据
            // 找第1个空格
            std::size_t pos = pkg.find(SPACE);
            if (std::string::npos == pos)
            {
                // 非法字符串,不认识这个格式的字节序列
                return false;
            }

            // 正常提取数据 "code+空格+result"
            _code = atoi(pkg.substr(0, pos).c_str());
            _result = atoi(pkg.substr(pos + SPACELEN).c_str());
            return true;
        }

    public:
        Response() {}
        Response(int result, int code)
            : _result(result), _code(code)
        {
        }
        ~Response() {}

    public:
        int _result; // 计算结果
        int _code;   // 计算结果的状态码,0正常,1除 2模0,-1非法操作符
    };

    std::string Recv(int sock)
    {
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
        if (s > 0)
        {
            return buffer;
        }
    }

    void Send(int sock, const std::string &pkg)
    {
        send(sock, pkg.c_str(), pkg.size(), 0);
    }
}

这个是简陋版本的应用层自定协议。其中还有很多小细节容易出bug,要注意不要踩坑。

如何保证Recv()函数中读到的buffer是一个完善的请求呢?(如客户端发了很多次消息,一开始服务端被阻塞着,然后一次性读取的情况)

上面讲了传输控制协议,在进行网络通信的时候,这里的代码是由TCP协议来控制何时发送/一次性发多少/出错了怎么办这些事情。即发送的次数和接收的次数是无关的,这就是面向字节流的。(比如数据是10字节,但是发送缓冲区的大小是2字节,要send5次,接收缓冲区的大小是5字节,要recv2次)。

所以,为了保证读取的数据是一个完整的包,就要有开始标识和结束标识,故应该将我们自己写的协议修改为 “本次数据大小+/r+/n+num1+空格+op+空格+num2+/r+/n”。

// 格式化报文数据
std::string Encode(std::string &s)
{
    std::string pkg = std::to_string(s.size());
    pkg += SEP;
    pkg += s;
    pkg += SEP;
    return pkg;
}

// 解析报文数据 本次数据大小length+/r+/n+num1+空格+op+空格+num2+/r+/n
std::string Decode(std::string &buffer)
{
    std::size_t pos = buffer.find(SEP);
    // 如果找不到"/r/n",说明此次读取的报文不完整
    if (std::string::npos == pos)
        return "";
    // 能读到一个分隔符SEP,再继续
    // 先取到协议里的数据长度
    int length = atoi(buffer.substr(0, pos).c_str());
    // 正文长度= buffer的长度 - length - 第1个分隔符长度SEPLEN - 第2个分隔符长度SEPLEN
    int content_size = buffer.size() - pos - SEPLEN - SEPLEN;
    if (content_size >= length)
    {
        // 说明至少具有一个合法完整的报文
        // 移除标识
        buffer.erase(0, pos + SEPLEN);
        // 取到正文
        std::string pkg = buffer.substr(0, length);
        // 再移除标识
        buffer.erase(0, length + SEPLEN);
        return pkg;
    }
    else
    {
        // 如果没有,就返回空
        return "";
    }
}

// 返回值是bool类型,表示本次读取成功/失败
// 协议形式2:本次数据大小+/r+/n+num1+空格+op+空格+num2+/r+/n
bool Recv(int sock, std::string *out)
{
    char buffer[1024];
    bool ret = false;

    ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
    if (s > 0)
    {
        buffer[s] = 0;
        *out += buffer; // 注意这里是+=,会将字符加在out末尾
        ret = true;
    }
    else if (0 == s)
    {
        // 表示对方关闭连接
        std::cout << "客户端关闭连接" << std::endl;
    }
    else
    {
        // 表示读取错误
        std::cout << "读取错误" << std::endl;
    }
    return ret;
}
void Send(int sock, const std::string &pkg)
{
    int n = send(sock, pkg.c_str(), pkg.size(), 0);
    if (n < 0)
    {
        std::cout << "发送错误" << std::endl;
    }
}
使用现成的json格式
class Request
{
public:
    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["num1"] = _num1;
        root["num2"] = _num2;
        root["op"] = _op;
        // 2.序列化
        Json::FastWriter fw;
        return fw.write(root);
    }
    // 反序列化
    bool Deserialize(const std::string &pkg)
    {
        Json::Value root;
        Json::Reader r;
        r.parse(pkg, root);
        _num1 = root["num1"].asInt();
        _num2 = root["num2"].asInt();
        _op = root["op"].asInt();
    }
public:
    int _num1; // 第一个操作数
    int _num2; // 第二个操作数
    char _op;  // 运算符 '+' '-' '*' '/' '%'
}
class Response
{
public:
    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["code"] = _code;
        root["result"] = _result;
        Json::FastWriter fw;
        return fw.write(root);
    }
    // 反序列化
    bool Deserialize(const std::string &pkg)
    {
        Json::Value root;
        Json::Reader r;
        r.parse(pkg, root);

        _code = root["code"].asInt();
        _result = root["result"].asInt();
    }
public:
    int _result; // 计算结果
    int _code;   // 计算结果的状态码,0正常,1除 2模0,-1非法操作符
}

捕捉SIGPIPE信号

建议在客户端和服务端的主线程处加上信号捕捉的代码,如果某一端关闭了套接字,再调用send函数就直接出错,容易找不到出错原因。加上这一句方便排查出错原因。

一般服务器都是要忽略SIGPIPE信号的。因为对一个对端已经关闭的socket调用两次write,第二次将会生成SIGPIPE信号, 该信号默认结束进程,就会结束服务器,这当然是我们不希望看到的。

-----------------客户端-----------------
static void handler(int signo)
{
    std::cout << "捕捉到一个信号:" << signo << std::endl;
}

int main()
{
    // 如果是sockfd被关闭了,还向该文件描述符中做写入动作,就会捕捉到13号信号
    signal(SIGPIPE, handler);
    //服务器端应写成
    signal(SIGPIPE,SIG_IGN);  
}
-----------------服务器端-----------------
static void handler(int signo)
{
    std::cout << "捕捉到一个信号:" << signo << std::endl;
}

int main()
{
    //服务器端应忽略SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);  
}

守护进程的添加

让服务器可以一直接受用户输入

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 基于socket通信的简单计算器可以通过以下步骤实现: 1. 创建一个服务器程序,使用socket API建立一个TCP/IP连接,等待客户端连接。 2. 当客户端连接到服务器时,服务器接受客户端的请求,解析请求中的计算表达式。 3. 服务器计算表达式的结果,并将结果发送回客户端。 4. 客户端接收服务器发送的结果,并将结果显示在屏幕上。 5. 客户端可以继续发送计算请求,服务器将继续处理请求并返回结果。 需要注意的是,服务器和客户端之间的通信需要遵循一定的协议,以确保数据的正确传输和处理。同时,需要考虑安全性和错误处理等问题,以提高程序的稳定性和可靠性。 ### 回答2: 基于socket通信的简单计算器可以通过网络进行远程计算。其实现过程分为服务器和客户端两部分。 服务器端: 1.创建服务端socket,绑定IP和端口号 2.监听客户端的连接请求,接受客户端发送的数据 3.根据客户端发送的数据进行计算,将计算结果返回给客户端 客户端: 1.创建客户端socket,连接到服务端指定的IP和端口号 2.将需要进行计算的数据发送给服务端 3.等待服务端返回的计算结果并输出 具体实现中,可以将数据发送和接收的格式规定为字符串,如“2+3”,“5-4”,“7*8”,“10/2”等,服务器端根据接收到的数据进行解析计算,客户端则接收并输出服务器端返回的结果。 在实现过程中,需要注意数据的合法性和异常处理。例如除数为0、数据格式不正确等情况都需要进行处理。 通过基于socket通信的简单计算器实现,我们可以深入理解socket通信的原理,并掌握socket编程的基本方法。同时,这也是一个简单而实用的网络应用程序,可以为我们的日常工作和学习带来方便。 ### 回答3: 基于socket通信的简单计算器实现通常涉及两个主要的组件:客户端和服务器。客户端向服务器发送计算请求,服务器收到请求后执行相应的计算操作,然后将结果发送回客户端。 首先,我们需要创建一个服务器程序来接收和处理客户端的计算请求。服务器可以使用Python中的socket库来实现。使用socket库中的socket()函数创建一个新的socket对象,并设置socket类型和地址族。在此之后,就可以使用bind()函数将socket绑定到一个特定的IP地址和端口号上。 一旦服务器启动并开始监听客户端连接请求,我们可以使用accept()方法接收客户端的连接请求。当连接完成后,服务器从客户端读取计算请求并执行相应的计算操作,然后将结果发送回客户端。最后,当客户端完成计算操作时,它会关闭连接。 对于客户端,它首先需要解析用户输入的数学表达式,并将它们转换为一个可以通过网络发送的格式,例如字符串。然后,客户端使用socket库中的connect()方法连接到服务器的IP地址和端口号上。 一旦连接已建立,客户端就可以向服务器发送计算请求并等待接收服务器返回的结果。当客户端收到服务器响应后,它将计算结果显示给用户,并关闭连接。 总之,通过socket通信我们可以很容易地实现基于网络的简单计算器。但是注意,在实际的应用中,还需要考虑安全性、性能和数据格式等诸多因素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值