个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、应用层

1.1、再谈 "协议"

1.2、网络版计算器

1.3、序列化 和 反序列化

2、重新理解全双工

3、Socket 封装

3.1、Socket类

3.2、TcpSocket类

3.2.1、基本结构

3.2.2、构造析构函数

3.2.3、创建套接字

3.2.4、绑定套接字 

3.2.5、监听套接字

3.2.6、获取连接

3.2.7、建立连接

3.2.8、其他函数

3.2.9、接收发送消息函数


1、应用层

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.

1.1、再谈 "协议"

协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?

其实,协议就是双方约定好的结构化的数据!

1.2、网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一(传结构体对象):

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;
  • ...
// struct request req = {10, 20, '+'}
// client: write {sockfd, &req, sizeof(req)}
// server: struct request req; read{sockfd, &req, sizeof(req)}
struct request
{
    int x;
    int y;
    char oper; // + - * / %
};

// struct request resp = {30,0}
struct response
{
    int result;
    int code; // 0: success 1: div error 2: 非法操作
};

不推荐直接传结构体对象,从技术和业务角度解释?

1、技术角度

  • 1、跨平台与兼容性

    • 结构体的大小和内存布局可能因编译器、操作系统或硬件平台的不同而有所差异。这可能导致在一个平台上发送的结构体在另一个平台上无法正确解析。

  • 2、内存对齐与填充

    • 为了优化内存访问速度,编译器可能会对结构体成员进行对齐和填充。这会导致结构体的实际大小大于其成员大小的总和。
    • 直接传输结构体可能会因为内存对齐和填充的问题而导致数据解析错误。
  • 3、指针与动态内存
    • 结构体中可能包含指针,这些指针指向动态分配的内存。直接传输结构体无法传递指针所指向的数据,而只能传递指针值,这可能导致数据丢失或内存泄漏。

2、业务角度

  • 1、数据安全性

    • 直接传输结构体可能会暴露数据的内部结构和实现细节,从而增加数据被恶意攻击的风险。
  • 2、数据版本控制

    • 随着业务的发展和变化,数据结构和格式可能需要进行调整和升级。

约定方案二(传字符串):

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 "序列化" 和 "反序列化"

1.3、序列化 和 反序列化

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程

  • 我们采用方案 2,我们也要体现协议定制的细节
  • 我们要引入序列化和反序列化,只不过我们课堂直接采用现成的方案 -- jsoncpp库
  • 我们要对 socket 进行字节流的读取处理 

2、重新理解全双工

所以:

  • 在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
  • 这就是为什么一个 tcp sockfd 读写都是它的原因 
  • 实际数据什么时候发,发多少,出错了怎么办由 TCP 控制,所以 TCP 叫做传输控制协议

1、read,write,send,recv本质是拷贝函数

2、发送数据的本质:是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方大的接收缓冲区!

3、tcp支持全双工通信的原因(有发送和接收缓冲区)!

4、有两个缓冲区这种模式就是生产者消费者模型

5、为什么IO函数要阻塞?本质是在维护同步关系

TCP协议是面向字节流的,客户端发的,不一定是全部是服务端收的,怎么保证读到的是一个完整的请求呢?

分割完整的报文!

3、Socket 封装

Socket类以模板方法类的设计模式进行封装,将算法的不变部分封装在抽象基类中,而将可变部分延迟到子类中实现

  1. 抽象类(Abstract Class)
    • 定义了多个抽象操作,这些操作在抽象类中不具体实现,由子类实现。
    • 定义了两个模板方法,这个方法通常调用了上面提到的抽象操作。模板方法的算法骨架是固定的,但其中一些步骤的具体实现会延迟到子类中。
  2. 具体子类(Concrete Class)
    • 实现抽象类中的抽象操作,提供具体的算法步骤实现。
    • 可以重写父类中的模板方法,但通常情况下不需要这么做,因为模板方法已经在抽象类中定义好了算法的骨架。

3.1、Socket类

Socket类定义多个抽象操作和两个模板方法! 

// 模板方法模式
class Socket
{
public:
    virtual void CreaterSocketOrDie() = 0;                                    // 创建套接字
    virtual void CreaterBindOrDie(uint16_t port) = 0;                         // 绑定套接字
    virtual void CreaterListenOrDie(int backlog = gbacklog) = 0;              // 监听套接字
    virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;                         // 获取连接
    virtual bool Connector(const std::string &peerip, uint16_t peerport) = 0; // 建立连接
    virtual int Sockfd() = 0;
    virtual void Close() = 0;

    virtual ssize_t Recv(std::string *out) = 0;      // 接收消息 
    virtual ssize_t Send(const std::string &in) = 0; // 发送消息

public:
    // 创建监听套接字
    void BuildListenSocket(uint16_t port) 
    {
        CreaterSocketOrDie();   // 创建
        CreaterBindOrDie(port); // 绑定
        CreaterListenOrDie();   // 监听
    }
    // 创建客户端套接字
    void BuildClientSocket(const std::string &peerip, uint16_t peerport) 
    {
        CreaterSocketOrDie(); // 创建
        Connector(peerip, peerport);
    }
    // 创建普通套接字
    // void BuildNormalSocket() 
    // {}
    // 创建Udp套接字
    // void BuildUdpSocket() 
    // {}
};

3.2、TcpSocket类

TcpSocket类继承Socket类,并具体实现父类的抽象操作!

全局静态变量和枚举常量:

const static int gbacklog = 8;

enum
{
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

3.2.1、基本结构

TcpSocket类有一个_sockfd成员即可,可以是监听套接字也可以是普通套接字! 

此处需要用到智能指针:

using SockSPtr = std::shared_ptr<Socket>;

注意:override 标记的函数必须是虚函数! 

 TcpSocket类

class TcpSocket : public Socket
{
public:
    TcpSocket();
    TcpSocket(int sockfd);
    ~TcpSocket();
    // 创建套接字
    void CreaterSocketOrDie() override;
    // 绑定套接字
    void CreaterBindOrDie(uint16_t port) override;     
    // 监听套接字
    void CreaterListenOrDie(int backlog) override;
    // 获取连接
    SockSPtr Accepter(InetAddr *cliaddr) override;
    // 建立连接
    bool Connector(const std::string &peerip, uint16_t peerport) override; 
    // 获取_sockfd
    int Sockfd();
    // 关闭套接字
    void Close();

private:
    int _sockfd; // 可以是listensock 也可以是普通sockfd
};

3.2.2、构造析构函数

构造函数可以实现两个,一个无参构造,一个有参构造(传参sockfd),用于初始化成员变量析构函数可以不做处理,后面关闭套接字自己调用关闭函数即可!

TcpSocket()
{}
TcpSocket(int sockfd) : _sockfd(sockfd)
{}
~TcpSocket()
{}

3.2.3、创建套接字

使用socket()函数按照需求格式创建套接字即可!

// 创建套接字
void CreaterSocketOrDie() override 
{
    // 创建socket
    _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
        LOG(FATAL, "socket create eror\n");
        exit(SOCKET_ERROR);
    }
    LOG(INFO, "socket create success,sockfd: %d\n", _sockfd); // 3
}

3.2.4、绑定套接字 

使用bind()函数绑定套接字即可!

// 绑定套接字
void CreaterBindOrDie(uint16_t port) override 
{
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = INADDR_ANY;

    // bind sockfd 和 socket addr
    if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        LOG(FATAL, "bind eror\n");
        exit(BIND_ERROR);
    }
    LOG(INFO, "bind success,sockfd: %d\n", _sockfd);
}

3.2.5、监听套接字

使用listen()函数监听套接字即可!

// 监听套接字
void CreaterListenOrDie(int backlog) override 
{
    // 因为tcp是面向连接的,tcp需要未来不短地获取连接
    // 老板模式,随时等待被连接
    if (::listen(_sockfd, backlog) < 0)
    {
        LOG(FATAL, "listen eror\n");
        exit(LISTEN_ERROR);
    }
    LOG(INFO, "listen success\n");
}

3.2.6、获取连接

使用accept()函数获取连接即可,此处返回SockSPtr智能指针!

// 获取连接
SockSPtr Accepter(InetAddr *cliaddr) override 
{
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    // 获取新连接
    int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
    // 获取失败继续获取
    if (sockfd < 0)
    {
        LOG(WARNING, "sccept reeor\n");
        return nullptr;
    }
    *cliaddr = InetAddr(client);
    LOG(INFO, "get a new link,client info: %s,sockfd:%d\n", cliaddr->AddrStr().c_str(), _sockfd);

    return std::make_shared<TcpSocket>(sockfd); // C++14
}

3.2.7、建立连接

使用connect()函数建立连接,连接成功返回true,连接失败返回false!

// 建立连接
bool Connector(const std::string &peerip, uint16_t peerport) override 
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(peerport);
    // server.sin_addr.s_addr =
    ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

    // 与服务端建立连接
    int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
    // 也可以重连
    if (n < 0)
    {
        std::cerr << "connect socket error" << std::endl;
        return false;
    }
    return true;
}

3.2.8、其他函数

其他函数包括获取_sockfd和关闭套接字函数第一个函数直接返回成员变量即可,第二个函数调用close()函数,前提是文件描述符存在

// 获取_sockfd
int Sockfd() override
{
    return _sockfd;
}
// 关闭套接字 
void Close() override
{
    if (_sockfd > 0)
    {
        ::close(_sockfd);
    }
}

3.2.9、接收发送消息函数

使用recv()函数和send()函数分别接收和发送消息!

// 接收消息 
ssize_t Recv(std::string *out) override
{
    char inbuffer[4096]; 
    ssize_t n = ::recv(_sockfd,inbuffer,sizeof(inbuffer) - 1, 0);
    if(n > 0)
    {
        inbuffer[n] = 0;
        *out = inbuffer;
    }
    return n;
}
// 发送消息
ssize_t Send(const std::string &in) override
{
    return ::send(_sockfd,in.c_str(),in.size(),0);
}

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐