Linux下套接字详解(八)----select模式下服务器(非阻塞,单进程+多进程+多线程)

前言


首先让我们回顾一下子我们之前都讲了那些

最开始我们将了一下子Linux下套接字详解(一)—-TCP/UDP的区别与联系

其中简要提到了三次握手与四次挥手,但是不是很详尽,于是我转载了一篇 Linux下套接字详解(补充)—- TCP协议中的三次握手和四次挥手(图解)

然后在第一篇博文中,我们讲解了socket的API接口,详情请见Linux下套接字详解(二)—-套接字Socket

好了现在我们有了基础了,基本的API也知道了,但是客户端和服尤其务器的框架是什么样的,有哪些服务器的框架呢
于是就有了Linux下套接字详解(三)—-几种套接字I/O模型

好了,现在我们可以专心的写服务器了,当然也是由浅入深了

阻塞I/O : 迭代型 -=> 多进程 -=> 多线程 -=> 线程池

Linux下套接字详解(四)—-简单的TCP套接字应用(迭代型)

Linux下套接字详解(五)—-基于fork多进程的TCP套接字(阻塞/同步/并发)

Linux下套接字详解(六)—-基于pthread的多线程的TCP套接字(阻塞/同步/并发)

Linux下套接字详解(七)—-线程池accept处理高并发connect

好了前面我们了解的服务器框架其实本质都是阻塞I/O类型的,从开始的迭代型,到后来为了能提升性能,提高并发性,因此改成了多进程,但是进程的开销还是很大的,因此我们又设计出了多线程的方式,但是随着性能的提升,传统的accept方式其阻塞性质开始成为性能瓶颈,因此我们使用了多个线程同时accept的方式来提高并发处理的能力。

但是其本质没有偏离阻塞I/O的基本很特性,其原因就是我们的API接口均是阻塞型的,没有请求就死等,没有连接就耗着。

因此提出了select模型

阻塞和非阻塞两种I/O模式


  • 阻塞模式: 执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字传统(connectacceptrecvrecvfrom这样的阻塞程序)默认为阻塞模式。可以通过多进程或者多线程技术进行优化处理。

阻塞模式下,进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞(死等在被阻塞的地方),函数不会立即返回

  • 非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回错误(当非阻塞socket的TCP连接正在进行时,Linux的错误号EINPROGRESS,Windows的错误号为WSAEWOULDBLOCK)。但功能强大。它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

非阻塞non-block模式下,进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以非阻塞模式效率较高

为了解决这个问题,提出了进行I/O操作的一些I/O模型。

linux下最常见的三种就是select模式,poll模式,epoll模型,当然也还有一些异步的或者事件驱动型的服务器模型。

同样windows下的Winsock也提供了一些I/O模型,常用的有有五种:select, WSAAsyncSelect,WSAEventSelect,Overlapped,Completion等。

select系统调用


select起源及描述


select最早于1983年出现在4.2BSD中,select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。

select系统调用是用来让我们的程序监视多个文件句柄(file descrīptor)的状态变化的。通过select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。

select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪些Socket或文件可读可写。

man select

select函数原型


NAME
       select,  pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous
       I/O multiplexing

SYNOPSIS
       /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

       #include <sys/select.h>

       int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

参数详解


  • ndfsselect监视的文件句柄数,视进程中打开的文件数而定,一般设为要监视各文件中的最大文件描述符值加1。

  • readfds:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。

  • writefds:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。

  • exceptfds:这个文件集将监视文件集中的任何文件是否发生错误,其实,它可用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select函数返回的时候,exceptfds将清除其中的其他文件描述符,只留下标记有OOB数据的文件描述符。

  • timeout:本次select()的超时结束时间。这个参数至关重要,它可以使select处于三种状态:

  • 若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

  • 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

  • timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

函数的返回值


返回值描述
正值表示监视的文件集中有文件描述符符合要求
零值表示select监视超时
负值表示发生了错误,错误值由errno指定

宏操作


返回值描述
FD_ZERO(fd_set *set)用来清除描述词组set的全部位
FD_SET(int fd,fd_set*set)用来设置描述词组set中相关fd的位
FD_ISSET(int fd,fd_set *set)用来测试描述词组set中相关fd 的位是否为真
FD_CLR(inr fd,fd_set* set)用来清除描述词组set中相关fd 的位

注意事项

  • 对于可写性的检查,最好放在需要写数据的时候进行检查。如果和可读性放在同一个地方进行检查,那么select很可能每次都会因为可写性检查成功而返回

  • select()调用会清空传递给它的集合参数中的内容,也就是会清空readfdswritefdexceptfds这三个指针参数所指定的描述符集合。因此,在每次调用select()之前,必须重新初始化并把需要监视的描述符填写到相应的描述符集合中。select()调用也会清空timeout指针所指向的struct timeval结构,所以在每次调用select()之前也要重新填充timeout指针所指向的struct timeval结构。

select使用流程


传统的单进程服务器一旦accept了客户端的TCP连接后,就转入客户请求的处理,处理完成后才能再一次的调用accept来接受下一个客户端的TCP连接和请求。

为了更加提高单进程server的性能,使用select这种IO复用的模式,同时监听已经连接的socket端口和正在监听的服务器listening端口,这样一来,就可以大大提升sever处理并发请求的能力。

select的使用方式如下:

定义fd_set


select允许我们监听来自标准输入,标准输出,标准错误输出的IO信号,监听标准输入IO信号集

注册将要被监听的fd


        FD_SET(  listenfd, &allset )

通过FD_SET和 FD_CLR可以注册和清除某个fd_set内的fd项,使得在调用select的时候可以监听或者取消监听某个fd

如果IO信号到达,识别并处理


通过FD_ISSET可以判断select所监听的fd_set上的IO是否有状态变化,一旦返回true,则可以对该fd进行操作

select使用事项及技巧


  • 使用select时应该注意,如果selecttimeout设置,那么每次select之前都要再重新设置一下timeout的值,因为select成功的话会修改timeout的值。

  • 本例中,如果我们在某次select中捕获到listenfd的IO状态有变,也就是说有新的客户端连接,我们不会马上做客户端的请求处理,而是把连接到的socket fd插入到select的监听集合中,然后继续探测其他监听集有IO状态变化(这里的其他监听集就是每个已经连接的客户端的socket fd的状态),如果有变化则马上处理client的请求。这样做的好处是我们及时处理了已连接的客户端的请求,而不是被新连接的客户端的请求所抢占,反正旧客户端被饿死的情况发生。

我们的程序在单进程服务器上使用select,所以适合简单客户请求处理,也就是短连接的情况,如果需要长时间服务于多个客户,可以使用fork多进程或者pthread多线程加以辅助

Linux Socket编程中select的常见用处


accept函数的非阻塞实现


当服务器程序调用了listen函数后,此时就可以接受客服端的connect请求,3次握手后,服务器把建立的连接存入队列中,等待accept函数的调用。每调用一次accept,程序就从队列中取出一个已建立连接的socket。默认方式下,accept处于阻塞状态,将套接字文件属性设置为非阻塞时,accept处于非阻塞状态(其实与该套接字相关的系统调用都是非阻塞的了)。

如果将正在listensocket设置到readfds中,调用select,如果有客户端connectselect将返回正值,通过宏FD_ISSET可检测到该socket可读,此时再用accept接受新的socket,进行读写。

因此当select返回正值,即检测到有数据请求时,

我们可以使用FD_ISSET(listen, &readset) 检测服务器的监听套接字是否有数据,如果有说明监听到了客户端的连接请求,那么我们调用accept来获取客户端的连接。

由此我们的accept函数的非阻塞实现就是非阻塞的,因为我们只有当监听到服务器监听套接字有连接请求时,才会accept

处理完毕后再循环判断其他客户端连接套接字connfd有没有数据请求。

connect函数的非阻塞实现(TCP)


  • 将打开的socket设为非阻塞的。

  • 发connect调用,这时返回-1,但是errno被设为EINPROGRESS,意即connect仍旧在进行还没有完成。

  • 将打开的socket放进被监视的可写(注意不是可读)文件集合中,用select进行监视,如果可写,用getsockopt 函数来得到error的值,如果为零则connect成功。

关于非阻塞connect的详细知识可参考:Linux网络编程 - 非阻塞connect

非阻塞Connect对于select时应注意问题

select检测对方socket连接关闭


使用select监视是否有数据可读,当监视到可读返回正值后,使用recv函数对套接字进行读操作,recv函数返回正值表示正常的读操作,若返回0或者-1表示对方连接已关闭。

用阻塞式socket发送和接受数据,但是由于没有建立心跳机制,远端服务器在一定时间内(系统设定)没有活动数据,就会断开连接。无奈只能在每次发送数据之前检测对方是否断开连接,当然在发送数据之前检测的目的是保证这次发收数据不会因为对方断开连接而接受数据失败(对方断开连接,发送还是会成功的)。这个方法比较挫~ 使用了select + recv


fd_set read_set; 
struct timeval t_o; 

FD_ZERO(&read_set); 
FD_SET(lSockFd,&read_set); 
t_o.tv_sec = n;/* 超时秒数*/ 
ret = select(lSockFd + 1,&read_set,NULL,NULL,&t_o); 
if(ret == 1) 
{  

  if(FD_ISSET(lSockFd,&read_set)

  {

     count = recv(lSockFd,buf,LEN,0); 
     if((count == 0)||(count == -1)) 
     { 
        /* 这两种情况都可认为是链路关闭*/ 
     }
  }
}

另外关于主动写socket时对方突然关闭连接的处理,则可以简单地捕捉信号SIGPIPE并作出相应关闭本地socket等等的处理。

SIGPIPE的解释是:写入无读者方的管道。

此外我们也可以使用TCP连接探测中的Keepalive 和心跳包的机制来保活或者判断客户端的断开

参见

TCP连接探测中的Keepalive 和心跳包

tcp socket 网线断开判断

代码


select模式的单进程非阻塞accept服务器


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>


#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

// for select
#include <sys/select.h>

#define TCP_SERVER_PORT     6666    /*  服务器的端口  */
#define BUFFER_SIZE         4096
#define IP_SIZE             20
#define MAX_FILENAME_SIZE   256
#define LISTEN_QUEUE        20




void sig_child(int signo)         //父进程对子进程结束的信号处理
{
    pid_t pid;
    int   stat;

    while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    {
        printf("child %d terminated\n",pid);
    }
    return;
}



/* 服务器接收从客户端传送来的文件  */
void
TcpServerPullFile(
           20int         connfd,                     /*  服务器与客户端通讯的套接字文件  */
            struct      sockaddr_in clientAddr,     /*  与之通信的客户端的信息  */
            char        *fileServerRoot)            /*  上传文件的存储路径  */
{
    char    buffer[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    char    fileServerPath[MAX_FILENAME_SIZE]/* = fileServerRoot*/;
    // 定义文件流
    FILE    *stream;

    int     count;              /*  发送文件名的字节数目  */
    int     dataLength;         /*  接收到的数据大小  */
    int     writeLength;        /* 实际写入的数据大小  */
    int     flag = 0;

    bzero(buffer, BUFFER_SIZE);
    /*
     *  向客户端提示输入文件路径提示...
     *
     * strcpy(buffer, "请输入要传输的文件的完整路径:");
    strcat(buffer, "\n");

    send(new_server_socket, buffer, BUFFER_SIZE, 0);
    bzero(buffer, BUFFER_SIZE);
    */

    /*  首先获取客户端发送过来的文件名  */
    count = recv(connfd, buffer, BUFFER_SIZE, 0);

    if(count < 0)
    {
        perror("获取文件名失败...\n");
        exit(1);
    }
    else
    {

        strncpy(filename, buffer, strlen(buffer) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(buffer));
        strcpy(fileServerPath, fileServerRoot);
        strcat(fileServerPath, filename);
        printf("\n获取客户端发送过来的文件名成功...\n");
        printf("文件名[%s]\n", filename);
        printf("文件存储路径[%s]\n\n", fileServerPath);
    }

    //  服务器接受数据, 首先打开一个文件流
    if((stream = fopen(fileServerPath, "w")) == NULL)
    {
        perror("file open error...\n");
        exit(1);
    }
    else
    {
        bzero(buffer,BUFFER_SIZE);
    }

    printf("正在接收来自%s的文件....\n",inet_ntoa(clientAddr.sin_addr));

    dataLength = 0;


    /*  先将数据接受到缓冲区buffer中,再写入到新建的文件中  */
    while((dataLength = recv(connfd, buffer, BUFFER_SIZE, 0)) > 0)
    {

        flag++;

        if(flag == 1)
        {
            printf("正在接收来自%s的文件....\n", inet_ntoa(clientAddr.sin_addr));
        }

        if(dataLength < 0)
        {
            printf("接收错误i\n");
            exit(1);
        }

        /*  向文件中写入数据  */
        writeLength = fwrite(buffer, sizeof(char), dataLength, stream);

        if(writeLength != dataLength)
        {
             printf("file write failed\n");
             exit(1);
        }
        bzero(buffer,BUFFER_SIZE);
    }

    if(flag > 0)
    {
        printf("%s的文件传送完毕\n", inet_ntoa(clientAddr.sin_addr));
    }
    if(flag==0)
    {
        printf("%s的文件传输失败\n", inet_ntoa(clientAddr.sin_addr));
    }

    fclose(stream);
    //rename("data",inet_ntoa(clientAddr.sin_addr));
    ///  BUG   这里其实有问题
    ///  因为客户端将文件发送完毕后, 服务器是不知道的,
    ///  因此当客户端文件发送完毕后, 服务器会陷入一个死等的循环
    ///  这时一个问题, 但是不是我们代码的重点,
    ///  因为我们的代码, 只是用于学习套接字网络编程
    ///
    ///  这个BUG其实很好处理, 因此我们在网络传输的过程中
    ///  客户端与服务器通信的数据肯定有我们自己的格式或者规范
    ///  比如 [request/response HEAD + LENGTH + DATA]的格式
    ///  要不然连基本的UDP丢包 和 TCP粘包问题都解决不了
    ///  一般情况下, 我们与客户

}



/*  服务器将文件发送到客户端
 *
 *  当用户选择了下载文件后,服务器将执行此操作
 *
 *  */
void TcpServerPushFile(
                    int         connfd,                  /*  服务器与客户端通讯的套接字文件  */
                    struct      sockaddr_in clientAddr,  /*  与之通信的客户端的信息  */
                    char        *filePath)              /*  带发送至客户端的文件路径  */
{
    //send file imformation

    char    buff[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    int     count;
    FILE    *stream;


    /* 先将文件名发送给客户端
     * 2015-4-13 21:38 Modify
     * 发送文件名时只需要发送filePath最后的文件名filename就可以了
     * */
    bzero(buff, BUFFER_SIZE);
    strcpy(filename, strrchr(filePath, '/') + 1);
    strncpy(buff, filename, strlen(filename) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(filename));
    count = send(connfd, buff, BUFFER_SIZE, 0);
    printf("服务器待发送的文件名[%s]..\n", filename);

    if(count < 0)
    {
        perror("Send file information");
        exit(1);
    }

    /*  服务器开始读取并且发送文件 : */
    if((stream = fopen(filePath, "rb")) == NULL)
    {
        printf("%s not found!\n", filePath);
    }
    printf("服务器打开文件成功...\n");
    printf("正在向客户端发送文件...\n");
    bzero(buff, BUFFER_SIZE);

    int fileBlockLength = 0;
    while((fileBlockLength = fread(buff, sizeof(char), BUFFER_SIZE, stream)) > 0)
    {
        printf("读取了:%d个数据...\n",fileBlockLength);
        if((count =send(connfd, buff, fileBlockLength, 0)) < 0)
        {
            perror("Send file error...\n");
            perror("向客户端发送文件失败...\n");
            exit(1);
        }

        bzero(buff,BUFFER_SIZE);
    }

    fclose(stream);
    printf("服务器发送文件成功\n");

}

/// 处理客户端的请求信息
void RaiseClientRequest(
        int connFd,     /*  客户端的连接套接字描述符, 用于发送和接收数据  */
        struct sockaddr_in  clientAddr) /*  客户端的信息, 用于显示一些客户端的信息  */
{

    printf("\n\n\n下面将依次测试  接收数据  发送数据  存储文件  推送文件\n\n\n");

    int     count = -1;
    char    buffer[BUFFER_SIZE];

    //  首先测试接收客户端发送来的数据
    printf("===========recv data start===========\n");
    bzero(buffer, BUFFER_SIZE);

    count = recv(connFd, buffer, BUFFER_SIZE, 0);

    if((count == 0)
    || (count == -1))       // 这两种情况都可认为是链路关闭
    {
        printf("recv data error from %s error, errno = %d...\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("接收来自 %s 的数据错误, 错误码errno = %d....\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("客户端可能已经关闭\n");
        close(connFd);
    }
    else
    {
        printf("recv %d data : %s\n", count, buffer);
        printf("接收%d个数据 : %s\n", count, buffer);
    }
    printf("===========recv data end=============\n\n\n");


    //  接着测试向客户端发送反馈数据
    printf("===========send data start===========\n");
    bzero(buffer, BUFFER_SIZE);
    strcpy(buffer, "I am fine !");
    if((count = send(connFd, buffer, strlen(buffer) + 1, 0)) < 0)
    {
        printf("send data[%s] error[errno = %d]...\n", buffer, errno);
        printf("发送数据[%s] 失败[错误码 = %d]...\n", buffer, errno);
    }
    else
    {
        printf("send data[%s] success...\n", buffer);
        printf("发送数据[%s]成功...\n", buffer);
    }
    printf("===========send data end=============\n\n\n");


    //  首先测试接收客户端发送来的数据
    //printf("===========pull file start============\n");
    //TcpServerPullFile(connFd, clientAddr, "./sdata/"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file end==============\n");

    //  首先测试接收客户端发送来的数据
    //printf("===========pull file============\n");
    //TcpServerPushFile(connFd, clientAddr, "./sdata/spush"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file============\n");


}


extern int errno;
int main(int argc, char *argv[])
{
    /**********************************************************
     *
     *  创建并初始化服务器套接字
     *
     **********************************************************/
    struct sockaddr_in      serverAddr;
    int                     listenFd;

    bzero(&serverAddr, sizeof(serverAddr));     /*  全部置零  */

    /* 设置地址相关的属性 */
    serverAddr.sin_family         =   AF_INET;
    serverAddr.sin_addr.s_addr    =   htons(INADDR_ANY);
    serverAddr.sin_port           =   htons(TCP_SERVER_PORT);

    /*  创建套接字  */
    listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenFd < 0)
    {
        perror("socket create error\n");
        exit(-1);
    }
    else
    {
        printf("socket create success...\n");
        printf("创建套接字成功[errno = %d]...\n", errno);
    }

    /*  绑定端口  */
    /**********************************************************
     *
     *  命名服务器的套接字, 进行BIND端口绑定
     *
     **********************************************************/
    if(bind(listenFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) > 0)
    {
        perror("bind error\n");
        exit(1);
    }
    else
    {
        printf("server bind port %d success...\n", TCP_SERVER_PORT);
        printf("服务器绑定端口%d成功...\n", TCP_SERVER_PORT);
    }

    /*  开始监听绑定的端口  */
    /**********************************************************
     *
     *  开始监听服务器绑定的端口
     *
     **********************************************************/
    //if(listen(listenFd, LISTEN_QUEUE))
    //  系统中每一个端口最大的监听队列的长度
    //  这是个全局的参数,默认值为128
    //  http://blog.csdn.net/taolinke/article/details/6800979
    if(listen(listenFd, SOMAXCONN))
    {
        printf("Server listen error[errno = %d]...\n", errno);
        exit(-1);
    }
    else
    {
        printf("Server listen success...\n");
        printf("服务器开始监听...\n");
    }


    //  values for select
    int             fd, maxfd, nready;
    fd_set          rset, allset;
    struct  timeval timeout;


    //  initial "select" elements
    maxfd  = listenFd;                     //新增listenfd,所以更新当前的最大fd

    //  allret用于保存清楚完标志的fd_ret信息, 在每次处理完后,赋值给rset
    FD_ZERO(&allset);
    FD_SET(listenFd, &allset);


    struct sockaddr_in  clientAddr;
    socklen_t           length = sizeof(clientAddr);
    int                 connFd;

    //signal(SIGCHLD,sig_child);

    while( 1 )
    {
        rset = allset;   //rset和allset的搭配使得新加入的fd要等到下次select才会被监听

        //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
        //  因为select会修改timeout的值。
        timeout.tv_sec = 0;
        timeout.tv_usec = 500000;


        if((nready = select(maxfd + 1, &rset, (fd_set *)NULL, (fd_set *)NULL, &timeout)) < 0) //一开始select监听的是监听口
        {
            //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
            //  因为select会修改timeout的值。
            perror("select error...\n");
            exit(-1);
        }
        else if(nready == 0)
        {
            printf(".");
            fflush(stdout);
            continue;
        }

        printf("%d", __LINE__);
        //  接收到数据请求后, 检测数据请求的套接字来自那些套接字
        //
        //  首先检测服务器监听套接字有没有数据,
        //  如果有的话说明监听到了客户端的,
        //  应该调用accept来获取客户端的连接
        //
        //  接着检测客户端的连接套接字有没有数据连接
        //  如果有的话,说明客户端跟服务器有通信请求

        //  首先检测服务器监听套接字有没有数据,
        if(FD_ISSET(listenFd, &rset))   //  测试socketFd有没有消息
        {
            if((connFd = accept(listenFd, (struct sockaddr*)&clientAddr, &length)) == -1)
            {
                perror("accept error...\n");
                continue;
            }
            else
            {
                printf("获取到从客户端%s : 端口%d的连接, 套接字描述符%d...\n",
                        inet_ntoa(clientAddr.sin_addr),
                        ntohs(clientAddr.sin_port),
                        connFd);
            }

            //  新加入的描述符,还没判断是否可以或者写
            //  将缓存的allset的对应connfd置位,下次循环时即可监听connfd
            FD_SET(connFd, &allset);
            if (connFd > maxfd)  //  maxfd是为了下次select,作为参数使用
            {
                maxfd = connFd;
            }

            //  至此监听套接字的信息已经被处理, 应该先清楚对应位
            FD_CLR(listenFd, &rset);

            if(--nready <= 0)  //  nready用来辅助计数,这样就不要遍历整个client数组
            {
                continue;
            }
        }
        /*else
        {
            printf("服务器套接字%d没有连接, 有客户端的请求数据\n", listenFd);
        }*/

        printf("%d", __LINE__);

        //  遍历套接字看那些客户端连接套接字有数据请求
        for(fd = 0;
            fd <= maxfd && nready > 0;
            fd++)
        {
            if(FD_ISSET(fd, &rset))         //  检测到客户端连接套接字fd有数据请求
            {
                //单进程的环境下,不可以阻塞在这里,可以选择非阻塞,线程,超时.也就无法防范拒绝服务的攻击
                比较适合短连接的情况

                //单进程不使用fork的情况!
                //test fork
                //          if((childpid=fork())==0)
                //          {
                //              close(listenFd);
                
                //
                //  这里填写服务器的处理代码
                //
                
                printf("开始与客户端通信, 套接字描述符fd = %d\n", fd);
                RaiseClientRequest(fd, clientAddr);

                //  清楚allset的对应位,以备fd可被继续select监听
                FD_CLR(fd, &allset);                  //清除,表示已被处理
                //close(fd);

                printf("can read : %d, %d\n", fd, nready);
            }

        }
    }


    return EXIT_SUCCESS;
}

##select模式的多进程非阻塞accept服务器

其实如果提高性能的话,可以采用预先开辟好一些进程,相当于进程池

参见Linux TCP server系列(7)-select模式下的prefork server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>


#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

// for select
#include <sys/select.h>

#define TCP_SERVER_PORT     6666    /*  服务器的端口  */
#define BUFFER_SIZE         4096
#define IP_SIZE             20
#define MAX_FILENAME_SIZE   256
#define LISTEN_QUEUE        20




void sig_child(int signo)         //父进程对子进程结束的信号处理
{
    pid_t pid;
    int   stat;

    while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    {
        printf("child %d terminated\n",pid);
    }
    return;
}



/* 服务器接收从客户端传送来的文件  */
void
TcpServerPullFile(
            int         connfd,                     /*  服务器与客户端通讯的套接字文件  */
            struct      sockaddr_in clientAddr,     /*  与之通信的客户端的信息  */
            char        *fileServerRoot)            /*  上传文件的存储路径  */
{
    char    buffer[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    char    fileServerPath[MAX_FILENAME_SIZE]/* = fileServerRoot*/;
    // 定义文件流
    FILE    *stream;

    int     count;              /*  发送文件名的字节数目  */
    int     dataLength;         /*  接收到的数据大小  */
    int     writeLength;        /* 实际写入的数据大小  */
    int     flag = 0;

    bzero(buffer, BUFFER_SIZE);
    /*
     *  向客户端提示输入文件路径提示...
     *
     * strcpy(buffer, "请输入要传输的文件的完整路径:");
    strcat(buffer, "\n");

    send(new_server_socket, buffer, BUFFER_SIZE, 0);
    bzero(buffer, BUFFER_SIZE);
    */

    /*  首先获取客户端发送过来的文件名  */
    count = recv(connfd, buffer, BUFFER_SIZE, 0);

    if(count < 0)
    {
        perror("获取文件名失败...\n");
        exit(1);
    }
    else
    {

        strncpy(filename, buffer, strlen(buffer) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(buffer));
        strcpy(fileServerPath, fileServerRoot);
        strcat(fileServerPath, filename);
        printf("\n获取客户端发送过来的文件名成功...\n");
        printf("文件名[%s]\n", filename);
        printf("文件存储路径[%s]\n\n", fileServerPath);
    }

    //  服务器接受数据, 首先打开一个文件流
    if((stream = fopen(fileServerPath, "w")) == NULL)
    {
        perror("file open error...\n");
        exit(1);
    }
    else
    {
        bzero(buffer,BUFFER_SIZE);
    }

    printf("正在接收来自%s的文件....\n",inet_ntoa(clientAddr.sin_addr));

    dataLength = 0;


    /*  先将数据接受到缓冲区buffer中,再写入到新建的文件中  */
    while((dataLength = recv(connfd, buffer, BUFFER_SIZE, 0)) > 0)
    {

        flag++;

        if(flag == 1)
        {
            printf("正在接收来自%s的文件....\n", inet_ntoa(clientAddr.sin_addr));
        }

        if(dataLength < 0)
        {
            printf("接收错误i\n");
            exit(1);
        }

        /*  向文件中写入数据  */
        writeLength = fwrite(buffer, sizeof(char), dataLength, stream);

        if(writeLength != dataLength)
        {
             printf("file write failed\n");
             exit(1);
        }
        bzero(buffer,BUFFER_SIZE);
    }

    if(flag > 0)
    {
        printf("%s的文件传送完毕\n", inet_ntoa(clientAddr.sin_addr));
    }
    if(flag==0)
    {
        printf("%s的文件传输失败\n", inet_ntoa(clientAddr.sin_addr));
    }

    fclose(stream);
    //rename("data",inet_ntoa(clientAddr.sin_addr));
    ///  BUG   这里其实有问题
    ///  因为客户端将文件发送完毕后, 服务器是不知道的,
    ///  因此当客户端文件发送完毕后, 服务器会陷入一个死等的循环
    ///  这时一个问题, 但是不是我们代码的重点,
    ///  因为我们的代码, 只是用于学习套接字网络编程
    ///
    ///  这个BUG其实很好处理, 因此我们在网络传输的过程中
    ///  客户端与服务器通信的数据肯定有我们自己的格式或者规范
    ///  比如 [request/response HEAD + LENGTH + DATA]的格式
    ///  要不然连基本的UDP丢包 和 TCP粘包问题都解决不了
    ///  一般情况下, 我们与客户

}



/*  服务器将文件发送到客户端
 *
 *  当用户选择了下载文件后,服务器将执行此操作
 *
 *  */
void TcpServerPushFile(
                    int         connfd,                  /*  服务器与客户端通讯的套接字文件  */
                    struct      sockaddr_in clientAddr,  /*  与之通信的客户端的信息  */
                    char        *filePath)              /*  带发送至客户端的文件路径  */
{
    //send file imformation

    char    buff[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    int     count;
    FILE    *stream;


    /* 先将文件名发送给客户端
     * 2015-4-13 21:38 Modify
     * 发送文件名时只需要发送filePath最后的文件名filename就可以了
     * */
    bzero(buff, BUFFER_SIZE);
    strcpy(filename, strrchr(filePath, '/') + 1);
    strncpy(buff, filename, strlen(filename) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(filename));
    count = send(connfd, buff, BUFFER_SIZE, 0);
    printf("服务器待发送的文件名[%s]..\n", filename);

    if(count < 0)
    {
        perror("Send file information");
        exit(1);
    }

    /*  服务器开始读取并且发送文件 : */
    if((stream = fopen(filePath, "rb")) == NULL)
    {
        printf("%s not found!\n", filePath);
    }
    printf("服务器打开文件成功...\n");
    printf("正在向客户端发送文件...\n");
    bzero(buff, BUFFER_SIZE);

    int fileBlockLength = 0;
    while((fileBlockLength = fread(buff, sizeof(char), BUFFER_SIZE, stream)) > 0)
    {
        printf("读取了:%d个数据...\n",fileBlockLength);
        if((count =send(connfd, buff, fileBlockLength, 0)) < 0)
        {
            perror("Send file error...\n");
            perror("向客户端发送文件失败...\n");
            exit(1);
        }

        bzero(buff,BUFFER_SIZE);
    }

    fclose(stream);
    printf("服务器发送文件成功\n");

}

/// 处理客户端的请求信息
void RaiseClientRequest(
        int connFd,     /*  客户端的连接套接字描述符, 用于发送和接收数据  */
        struct sockaddr_in  clientAddr) /*  客户端的信息, 用于显示一些客户端的信息  */
{

    printf("\n\n\n下面将依次测试  接收数据  发送数据  存储文件  推送文件\n\n\n");

    int     count = -1;
    char    buffer[BUFFER_SIZE];

    //  首先测试接收客户端发送来的数据
    printf("===========recv data start===========\n");
    bzero(buffer, BUFFER_SIZE);

    count = recv(connFd, buffer, BUFFER_SIZE, 0);

    if((count == 0)
    || (count == -1))       // 这两种情况都可认为是链路关闭
    {
        printf("recv data error from %s error, errno = %d...\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("接收来自 %s 的数据错误, 错误码errno = %d....\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("客户端可能已经关闭\n");
        close(connFd);
    }
    else
    {
        printf("recv %d data : %s\n", count, buffer);
        printf("接收%d个数据 : %s\n", count, buffer);
    }
    printf("===========recv data end=============\n\n\n");


    //  接着测试向客户端发送反馈数据
    printf("===========send data start===========\n");
    bzero(buffer, BUFFER_SIZE);
    strcpy(buffer, "I am fine !");
    if((count = send(connFd, buffer, strlen(buffer) + 1, 0)) < 0)
    {
        printf("send data[%s] error[errno = %d]...\n", buffer, errno);
        printf("发送数据[%s] 失败[错误码 = %d]...\n", buffer, errno);
    }
    else
    {
        printf("send data[%s] success...\n", buffer);
        printf("发送数据[%s]成功...\n", buffer);
    }
    printf("===========send data end=============\n\n\n");


    //  首先测试接收客户端发送来的数据
    //printf("===========pull file start============\n");
    //TcpServerPullFile(connFd, clientAddr, "./sdata/"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file end==============\n");

    //  首先测试接收客户端发送来的数据
    //printf("===========pull file============\n");
    //TcpServerPushFile(connFd, clientAddr, "./sdata/spush"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file============\n");


}


extern int errno;
int main(int argc, char *argv[])
{
    /**********************************************************
     *
     *  创建并初始化服务器套接字
     *
     **********************************************************/
    struct sockaddr_in      serverAddr;
    int                     listenFd;

    bzero(&serverAddr, sizeof(serverAddr));     /*  全部置零  */

    /* 设置地址相关的属性 */
    serverAddr.sin_family         =   AF_INET;
    serverAddr.sin_addr.s_addr    =   htons(INADDR_ANY);
    serverAddr.sin_port           =   htons(TCP_SERVER_PORT);

    /*  创建套接字  */
    listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenFd < 0)
    {
        perror("socket create error\n");
        exit(-1);
    }
    else
    {
        printf("socket create success...\n");
        printf("创建套接字成功[errno = %d]...\n", errno);
    }

    /*  绑定端口  */
    /**********************************************************
     *
     *  命名服务器的套接字, 进行BIND端口绑定
     *
     **********************************************************/
    if(bind(listenFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) > 0)
    {
        perror("bind error\n");
        exit(1);
    }
    else
    {
        printf("server bind port %d success...\n", TCP_SERVER_PORT);
        printf("服务器绑定端口%d成功...\n", TCP_SERVER_PORT);
    }

    /*  开始监听绑定的端口  */
    /**********************************************************
     *
     *  开始监听服务器绑定的端口
     *
     **********************************************************/
    //if(listen(listenFd, LISTEN_QUEUE))
    //  系统中每一个端口最大的监听队列的长度
    //  这是个全局的参数,默认值为128
    //  http://blog.csdn.net/taolinke/article/details/6800979
    if(listen(listenFd, SOMAXCONN))
    {
        printf("Server listen error[errno = %d]...\n", errno);
        exit(-1);
    }
    else
    {
        printf("Server listen success...\n");
        printf("服务器开始监听...\n");
    }


    //  values for select
    int             fd, maxfd, nready;
    fd_set          rset, allset;
    struct  timeval timeout;


    //  initial "select" elements
    maxfd  = listenFd;                     //新增listenfd,所以更新当前的最大fd

    //  allret用于保存清楚完标志的fd_ret信息, 在每次处理完后,赋值给rset
    FD_ZERO(&allset);
    FD_SET(listenFd, &allset);


    struct sockaddr_in  clientAddr;
    socklen_t           length = sizeof(clientAddr);
    int                 connFd;

    //signal(SIGCHLD,sig_child);

    while( 1 )
    {
        rset = allset;   //rset和allset的搭配使得新加入的fd要等到下次select才会被监听

        //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
        //  因为select会修改timeout的值。
        timeout.tv_sec = 0;
        timeout.tv_usec = 500000;


        if((nready = select(maxfd + 1, &rset, (fd_set *)NULL, (fd_set *)NULL, &timeout)) < 0) //一开始select监听的是监听口
        {
            //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
            //  因为select会修改timeout的值。
            perror("select error...\n");
            exit(-1);
        }
        else if(nready == 0)
        {
            printf(".");
            fflush(stdout);
            continue;
        }

        printf("%d", __LINE__);
        //  接收到数据请求后, 检测数据请求的套接字来自那些套接字
        //
        //  首先检测服务器监听套接字有没有数据,
        //  如果有的话说明监听到了客户端的,
        //  应该调用accept来获取客户端的连接
        //
        //  接着检测客户端的连接套接字有没有数据连接
        //  如果有的话,说明客户端跟服务器有通信请求

        //  首先检测服务器监听套接字有没有数据,
        if(FD_ISSET(listenFd, &rset))   //  测试socketFd有没有消息
        {
            if((connFd = accept(listenFd, (struct sockaddr*)&clientAddr, &length)) == -1)
            {
                perror("accept error...\n");
                continue;
            }
            else
            {
                printf("获取到从客户端%s : 端口%d的连接, 套接字描述符%d...\n",
                        inet_ntoa(clientAddr.sin_addr),
                        ntohs(clientAddr.sin_port),
                        connFd);
            }

            //  新加入的描述符,还没判断是否可以或者写
            //  将缓存的allset的对应connfd置位,下次循环时即可监听connfd
            FD_SET(connFd, &allset);
            if (connFd > maxfd)  //  maxfd是为了下次select,作为参数使用
            {
                maxfd = connFd;
            }

            //  至此监听套接字的信息已经被处理, 应该先清楚对应位
            FD_CLR(listenFd, &rset);

            if(--nready <= 0)  //  nready用来辅助计数,这样就不要遍历整个client数组
            {
                continue;
            }
        }
        /*else
        {
            printf("服务器套接字%d没有连接, 有客户端的请求数据\n", listenFd);
        }*/

        pid_t   childpid;
        //  遍历套接字看那些客户端连接套接字有数据请求
        for(fd = 0;
            fd <= maxfd && nready > 0;
            fd++)
        {
            if(FD_ISSET(fd, &rset))         //  检测到客户端连接套接字fd有数据请求
            {
                //单进程的环境下,不可以阻塞在这里,可以选择非阻塞,线程,超时.也就无法防范拒绝服务的攻击
                比较适合短连接的情况

                //  使用fork的情况!
                if((childpid = fork()) == 0)
                {
                    printf("分叉了一个新的进程%d用于处理客户端的连接信息\n", getpid());
                    close(listenFd);
                    
                    //
                    //  这里填写服务器的处理代码
                    //
                    
                    printf("开始与客户端通信, 套接字描述符fd = %d\n", fd);

                    RaiseClientRequest(fd, clientAddr);

                    //  清楚allset的对应位,以备fd可被继续select监听
                    FD_CLR(fd, &allset);                  //清除,表示已被处理
                    //close(fd);

                    printf("can read : %d, %d\n", fd, nready);
                }
            }


        }
    }


    return EXIT_SUCCESS;
}



select模式的多线程非阻塞accept服务器


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>


#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

// for select
#include <sys/select.h>
#include <pthread.h>
#define TCP_SERVER_PORT     6666    /*  服务器的端口  */
#define BUFFER_SIZE         4096
#define IP_SIZE             20
#define MAX_FILENAME_SIZE   256
#define LISTEN_QUEUE        20




void sig_child(int signo)         //父进程对子进程结束的信号处理
{
    pid_t pid;
    int   stat;

    while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    {
        printf("child %d terminated\n",pid);
    }
    return;
}



/* 服务器接收从客户端传送来的文件  */
void
TcpServerPullFile(
            int         connfd,                     /*  服务器与客户端通讯的套接字文件  */
            struct      sockaddr_in clientAddr,     /*  与之通信的客户端的信息  */
            char        *fileServerRoot)            /*  上传文件的存储路径  */
{
    char    buffer[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    char    fileServerPath[MAX_FILENAME_SIZE]/* = fileServerRoot*/;
    // 定义文件流
    FILE    *stream;

    int     count;              /*  发送文件名的字节数目  */
    int     dataLength;         /*  接收到的数据大小  */
    int     writeLength;        /* 实际写入的数据大小  */
    int     flag = 0;

    bzero(buffer, BUFFER_SIZE);
    /*
     *  向客户端提示输入文件路径提示...
     *
     * strcpy(buffer, "请输入要传输的文件的完整路径:");
    strcat(buffer, "\n");

    send(new_server_socket, buffer, BUFFER_SIZE, 0);
    bzero(buffer, BUFFER_SIZE);
    */

    /*  首先获取客户端发送过来的文件名  */
    count = recv(connfd, buffer, BUFFER_SIZE, 0);

    if(count < 0)
    {
        perror("获取文件名失败...\n");
        exit(1);
    }
    else
    {

        strncpy(filename, buffer, strlen(buffer) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(buffer));
        strcpy(fileServerPath, fileServerRoot);
        strcat(fileServerPath, filename);
        printf("\n获取客户端发送过来的文件名成功...\n");
        printf("文件名[%s]\n", filename);
        printf("文件存储路径[%s]\n\n", fileServerPath);
    }

    //  服务器接受数据, 首先打开一个文件流
    if((stream = fopen(fileServerPath, "w")) == NULL)
    {
        perror("file open error...\n");
        exit(1);
    }
    else
    {
        bzero(buffer,BUFFER_SIZE);
    }

    printf("正在接收来自%s的文件....\n",inet_ntoa(clientAddr.sin_addr));

    dataLength = 0;


    /*  先将数据接受到缓冲区buffer中,再写入到新建的文件中  */
    while((dataLength = recv(connfd, buffer, BUFFER_SIZE, 0)) > 0)
    {

        flag++;

        if(flag == 1)
        {
            printf("正在接收来自%s的文件....\n", inet_ntoa(clientAddr.sin_addr));
        }

        if(dataLength < 0)
        {
            printf("接收错误i\n");
            exit(1);
        }

        /*  向文件中写入数据  */
        writeLength = fwrite(buffer, sizeof(char), dataLength, stream);

        if(writeLength != dataLength)
        {
             printf("file write failed\n");
             exit(1);
        }
        bzero(buffer,BUFFER_SIZE);
    }

    if(flag > 0)
    {
        printf("%s的文件传送完毕\n", inet_ntoa(clientAddr.sin_addr));
    }
    if(flag==0)
    {
        printf("%s的文件传输失败\n", inet_ntoa(clientAddr.sin_addr));
    }

    fclose(stream);
    //rename("data",inet_ntoa(clientAddr.sin_addr));
    ///  BUG   这里其实有问题
    ///  因为客户端将文件发送完毕后, 服务器是不知道的,
    ///  因此当客户端文件发送完毕后, 服务器会陷入一个死等的循环
    ///  这时一个问题, 但是不是我们代码的重点,
    ///  因为我们的代码, 只是用于学习套接字网络编程
    ///
    ///  这个BUG其实很好处理, 因此我们在网络传输的过程中
    ///  客户端与服务器通信的数据肯定有我们自己的格式或者规范
    ///  比如 [request/response HEAD + LENGTH + DATA]的格式
    ///  要不然连基本的UDP丢包 和 TCP粘包问题都解决不了
    ///  一般情况下, 我们与客户

}



/*  服务器将文件发送到客户端
 *
 *  当用户选择了下载文件后,服务器将执行此操作
 *
 *  */
void TcpServerPushFile(
                    int         connfd,                  /*  服务器与客户端通讯的套接字文件  */
                    struct      sockaddr_in clientAddr,  /*  与之通信的客户端的信息  */
                    char        *filePath)              /*  带发送至客户端的文件路径  */
{
    //send file imformation

    char    buff[BUFFER_SIZE];
    char    filename[MAX_FILENAME_SIZE];
    int     count;
    FILE    *stream;


    /* 先将文件名发送给客户端
     * 2015-4-13 21:38 Modify
     * 发送文件名时只需要发送filePath最后的文件名filename就可以了
     * */
    bzero(buff, BUFFER_SIZE);
    strcpy(filename, strrchr(filePath, '/') + 1);
    strncpy(buff, filename, strlen(filename) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(filename));
    count = send(connfd, buff, BUFFER_SIZE, 0);
    printf("服务器待发送的文件名[%s]..\n", filename);

    if(count < 0)
    {
        perror("Send file information");
        exit(1);
    }

    /*  服务器开始读取并且发送文件 : */
    if((stream = fopen(filePath, "rb")) == NULL)
    {
        printf("%s not found!\n", filePath);
    }
    printf("服务器打开文件成功...\n");
    printf("正在向客户端发送文件...\n");
    bzero(buff, BUFFER_SIZE);

    int fileBlockLength = 0;
    while((fileBlockLength = fread(buff, sizeof(char), BUFFER_SIZE, stream)) > 0)
    {
        printf("读取了:%d个数据...\n",fileBlockLength);
        if((count =send(connfd, buff, fileBlockLength, 0)) < 0)
        {
            perror("Send file error...\n");
            perror("向客户端发送文件失败...\n");
            exit(1);
        }

        bzero(buff,BUFFER_SIZE);
    }

    fclose(stream);
    printf("服务器发送文件成功\n");

}

/// 处理客户端的请求信息
void RaiseClientRequest(
        int connFd,     /*  客户端的连接套接字描述符, 用于发送和接收数据  */
        struct sockaddr_in  clientAddr) /*  客户端的信息, 用于显示一些客户端的信息  */
{

    printf("\n\n\n下面将依次测试  接收数据  发送数据  存储文件  推送文件\n\n\n");

    int     count = -1;
    char    buffer[BUFFER_SIZE];

    //  首先测试接收客户端发送来的数据
    printf("===========recv data start===========\n");
    bzero(buffer, BUFFER_SIZE);

    count = recv(connFd, buffer, BUFFER_SIZE, 0);

    if((count == 0)
    || (count == -1))       // 这两种情况都可认为是链路关闭
    {
        printf("recv data error from %s error, errno = %d...\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("接收来自 %s 的数据错误, 错误码errno = %d....\n", inet_ntoa(clientAddr.sin_addr), errno);
        printf("客户端可能已经关闭\n");
        close(connFd);
    }
    else
    {
        printf("recv %d data : %s\n", count, buffer);
        printf("接收%d个数据 : %s\n", count, buffer);
    }
    printf("===========recv data end=============\n\n\n");


    //  接着测试向客户端发送反馈数据
    printf("===========send data start===========\n");
    bzero(buffer, BUFFER_SIZE);
    strcpy(buffer, "I am fine !");
    if((count = send(connFd, buffer, strlen(buffer) + 1, 0)) < 0)
    {
        printf("send data[%s] error[errno = %d]...\n", buffer, errno);
        printf("发送数据[%s] 失败[错误码 = %d]...\n", buffer, errno);
    }
    else
    {
        printf("send data[%s] success...\n", buffer);
        printf("发送数据[%s]成功...\n", buffer);
    }
    printf("===========send data end=============\n\n\n");


    //  首先测试接收客户端发送来的数据
    //printf("===========pull file start============\n");
    //TcpServerPullFile(connFd, clientAddr, "./sdata/"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file end==============\n");

    //  首先测试接收客户端发送来的数据
    //printf("===========pull file============\n");
    //TcpServerPushFile(connFd, clientAddr, "./sdata/spush"); /*  将客户端发送来的文件存储在./sdata目录下  */
    //printf("===========pull file============\n");


}


typedef struct arg_type
{
    int                 connFd;
    struct sockaddr_in  clientAddr;
} arg_type;


void *RaiseThreadFunc(void *args)
{
    arg_type            *arg        = (arg_type *)args;

    int                 connFd      = arg->connFd;
    struct sockaddr_in  clientAddr  = arg->clientAddr;

    pthread_detach(pthread_self());

    RaiseClientRequest(connFd, clientAddr);

    return NULL;
}


extern int errno;
int main(int argc, char *argv[])
{
    /**********************************************************
     *
     *  创建并初始化服务器套接字
     *
     **********************************************************/
    struct sockaddr_in      serverAddr;
    int                     listenFd;

    bzero(&serverAddr, sizeof(serverAddr));     /*  全部置零  */

    /* 设置地址相关的属性 */
    serverAddr.sin_family         =   AF_INET;
    serverAddr.sin_addr.s_addr    =   htons(INADDR_ANY);
    serverAddr.sin_port           =   htons(TCP_SERVER_PORT);

    /*  创建套接字  */
    listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenFd < 0)
    {
        perror("socket create error\n");
        exit(-1);
    }
    else
    {
        printf("socket create success...\n");
        printf("创建套接字成功[errno = %d]...\n", errno);
    }

    /*  绑定端口  */
    /**********************************************************
     *
     *  命名服务器的套接字, 进行BIND端口绑定
     *
     **********************************************************/
    if(bind(listenFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) > 0)
    {
        perror("bind error\n");
        exit(1);
    }
    else
    {
        printf("server bind port %d success...\n", TCP_SERVER_PORT);
        printf("服务器绑定端口%d成功...\n", TCP_SERVER_PORT);
    }

    /*  开始监听绑定的端口  */
    /**********************************************************
     *
     *  开始监听服务器绑定的端口
     *
     **********************************************************/
    //if(listen(listenFd, LISTEN_QUEUE))
    //  系统中每一个端口最大的监听队列的长度
    //  这是个全局的参数,默认值为128
    //  http://blog.csdn.net/taolinke/article/details/6800979
    if(listen(listenFd, SOMAXCONN))
    {
        printf("Server listen error[errno = %d]...\n", errno);
        exit(-1);
    }
    else
    {
        printf("Server listen success...\n");
        printf("服务器开始监听...\n");
    }


    //  values for select
    int             fd, maxfd, nready;
    fd_set          rset, allset;
    struct  timeval timeout;


    //  initial "select" elements
    maxfd  = listenFd;                     //新增listenfd,所以更新当前的最大fd

    //  allret用于保存清楚完标志的fd_ret信息, 在每次处理完后,赋值给rset
    FD_ZERO(&allset);
    FD_SET(listenFd, &allset);


    struct sockaddr_in  clientAddr;
    socklen_t           length = sizeof(clientAddr);
    int                 connFd;

    //signal(SIGCHLD,sig_child);

    while( 1 )
    {
        rset = allset;   //rset和allset的搭配使得新加入的fd要等到下次select才会被监听

        //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
        //  因为select会修改timeout的值。
        timeout.tv_sec = 0;
        timeout.tv_usec = 500000;


        if((nready = select(maxfd + 1, &rset, (fd_set *)NULL, (fd_set *)NULL, &timeout)) < 0) //一开始select监听的是监听口
        {
            //  如果有timeout设置,那么每次select之前都要再重新设置一下timeout的值
            //  因为select会修改timeout的值。
            perror("select error...\n");
            exit(-1);
        }
        else if(nready == 0)
        {
            printf(".");
            fflush(stdout);
            continue;
        }

        printf("%d", __LINE__);
        //  接收到数据请求后, 检测数据请求的套接字来自那些套接字
        //
        //  首先检测服务器监听套接字有没有数据,
        //  如果有的话说明监听到了客户端的,
        //  应该调用accept来获取客户端的连接
        //
        //  接着检测客户端的连接套接字有没有数据连接
        //  如果有的话,说明客户端跟服务器有通信请求

        //  首先检测服务器监听套接字有没有数据,
        if(FD_ISSET(listenFd, &rset))   //  测试socketFd有没有消息
        {
            if((connFd = accept(listenFd, (struct sockaddr*)&clientAddr, &length)) == -1)
            {
                perror("accept error...\n");
                continue;
            }
            else
            {
                printf("获取到从客户端%s : 端口%d的连接, 套接字描述符%d...\n",
                        inet_ntoa(clientAddr.sin_addr),
                        ntohs(clientAddr.sin_port),
                        connFd);
            }

            //  新加入的描述符,还没判断是否可以或者写
            //  将缓存的allset的对应connfd置位,下次循环时即可监听connfd
            FD_SET(connFd, &allset);
            if (connFd > maxfd)  //  maxfd是为了下次select,作为参数使用
            {
                maxfd = connFd;
            }

            //  至此监听套接字的信息已经被处理, 应该先清楚对应位
            FD_CLR(listenFd, &rset);

            if(--nready <= 0)  //  nready用来辅助计数,这样就不要遍历整个client数组
            {
                continue;
            }
        }
        /*else
        {
            printf("服务器套接字%d没有连接, 有客户端的请求数据\n", listenFd);
        }*/

        printf("%d", __LINE__);

        //  遍历套接字看那些客户端连接套接字有数据请求
        for(fd = 0;
            fd <= maxfd && nready > 0;
            fd++)
        {
            if(FD_ISSET(fd, &rset))         //  检测到客户端连接套接字fd有数据请求
            {
                //单进程的环境下,不可以阻塞在这里,可以选择非阻塞,线程,超时.也就无法防范拒绝服务的攻击
                比较适合短连接的情况

                //单进程不使用fork的情况!
                //test fork
                //          if((childpid=fork())==0)
                //          {
                //              close(listenFd);
                
                //
                //  这里填写服务器的处理代码
                //
                
                printf("开始与客户端通信, 套接字描述符fd = %d\n", fd);

                pthread_t   pthread;
                arg_type    args = { fd, clientAddr };
                if(pthread_create(&pthread, NULL, RaiseThreadFunc, &args) != 0)
                {
                    perror("thread create error...\n");
                }

                //  清楚allset的对应位,以备fd可被继续select监听
                FD_CLR(fd, &allset);                  //清除,表示已被处理
                //close(fd);

                printf("can read : %d, %d\n", fd, nready);
            }

        }
    }


    return EXIT_SUCCESS;
}

参照
Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页