Day4: Lars V0.4连接和消息封装 + 客户端接口

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的回显

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值