Socket

2024-02-07 07:13:07 浏览数 (3)

封装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;
}

结果 

0 人点赞