前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

Socket

作者头像
二肥是只大懒蓝猫
发布2024-02-07 07:13:07
760
发布2024-02-07 07:13:07
举报
文章被收录于专栏:热爱C嘎嘎热爱C嘎嘎

封装socket接口,方便后续的使用。

Socket模块介绍

Socket模块简单理解就是对socket套接字的封装,当然不是简单的对socket套接字接口的封装,还需要实现一些方法,比如启动非阻塞通信、创建客户端连接、创建服务器连接等。

其意义是程序中对于套接字的各项操作更加简便。

功能

主要实现的是功能是:

创建套接字(socket()) 绑定地址信息(bind()) 开始监听(listen()) 获取新连接(accept()) 向服务器发起连接(connect()) 发送数据(send()) 接收数据(recv()) 启动非阻塞通信(发送非阻塞,接收非阻塞,套接字非阻塞) 创建客户端连接 创建服务器连接 关闭套接字 获取套接字 启动地址端口重用。

代码实现将其细节

成员变量

成员变量只需一个就好,即描述符变量。

代码语言:javascript
复制
private:
    int _sockfd;/*套接字描述符*/

1.构造方法 

在无参构造中,直接将成员变量_sockfd先初始化为-1,以免随机值作乱。而有参构造中,在使用者的视角下,可以传入一个描述符,比如监听描述符进行Socket对象的创建。

代码语言:javascript
复制
    Socket()
        :_sockfd(-1)
    {}
    Socket(int sockfd)
        :_sockfd(sockfd)
    {}

2.创建套接字

调用socket()方法,创建出sock套接字,接着将其赋值给成员变量_sockfd。在创建时,选择TCP协议和ipv4。

代码语言:javascript
复制
    bool CreateSockfd()
    {
        /*协议域、套接字类型 指定特定协议*/
        _sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(_sockfd < 0)
        {
            ERR_LOG("SCOKFD CREATE FALIED!");
            return false;
        }
        /*创建成功*/
        return true;
    }

3.绑定地址信息

通过调用bind()方法,绑定服务器自身的ip和端口号。

代码语言:javascript
复制
bool Bind(const std::string &ip,uint16_t port)
{
    //int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    struct sockaddr_in addr;//先创建sockaddr结构体,用于填充服务器绑定的ip和端口号
    addr.sin_family = AF_INET;/*使用IPV4*/
    addr.sin_port = htons(port);/*将端口号转换成网络字节序*/
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(struct sockaddr_in);/*获取结构体的长度*/
    int ret = bind(_sockfd,(struct sockaddr*)&addr,len);
    if(ret < 0)
    {
        ERR_LOG("BIND FAILED!");
        return false;
    }
    return true;
}

绑定地址信息的操作是:需要创建sockaddr_in结构体,用于存储地址、端口号和协议版本。

创建出结构体后,分别填入协议版本,端口号和ip。

端口号需要将其转成网络字节序,是为了确保不同平台之间的数据交换一致性,htons将主机字节序的短整型数转换为网络字节序的短整型数,网络字节序默认为升序。同样的,在填充ip的时候,inet_addr会接收一个点分十进制的ip地址,inet_addr还会将其转化成网络字节序。

在使用bind绑定的时候,因为struct sockaddr是通用结构体,因此在绑定的时候,需要转化成struct sockaddr类型。

sockaddr_in提供了一个明确的、针对 IPv4 地址的结构,程序员可以直接操作 sin_portsin_addr 成员,而不需要关心如何在 sa_data 字段中编码这些信息,因此我们先使用sockaddr_in结构体进行填充,再转化成sockaddr。

4.开始监听

listen方法需要传入最大监听数量,使用宏定义定义了MAX_LISTEN,直接传入即可。

代码语言:javascript
复制
bool Listen(int backlog = MAX_LISTEN)
{
    //int listen(int sockfd, int backlog);
    int ret = listen(_sockfd,backlog);
    if(ret < 0)
    {
        ERR_LOG("LISTEN FALIED!");
        return false;
    }
    return true;
}

5.获取新连接

代码语言:javascript
复制
int Accept()
{
    //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    int newfd = accept(_sockfd,NULL,NULL);/*返回一个用于通信的套接字*/
    if(newfd < 0)
    {
        ERR_LOG("ACCEPT FALIED!");
        return -1;
    }
    return newfd;
}

这个方法是用于传入监听套接字,创建用于通信的套接字的方法,因此需要返回newfd。由于这次服务器并不需要关心客户端的ip端口,并且在后续创建服务器连接的时候,服务器会绑定"0.0.0.0"所有可用的网络接口,因此填入NULL即可。

6.向服务器发起连接

connect是客户端向服务器发起连接的操作,需要得到目标服务器的地址、端口号和协议版本。

代码语言:javascript
复制
bool Connect(const string &ip,uint16_t port)
{
    //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    struct sockaddr_in addr;//先创建sockaddr结构体,用于填充服务器绑定的ip和端口号
    addr.sin_family = AF_INET;/*使用IPV4*/
    addr.sin_port = htons(port);/*将端口号转换成网络字节序*/
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(struct sockaddr_in);/*获取结构体的长度*/        
    int ret = connect(_sockfd,(struct sockaddr*)&addr,len);
    if(ret < 0)
    {
        ERR_LOG("CONNECT FAILED!");
        return false;
    }
    return true;
}

7.发送数据

通过封装send方法,直接传入装有数据的容器,数据的长度和标志(阻塞或非阻塞)。需要注意的是如果在发送出错的时候,如果是目标接收缓冲区已满,或者是在发送期间受到了中断信号,返回0,建议重新发送,除此之外返回-1,表示发送出错。如果没出错,返回发送的数据量。

代码语言:javascript
复制
/*将buf中的数据通过套接字发送出去*/
ssize_t Send(void* buf,size_t len,int flag = 0)
{
    //ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t ret = send(_sockfd,buf,len,flag);
    if(ret < 0)
    {
        /*EAGAIN:在非阻塞模式下,如果 socket 缓冲区已满,send 会返回此错误,表示应稍后重试发送操作*/
        /*EINTR:表示在 send 调用期间收到了中断信号,这种情况下也建议进行重试发送操作*/
        if(errno==EAGAIN||errno==EINTR)
        {
            return 0;
        }
        ERR_LOG("SOCKET SEND FAILED!");
        return -1;
    }
    return ret;
}

8.非阻塞发送数据

非阻塞发送数据,即进一步封装Send,对于flag参数直接传入MSG_DONTWAIT标志,表示进行非阻塞发送数据,前提是socket套接字是非阻塞的。

代码语言:javascript
复制
ssize_t NonBlockSend(void* buf,size_t len)
{
    if(len == 0) return 0;
    return Send(buf,len,MSG_DONTWAIT);
}

9.接收数据

通过封装recv接口,与send一样,如果在接收数据的时候出错, 如果出错信息是EAGAIN(没有数据可读)或者EINTR(接收期间中断),那么不会视为严重的出错,会给用户返回0,表示让用户重新接收,否则会返回-1,表示接收出错。如果没有出错,那么返回接收数据的字节数。

代码语言:javascript
复制
ssize_t Recv(void *buf,size_t len,int flag = 0)
{
    //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    ssize_t ret = recv(_sockfd,buf,len,flag);
    if(ret < 0)
    {
        if(errno==EAGAIN||errno==EINTR)
        {
            return 0;
        }
        ERR_LOG("SOCKET RECV FAILED!");
        return -1;
    }
    return ret;
}

10.非阻塞接收数据

进一步封装Recv,对Recv的第三个参数flag直接传入MSG_DONTWAIT,表示非阻塞接收数据。

代码语言:javascript
复制
ssize_t NonBlockRecv(void *buf,ssize_t len)
{
    return Recv(buf,len,MSG_DONTWAIT);
}

11.将套接字设置为非阻塞

将套记者设置为非阻塞的操作是通过系统提供的fcntl接口进行的。操作分为两步:

①先通过fcntl,将其命令参数设置为**F_GETFL**,意思是获取套接字_sockfd的文件状态标志,并赋予给变量flag。

②再次通过fcntl,将其命令参数设置为**F_SETFL**,接着使用**O_NONBLOCK**标志位跟flag进行或运算,O_NONBLOCK 是一个标志位,表示非阻塞模式。通过按位或(|)操作将 O_NONBLOCK 添加到先前获取的 flag 中,然后将结果作为新的标志值传递给 fcntl 函数,从而将套接字设置为非阻塞模式。

流程简单来说就是:先获取套接字的文件状态标志,然后将非阻塞属性跟套接字的文件状态标志设置在一起,从而让套接字变成非阻塞。

代码语言:javascript
复制
void NonBlock()
{
    //int fcntl(int fd, int cmd, ... /* arg */ );
    int flag = fcntl(_sockfd,F_GETFL,0);
    fcntl(_sockfd,F_SETFL,flag|O_NONBLOCK);
}

12.启用地址端口重用

通过setsockopt方法对地址和端口设置重用。首先定义val变量,初始化为1,val的作用在setsockopt方法中是用于控制是否启用套接字重用选项。val为1表示开启相应选项,val为0表示禁止相应选项。

重用地址:首先传入需要设置的描述符_sockfd,接着是选项的级别是套接字级别SOL_SOCKET,接着是操作SO_REUSEADDR,表示地址重用,最后将变量 val 的地址(通过 (void*)&val 获取)和其大小(sizeof(int))作为参数传递给 setsockopt 函数,从而启用地址重用选项。

重用端口号:跟重用地址的操作一样,操作改为SO_REUSEPORT即可。

代码语言:javascript
复制
void ReuseAddress()
{
    //int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
    int val = 1;
    setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR,(void*)&val,sizeof(int));
    val = 1;
    setsockopt(_sockfd,SOL_SOCKET,SO_REUSEPORT,(void*)&val,sizeof(int));
}

13.创建客户端连接

创建客户端连接的步骤只有两步,那就是先创建出套接字,然后向指定服务器发送连接。客户端的套接字通常**不需要手动绑定**(bind)地址和端口号,是因为:

①通常当客户端创建一个套接字的时候,系统会自动分配端口号,不需要用户在创建时显示绑定,系统会自动绑定的。 ②一般是客户端主动发起连接,不是服务器主动发起连接,因此这也说明了服务器是需要显示绑定,而客户端不需要显示绑定。 ③客户端的ip地址是动态获取的。

代码语言:javascript
复制
bool CreateClient(uint16_t port,const std::string &ip)
{
    /*创建客户端连接的步骤为:1.创建套接字 2.发起连接*/
    if(CreateSockfd()==false) return false;
    if(Connect(ip,port)==false) return false;
    return true;
}

14.创建服务器连接

创建服务端连接的步骤有四步:创建套接字、绑定地址信息、是否开启非阻塞、开始监听、开启地址端口重用。在绑定的地址信息中,选择"0.0.0.0"作为默认参数的作用是:

①"0.0.0.0" 是一个特殊的IPv4地址,被称为“任意”或“全零”地址。当服务端绑定到这个地址时,它表示服务端将监听所有可用的网络接口(包括本地回环接口和所有配置的公网接口)。

②通过绑定到 "0.0.0.0",服务端可以接受来自任何网络接口上客户端的连接请求。这意味着无论客户端是通过本地网络还是互联网进行连接,只要它们能够到达服务器所在的网络,服务端都能够响应。

服务端选择是否开启非阻塞的原因是:服务端通常需要处理来自多个客户端的并发连接。在非阻塞模式下,服务端可以使用 I/O 多路复用技术(如 epoll、kqueue 或 select 等)来同时监控多个套接字的事件,从而提高服务端的并发性能和效率。因此,服务端可以根据需求选择是否开启非阻塞通信。

代码语言:javascript
复制
bool CreateServer(uint16_t port,const std::string &ip = "0.0.0.0",bool block_flag = false)
{
    /*创建服务端连接的步骤为:1.创建套接字 2.绑定地址信息 3.是否非阻塞 4.开始监听 4.开启地址端口重用*/
    if(CreateSockfd()==false) return false;
    if(block_flag==true) NonBlock();
    if(Bind(ip,port)==false) return false;
    if(Listen()==false) return false;
    ReuseAddress();
    return true;
}

15.关闭套接字

代码语言:javascript
复制
void Close()
{
    if(_sockfd!=-1)
    {
        close(_sockfd);
        _sockfd = -1;
    }
}

16.获取套接字

代码语言:javascript
复制
int GetFd()
{
    return _sockfd;
}

17.析构

代码语言:javascript
复制
~Socket()
{
    Close();
}

完整代码

代码语言:javascript
复制
#define MAX_LISTEN 1024
/*Socket模块*/
class Socket
{
private:
    int _sockfd;/*套接字描述符*/
public:
    Socket()
        :_sockfd(-1)
    {}
    Socket(int sockfd)
        :_sockfd(sockfd)
    {}
    bool CreateSockfd()
    {
        /*协议域、套接字类型 指定特定协议*/
        _sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(_sockfd < 0)
        {
            ERR_LOG("SCOKFD CREATE FALIED!");
            return false;
        }
        /*创建成功*/
        return true;
    }

    bool Bind(const std::string &ip,uint16_t port)
    {
        //int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in addr;//先创建sockaddr结构体,用于填充服务器绑定的ip和端口号
        addr.sin_family = AF_INET;/*使用IPV4*/
        addr.sin_port = htons(port);/*将端口号转换成网络字节序*/
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);/*获取结构体的长度*/
        int ret = bind(_sockfd,(struct sockaddr*)&addr,len);
        if(ret < 0)
        {
            ERR_LOG("BIND FAILED!");
            return false;
        }
        return true;
    }

    bool Listen(int backlog = MAX_LISTEN)
    {
        //int listen(int sockfd, int backlog);
        int ret = listen(_sockfd,backlog);
        if(ret < 0)
        {
            ERR_LOG("LISTEN FALIED!");
            return false;
        }
        return true;
    }

    int Accept()
    {
        //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        int newfd = accept(_sockfd,NULL,NULL);/*返回一个用于通信的套接字*/
        if(newfd < 0)
        {
            ERR_LOG("ACCEPT FALIED!");
            return -1;
        }
        return newfd;
    }

    bool Connect(const std::string &ip,uint16_t port)
    {
        //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in addr;//先创建sockaddr结构体,用于填充服务器绑定的ip和端口号
        addr.sin_family = AF_INET;/*使用IPV4*/
        addr.sin_port = htons(port);/*将端口号转换成网络字节序*/
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);/*获取结构体的长度*/        
        int ret = connect(_sockfd,(struct sockaddr*)&addr,len);
        if(ret < 0)
        {
            ERR_LOG("CONNECT FAILED!");
            return false;
        }
        return true;
    }
    /*将buf中的数据通过套接字发送出去*/
    ssize_t Send(const void* buf,size_t len,int flag = 0)
    {
        //ssize_t send(int sockfd, const void *buf, size_t len, int flags);
        ssize_t ret = send(_sockfd,buf,len,flag);
        if(ret < 0)
        {
            /*EAGAIN:在非阻塞模式下,如果 socket 缓冲区已满,send 会返回此错误,表示应稍后重试发送操作*/
            /*EINTR:表示在 send 调用期间收到了中断信号,这种情况下也建议进行重试发送操作*/
            if(errno==EAGAIN||errno==EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET SEND FAILED!");
            return -1;
        }
        return ret;
    }

    ssize_t NonBlockSend(void* buf,size_t len)
    {
        if(len == 0) return 0;
        return Send(buf,len,MSG_DONTWAIT);
    }

    ssize_t Recv(void *buf,size_t len,int flag = 0)
    {
        //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
        ssize_t ret = recv(_sockfd,buf,len,flag);
        if(ret < 0)
        {
            if(errno==EAGAIN||errno==EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET RECV FAILED!");
            return -1;
        }
        return ret;
    }

    ssize_t NonBlockRecv(void *buf,ssize_t len)
    {
        return Recv(buf,len,MSG_DONTWAIT);
    }

    void NonBlock()
    {
        //int fcntl(int fd, int cmd, ... /* arg */ );
        int flag = fcntl(_sockfd,F_GETFL,0);
        fcntl(_sockfd,F_SETFL,flag|O_NONBLOCK);
    }

    void ReuseAddress()
    {
        //int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
        int val = 1;
        setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR,(void*)&val,sizeof(int));
        val = 1;
        setsockopt(_sockfd,SOL_SOCKET,SO_REUSEPORT,(void*)&val,sizeof(int));
    }

    bool CreateClient(uint16_t port,const std::string &ip)
    {
        /*创建客户端连接的步骤为:1.创建套接字 2.发起连接*/
        if(CreateSockfd()==false) return false;
        if(Connect(ip,port)==false) return false;
        return true;
    }

    bool CreateServer(uint16_t port,const std::string &ip = "0.0.0.0",bool block_flag = false)
    {
        /*创建服务端连接的步骤为:1.创建套接字 2.绑定地址信息 3.是否非阻塞 4.开始监听 4.开启地址端口重用*/
        if(CreateSockfd()==false) return false;
        if(block_flag==true) NonBlock();
        if(Bind(ip,port)==false) return false;
        if(Listen()==false) return false;
        ReuseAddress();
        return true;
    }

    void Close()
    {
        if(_sockfd!=-1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

    int GetFd()
    {
        return _sockfd;
    }

    ~Socket()
    {
        Close();
    }
};

简单测试

客户端tcpclient.cc

代码语言:javascript
复制
#include"../server.hpp"

int main()
{
    /*创建客户端连接,发送数据、接收来自服务器的数据*/
    Socket cli_sock;
    cli_sock.CreateClient(8100,"127.0.0.1");
    while(1)
    {
        std::string str = "hello,server!";
        cli_sock.Send(str.c_str(),str.size());
        char buf[1024]={0};
        cli_sock.Recv(buf,1023);
        DBG_LOG("server resp:%s",buf);
        sleep(3);
    }
    return 0;
}

服务器tcpserver.cc

代码语言:javascript
复制
#include"../server.hpp"

int main()
{
    /*创建服务器连接---接收来自客户端的数据,向服务器发送数据*/
    Socket lis_sock;
    lis_sock.CreateServer(8100);
    int newfd = lis_sock.Accept();
    Socket cli_sock(newfd);
    while(1)
    {
        char buf[1024]={0};
        int ret = cli_sock.Recv(buf,1023);
        if(ret < 0)
        {
            cli_sock.Close();
            continue;
        }
        DBG_LOG("client said:%s",buf);
        std::string str = "hello,client!";
        cli_sock.Send(str.c_str(),str.size());
        sleep(3);
    }
    lis_sock.Close();

    return 0;
}

结果 

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Socket模块介绍
  • 功能
  • 代码实现将其细节
    • 成员变量
      • 1.构造方法 
        • 2.创建套接字
          • 3.绑定地址信息
            • 4.开始监听
              • 5.获取新连接
                • 6.向服务器发起连接
                  • 7.发送数据
                    • 8.非阻塞发送数据
                      • 9.接收数据
                        • 10.非阻塞接收数据
                          • 11.将套接字设置为非阻塞
                            • 12.启用地址端口重用
                              • 13.创建客户端连接
                                • 14.创建服务器连接
                                  • 15.关闭套接字
                                    • 16.获取套接字
                                      • 17.析构
                                        • 完整代码
                                        • 简单测试
                                          • 客户端tcpclient.cc
                                            • 服务器tcpserver.cc
                                              • 结果 
                                              相关产品与服务
                                              容器服务
                                              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档