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