epoll

Epoll使用详解

epoll - I/O event notification facility

 

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:

#define __FD_SETSIZE    1024

表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

 

epoll的接口非常简单,一共就三个函数:

1. int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

 

 

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

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct 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可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

 

 

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

等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

 

 

4、关于ET、LT两种工作模式:

可以得出这样的结论:

ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.

 

 

那么究竟如何来使用epoll呢?其实非常简单。

通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。

 

首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

 

之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

 

epoll_wait范围之后应该是一个循环,遍利所有的事件。

 

几乎所有的epoll程序都使用下面的框架:

 

    for( ; ; )

    {

        nfds = epoll_wait(epfd,events,20,500);

        for(i=0;i<nfds;++i)

        {

            if(events[i].data.fd==listenfd) //有新的连接

            {

                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接

                ev.data.fd=connfd;

                ev.events=EPOLLIN|EPOLLET;

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中

            }

            else if( events[i].events&EPOLLIN ) //接收到数据,读socket

            {

                n = read(sockfd, line, MAXLINE)) < 0    //读

                ev.data.ptr = md;     //md为自定义类型,添加数据

                ev.events=EPOLLOUT|EPOLLET;

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓

            }

            else if(events[i].events&EPOLLOUT) //有数据待发送,写socket

            {

                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据

                sockfd = md->fd;

                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据

                ev.data.fd=sockfd;

                ev.events=EPOLLIN|EPOLLET;

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据

            }

            else

            {

                //其他的处理

            }

        }

    }

 

 

 

下面给出一个完整的服务器端例子:

 

 

#include <iostream>

#include <sys/socket.h>

#include <sys/epoll.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <errno.h>

 

using namespace std;

 

#define MAXLINE 5

#define OPEN_MAX 100

#define LISTENQ 20

#define SERV_PORT 5000

#define INFTIM 1000

 

void setnonblocking(int sock)

{

    int opts;

    opts=fcntl(sock,F_GETFL);

    if(opts<0)

    {

        perror("fcntl(sock,GETFL)");

        exit(1);

    }

    opts = opts|O_NONBLOCK;

    if(fcntl(sock,F_SETFL,opts)<0)

    {

        perror("fcntl(sock,SETFL,opts)");

        exit(1);

    }

}

 

int main(int argc, char* argv[])

{

    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;

    ssize_t n;

    char line[MAXLINE];

    socklen_t clilen;

 

 

    if ( 2 == argc )

    {

        if( (portnumber = atoi(argv[1])) < 0 )

        {

            fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);

            return 1;

        }

    }

    else

    {

        fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);

        return 1;

    }

 

 

 

    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

 

    struct epoll_event ev,events[20];

    //生成用于处理accept的epoll专用的文件描述符

 

    epfd=epoll_create(256);

    struct sockaddr_in clientaddr;

    struct sockaddr_in serveraddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    //把socket设置为非阻塞方式

 

    //setnonblocking(listenfd);

 

    //设置与要处理的事件相关的文件描述符

 

    ev.data.fd=listenfd;

    //设置要处理的事件类型

 

    ev.events=EPOLLIN|EPOLLET;

    //ev.events=EPOLLIN;

 

    //注册epoll事件

 

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

    bzero(&serveraddr, sizeof(serveraddr));

    serveraddr.sin_family = AF_INET;

    char *local_addr="127.0.0.1";

    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

 

    serveraddr.sin_port=htons(portnumber);

    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

    listen(listenfd, LISTENQ);

    maxi = 0;

    for ( ; ; ) {

        //等待epoll事件的发生

 

        nfds=epoll_wait(epfd,events,20,500);

        //处理所发生的所有事件

 

        for(i=0;i<nfds;++i)

        {

            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。

 

            {

                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                if(connfd<0){

                    perror("connfd<0");

                    exit(1);

                }

                //setnonblocking(connfd);

 

                char *str = inet_ntoa(clientaddr.sin_addr);

                cout << "accapt a connection from " << str << endl;

                //设置用于读操作的文件描述符

 

                ev.data.fd=connfd;

                //设置用于注测的读操作事件

 

                ev.events=EPOLLIN|EPOLLET;

                //ev.events=EPOLLIN;

 

                //注册ev

 

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

            }

            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。

 

            {

                cout << "EPOLLIN" << endl;

                if ( (sockfd = events[i].data.fd) < 0)

                    continue;

                if ( (n = read(sockfd, line, MAXLINE)) < 0) {

                    if (errno == ECONNRESET) {

                        close(sockfd);

                        events[i].data.fd = -1;

                    } else

                        std::cout<<"readline error"<<std::endl;

                } else if (n == 0) {

                    close(sockfd);

                    events[i].data.fd = -1;

                }

                line[n] = '\0';

                cout << "read " << line << endl;

                //设置用于写操作的文件描述符

 

                ev.data.fd=sockfd;

                //设置用于注测的写操作事件

 

                ev.events=EPOLLOUT|EPOLLET;

                //修改sockfd上要处理的事件为EPOLLOUT

 

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

 

            }

            else if(events[i].events&EPOLLOUT) // 如果有数据发送

 

            {

                sockfd = events[i].data.fd;

                write(sockfd, line, n);

                //设置用于读操作的文件描述符

 

                ev.data.fd=sockfd;

                //设置用于注测的读操作事件

 

                ev.events=EPOLLIN|EPOLLET;

                //修改sockfd上要处理的事件为EPOLIN

 

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }

        }

    }

    return 0;

}

 

 

客户端直接连接到这个服务器就好了。。

从man手册中,得到ET和LT的具体描述如下

 

EPOLL事件有两种模型:

Edge Triggered (ET)

Level Triggered (LT)

 

假如有这样一个例子:

1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

2. 这个时候从管道的另一端被写入了2KB的数据

3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

4. 然后我们读取了1KB的数据

5. 调用epoll_wait(2)......

 

Edge Triggered 工作模式:

如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

   i    基于非阻塞文件句柄

   ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

 

Level Triggered 工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

 

 

然后详细解释ET, LT:

 

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

 

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。

 

在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)

 

 

 

另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,

读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:

while(rs)

{

  buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);

  if(buflen < 0)

  {

    // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读

    // 在这里就当作是该次事件已处理处.

    if(errno == EAGAIN)

     break;

    else

     return;

   }

   else if(buflen == 0)

   {

     // 这里表示对端的socket已正常关闭.

   }

   if(buflen == sizeof(buf)

     rs = 1;   // 需要再次读取

   else

     rs = 0;

}

 

 

还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

 

ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)

{

  ssize_t tmp;

  size_t total = buflen;

  const char *p = buffer;

 

  while(1)

  {

    tmp = send(sockfd, p, total, 0);

    if(tmp < 0)

    {

      // 当send收到信号时,可以继续写,但这里返回-1.

      if(errno == EINTR)

        return -1;

 

      // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,

      // 在这里做延时后再重试.

      if(errno == EAGAIN)

      {

        usleep(1000);

        continue;

      }

 

      return -1;

    }

 

    if((size_t)tmp == total)

      return buflen;

 

    total -= tmp;

    p += tmp;

  }

 

  return tmp;

}

Epoll 详解

2010-01-24 21:20

我们目前的网络模型大都是epoll的,因为epoll模型会比select模型性能高很多, 尤其在大连接数的情况下,作为后台开发人员需要理解其中的原因。

select/epoll的特点

select的特点:select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知,因此效率是非常低。但是如果连接很少的情况下, select和epoll的LT触发模式相比, 性能上差别不大。
这里要多说一句,select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, TAF就出现过这个问题, 调试了n天,才发现:)当然可以通过修改linux的socket内核调整这个参数。
epoll的特点:epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的。
对于epoll而言还有ET和LT的区别,LT表示水平触发,ET表示边缘触发,两者在性能以及代码实现上差别也是非常大的。

LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采用LT模式下, 如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。
但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环accpet,直到返回-1,且errno==EAGAIN,TAF里面的示例代码:

if(ev.events & EPOLLIN)
{
    do
     {
        struct sockaddr_in stSockAddr;
         socklen_t iSockAddrSize = sizeof(sockaddr_in);
         TC_Socket cs;
         cs.setOwner(false);
        //接收连接
         TC_Socket s;
         s.init(fd, false, AF_INET);
        int iRetCode = s.accept(cs, (struct sockaddr *) &stSockAddr, iSockAddrSize);
        if (iRetCode > 0)
         {
             …建立连接
         }
        else
         {
            //直到发生EAGAIN才不继续accept
            if(errno == EAGAIN)
             {
                break;
             }
         }
     }while(true);
}

同样,recv/send等函数, 都需要到errno==EAGAIN

从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。

epoll ET详解

ET模型的逻辑:内核的读buffer有内核态主动变化时,内核会通知你, 无需再去mod。写事件是给用户使用的,最开始add之后,内核都不会通知你了,你可以强制写数据(直到EAGAIN或者实际字节数小于 需要写的字节数),当然你可以主动mod OUT,此时如果句柄可以写了(send buffer有空间),内核就通知你。
这里内核态主动的意思是:内核从网络接收了数据放入了读buffer(会通知用户IN事件,即用户可以recv数据)
并且这种通知只会通知一次,如果这次处理(recv)没有到刚才说的两种情况(EAGIN或者实际字节数小于 需要读写的字节数),则该事件会被丢弃,直到下次buffer发生变化。
与LT的差别就在这里体现,LT在这种情况下,事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。

另外对于ET而言,当然也不一定非send/recv到前面所述的结束条件才结束,用户可以自己随时控制,即用户可以在自己认为合适的时候去设置IN和OUT事件:
1 如果用户主动epoll_mod OUT事件,此时只要该句柄可以发送数据(发送buffer不满),则epoll
_wait就会响应(有时候采用该机制通知epoll_wai醒过来)。
2 如果用户主动epoll_mod IN事件,只要该句柄还有数据可以读,则epoll_wait会响应。
这种逻辑在普通的服务里面都不需要,可能在某些特殊的情况需要。 但是请注意,如果每次调用的时候都去epoll mod将显著降低效率,已经吃过几次亏了!

因此采用et写服务框架的时候,最简单的处理就是:
建立连接的时候epoll_add IN和OUT事件, 后面就不需要管了
每次read/write的时候,到两种情况下结束:
1 发生EAGAIN
2 read/write的实际字节数小于 需要读写的字节数
对于第二点需要注意两点:
A:如果是UDP服务,处理就不完全是这样,必须要recv到发生EAGAIN为止,否则就丢失事件了
因为UDP和TCP不同,是有边界的,每次接收一定是一个完整的UDP包,当然recv的buffer需要至少大于一个UDP包的大小
随便再说一下,一个UDP包到底应该多大?
对于internet,由于MTU的限制,UDP包的大小不要超过576个字节,否则容易被分包,对于公司的IDC环境,建议不要超过1472,否则也比较容易分包。

B 如果发送方发送完数据以后,就close连接,这个时候如果recv到数据是实际字节数小于读写字节数,根据开始所述就认为到EAGIN了从而直接返回,等待下一次事件,这样是有问题的,close事件丢失了!
因此如果依赖这种关闭逻辑的服务,必须接收数据到EAGIN为止,例如lb。

Linux2.6内核中epoll用法详解

引言

epoll是linux2.6内核中才有的机制,其他版本内核中是没有的,是Linux2.6内核引入的多路复用IO的一种方式,用于提高网络IO性能的方法。在linux网络编程中,很长一段时间都是采用select来实现多事件触发处理的。Select存在如下几个方面的问题:一是每次调用时要重复地从用户态读入参数,二是每次调用时要重复地扫描文件描述符,三是每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。Select采用轮询的方式来处理事件触发,当随着监听socket的文件描述符fd的数量增加时,轮询的时间也就越长,造成效率低下。而且linux/posix_types.h中有#define __FD_SETSIZE 1024(也有说2048的)的定义,也就是说linux select能监听的最大fd数目是1024个,虽然能通过内核修改此参数,但这是治标不治本。
    epoll的出现可以有效的解决select效率低下的问题,epoll把参数拷贝到内核态,在每次轮询时不会重复拷贝。epoll有ET和LT两种工作模式,ET是高速模式只能以非阻塞方式进行,LT相当于快速的select,可以才有阻塞和非阻塞两种方式,epoll通过把操作拆分为epoll_create,epoll_ctl,epoll_wait三个步骤避免重复地遍历要监视的文件描述符。

Epoll介绍

epoll机制可以运转在两种模式下:Edge Triggered (ET)和Level Triggered (LT)。首先来看一下man手册中的一个例子:
      1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
      2. 这个时候从管道的另一端被写入了2KB的数据
      3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
      4. 然后我们读取了1KB的数据
      5. 调用epoll_wait(2)......

      Edge Triggered 工作模式:
      如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
       i    基于非阻塞文件句柄
       ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

      Level Triggered 工作模式
      相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll/select,在poll能用的地方epoll都可以用,因为他们具有同样的职能。即使使用ET模式的epoll,在收到多个数据包的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl处理文件句柄就成为调用者必须作的事情。

      以上是man手册对epoll中两种模式的简要介绍,这里有必要对两种模式进行详细的介绍:

LT是缺省的工作方式,并且同时支持block和no-block socket;在这种做法中,内核会告诉调用者一个文件描述符是否就绪了,然后调用者可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知调用者的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。LT模式跟select有一样的语义。就是如果可读就触发。比如某管道原来为空,如果有一个进程写入2k数据,就会触发。如果处理进程读取1k数据,下次轮询时继续触发。该模式下,默认不可读,只有epoll通知可读才是可读,否则不可读。

ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉调用者,然后它会假设调用者知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到调用者做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知。该模式与select有不同的语义,只有当从不可读变为可读时才触发。上面那种情况,还有1k可读,所以不会触发,当继续读,直到返回EAGAIN时,变为不可读,如果再次变为可读就触发。默认可读,调用者可以随便读,直到发生EAGAIN。可读时读和不读,怎么读都由调用者自己决定,中间epoll不管。EAGAIN后不可读了,等到再次可读,epoll会再通知一次。理解ET模式最重要的就是理解状态的变化,对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化。但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化。

Epoll的调用很简单只涉及到三个函数分别是:

1.int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目最大值。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。


  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,数据结构如下:

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可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,就会把这个fd从epoll的队列中删除。如果还需要继续监听这个socket的话,需要再次把这个fd加入到EPOLL队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时      

 

epoll简单例子

下面给出一个简单使用epoll的例子以加深理解

服务端代码:
#include <strings.n>

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



#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }   
}

int main()
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    struct epoll_event ev,events[20];
    //生成用于处理acceptepoll专用的文件描述符
    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;

clilen=sizeof(clientaddr);
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //socket设置为非阻塞方式
   setnonblocking(listenfd);
    //设置与要处理的事件相关的文件描述符
    ev.data.fd=listenfd;
    //设置要处理的事件类型
    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;
    //注册epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
    serveraddr.sin_port=htons(SERV_PORT);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生
        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件     
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//有客户连接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                setnonblocking(connfd);
                char *str = inet_ntoa(clientaddr.sin_addr);
                printf( "accapt a connection from %s \n",str);
                //设置用于读操作的文件描述符
                ev.data.fd=connfd;
                //设置用于注测的读操作事件
                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;
                //注册ev
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN) //客户端socket可读事件
            {
               recv(events[i].data.fd, line,MAXLINE,0);

printf(“recv line %s \n ”, line);
       }
    }
    return 0;
}
客户端的代码用perl写的如下:
#!/usr/bin/perl

use IO::Socket;

my $host = "127.0.0.1";
my $port = 5000;

my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
my $msg_out = "1234567890";
print $socket $msg_out;
print "now send over, go to sleep\n";

while (1)
{
    sleep(1);
}
  同时运行服务端和客户端程序,会发现服务端在接收5字节数据之后就不会在触发EPOLLIN事件了,因为采用的是ET模式,客户端发送的10字节数据中,只读取了5字节的数据,还有5字节数据可读,也就是状态未发生改变。所以服务端不会在触发EPOLLIN事件。而如果把ET模式改成LT模式,那么服务端还是会触发EPOLLIN事件,将剩余的5字节数据读取。

总结

本文主要介绍了linux epoll的使用方法,对其中的epoll的两种模式进行了详细的分析,在服务器处理中要等待用户socket连接,由于epoll的性能较高,可以有效的处理用户请求。对于多用户连接时还要要注意在服务端accept时,有可能同时到达多个连接,由于采用ET模式,此时服务器端可能只会读取一个连接而忽略其他连接,所以采用ET模式时应该采用while(1)这样的方式来读取连接。本文还给出了一个简单的来说明epoll的用法,本例只是演示作用,对于实际应用中应考虑上述多用户情况,以及采用epoll+线程池的方法。对于参考资料2中的例子,并不会按作者说的那样只输出5字节,而是在出发EPOLLOUT之后还会出发EPOLLIN事件,也就是会出来后面的67890五个字节,对作者的这个例子研究了好久,才明白是这样的,不知道是不是没有深刻理解man的原因,得再好好看看man

socket函数read write send和recv

2009-07-01 18:13

一旦,我们建立好了tcp连接之后,我们就可以把得到的fd当作文件描述符来使用。
由此网络程序里最基本的函数就是read和write函数了。
ssize_t write(int fd, const void*buf,size_t nbytes);
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两可能.
1)write的返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.
如果错误为EINTR表示在写的时候出现了中断错误.
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).
为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.

int my_write(int fd,void *buffer,int length)
{
int bytes_left;
int written_bytes;
char *ptr;

ptr=buffer;
bytes_left=length;
while(bytes_left>0)
{
         /* 开始写*/
         written_bytes=write(fd,ptr,bytes_left);
         if(written_bytes<=0) /* 出错了*/
         {       
                 if(errno==EINTR) /* 中断错误 我们继续写*/
                         written_bytes=0;
                 else             /* 其他错误 没有办法,只好撤退了*/
                         return(-1);
         }
         bytes_left-=written_bytes;
         ptr+=written_bytes;     /* 从剩下的地方继续写   */
}
return(0);
}

读函数read
ssize_t read(int fd,void *buf,size_t nbyte)
read函数是负责从fd中读取内容.当读成功 时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误.如果错误为EINTR说明读是由中断引起 的, 如果是ECONNREST表示网络连接出了问题. 和上面一样,我们也写一个自己的读函数.

int my_read(int fd,void *buffer,int length)
{
int bytes_left;
int bytes_read;
char *ptr;
  
bytes_left=length;
while(bytes_left>0)
{
    bytes_read=read(fd,ptr,bytes_read);
    if(bytes_read<0)
    {
      if(errno==EINTR)
         bytes_read=0;
      else
         return(-1);
    }
    else if(bytes_read==0)
        break;
     bytes_left-=bytes_read;
     ptr+=bytes_read;
}
return(length-bytes_left);
}

数据的传递
有了上面的两个函数,我们就可以向客户端或者是服务端传递数据了.比如我们要传递一个结构.可以使用如下方式

/*   客户端向服务端写 */

struct my_struct my_struct_client;
write(fd,(void *)&my_struct_client,sizeof(struct my_struct);

/* 服务端的读*/
char buffer[sizeof(struct my_struct)];
struct *my_struct_server;
read(fd,(void *)buffer,sizeof(struct my_struct));
my_struct_server=(struct my_struct *)buffer;    

在网络上传递数据时我们一般都是把数据转化为char类型的数据传递.接收的时候也是一样的 注意的是我们没有必要在网络上传递指针(因为传递指针是没有任何意义的,我们必须传递指针所指向的内容)


6.1 recv和send
recv和send函数提供了和read和write差不多的功能.不过它们提供 了第四个参数来控制读写操作.

int recv(int sockfd,void *buf,int len,int flags)
int send(int sockfd,void *buf,int len,int flags)

前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合
_______________________________________________________________
| MSG_DONTROUTE | 不查找表 |
| MSG_OOB | 接受或者发送带外数据 |
| MSG_PEEK | 查看数据,并不从系统缓冲区移走数据 |
| MSG_WAITALL | 等待所有数据 |
|--------------------------------------------------------------|

MSG_DONTROUTE:是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
MSG_OOB:表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.

MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.

MSG_WAITALL是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者 是发生了错误. 1)当读到了指定的字节时,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)

MSG_NOSIGNAL is a flag used by send() in some implementations of the Berkeley sockets API.

This flag requests that the implementation does not to send a SIGPIPE signal on errors on stream oriented sockets when the other end breaks the connection. The EPIPE error is still returned as normal.

Though it is in some Berkely sockets APIs (notably Linux) it does not exist in what some refer to as the reference implementation, FreeBSD, which instead uses a socket option SO_NOSIGPIPE?. 对于服务器端,我们可以使用这个标志。目的是不让其发送SIG_PIPE信号,导致程序退出。

如果flags为0,则和read,write一样的操作.还有其它的几个选项,不过我们实际上用的很少,可以查看 Linux Programmer's Manual得到详细解释.

socket中的疑问:read,write和send,recv的区别

2007-05-24 00:22

网上的一些回复

 

初学,对read,write和send,recv的区别不是很明白,还请各位指点

 

pacman)(影子传说 [等级:★★(中级)] (信誉值: 116) 回复于: 2006-4-24 19:03:38 Top

send,recv可以按flag标志来发送接收数据。read,write则是和文件统一的io接口。

 

匿名 [等级:◆(初级)] (信誉值: 100) 回复于: 2006-4-24 19:07:27 Top

一般read,write可用于TCP连接,而send,recv用于UDP。

下面以solaris中read,write来看
参考了http://fanqiang.chinaunix.net/a4/b7/20010508/112052.html
《solaris系统编程》

4.9写函数write  
#include <unistd.h>
ssize_t write(int fildes,const void *buf,size_t nbyte)

write函数将buf中的nbyte字节内容写入文件描述符fd引用的打开文件中,成功时返回写的字节数,失败时返回-1。 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两种可能.  

1)write的返回值大于0,表示写了部分或者是全部的数据.  
2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.  
如果错误为EINTR表示在写的时候出现了中断错误.  
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).  

为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.  


ssize_t writen (int fd, const void *buf, size_t num)
{
ssize_t res;
size_t n;
const char *ptr;

n = num;
ptr = buf;
while (n > 0) {
/* 开始写*/
     if ((res = write (fd, ptr, n)) <= 0) {
      if (errno == EINTR)
       res = 0;
      else
       return (-1);
     }

     ptr += res;/* 从剩下的地方继续写     */
     n -= res;
}

return (num);
}
4.8 读函数read  
ssize_t read(int fildes,void *buf,size_t nbyte) read函数是负责从fd引用的打开文件中读取nbyte字节到buf指向的缓冲区,成功返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了。小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的, 如果是ECONNREST表示网络连接出了问题。可能read读取的字节数少于要求的nbyte字节,有很多原因:1从规则文件中读取,从终端读取通常是每次读取一行,从TCP套接字读取根据如何接受包可以返回的任意字节数。 和上面一样,我们也写一个自己的读函数。

ssize_t readn (int fd, void *buf, size_t num)
{
ssize_t res;
size_t n;
char *ptr;

n = num;
ptr = buf;
while (n > 0) {
     if ((res = read (fd, ptr, n)) == -1) {
      if (errno == EINTR)
       res = 0;
      else
       return (-1);
     }
     else if (res == 0)
      break;

     ptr += res;
     n -= res;
}

return (num - n);
}


21.2 数据的传递 (adavanced_ipc/pass_fd.c)
有了上面的两个函数,我们就可以向客户端或者是服务端传递数据了.比如我们要传递一个结构.可以使用如下方式  

/*     客户端向服务端写 */

int send_fd (int pipe_fd, int fd)
{
char buf;

buf = 0;

if (writen (pipe_fd, &buf, 1) != 1)
     return (-1);

if (ioctl (pipe_fd, I_SENDFD, fd) == -1)
     return (-1);
/*以I_SENDFD的Request调用ioctl,并将文件描述符发给接收者,ioctl的第三个参数是希望发送的实际文件描述符*/
   return (0);
}
int send_err (int pipe_fd, char status)
{
if (status == 0)
   status = EINVAL;

return (writen (pipe_fd, &status, 1));
}

/* 服务端的读*/  
这是solaris系统编程的例子,在客户端向服务端写中中用了writen()替换write(),但是为什么在recv_fd中没有用readn(),而是直接用系统函数read,这里的read()是否可以替换为readn()?
int recv_fd (int pipe_fd)
{
int new_fd;
ssize_t n;
char buf;
struct strrecvfd recvfd;

if ((n = read (pipe_fd, &buf, 1)) == -1)
     err_msg ("read failed");

if (n == 0) {
     err_ret ("Connection close by server");
     new_fd = -1;
}
else {
     if (buf == 0) {
      if (ioctl (pipe_fd, I_RECVFD, &recvfd) == -1)
       return (-1);
      new_fd = recvfd.fd;
     }
     else {
      errno = buf;
      new_fd = -1;
     }
}

return (new_fd);
}
   

在网络上传递数据时我们一般都是把数据转化为char类型的数据传递.接收的时候也是一样的 注意的是我们没有必要在网络上传递指针(因为传递指针是没有任何意义的,我们必须传递指针所指向的内容)  

send recv 代替 write read 

[说明文]I/O处理部分[BSD Socket IV]

 

writer: demonalex

email: demonalex#dark2s.org

 

 

前言:环境是:FreeBSD 5.1-RELEASE  &&  Linux 2.6.5-1.358。

 

 

 

BSD Socket的I/O处理调用方式有三种基本的配搭方式,当然,你可以在自己的需要在不违背运作机制的情况

“混合”这些方式来进行I/O处理的。

 

 

1)connect()+write()+read()  [适用于TCP]

这是一个“古老”的I/O搭配了,“远在”无名管道的工作部分就已经有详细地记录了,不过在这里偶还是要

讲一下。write()+read()主要出现在管道I/O工作上,无论是有名管道还是无名管道,它们主要都是依靠writ

e()+read()进行基本的数据通信的,这个搭配属于UNIX系统中非常低层的I/O系统调用了,而TCP开始设计时

就被当作是网络上传输管道数据的替代品,当然也就顺理成章地继承这个基本的I/O调用搭配了。下面讲解的

write()+read()调用主要是以BSD Socket下的运用方式为主。关于connect()因为在上一篇文中已作了介绍,

所以这里就不多作说明了。

 

必须头:

#include <sys/types.h>

#include <sys/uio.h>

#include <unistd.h>

 

关于write()

ssize_t write(int d,const void *buf,size_t nbytes);

调用成功返回成功写入的字节数,调用失败则返回-1。参数1为对象的句柄;参数2是写入的内容;参数3是前

者的大小。

 

关于read()

ssize_t read(int d,void *buf,size_t nbytes);

正常调用返回成功读入的字节数,当读到句柄对象的底部时返回0,调用失败返回-1。参数1为对象句柄;参

数2是读入容器的地址;参数3是前者的大小。

 

 

2)sendto()+recvfrom()  [适用于TCP、UDP,多数用于UDP]

这是一个比较“游动”的I/O方式调用,仅适用于UDP传输(也是UDP的I/O调用的魅力所在),因为UDP本身的

特性(不需要三次握手),所以它需要一种非常灵活的传输模式,有了这个模式,服务端与客户端的模式就

没有一个很笼统的分化了(在范例中我们可以看出服务端与客户端在程序的编排上几乎是一致的,除了服务

端多了一个bind()调用与一个循环结构以外),程序设计者可以按照需求更巧妙地设计自己的程序。

 

必须头:

#include <sys/types.h>

#include <sys/socket.h>

 

关于sendto()

ssize_t sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t

 tolen);

调用成功返回成功写入的字节数,调用失败则返回-1。参数1是套接字句柄;参数2是需要发送的数据;参数3

是前者的大小;参数4是特殊传输标识,其值多为0;参数5是发送目的地的sockaddr结构主机地址的指针值;

参数6是前者的长度。

 

关于recvfrom()

ssize_t recvfrom(int s,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen

);

正常调用返回成功读入的字节数,调用失败返回-1。参数1是套接字句柄;参数2是成功接收到远程传输过来

后的数据时放入的变量地址;参数3是前者的大小;参数4是特殊传输标识,其值多为0;参数5是接收到的数

据的发送端的sockaddr结构主机地址的指针值;参数6是前者长度的地址值。

 

/*

以前总以为sendto()+recvfrom()调用只能用于UDP传输,原来这个论点是错的,这里偶做了个小实验...服务

端用我在《BSD Socket在传输层中的应用范例(TCP)》一文中的服务端,而客户端是:

#include<sys/socket.h>

#include<sys/types.h>

#include<netinet/in.h>

 

int main(int argc,char *argv[]){

int sock;

struct sockaddr_in addr;

char msg[101];

bzero((char *)msg,101);

 

if((sock=socket(AF_INET,SOCK_STREAM,0))==-1){  //这里用的是TCP

printf("fail for socket.\n");

exit(-1);

}

 

bzero((char *)&addr,sizeof(addr));

addr.sin_family=AF_INET;

addr.sin_addr.s_addr=inet_addr(argv[1]);

addr.sin_port=htons(atoi(argv[2]));

 

printf("message:");

scanf("%100s",msg);

if(sendto(sock,msg,sizeof(msg),0,(struct sockaddr *)&addr,sizeof(addr))){

//I/O这里我用了sendto()来代替send()

printf("send is ok.\n");

close(sock);

exit(0);

}else{

printf("send is fail.\n");

close(sock);

exit(-1);

}

}

上面我用了sendto()来代替send(),并去除开始时的connect()调用(根据UDP的调用模式),结果发现必须

运行两次服务端才能接收msg的数据,可见第一次sendto()代替connect()了,这样才能正常地运作。但如果

在前面一开始就调用connect()的话sendto()是可以完全代替send()的,不过因为sendto()多了两个参数,所

以相信还不会有人会这样调用的,这样的使用方法仅仅用于实验用途。

*/

 

 

3)connect()+send()+recv()  [适用于TCP、UDP,多数用于TCP]

因为connect()调用在前一篇文中已经介绍过了,所以我就不多说了,下面主要谈谈send()+recv()。在TCP的

I/O传输中它们可以完全地取代write()+read()(而且在功能上回更优胜一些,看它的最后一个调用参数就知

道了);在UDP则不能完全取代sendto()+recvfrom(),原因显而易见,因为send()+recv()少了“游动”地址

部分,所以如果在UDP中调用它们的话套接字根本就不知道应该发到哪里去,这也是为什么在UDP中要用conne

ct()调用作为绑定了,又因为UDP不存在所谓的“三次握手”(需要发送连接请求),所以在UDP中我们可以

把“绑定”这种行为简单地归结为:bind()调用绑定本地地址,connect()调用绑定远程地址。

 

必须头:

#include <sys/types.h>

#include <sys/socket.h>

 

关于send()

ssize_t send(int s, const void *msg, size_t len, int flags);

调用成功返回成功写入的字节数,调用失败则返回-1。参数1是套接字句柄;参数2是需要发送的数据;参数3

是前者的大小;参数4是特殊传输标识,其值多为0。

 

关于recv()

ssize_t recv(int s, void *buf, size_t len, int flags);

正常调用返回成功读入的字节数,调用失败返回-1。参数1是套接字句柄;参数2是成功接收到远程传输过来

后的数据时放入的变量地址;参数3是前者的大小;参数4是特殊传输标识,其值多为0。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值