1 消息封装
msg_head:包含int msgid, int msglen 两个属性
#pragma once
/*解决tcp粘包问题的消息封装*/
struct msg_head
{
int msgid; //当前消息类型
int msglen; //消息体长度
};
//消息头长度,为固定值
#define MESSAGE_HEAD_LEN 8
//消息头+体的最大长度限制
#define MESSAGE_LENGTH_LIMIT (65535-MESSAGE_HEAD_LEN)
2 连接封装tcp_conn
2.1 属性
_connfd:当前连接的套接字
event_loop *_loop:当前所关联的epoll堆
输出缓冲+输入缓冲
class tcp_conn
{
public:
// 初始化conn
tcp_conn(int connfd, event_loop *loop);
// 被动处理读业务方法 由event_loop监听触发
void do_read(event_loop *loop, int fd, void *args);
// 被动处理写业务方法
void do_write(event_loop *loop, int fd, void *args);
// 主动发送消息的方法
int send_message(const char *data, int msglen, int mgsid);
// 销毁当前链接
void clean_conn();
private:
// 当前连接的套接字fd
int _connfd;
// 当前连接归属于哪个event_loop
event_loop *_loop;
// 输出output_buf
output_buf obuf;
// 输入input_buf
input_buf ibuf;
};
2.2 方法
2.2.1 构造函数
设置conn为非阻塞
将conn的读事件和回调加入到event_loop
tcp_conn::tcp_conn(int connfd, event_loop *loop)
{
_connfd = connfd;
_loop = loop;
//1 将connfd设置为非阻塞状态
int flag = fcntl(_connfd, F_SETFL, 0); //将connfd的全部状态清空
fcntl(_connfd, F_SETFL, O_NONBLOCK|flag); //设置为非阻塞
// 2 设置TCP_NODELAY,禁止读写缓存,降低小包延迟
int op = 1;
setsockopt(_connfd, IPPROTO_TCP, TCP_NODELAY, &op, sizeof(op));
//将当前的tcp_conn的读事件 加入到loop中进行监听
_loop->add_io_envent(_connfd, conn_rd_callback, EPOLLIN, this);
}
2.2.2 do_read
处理读事件的回调函数,用于解析包头和包体,调用回显业务------>send_message()方法
// 被动处理读业务方法 由event_loop监听触发
void tcp_conn::do_read(event_loop *loop, int fd, void *args) //隐藏一个this指针
{
// 1 从connfd中读数据
int ret = ibuf.read_data(_connfd);
if(ret==-1)
{
fprintf(stderr, "read data from socket\n");
this->clean_conn();
return;
}
else if(ret==0)
{
// 对端客户端正常关闭
printf("peer client closed\n");
this->clean_conn();
}
// 读出来的消息头
msg_head head;
// 2 读过来的数据是否满足8字节
while(ibuf.length()>=MESSAGE_HEAD_LEN)
{
// 2.1 先读头部,得到msgid, msglen
memcpy(&head, ibuf.data(), MESSAGE_HEAD_LEN);
if(head.msglen>MESSAGE_LENGTH_LIMIT || head.msglen<0){
fprintf(stderr, "data format error\n");
this->clean_conn();
break;
}
ibuf.pop(MESSAGE_HEAD_LEN);
// 2.2 判断得到的消息体长度和头部里的长度是否一致
if(ibuf.length()<MESSAGE_HEAD_LEN+head.msglen)
{
// 缓存中buf剩余的收数据小于应该接收到的数据,说明当前不是一个完整包
break;
}
// 表示当前包是合法的
ibuf.pop(MESSAGE_HEAD_LEN);
// 处理ibuf.data()业务数据
printf("read data = %s\n", ibuf.data());
callback_busi(ibuf.data(), head.msglen, head.msgid, NULL, this);
// 消息处理结束
ibuf.pop(head.msglen);
}
ibuf.adjust();
return;
}
2.2.3 send_message
将读到的数据发送给对端,实际为填充obuf的缓冲区,激活conn写事件,让event_loop触发,将conn的写事件和回调加入到event_loop中。
// 主动发送消息的方法
int tcp_conn::send_message(const char *data, int msglen, int msgid)
{
printf("server send message: data: %s, length: %d \n", data, msglen);
bool active_epollout = false;
if(obuf.length()==0)
{
// obuf为空,需要再次发送。激活epoll的写事件回调
// obuf不为空,数据还没有完全写完到对端,等全部写完再激活
active_epollout = true;
}
// 1 封装一个message包头
msg_head head;
head.msgid = msgid;
head.msglen = msglen;
int ret = obuf.send_data((const char *)&head, MESSAGE_HEAD_LEN);
if(ret!=0){
fprintf(stderr, "send head error\n");
return -1;
}
// 2 写消息体
ret = obuf.send_data(data, msglen);
if(ret!=0){
fprintf(stderr, "send data error\n");
// 如果消息体写失败,消息头需要弹出
obuf.pop(MESSAGE_HEAD_LEN);
return -1;
}
// 3 将_connfd添加一个写事件 EPOLLOUT
if(active_epollout==true)
{
_loop->add_io_envent(_connfd, conn_wt_callback, EPOLLOUT, this);
}
return 0;
}
2.2.4 do_write
处理写事件的回调函数。将obuf中的数据通过io写给客户端,如果obuf数据传送完,则从event_loop中删除conn的写事件。
// 被动处理写业务方法
void tcp_conn::do_write(event_loop *loop, int fd, void *args)
{
// do_write就表示obuf中已经有要写的数据,将obuf的数据io write发给对端
while(obuf.length())
{
int write_num = obuf.write2fd(_connfd);
if(write_num==-1)
{
fprintf(stderr, "tcp_conn write error\n");
this->clean_conn();
return ;
}
else if(write_num==0)
{
//当前不可写
break;
}
}
if(obuf.length()==0)
{
//数据已经全部写完,将_connfd的写事件删除
_loop->del_io_event(_connfd, EPOLLOUT);
}
return ;
}
3 tcp_server接收
tcp_server在accept成功之后,将得到的新的connfd构造一个tcp_conn对象。
//开始提供创建链接的服务
void tcp_server::do_accept(){
int connfd;
while(1){
//1 accept
connfd = accept(_sockfd, (struct sockaddr*)&_connaddr, &_addrlrn);
if(connfd==-1)
{
if(errno==EINTR){
fprintf(stderr, "accept error = EINTR\n"); //中断错误
continue;
}
else if(errno==EAGAIN){
fprintf(stderr, "accept error = EAGAIN\n");
break;
}
else if(errno == EMFILE){
//建立连接过多,资源不够
fprintf(stderr, "accpet error = EMFILE\n");
continue;
}
else exit(1);
}else{
//accept success
//添加心跳机制...
//添加消息队列机制...
// 创建一个新的tcp_conn连接对象
tcp_conn *conn = new tcp_conn(connfd, _loop);
if(conn==NULL)
{
fprintf(stderr, "new tcp_conn error\n");
exit(1);
}
printf("get noew conn\n");
break;
}
}
}
tcp_server已经构造完毕,当client进行连接, server进行accpet(),得到新的connfd------->server将新的connfd构造成一个tcp_conn对象,该对象进行基本的读写业务等(已经将连接封装为一个tcp_conn对象)
4 Lars V0.4客户端接口
4.1 封装tcp_client类
4.1.1 属性
_sockfd:客户端连接套接字
event_loop:事件监听机制
允许开发注册的客户端回调函数,用于处理服务端返回数据的回调业务。
typedef void msg_callback(const char *data, uint32_t len, int msgid, tcp_client *client, void *args);
class tcp_client
{
public:
tcp_client(event_loop *loop, const char *ip, unsigned short port);
// 发送方法
int send_message(const char *data, int msglen, int msgid);
// 处理读写业务
void do_read();
void do_write();
// 释放连接
void clean_conn();
// 连接服务器
void do_connect();
// 设置业务处理的回调函数
void set_msg_callback(msg_callback *msg_cb)
{
this->msg_callback = msg_cb;
}
// 输入缓冲
input_buf ibuf;
// 输出缓冲
output_buf obuf;
// server端ip地址
struct sockaddr_in _server_addr;
private:
int _sockfd; //当前客户端连接
socklen_t _addrlen;
// 客户端时间的处理机制
event_loop *_loop;
// 客户端处理服务器消息的回调业务
msg_callback *msg_callback;
};
4.1.2 方法
(1)构造:客户端套接字创建
tcp_client::tcp_client(event_loop *loop, const char *ip, unsigned short port)
{
_sockfd = -1;
msg_callback = NULL;
_loop = loop;
// 封装即将要连接的远程server的IP地址
ZeroMemory(&_server_addr, sizeof(_server_addr));
_server_addr.sin_family = AF_INET;
inet_pton(ip, &_server_addr.sin_addr);
_server_addr.sin_port = htons(port);
// 连接远程
this->do_connect();
}
(2)send_message():主动发送消息的方法
int tcp_client::send_message(const char *data, int msglen, int msgid)
{
bool active_epollout = false;
if(obuf.length()==0)
{
active_epollout = true;
}
msg_head head;
head.msgid = msgid;
head.msglen = msglen;
// 写消息头
int ret = obuf.send_data((const char*)&head, MESSAGE_HEAD_LEN);
if(ret!=0)
{
fprintf(stderr, "send head error\n");
return -1;
}
// 消息体
ret = obuf.send_data(data, msglen);
if(ret!=0)
{
fprintf(stderr, "send dara error\n");
obuf.pop(MESSAGE_HEAD_LEN);
return -1;
}
if(active_epollout==true)
{
_loop->add_io_envent(_sockfd, write_callback, EPOLLOUT, this);
}
}
(3)do_read():处理读事件的回调函数
void tcp_client::do_read()
{
// 1 从connfd中读数据
int ret = ibuf.read_data(_sockfd);
if(ret==-1)
{
fprintf(stderr, "read data from socket\n");
this->clean_conn();
return;
}
else if(ret==0)
{
// 对端客户端正常关闭
printf("peer server closed\n");
this->clean_conn();
}
msg_head head;
// 2 读过来的数据是否满足8字节
while(ibuf.length()>=MESSAGE_HEAD_LEN)
{
// 2.1 先读头部,得到msgid, msglen
memcpy(&head, ibuf.data(), MESSAGE_HEAD_LEN);
if(head.msglen>MESSAGE_LENGTH_LIMIT || head.msglen<0){
fprintf(stderr, "data format error\n");
this->clean_conn();
break;
}
ibuf.pop(MESSAGE_HEAD_LEN);
// 2.2 判断得到的消息体长度和头部里的长度是否一致
if(ibuf.length()<MESSAGE_HEAD_LEN+head.msglen)
{
// 缓存中buf剩余的收数据小于应该接收到的数据,说明当前不是一个完整包
break;
}
// 表示当前包是合法的
ibuf.pop(MESSAGE_HEAD_LEN);
// 处理ibuf.data()业务数据
if(msg_callback!=NULL)
{
this->msg_callback(ibuf.data(), head.msglen, head.msgid, this, NULL);
}
// 消息处理结束
ibuf.pop(head.msglen);
}
ibuf.adjust();
return;
}
(4)do_write():处理写事件的回调函数
void tcp_client::do_write()
{
while(obuf.length())
{
int write_num = obuf.write2fd(_sockfd);
if(write_num==-1)
{
fprintf(stderr, "tcp_client write error\n");
this->clean_conn();
return ;
}
else if(write_num==0)
{
//当前不可写
break;
}
}
if(obuf.length()==0)
{
//数据已经全部写完,将_sockfd的写事件删除
_loop->del_io_event(_sockfd, EPOLLOUT);
}
return ;
}
(5)clean_conn():清空对象资源
void tcp_client::clean_conn()
{
if(_sockfd!=-1)
{
printf("clean conn, del socket\n");
_loop->delete_io_event(_sockfd);
close(_sockfd);
}
}
(6)do_connect():主动和服务器创建链接
void tcp_client::do_connect()
{
if(_sockfd!=-1)
{
close(_sockfd);
}
// 创建套接字(非阻塞)
_sockfd = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP);
if(_sockfd==-1)
{
fprintf(stderr, "create tcp client socker error\n");
exit(1);
}
int ret = connect(_sockfd, (const sockaddr*)&_server_addr, _addrlen);
if(ret==0)
{
// 创建连接成功,主动调用send_message发包
printf("connect %s:%d succ!\n", inet_ntoa(_server_addr.sin_addr), ntohs(_server_addr.sin_port));
conn_succ(_loop, _sockfd, this);
}else{
// 失败
if(errno==EINPROGRESS)
{
// fd是非阻塞的,会出现这个错误,但不代表创建连接失败
// 如果fd可写,表示连接创建成功
printf("do connect, EINPROGRESS\n");
// event_loop检测当前sockfd是否可写,如果回调被执行,说明可写,连接创建成功
_loop->add_io_envent(_sockfd, conn_succ, EPOLLOUT, this);
}
fprintf(stderr, "connection error\n");
exit(1);
}
}
tcp_client的主要流程:首先,创建一个当前客户端的套接字,去执行do_connect(如果成功,直接调用业务;如果失败,判断errno,是否可写,执行连接成功的业务);连接成功时,强制给server端回信,调用send_message()将数据写到cli的obuf中,同时激活写事件,调用write_callback回调方法,其中执行do_write,将数据写给对端之后删掉_connfd的写事件。client接收server回写的相同数据,触发cli fd的读回调,执行do_read,从包中读数据,执行msg_callback的回显