day8-将连接抽象出来
在之前的代码中,我们分离出了Acceptor、Reactor(EventLoop)以及Handler(Channel),但是在newConnection方法中我们缺少delete,会导致内存泄露的发生,考虑一下为什么会导致不使用delete释放呢?
个人理解是由于不知道该什么时候释放,这里使用智能指针的话可能会更好,不用就是为了我们理解。
最好的解决方式就是建立一个Connection类以方便管理。
那么今天的任务就来了,分离connection,能学习的点就在抽象对象更为通用且方便管理。
再明确一次socket:
客户端在于服务器建立链接的时候创建一个socket,通过这个socket可以进行数据传输
服务器在建立时就会带有一个socket,通过这个socket可以进行数据传输
通信时是利用两个socket进行数据传输。
1、错误检测机制(没变)
2、地址创建(没变)
3、socket创建(没变)
4、并发epoll(没变)
5、Channel(没变)
6、EventLoop(没变)
7、Acceptor
这里开始发生变化了,之前有说过Acceptor的作用就是建立连接的作用,但是什么时候释放这个连接所创建出来地址等信息是不明确的,应当是在关于该连接的一切行为都结束了在将这个连接delete,但是没有办法知道什么时候完全结束。。。
还是先从声明开始解读:
class EventLoop;
class Socket;
class Channel;
class Acceptor
{
private:
EventLoop *loop;
Socket *sock;
Channel *acceptChannel;
std::function<void(Socket*)> newConnectionCallback;
public:
Acceptor(EventLoop *_loop);
~Acceptor();
void acceptConnection();
void setNewConnectionCallback(std::function<void(Socket*)>);
};
和之前的相比少了一个对于地址的创建方法,应该是全部交接给了Connection类来完成,从实现来看看尝试找到变化发生的依据(因为这部分没有添加其他功能,因此,应该就是将某些东西抽象出去了)
先看看构造函数和析构函数,没发生什么变化,只不过我们发现Acceptor的地址属性少了以后释放的位置改了。
Acceptor::Acceptor(EventLoop *_loop) : loop(_loop), sock(nullptr), acceptChannel(nullptr){
sock = new Socket();
InetAddress *addr = new InetAddress("127.0.0.1", 1234);
sock->bind(addr);
sock->listen();
sock->setnonblocking();
acceptChannel = new Channel(loop, sock->getFd());
std::function<void()> cb = std::bind(&Acceptor::acceptConnection, this);
acceptChannel->setCallback(cb);
acceptChannel->enableReading();
delete addr;
}
Acceptor::~Acceptor(){
delete sock;
delete acceptChannel;
}
之后是acceptConnection和setNewConnectionCallback,在原本acceptConnection是从服务器的方法上走的,现在将这个放在Acceptor中,使得服务器类更纯粹,可预见的是服务器中应当没有这个方法了。
void Acceptor::acceptConnection(){
InetAddress *clnt_addr = new InetAddress();
Socket *clnt_sock = new Socket(sock->accept(clnt_addr));
printf("new client fd %d! IP: %s Port: %d\n", clnt_sock->getFd(), inet_ntoa(clnt_addr->getAddr().sin_addr), ntohs(clnt_addr->getAddr().sin_port));
clnt_sock->setnonblocking();
newConnectionCallback(clnt_sock);
delete clnt_addr;
}
void Acceptor::setNewConnectionCallback(std::function<void(Socket*)> _cb){
newConnectionCallback = _cb;
}
8、Connection
新的类,到目前应该还是懵的状态,因为并没看到Connection做了什么事情,分离了哪些功能。老规矩,看声明
class EventLoop;
class Socket;
class Channel;
class Connection
{
private:
EventLoop *loop;
Socket *sock;
Channel *channel;
std::function<void(Socket*)> deleteConnectionCallback;
public:
Connection(EventLoop *_loop, Socket *_sock);
~Connection();
void echo(int sockfd);
void setDeleteConnectionCallback(std::function<void(Socket*)>);
};
是不是和Acceptor非常像,那么推测他们应该是并行关系,即一个用来完成connection的创建一个用来完成connection的释放,看看猜测是否正确。
构造函数和析构函数是很像的,但是在这部分中不需要创建服务器的任务,会少一部份工作,该部分的回调函数echo需要看一下
Connection::Connection(EventLoop *_loop, Socket *_sock) : loop(_loop), sock(_sock), channel(nullptr){
channel = new Channel(loop, sock->getFd());
std::function<void()> cb = std::bind(&Connection::echo, this, sock->getFd());
channel->setCallback(cb);
channel->enableReading();
}
Connection::~Connection(){
delete channel;
delete sock;
}
回调函数及设置回调函数的方法,这部分和我们想象的不一样,这不是将原本服务器中的read方法给拿过来了吗?但是有个改变是将原本的close(sockfd);换成了deleteConnectionCallback(sock);
发现了!!!!其是将读取事件是否完成作为标志,判断事件是否结束,而close(sockfd);只是关闭了socket对于其他信息并没有进行析构,判断deleteConnectionCallback应该会更加完善。
void Connection::echo(int sockfd){
char buf[READ_BUFFER];
while(true){ //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
bzero(&buf, sizeof(buf));
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
if(bytes_read > 0){
printf("message from client fd %d: %s\n", sockfd, buf);
write(sockfd, buf, sizeof(buf));
} else if(bytes_read == -1 && errno == EINTR){ //客户端正常中断、继续读取
printf("continue reading");
continue;
} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕
printf("finish reading once, errno: %d\n", errno);
break;
} else if(bytes_read == 0){ //EOF,客户端断开连接
printf("EOF, client fd %d disconnected\n", sockfd);
// close(sockfd); //关闭socket会自动将文件描述符从epoll树上移除
deleteConnectionCallback(sock);
break;
}
}
}
void Connection::setDeleteConnectionCallback(std::function<void(Socket*)> _cb){
deleteConnectionCallback = _cb;
}
9、服务器类
这部分应该是变化很大的,因为将几乎所有方法都抽象出去了,首先看看声明
class EventLoop;
class Socket;
class Acceptor;
class Connection;
class Server
{
private:
EventLoop *loop;
Acceptor *acceptor;
std::map<int, Connection*> connections;
public:
Server(EventLoop*);
~Server();
void newConnection(Socket *sock);
void deleteConnection(Socket *sock);
};
多了一个方法和一个map属性,connections应该记录的是socket和对应的Connection,并且我们对于多出来的deleteConnection还是十分感兴趣的想知道其和close(sockfd)有什么差别,还是看看实现。
首先是构造/析构函数,没什么变化,依旧是从Acceptor中创建并建立客户端连接
Server::Server(EventLoop *_loop) : loop(_loop), acceptor(nullptr){
acceptor = new Acceptor(loop);
std::function<void(Socket*)> cb = std::bind(&Server::newConnection, this, std::placeholders::_1);
acceptor->setNewConnectionCallback(cb);
}
Server::~Server(){
delete acceptor;
}
之后是newConnection,在这个方法中我们创建了一个Connection 用以保存事件,并将它写入connections数组,防止之后找不到该套接字。
void Server::newConnection(Socket *sock){
Connection *conn = new Connection(loop, sock);
std::function<void(Socket*)> cb = std::bind(&Server::deleteConnection, this, std::placeholders::_1);
conn->setDeleteConnectionCallback(cb);
connections[sock->getFd()] = conn;
}
然后是对于deleteConnection方法的定义,重点看看其和close有什么差别。
void Server::deleteConnection(Socket * sock){
Connection *conn = connections[sock->getFd()];
connections.erase(sock->getFd());
delete conn;
}
首先将socket从connections中移除,然后将该Connection释放,close仅关闭了套接字,对于和socket相关的其他信息没有释放,而Connection 中包含了这些信息,仅释放就足够了。
10、总结
感觉还是挺乱的,那么总结一下这个最终变成什么关系了呢?
Server类目前只作为一个对外的接口,其功能包含创建服务器(Acceptor)、创建连接(Acceptor)、处理事件(Connection)以及关闭连接(Server)四个功能
main-Reactor(EventLoop)单独创建,其对每一个事件进行分配处理任务,即找到相关任务处理方。