TCP/IP网络编程(6)——基于I/O复用(epoll)编写简易聊天室

目录

前言

一、 实现epoll的相关函数和结构体

1. event结构体

2. epoll_create

3. epoll_ctl

4. epoll_wait

二、 条件触发版本的聊天室服务端

三、 边缘触发版本的聊天室服务端


前言

在本文中我将使用linux下的epoll技术替换上一篇文章中的select技术,以此来实现一个更优化的服务端,上一篇文章链接如下:

TCP/IP网络编程(5)——基于I/O复用(select)和多进程编写简易聊天室

相较于select函数,epoll有以下两个优点:第一,不需要对所有文件描述符进行遍历来找出发生事件的文件描述符。epoll函数会把发生事件的文件描述符单独保存在一个空间(数组)中;第二,epoll调用前不需要传递监视对象信息,而是直接由操作系统保存(就是select函数的fd_set参数,因为select调用完,fd_set会变化,所以每次调用前都要传递)。由于是向操作系统传递监视对象信息,所以这个开销很大。

epoll技术分为条件触发和边缘触发两种方式:条件触发即只要满足条件,就注册一次事件,比如事件为读取缓冲区中的值,只要缓冲区中有值,那么就会注册事件。边缘触发即只有事件发生时才注册事件,即使缓冲区还有值,也不会再注册事件。所以边缘触发的性能优于条件触发,但是需要保证在一次触发中处理完事件,条件触发则不用担心。

一、 实现epoll的相关函数和结构体

1. event结构体

该结构体用于声明监视对象和对应的事件

struct epoll_event{
    __uint32_t events; 
    epoll_data_t data;
    }
    typedef union epoll_data{
        void* ptr; 
        int fd; 
        __unit32_t u32;                 
        __unit64_t u64;
        }epoll_data_t;

常用的事件类型如下:

EPOLLIN:需要读取数据

EPOLLOUT:可以立即发送数据

EPOLLERR:发生错误

EPOLLRDHUP:断开连接或半关闭

EPOLLET:以边缘触发方式得到事件通知

当同时绑定多个事件时,用或运算符“|”

2. epoll_create

该函数用来创建保存epoll文件描述符的空间,这个空间称为epoll例程

int epoll_create(int size);

成功时返回epoll文件描述符,失败返回-1

(1)size:epoll实例的大小。但是操作系统会完全忽略传入的size参数

3. epoll_ctl

该函数用于向空间注册或注销文件描述符。对应select方法中的FD_SET、FD_CLR

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

成功返回0,失败返回-1

(1)epfd:epoll例程的文件描述符

(2)op:选择对于监视对象的操作。常见的操作有:EPOLL_CTL_ADD(添加)、EPOLL_CTL_DEL(删除)、EPOLL_CTL_MOD(更改)

(3)fd:监视对象的文件描述符

(4)event:事件类型

4. epoll_wait

该函数用于等待文件描述符发生变化。对应select方法中的select函数

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

成功返回发生事件的文件描述符数,失败返回-1

(1)epfd:epoll例程的文件描述符

(2)events:保存发生事件文件描述符集合的地址(event结构体数组)

(3)maxevents:events可以保存的最大事件数

(4)timeout:等待时间,传递-1表示一直等待

二、 条件触发版本的聊天室服务端

完整代码如下:

#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/epoll.h>
#include<map>
using namespace std;

void addUser(int,char*,map<int,char*>&);  //用户上线
void removeUser(int,map<int,char*>&);     //用户下线
int findUser(const char*,const map<int,char*>&);     //用户查找
char* findName(int,map<int,char*>&);  //用户名查找
void showUser(char*,const map<int,char*>&);   //展示在线用户

int main()
{
    int epollfd;  //epoll的例程文件描述符
    int eventcount;  //记录epoll_wait返回值
    epollfd = epoll_create(50);   //创建epoll例程空间
    struct epoll_event event;   //创建epoll事件结构体
    struct epoll_event events[50];   //设置最大event的个数

    char* ip = "127.0.0.1";
    char* port = "9955";
    int opt = 1;

    map<int,char*> users;  //记录客户端套接字和用户的对应关系
    char choice;  //记录客户端请求的服务,0:注册、1:通信、2:查看在线用户、3:下线
    char name[20];  //要通信的用户名
    char msg[1000]; //信息
    char buf[1024]; //存储所有数据

    int server_socket, client_socket, to;
    struct sockaddr_in server_addr, client_addr;
    socklen_t server_addr_len = sizeof(server_addr), client_addr_len = sizeof(client_addr);

    server_socket = socket(PF_INET,SOCK_STREAM,0);
    setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); //创建套接字并避免timeout

    memset(&server_addr,0,sizeof(server_addr)); //设置地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(ip);
    server_addr.sin_port = htons(atoi(port));

    if(bind(server_socket,(struct sockaddr*)&server_addr,server_addr_len)==-1) //分配套接字地址
    cout << "bind error" << endl;

    if(listen(server_socket,5)==-1) //开启监听
    cout << "listen error" << endl;

    event.events = EPOLLIN;
    event.data.fd = server_socket;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket,&event);  //监控输入方式注册server_socket

    while(1)
    {
        eventcount = epoll_wait(epollfd,events,50,-1);  //-1表示一直等待
        if(eventcount == -1) 
        cout << "epoll_wait error" << endl;
        else //有文件描述符有输入
        {
            for(int i=0;i<eventcount;i++)
            {
                if(events[i].data.fd == server_socket)  //说明有连接请求
                {
                    client_socket = accept(server_socket,
                        (struct sockaddr*)&client_addr,&client_addr_len);
                    event.events = EPOLLIN;
                    event.data.fd = client_socket;
                    epoll_ctl(epollfd,EPOLL_CTL_ADD,client_socket,&event);
                    cout << "connect..." << client_socket << endl;
                }
                else //客户端有输入
                {
                    memset(buf,0,sizeof(buf));
                    recv(events[i].data.fd,buf,sizeof(buf),0);
                    choice = buf[0];
                    switch(choice) 
                    {
                        case '0':
                            strncpy(name,&buf[1],20);
                            if(findUser(name,users)==-1)  //检查是否已经登陆
                            {
                                addUser(events[i].data.fd,name,users);  //添加用户
                                send(events[i].data.fd,"log in",7,0);
                            }
                            else
                            send(events[i].data.fd,"already log in",15,0);
                            continue;
                        case '1':
                            strncpy(name,&buf[1],20);
                            strncpy(msg,&buf[21],1000);
                            to = findUser(name,users);
                            if(to==-1)
                            send(events[i].data.fd,"not found user",15,0);
                            else
                            {
                                int len = 0;   //构造消息
                                char* fromname = findName(events[i].data.fd,users);
                                strncpy(&buf[len],fromname,strlen(fromname));
                                len+=strlen(fromname);
                                buf[len++] = ':';
                                strncpy(&buf[len],msg,strlen(msg));
                                send(to,buf,sizeof(buf),0);
                                send(events[i].data.fd,"send successfully",18,0);
                            }
                            continue;
                        case '2':
                            showUser(buf,users);  //展示用户
                            send(events[i].data.fd,buf,sizeof(buf),0);
                            continue;
                        case '3':
                            removeUser(events[i].data.fd,users);  //删除用户
                            send(events[i].data.fd,"log out",8,0);
                            epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
                            close(events[i].data.fd);
                            continue;
                    }
                }
            }
        }
    }
    close(server_socket);
    return 0;
}

void addUser(int client_socket,char* username,map<int,char*>& users)
{
    char* name = new char(20);    //创建一个新地址存放名字并存储到map字典中
    strcpy(name,username);
    if(users.count(client_socket))
    return;
    else
    {
        users.insert(pair<int,char*>(client_socket,name));
        cout << "client " << username << " log in" << endl;
    }
}

void removeUser(int client_socket,map<int,char*>& users)
{
    if(users.count(client_socket))
    {
        delete[] users.find(client_socket)->second;   //释放内存
        users.erase(client_socket);
        cout << "client " << client_socket << " log out" << endl; 
    }
    else
    return;
}

int findUser(const char* username,const map<int,char*>& users)
{
    map<int,char*>::const_iterator it = users.begin();
    for(;it!=users.end();it++)
    {
        if(!strcmp(it->second,username))
        return it->first;
    }
    return -1;
}

void showUser(char* buf,const map<int,char*>& users)
{
    int len = 0;
    map<int,char*>::const_iterator it = users.begin();
    for(;it!=users.end();it++)
    {
        strncpy(&buf[len],it->second,strlen(it->second));
        len+=strlen(it->second);
        buf[len++] = ' ';
    }
}

char* findName(int client_socket,map<int,char*>& users)
{
    map<int,char*>::iterator it;
    it = users.find(client_socket);
    return it->second;
}

三、 边缘触发版本的聊天室服务端

上面说过,边缘触发的性能优于条件触发。使用边缘触发需要额外注意以下三点:

1. 因为事件只会触发一次,所以数据必须一次性处理完,可以通过errno变量来判断数据处理情况(文件头errno.h)

2. 文件描述符注册事件需要添加EPOLLET

3. 必须更改套接字特性,实现非阻塞I/O。因为边缘触发下如果使用阻塞I/O,会导致数据处理不完整或者影响其他事件的处理。具体方法如下,使用fcntl函数设置文件描述符

int flag = fcntl(client_socket,F_GETFL,0);  
fcntl(client_socket,F_SETFL,flag|O_NONBLOCK);

首先获取当前文件描述符属性记录在flag变量中,然后用或操作添加非阻塞属性

完整代码如下:

#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/epoll.h>
#include<map>
#include<fcntl.h>
#include<errno.h>
using namespace std;

void addUser(int,char*,map<int,char*>&);  //用户上线
void removeUser(int,map<int,char*>&);     //用户下线
int findUser(const char*,const map<int,char*>&);     //用户查找
char* findName(int,map<int,char*>&);  //用户名查找
void showUser(char*,const map<int,char*>&);   //展示在线用户

int main()
{
    int epollfd;  //epoll的例程文件描述符
    int eventcount;  //记录epoll_wait返回值
    epollfd = epoll_create(50);   //创建epoll例程空间
    struct epoll_event event;   //创建epoll事件结构体
    struct epoll_event events[50];   //设置最大event的个数
    int flag;   //记录文件描述符的模式信息

    char* ip = "127.0.0.1";
    char* port = "9955";
    int opt = 1;

    map<int,char*> users;  //记录客户端套接字和用户的对应关系
    char choice;  //记录客户端请求的服务,0:注册、1:通信、2:查看在线用户、3:下线
    char name[20];  //要通信的用户名
    char msg[1000]; //信息
    char buf[1024]; //存储所有数据

    int server_socket, client_socket, to;
    struct sockaddr_in server_addr, client_addr;
    socklen_t server_addr_len = sizeof(server_addr), client_addr_len = sizeof(client_addr);

    server_socket = socket(PF_INET,SOCK_STREAM,0);
    setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); //创建套接字并避免timeout

    memset(&server_addr,0,sizeof(server_addr)); //设置地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(ip);
    server_addr.sin_port = htons(atoi(port));

    if(bind(server_socket,(struct sockaddr*)&server_addr,server_addr_len)==-1) //分配套接字地址
    cout << "bind error" << endl;

    if(listen(server_socket,5)==-1) //开启监听
    cout << "listen error" << endl;

    flag = fcntl(server_socket,F_GETFL,0);  
    fcntl(server_socket,F_SETFL,flag|O_NONBLOCK);  //把文件描述符添加一个为非阻塞I/O状态
    event.events = EPOLLIN;
    event.data.fd = server_socket;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket,&event);  //监控输入方式注册server_socket

    while(1)
    {
        eventcount = epoll_wait(epollfd,events,50,-1);  //-1表示一直等待
        if(eventcount == -1) 
        cout << "epoll_wait error" << endl;
        else //有文件描述符有输入
        {
            for(int i=0;i<eventcount;i++)
            {
                if(events[i].data.fd == server_socket)  //说明有连接请求
                {
                    client_socket = accept(server_socket,
                        (struct sockaddr*)&client_addr,&client_addr_len);
                    flag = fcntl(client_socket,F_GETFL,0);  
                    fcntl(client_socket,F_SETFL,flag|O_NONBLOCK);  //把文件描述符添加一个为非阻塞I/O状态
                    event.events = EPOLLIN|EPOLLET;   //将事件注册为边缘触发
                    event.data.fd = client_socket;
                    epoll_ctl(epollfd,EPOLL_CTL_ADD,client_socket,&event);
                    cout << "connect..." << client_socket << endl;
                }
                else //客户端有输入
                {
                    memset(buf,0,sizeof(buf));
                    int len = 0;
                    int readlen = 0;
                    while(1)
                    {
                        readlen = recv(events[i].data.fd,&buf[len],sizeof(buf)-len,0);
                        if(readlen < 0)
                        {
                            if(errno == EAGAIN)
                            break;
                        }
                        else if(readlen > 0)
                        len += readlen;
                        else
                        break;
                    }
                    choice = buf[0];
                    switch(choice) 
                    {
                        case '0':
                            strncpy(name,&buf[1],20);
                            if(findUser(name,users)==-1)  //检查是否已经登陆
                            {
                                addUser(events[i].data.fd,name,users);  //添加用户
                                send(events[i].data.fd,"log in",7,0);
                            }
                            else
                            send(events[i].data.fd,"already log in",15,0);
                            continue;
                        case '1':
                            strncpy(name,&buf[1],20);
                            strncpy(msg,&buf[21],1000);
                            to = findUser(name,users);
                            if(to==-1)
                            send(events[i].data.fd,"not found user",15,0);
                            else
                            {
                                int len = 0;   //构造消息
                                char* fromname = findName(events[i].data.fd,users);
                                strncpy(&buf[len],fromname,strlen(fromname));
                                len+=strlen(fromname);
                                buf[len++] = ':';
                                strncpy(&buf[len],msg,strlen(msg));
                                send(to,buf,sizeof(buf),0);
                                send(events[i].data.fd,"send successfully",18,0);
                            }
                            continue;
                        case '2':
                            showUser(buf,users);  //展示用户
                            send(events[i].data.fd,buf,sizeof(buf),0);
                            continue;
                        case '3':
                            removeUser(events[i].data.fd,users);  //删除用户
                            send(events[i].data.fd,"log out",8,0);
                            epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
                            close(events[i].data.fd);
                            continue;
                    }
                }
            }
        }
    }
    close(server_socket);
    return 0;
}

void addUser(int client_socket,char* username,map<int,char*>& users)
{
    char* name = new char(20);    //创建一个新地址存放名字并存储到map字典中
    strcpy(name,username);
    if(users.count(client_socket))
    return;
    else
    {
        users.insert(pair<int,char*>(client_socket,name));
        cout << "client " << username << " log in" << endl;
    }
}

void removeUser(int client_socket,map<int,char*>& users)
{
    if(users.count(client_socket))
    {
        delete[] users.find(client_socket)->second;   //释放内存
        users.erase(client_socket);
        cout << "client " << client_socket << " log out" << endl; 
    }
    else
    return;
}

int findUser(const char* username,const map<int,char*>& users)
{
    map<int,char*>::const_iterator it = users.begin();
    for(;it!=users.end();it++)
    {
        if(!strcmp(it->second,username))
        return it->first;
    }
    return -1;
}

void showUser(char* buf,const map<int,char*>& users)
{
    int len = 0;
    map<int,char*>::const_iterator it = users.begin();
    for(;it!=users.end();it++)
    {
        strncpy(&buf[len],it->second,strlen(it->second));
        len+=strlen(it->second);
        buf[len++] = ' ';
    }
}

char* findName(int client_socket,map<int,char*>& users)
{
    map<int,char*>::iterator it;
    it = users.find(client_socket);
    return it->second;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值