Android c/c++ Socket(一)非阻塞模式

最近接了一个第三方的C库。是直接在native层创建socket通信的。

之前只了解java层的阻塞模式socket。于是初看到c端的socket写法非常困惑。最后发现是线程模型不一样,c端socket类似于 Java端的NIO(非阻塞模式)。

首先是File Description,简写FD,它是Linux特有的东西,类似于windows的句柄

Linux实现非阻塞式Socket靠的是一个struct和几个函数:

1. fd_set

typedef struct {
  fd_mask fds_bits[FD_SETSIZE/NFDBITS];
} fd_set;

fd_set是一个结构体,成员只有一个unsigned long类型的数组。 fd_mask是unsigned long。

我把fd_set称为观察数组。观察的是句柄所指向的内容, 当发生改变后就会通知你。


数组成员一一对应一个文件句柄(socket、文件、管道、设备等)建立联系。调用FD_SET()建立联系。

2. FD_ZERO()

FD_ZERO就是把fd_set这个结构体初始化为0。
/* Inline loop so we don't have to declare memset. */
#define FD_ZERO(fd_set) 
  do { 
    size_t __i; 
    for (__i = 0; __i < sizeof(fd_set)/sizeof(fd_mask); ++__i) { \
      (fd_set)->fds_bits[__i] = 0; 
    } 
  } while (0)

将fd_set观察数组初始化清零。就像memset()一样.

3. FD_SET()

把需要观察的socket fd加入到fd_set的观察数组

4. FD_CLR()

从fd_set观察数组中移除。在这次socket编程中没有用到。

5. select()

int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);

这是实现非阻塞式socket最关键的函数。

当把需要观察的socket加入到fd_set以后。就开始循环调用 select()当调用select()时,Linux内核会根据IO状态修改fd_set的内容,由此来通知哪个句柄发生了改变,有可读内容或有可写的机会了。一定要设置超时时间,一般是5ms。不然等待时间太长,thread就无法顺利结束。这里犯过错。

    1.timeout=NULL(阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    2.timeout所指向的结构设为非零时间(等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    3.timeout所指向的结构,时间设为0(非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)

参数maxfd是需要监视的最大的文件描述符值+1;rdset是可读文件描述符的观察数组;wrset是可写文件描述符的观察数组,以及异常文件描述符的观察数组。

6. FD_ISSET()

当select()函数返回有改变时,调用FD_ISSET来确定发送改变的就是你关心的socket fd。

7. accept()

这个函数就建立server socket的时候使用的,类似于java的accept方法,阻塞等待客户端的scoket过来连接。但是这儿要讲的accept函数是不阻塞的。通过循环调用select来通知有客户端来建立连接, FD_ISSUT()函数来确定是客户端发过来的连接请求。

8.recv()

接收客户端或者服务端的数据。如何有可接收的数据,也是通过循环调用select函数来通知你的。再通过FD_ISSUT()函数来确定就是你关心的那个客户端或服务端。

下面是简单的流程:

static THREAD_RETVAL server_socket_thread(void *arg) {
    server_socket *server_socket = arg;
    int stream_fd = -1;
    unsigned char packet[128];
    memset(packet, 0, 128);
    unsigned int readstart = 0;

    while (1) {
        fd_set rfds;
        struct timeval tv;
        int nfds, ret;
        MUTEX_LOCK(server_socket->run_mutex);
        if (!server_socket->running) {
            MUTEX_UNLOCK(server_socket->run_mutex);
            break;
        }

        MUTEX_UNLOCK(server_socket->run_mutex);
        //设置超时时间5ms
        tv.tv_sec = 0;
        tv.tv_usec = 5000;

        //rfds清零
        FD_ZERO(&rfds);
        if (stream_fd == -1) {
            //server_socket->data_sock是做为服务端的socket句柄。放到观察数组里去
            //以此来监听是否有客户端来建立连接。
            FD_SET(server_socket->data_sock, &rfds);
            nfds = server_socket->data_sock + 1; //最新监听句柄加一,just规则。
        } else {
            FD_SET(stream_fd, &rfds);
            nfds = stream_fd + 1;
        }

        //检查是否有变化的fd, 这里的变化是有可读可写等等。
        ret = select(nfds, &rfds, NULL, NULL, &tv);
        if (ret == 0) {
            /* Timeout happened */
            continue;
        } else if (ret == -1) {
             //出现异常需要关闭
            logger_log(server_socket->logger, LOGGER_INFO, "Error in select");
            break;
        }

//判断是否有客户端过来建立连接
        if (stream_fd == -1 && FD_ISSET(server_socket->data_sock, &rfds)) {
            struct sockaddr_storage saddr;
            socklen_t saddrlen;

            logger_log(server_socket->logger, LOGGER_INFO, "Accepting client");
            saddrlen = sizeof(saddr);
            stream_fd = accept(server_socket->data_sock, (struct sockaddr *) &saddr,
                               &saddrlen);
            if (stream_fd == -1) {
                //出现异常并关闭整个线程。
                logger_log(server_socket->logger, LOGGER_INFO, "Error in accept %d %s", errno,
                           strerror(errno));
                break;
            }
        }

        //这里就是读取客户端的数据了。

        if (stream_fd != -1 && FD_ISSET(stream_fd, &rfds)) {
            // packetlen初始0
            ret = recv(stream_fd, packet + readstart, 4 - readstart, 0);

            if (ret == 0) {
                /* TCP socket closed */
                logger_log(server_socket->logger, LOGGER_INFO, "TCP socket closed");
                break;
            } else if (ret == -1) {
                /* FIXME: Error happened */
                logger_log(server_socket->logger, LOGGER_INFO, "Error in recv");
                break;
            }

            readstart += ret;
            if (readstart < 4) {
                continue;
            }

                // 普通数据块
                do {
                    // 读取剩下的124字节
                    ret = recv(stream_fd, packet + readstart, 128 - readstart, 0);
                    readstart = readstart + ret;
                } while (readstart < 128);

//-----------------
在这里接受处理客户端数据,主要是根据协商好的协议读出数据
//-----------------
                memset(packet, 0, 128);
                readstart = 0;
        }
    }

    //关闭server socket
    if (stream_fd != -1) {
        closesocket(stream_fd);
    }

    logger_log(server_socket->logger, LOGGER_INFO, "Exiting TCP server_socket_thread ");

    return 0;
}

鉴于对Linux的Socket实现方式, JAVA NIO的原理就很好理解了,背后就是用了select模式。

最后再说一下阻塞模式和非阻塞模式:

1. 阻塞模式, 有一个缺点,就是严重浪费线程资源。服务端需要一对一的开启线程,给每一个客户端来处理数据交换。

2. 服务端和客户端连接后,并不是时时刻刻都有数据交换的,所以socket管道本身时间利用率就不高,完全可以通过不断查询客户端socket有没有可读可写的变化,再来处理读写。

3. 阻塞模式没有fd系列方法, 也没有select方法。只有accept(), rec()这两个阻塞的方法,有连接请求或者有数据可读时才返回。而非阻塞模式只要一条线程就可以处理很多客户端的连接。只要用select()不断的查询IO变化就可以了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值