linux下socket编程和epoll的使用

时间:2022-08-01 16:15:07
    这两天在学Linux下的网络编程,于是便看了些关于socket和epoll的资料。
    首先介绍socket,socket编程我之前也接触过,不过是在windows下接触的。和windows不同的是,windows下关于socket编程,是直接给你一个socket的类,在上面建立自己的实例。而在linux中,你在建立socket时,它会给你一个文件描述符(其实就是一个整数),这个整数和内核为你建立的socket相联系,这个整数其实就代表着建立的socket(在网上查到的是说,linux下一切皆文件,socket其实也就是一种特殊的文件,而文件用文件描述符来标记)。接着就是将这个文件描述符(以下以sockfd代替)用bind函数与地址绑定(之后详细解释),如果是监听socket就开始listen,如果是连接socket就与server连接。其实感觉无论在windows上还是在linux上,socket都是这么使用的,下面讲解下在这个过程中使用的函数。
    首先是使用socket函数,原型如下:
int socket(int domain, int type, int protocol);
    函数返回一个整型值,就是所建立的socket的文件描述符。当返回值为-1是,说明建立socket失败。第一个参数domain指定,它用于确定所建立的socket的通信域,例如AF_INET就是ipv4,第二个参数type,指定建立的socket的类型,它定义了通信的语义,例如SOCK_STREAM提供顺序,可靠,双向,基于连接的字节流。最后一个参数protocol指定与套接字相匹配的协议,通常,只有单个协议存在以支持给定协议族内的特定套接字类型,在这种情况下协议可以被指定为0。但是可能存在多个协议和套接字匹配,此时就要手动指定协议。(三个参数的具体取值查询man)。
    在建立了socket后,就是使用bind函数将其与地址绑定在一起。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    当绑定成功后,函数返回0,失败时返回-1,此时可查询errno来确定错误原因。此函数有三个参数,第一个参数就是你需要绑定的socket的文件描述符。第二个参数是需要绑定的地址,第三个参数是地址的长度(字节数)。对于第二个参数,对于ip协议,常使用sockaddr_in来代替,在绑定的时候强制转换成sockaddr,((struct sockaddr*)&addr,addr为一个sockaddr_in类型的参数)。        
struct sockaddr_in
{
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr 
{
    uint32_t       s_addr;     /* address in network byte order */
};
    对于ip协议,sin_family永远被设为AF_INET,sin_port为端口号,sin_addr为具体的IP地址,可以将其设为INADDR_ANY来指定为任意ip地址(也可以近似于认为是本机地址),使用“127.0.0.1”来指定本机地址,或者自定义ip地址。使用inet_aton来将字符串形式的ip地址转换为标准的IP地址形式,`int inet_aton(const char *cp, struct in_addr *inp)`,第一个参数是字符串形式ip地址,第二个参数是需要得到的地址,还有一些其他转换的方式,具体见man。最后,注意主机字节序和网络字节序的差别。
    绑定成功后,便可以开始监听socket了,使用listen函数:
int listen(int sockfd, int backlog);
    和bind函数一样,若是函数成功,返回0,若是失败,返回-1。第一个参数是监听socket的文件描述符,第二个参数指定sockfd监听队列的最大长度,当sockfd的监听队列已满时,若还有新的连接,便会出现错误。      
    对于客户端的连接socket,使用connect函数连接到服务器socket上。(貌似对于客户端socket不需要绑定地址,系统会自动为其指派地址和端口,这个具体还不太清楚,之后若是确定了会写出来)。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    函数的返回值规则和上述函数一样,0为成功,-1为失败。第一个参数为client的socket的文件描述符,后面两个参数则分别为所要连接到的server的ip地址和地址长度。地址使用和bind函数一样。
    这样,便完成了client和server的连接。
    连接成功后便可以使用send和recv函数来收发数据了。具体的例子如下(先只放出client的例子,server的代码之后和epoll代码一起放出。):
    client:
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<iostream>
#include<netinet/in.h>
#include<unistd.h>
#include<errno.h>
#include<arpa/inet.h>

using namespace std;

int main(void)
{
    int ClientFd;
    sockaddr_in ClientAddr;

    ClientFd=socket(AF_INET,SOCK_STREAM,0);
    if(ClientFd==-1)
    {
        cout<<"Client socket created falied!!"<<errno<<endl;
        return 0;
    }

    int ServerFd;
    sockaddr_in ServerAddr;
    ServerAddr.sin_family=AF_INET;
    if(inet_aton("127.0.0.1",&ServerAddr.sin_addr)==0)
    {
        cout<<"server IPAddress error!!"<<endl;
        return 0;
    }

    string ipAddress=inet_ntoa(ServerAddr.sin_addr);
    cout<<ipAddress<<endl;
    ServerAddr.sin_port=htons(8000);
    socklen_t ServerLen=sizeof(ServerAddr);

    if(connect(ClientFd,(struct sockaddr*)&ServerAddr,ServerLen)==-1)
    {
        cout<<"can't connect to server!!"<<endl;
        cout<<errno<<endl;
        return 0;
    }

    const char *buffer="Hello, My Server!!";
    send(ClientFd,buffer,18,0);
    shutdown(ClientFd,SHUT_RDWR);
    if(close(ClientFd)==-1)
        cout<<"close Client failed"<<endl;
    return 0;
}

——————————————————————-华丽的分界线—————————————————————————

    以上就是关于我关于socket的一些理解。下面介绍下epoll。
    在学习关于epoll之前,我曾经使用过完成端口,感觉和完成端口相比,epoll的使用就简单很多了,只需要三个函数,epoll_create,epoll_vtl,和epoll_wait.当然完成epoll的第一步就是先建立一个监听socket,将其作为第一个socket加进epoll的文件描述符中,之后每有一个客户端连接到这个sockfd时,就将这个客户端加进epoll中。首先介绍epoll_create函数:
int epoll_create(int size);
    在过去的版本中,size参数用来指定能在epoll中添加的sockfd的数量,但从Linux 2.6.8开始,size参数被忽略,但必须为一个大于0的数。此函数返回一个新的epoll的实例的文件描述符,此文件描述符用于之后对epoll的操作。当不再需要此文教描述符时,应该使用close函数关闭此文件描述符,当所有引用此epoll实例的文件描述符关闭时,系统内核会销毁此epoll实例以释放资源(这句话应该也说明了能够以不同的文件描述符调用一个epoll实例)。当函数返回-1时,说明函数失败,可以查看errno来确定错误原因。
    在创建了epoll实例,得到引用其的文件描述符之后,就可以调用epoll_ctl函数将已经建立好的监听socket加入epoll队列中了。
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll_ctl有四个参数,第一个参数epfd为epoll的文件描述符,也就是刚刚使用epoll_create建立的epoll文件描述符。第二个参数op(operation),它指定需要对目标文件描述符fd进行的动作,op参数有效值为以下三个:EPOLL_CTL_ADD,它将目标文件描述符fd注册到epoll的队列之中,并且使fd文件描述符所引用的文件和event(第四个参数)相关联;EPOLL_CTL_MOD,用来改变目标文件描述符fd相关联的事件event;EPOLL_CTL_DEL,用来将目标文件描述符fd,从epoll队列中移除,在这个操作下,event参数被忽略,可以被设置为NULL。第三个参数fd就是需要被操作的目标文件描述符fd。第四个参数event描述和fd连接到一起的操作(请原谅我的语文水平,不过看到对参数值的讲解时应该都能够理解event的含义)。epoll_event的结构如下:
           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };
    其中events变量是一组位掩码,可以使用|来同时选中几个不同的events参数。可选择的参数如下:EPOLLIN,相关文件可用于read操作;EPOLLOUT,相关文件可用于write操作;对于stream socket来说,可以检测出对面客户端关闭(close)或者半关闭连接(shutdown)(但我在代码中测试过,没有能够成功检测出来,可能是我的代码写错了,但是可以用recv来检测对面是否关闭连接,如果recv的返回值为0的话说明对面关闭了连接,这点我测试过);还有一些其他的events参数,如EPOLLPRI,EPOLLERR等,我也没有进行测试,可以查询man来了解具体含义。
    在将监听socket注册到epoll中后,便可以调用epoll_wait来开始进行epoll的监听工作。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    epoll_wait函数等待发生在epoll队列中的文件描述符的事件,如果没有事件发生,它会阻塞住程序。第一个参数epfd还是epoll实例的文件描述符。第二个参数events指向记录发生的事件的内存区域,可用events[i]来调用发生的事件。第三个参数maxevents指定epoll所能返回的最大的事件数量,此参数必须大于0,第四个参数timeout指定epoll_wait阻塞住程序的超时时间(epoll_wait将会阻塞住程序直到以下三种情况之一发生:epoll队列中的一个文件描述符上发生了事件,epoll-wait被信号中断,超时时间到)。当timeout为-1时会导致如果没有事件到达,程序将会被无限期阻塞住,而如果timeout为0,epoll-wait将会立即返回,即使任何事件都没有发生。epoll-wait函数返回发生的事件数目,如果为0,说明超时,没有时间发生,如果返回-1,说明函数错误,可查询errno来确定错误原因。
    在调用完epoll_wait后,如果有客户端连接到监听socket,便可以接收到,接收到后便新建一个sockfd来专门和这个客户端进行通信,再调用epoll_ctl函数将这个sockfd注册到epoll中,如此循环便可。程序如下:

epoll.h:

#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<iostream>
#include<string>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>

#define MAX_SIZE 500
#define BUFF_SIZE 1000
#define MAX_EVENTS 100

using namespace std;
class myEpollServer
{
public:
    myEpollServer(){};
    ~myEpollServer(){};
    int setnonblocking(int socketFd);

    void start();

    bool initializeEpoll(int maxSize);
    bool initializeServerSocket();
private:
    //listen socket info
    int ServerFd=0;
    sockaddr_in ServerAddr;
    int ServerPort; 
};

epoll.cpp

#include"MyEpollPort.h"

bool myEpollServer::initializeServerSocket()
{
    using namespace std;

    if((ServerFd=socket(AF_INET,SOCK_STREAM,0))==-1)
    {

        cout<<"create server socket fail!!,error:"<<strerror(errno)<<endl;
        return false;
    }   

    memset(&ServerAddr,0,sizeof(ServerAddr));
    ServerAddr.sin_family=AF_INET;
    ServerAddr.sin_addr.s_addr=htons(INADDR_ANY);
    ServerAddr.sin_port=htons(8000);

    if(bind(ServerFd,(struct sockaddr*)&ServerAddr,sizeof(ServerAddr))==-1)
    {
        cout<<"bind server addr fail,error:"<<strerror(errno)<<endl;
        return false;
    }   

    if(listen(ServerFd,10)==-1)
    {
        cout<<"server listen fail,error:"<<strerror(errno)<<endl;
        return false;
    }  
    return true;
}

bool myEpollServer::initializeEpoll(int maxSize)
{
    struct epoll_event ev,events[100];
    int connectFd,nfds,epollFd;

    epollFd=epoll_create(maxSize);
    if(epollFd==-1)
    {
        perror("epoll_create");
        return false;
    }
    ev.events=EPOLLIN|EPOLLRDHUP;
    ev.data.fd=ServerFd;
    if(epoll_ctl(epollFd,EPOLL_CTL_ADD,ServerFd,&ev)==-1)
    {
        perror("epoll_ctl:ServerFd");
        return false;
    }

    for(;;)
    {
        nfds=epoll_wait(epollFd,events,MAX_EVENTS,-1);
        if(nfds==-1)
        {
            perror("epoll_wait");
            close(ServerFd);
            return false;
        }

        for(int i=0;i<nfds;++i)
        {
            cout<<i<<endl;
            if(events[i].data.fd==ServerFd)
            {
                cout<<1<<endl;
                sockaddr_in clientAddr;
                socklen_t len;
                connectFd=accept(ServerFd,(struct sockaddr*)&clientAddr,&len);
                if(connectFd==-1)
                {
                    perror("accept");
                    close(ServerFd);
                    return false;
                }
                cout<<"client addr is: "<<inet_ntoa(clientAddr.sin_addr)<<endl;
                setnonblocking(connectFd);
                ev.events=EPOLLIN|EPOLLET;
                ev.data.fd=connectFd;
                if(epoll_ctl(epollFd,EPOLL_CTL_ADD,connectFd,&ev)==-1)
                {
                    perror("epoll_ctl:connectFd");
                    close(ServerFd);
                    return false;
                }
            }
            else
            {
                cout<<2<<endl;
                if(events[i].events&EPOLLRDHUP||events[i].events&EPOLLERR)
                {
                    sockaddr_in addr;
                    socklen_t len=sizeof(addr);
                    if(getpeername(events[i].data.fd,(struct sockaddr*)&addr,&len)==-1)
                    {
                        cout<<"get client address fail!1"<<endl;
                    }
                    cout<<"client:"<<inet_ntoa(addr.sin_addr)<<"out of link!!"<<endl;

                }
                char buff[BUFF_SIZE]; 
                int n=recv(events[i].data.fd,buff,1000,0);
                if(n==0)
                {
                    sockaddr_in addr;
                    socklen_t len=sizeof(addr);
                    if(getpeername(events[i].data.fd,(struct sockaddr*)&addr,&len)==-1)
                    {
                        cout<<"get client address fail!1"<<endl;
                    }
                    cout<<"client:"<<inet_ntoa(addr.sin_addr)<<"out of link!!"<<endl;


                }
                buff[n]='\0';
                cout<<buff<<endl;
            }
        }
    }

    return true;

}


void myEpollServer::start()
{
    initializeServerSocket();
    initializeEpoll(100);
}

int myEpollServer::setnonblocking(int sockFd)
{
    if(fcntl(sockFd,F_SETFL,fcntl(sockFd,F_GETFD,0)|O_NONBLOCK)==-1)
    {
        return -1;
    }
    return 0;
}