• 28、vSocket模型详解及select应用详解


    在上片文章已经讲过了TCP协议的基本结构和构成并举例,也粗略的讲过了SOCKET,但是讲解的并不完善,这里详细讲解下关于SOCKET的编程的I/O复用函数。

    1、I/O复用:selec函数

      在介绍socket编程之前,首先要熟悉下I/O多路转接技术,尽管SOCKET通信编程有很多模型,但是,在UNIX环境下,使用I/O多路转接模型无疑是一种更好的选择,UNIX下有5种I/0模型,分别是阻塞式I/O.非阻塞式I/O、I/O复用(select和poll)、信号驱动式I/O,异步I/O。这5种方式都可用SOCKET编程,这里只介绍阻塞式I/O和I/O复用,如果向详细了解I/O模型的,可以参考《UNIX网络编程卷一:套接字联网API》,或着查看

    Socket模型详解(转)

      (1)阻塞式I/O

      阻塞式I/O很好理解,一个线程利用recvfrom系统接收来自一个socket上的数据,没有数据的时候就等待,一直等到有数据,将数据交给应用去处理,之前降到的connect、accept、recv、recvfrom都是属于阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。

      (2)I/O复用

      对于只监控一个socket的应用程序来说,阻塞式I/O模型会工作的很好,但是如果需要监控多个socket的话,阻塞式I/O处理起来就比较麻烦了,这时候就可以使用I/O复用很好的解决这个问题。使用I/O复用时,所有的程序将需要监控的socket交给select函数,当某个socket上有可用数据时,select函数会返回通知应用程序。

      (3)select函数

    1、select函数原型(UNIX中提供POLL函数与之类似)

      select函数允许进程指示内核等待多个事件中的任何一个发生,并只在一个或多个事件发生或经历一段指定时间后才唤醒它。其原型如下:

    int select(int maxfd,  fd_set *readfds,  fd_set *writefds,  fe_set  *esceptfds,  const struct  timeval  *timeout);

    2、select函数参数

      第一个参数是需要监控的描述符个数,他的值是最大描述符加1。中间三个参数是要让内核测试读、写和异常条件的描述符,参数readfds指定了被读监控的文件描述集;参数writefds指定了被写监控的文件描述符集,而参数exceptfds被指定了异常监控的文件描述符集,如果我们对某一个条件不感兴趣,就可以设置为空指针。

      这里异常条件中包含了TCP的带外数据到达,大多数嵌入的API返回一个int返回代码。如果该码是<0,则一个错误已发生。错误的性质可以利用所谓的“错误号”,然而,在没有全局变量的多任务工作 环境中,socket领域,我们可以带哦用以下代码来查询错误。

    int espx_last_socket_errno(int socket) {
    
       int ret = 0;
    
       u32_t optlen = sizeof(ret);
    
       getsockopt(socket, SOL_SOCKET, SO_ERROR, &ret, &optlen);
    
       return ret;
    
    }
    

      

        参数timeout起到了定时器的作用,到来指定的时间,无论是否有设备准备好,都返回调用,timeval的结构定义如下:

    struct timeval
    {
        long tv_sec;//
        long tv_usec;//微秒
    };

      timeout 取不同值时,该调用就表现不同的性质:

      1)、timeout为0,即两个成员的取值必须为0;select调用立即返回,这样就类似为轮询。

      2)、timeout为NULL,select()调用就阻塞,也就是永远等待,直到有描述符就绪。

      3)、timeout为整数,就是等待一段时间,在这段时间内如果有描述符准备就绪,即立即返回,如果超过时间不管有没有准备符也立即返回。

      对于这个参数在UNIX系统下的使用值得注意的是:当timeout设置为永远等待或者等待一段时间的模式时,select函数可能会被信号中断,有些unix的内核在这种情况下,不会再次重启select,因此程序中应该要有处理select返回EINTR错误的准备。

      另外,尽管timeout参数可以表示很大的数值,但部分系统不一定会至此,可能会返回错误。

      如果,select函数中间三个参数都为空的话,select就成为了一个比sleep更为精确的休眠函数。

    3、select函数返回值

      select函数调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writerfds、和exceptfds中的没有就绪的描述符。这个情况是值得注意的,在一个典型的socket接收数据的程序中,当select检测到有数据过来,select函数返回,称故乡继续处理接收到的数据,数据处理结束,然后继续调用select监测,这个时候应该使用FD系列(windows和UNIX下)宏重新设置描述符集,在Solaris下,不重新设置的话,有时还能正常工作,但是在部分linux下就不会正常工作。

      select的返回值有如下情况:

    1)正常情况下返回就绪的文件描述符个数。

    2)经历过timeout时长后仍无设备准备好,返回0;

    3)、如果select被某个信号中断,他将返回-1并设置error为EINTR.

    4)如果出错,返回-1并设置相应的errno;

    2、FD系列宏

      系统提供了4个宏对描述符集进行操作:

      1)void FD_SET(int fd,fd_set *fdset);//宏FD_SET设置文件描述符集fdset中对应的fd的位(设置为1)

      2)void FD_CLR(int fd,fd_set *fdset);//宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)

      3)void FD_ISSET(int fd,fd_set *fdset);//宏FD_ZERO清除文件描述符集fdset中的所有位(即把所有位都设置为0)

      4)void FD_ZERO(fd_set *fdset);//检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。

       前三个宏在调用select前被描述符屏蔽位,在调用select后使用FD_ISSET进行检测。

    3、select函数应用举例

    1、通过前面的学习,我们可以编程如下来首先完成一个客户端的连接:

    /*
            文件名称:socket.c
            功能说明:通过selct实现TCP服务器的非阻塞模式
            使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据
            编写日期:2017-11-18
            修改历史:无
    */
    //包含文件申明
    #include <stdio.h>
    #include<io.h>
    #include <string.h>
    #include <WinSock2.h>
    
    #pragma comment(lib, "ws2_32.lib")
    
    
    //全局变量定义初始化
    #define S_ADDR S_un.S_addr
    #define CLOSE closesocket
    struct socketaddr_in
    {
        short sin_family;
        u_short sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
    };
    
    
    /************************************************************
            函数名称:socket_init
            函数功能:打开windows下的socket,UNIX下不用
            参数:
                数据类型        输入/输出描述
                
            说明:socket设置失败返回1,socket设置成功返回0
    ************************************************************/
    void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。
    {
        WSADATA wsa;
    
        printf("
    初始化中Initialising Winsock...
    ");
        if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
            printf("Failed. Error Code : %d", WSAGetLastError());
            return 1;
        }
        printf("初始化成功Initialised.
    ");
    }
    
    /************************************************************
    函数名称:tcp_client
    函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP
    参数:
    数据类型        输入/输出描述
    
    说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询
    ************************************************************/
    void tcp_client()
    {
        int sockfd;
        struct sockaddr_in    ServAddr;
        int ret;
        char* SendStr = "Hi,Tcp";    //定义一个发送的指针,指针指向要发送的数据的地址
        int len;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数
    
        ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的
    
        if (ret != 0)
        {
            return;
        }
        len = send(sockfd, SendStr, strlen(SendStr), 0);
        //发送数据到服务器
        if (len <= 0)
        {
            printf("send data error
    ");
        }
        CLOSE(sockfd);//关闭socket函数
        return;
    }
    /************************************************************
    函数名称:tcp_server
    函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示
    参数:
    数据类型        输入/输出描述
    
    说明:无
    ************************************************************/
    void tcp_server()
    {
        int listensockfd;
        int connfd;
        struct sockaddr_in  ServAddr;
        fd_set RecvdFd;
        struct timeval timeout;
        int ret;
        char RecvBuf[64];
        int len;
        struct sockaddr ConnAddr;
        int ConnAddrLen = sizeof(struct sockaddr);
        int maxfd;
    
        listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数
    
        maxfd = listensockfd;
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器
                                                     //
        if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)//
                                                                                   //将本地的一个IP地址和套接字绑定在一起
        {
            printf("bind error
    ");
            return;
        }
        if (listen(listensockfd, 1) != 0)
            //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
        {
            printf("listen error
        ");
            return;
        }
        timeout.tv_sec = 60;
        timeout.tv_usec = 0;
    
    
        FD_ZERO(&RecvdFd);
        //清除监听select函数描述符的监听符
        for (;;)
        {
            FD_SET(listensockfd, &RecvdFd);
            //设置文件描述符集,将监听socket的描述符加入到select的监控中
            memset(RecvBuf, 0, sizeof(RecvBuf));
            //分配接受数据的内存
            ret = select(maxfd + 1, &RecvdFd, NULL, NULL, &timeout);
            //配置select函数监控设定的描述符,并且只对“读”事件关心
            if (-1 == ret)
            {
                continue;
            }
    
            if (FD_ISSET(listensockfd, &RecvdFd))
                //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生
                //
            {
                connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen);
                //如果是,则调用accept函数进行事件处理,
                if (-1 == connfd)
                {
                    printf("accept error
    ");
                    continue;
                }
                FD_SET(connfd, &RecvdFd);
                //将accept返回的客户连接描述符加入到select监控位
            }
            if (FD_ISSET(connfd, &RecvdFd))
                //select函数返回,通知有事件,判断是否是客户连接成功的描述符上有事件发生
            {
                len = recv(connfd, RecvBuf, 6, 0);
                //如果是,调用recv函数进行数据接收
                if (len <= 0)
                    //如果recv接受长度小于0,则表示对端关闭
                {
                    FD_CLR(connfd, &RecvdFd);
                    CLOSE(connfd);
                    //调用socket关闭函数,关闭本端socket
                    continue;
                }
                printf("%s
    ", RecvBuf);
                //将接收的数据显示出来
            }
        }
        return;
    }
    
    /************************************************************
    函数名称:main
    函数功能:主函数
    参数:
            数据类型        输入/输出描述
    argc        int            输入的数据argc
    argv        char*        输入的数组argv[]
    说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
    ************************************************************/
    void    main(int argc, char* argv[])
    {
        argc = 2;//是2是为了保证我们设定参数
        argv[1] = "2";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
        int type;
    
        socket_init();
        if (argc != 2)
        {
            printf("Usage:< s% [Mode 1|2]>
    ", argv[0]);
            return;
        }
        type = strtol(argv[1], NULL, 10);
        if (1 == type)
        {
            printf("Server  Mode Run!
        ");
            tcp_server();
            return;
        }
        if (2 == type)
        {
            printf("Client     Mode Run!
    ");
            tcp_client();
            return;
        }
    
        return;
    }

    实验现象:

    先运行argv[1]赋值为1的程序

    再运行argv[1]赋值为2的程序,可以看到如下结果

     

    可以看到数据发送成功。

     2、为了体现select函数的优越性,将程序进行修改,让客户端发送一系列字符串,服务器端将字符串打印出来,并且让其同时处理两个客户端连接。

    /*
    文件名称:socket.c
    功能说明:通过selct实现TCP服务器的非阻塞模式
    使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据
    编写日期:2017-11-18
    修改历史:
    2017-11-18  修改发送数据,使client发送字符串,优化服务器,使服务器可处理多个客户端数据
    */
    //包含文件申明
    #include <stdio.h>
    #include<io.h>
    #include <string.h>
    #include <WinSock2.h>
    
    #pragma comment(lib, "ws2_32.lib")
    
    
    //全局变量定义初始化
    #define S_ADDR S_un.S_addr
    #define CLOSE closesocket
    struct socketaddr_in
    {
        short sin_family;
        u_short sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
    };
    
    
    /************************************************************
    函数名称:socket_init
    函数功能:打开windows下的socket,UNIX下不用
    参数:
    数据类型        输入/输出描述
    
    说明:socket设置失败返回1,socket设置成功返回0
    ************************************************************/
    void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。
    {
        WSADATA wsa;
    
        printf("
    初始化中Initialising Winsock...
    ");
        if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
            printf("Failed. Error Code : %d", WSAGetLastError());
            return 1;
        }
        printf("初始化成功Initialised.
    ");
    }
    
    /************************************************************
    函数名称:tcp_client
    函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP
    参数:
    数据类型        输入/输出描述
    
    说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询
    修改历史:
    2017-11-18     将字符发送优化为字符串发送
    ************************************************************/
    void tcp_client()
    {
        int sockfd;
        struct sockaddr_in    ServAddr;
        int ret;
        char* SendStr[] =
        { 
        "Hi,Tcp",
        "HI,TCP,nice to meet you",
        "HI,TCP,are you ok???"
    
        };//定义一个发送的指针,指针指向要发送的数据的地址
        unsigned int SendStrlen = 0;
        unsigned int SendLen = 0;
        int loop = 0;
        int len;
    
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数
    
        ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的
    
        if (ret != 0)
        {
            return;
        }
        for (loop = 0; loop < 5; loop++)
        {
            SendStrlen = strlen(SendStr[loop]);
            SendLen = htonl(SendStrlen);
            len = send(sockfd, (char*)&SendLen, sizeof(SendLen), 0);
            if (len <= 0)
            {
                printf("send data error
    ");
                break;
            }
            len = send(sockfd, SendStr[loop], SendStrlen, 0);
            if (len <= 0)
            {
                printf("send data error
    ");
                break;
            }
        }
        CLOSE(sockfd);//关闭socket函数
        return;
    }
    /************************************************************
    函数名称:tcp_server
    函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示
    参数:
    数据类型        输入/输出描述
    
    说明:无
    ************************************************************/
    void tcp_server()
    {
        int listensockfd;
        int connfd;
        struct sockaddr_in  ServAddr;
        fd_set RecvdFd;
        struct timeval timeout;
        int ret;
        char RecvBuf[64];
        int len;
        struct sockaddr ConnAddr;
        int ConnAddrLen = sizeof(struct sockaddr);
        int maxfd;
    
        int maxconn = 2;
        int clientfd[2] = { -1,-1 };
        int loop = 0;
        unsigned int RecvStrlen = 0;
        unsigned int RecvedTotallen = 0;
    
    
        listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数
    
        maxfd = listensockfd;
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器
                                                     //
        if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)//
                                                                                   //将本地的一个IP地址和套接字绑定在一起
        {
            printf("bind error
    ");
            return;
        }
        if (listen(listensockfd, 1) != 0)
            //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
        {
            printf("listen error
        ");
            return;
        }
        timeout.tv_sec = 60;
        timeout.tv_usec = 0;
    
    
        FD_ZERO(&RecvdFd);
        //清除监听select函数描述符的监听符
        for (;;)
        {
            FD_SET(listensockfd, &RecvdFd);
            //设置文件描述符集,将监听socket的描述符加入到select的监控中
            memset(RecvBuf, 0, sizeof(RecvBuf));
            //分配接受数据的内存
            ret = select(maxfd + 1, &RecvdFd, NULL, NULL, &timeout);
            //配置select函数监控设定的描述符,并且只对“读”事件关心
            if (-1 == ret)
            {
                continue;
            }
    
            if (FD_ISSET(listensockfd, &RecvdFd))
                //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生
            {
                connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen);
                //如果是,则调用accept函数进行事件处理,
                if (-1 == connfd)
                {
                    printf("accept error=%d
    ", GetLastError());
                    continue;
                }
                for (loop = 0; loop < 2; loop++)//最多允许两个客户输入,并将客户连接的socket描述符保存好。
                {
                    if (-1 == clientfd[loop])
                    {
                        FD_SET(connfd, &RecvdFd);
                        //将accept返回的客户连接描述符加入到select监控位
                        clientfd[loop] = connfd;
                        maxfd = (maxfd > connfd ? maxfd : connfd);
                        break;
                    }
                }
                if (loop >= 2)
                {
                    printf("Max connect reached.
    ");
                }
            }
            for (loop = 0; loop < 2; loop++)//循环检查来两个客户端的socket描述符,看是否有数据可读。
            {
                if (FD_ISSET(clientfd[loop], &RecvdFd))
                    //select函数返回,通知有事件,判断是否是客户连接成功的描述符上有事件发生
                {
                    RecvStrlen = 0;
                    len = recv(connfd, (char*)&RecvStrlen, sizeof(RecvStrlen), 0);
                    //如果是,调用recv函数进行数据接收
                    if (len <= 0)
                        //如果recv接受长度小于0,则表示对端关闭
                    {
                        FD_CLR(connfd, &RecvdFd);
                        CLOSE(connfd);
                        //调用socket关闭函数,关闭本端socket
                        continue;//先接收四个字节长度的字符串长度
                    }
                    memset(RecvBuf, 0, sizeof(RecvBuf));
                    RecvStrlen = ntohl(RecvStrlen);//将接收到的长度进行字节转换,转换成本机序
                    RecvedTotallen = 0;
                    while (1)//循环接收每次发送的字符串,直到制定长度
                    {
                        len = recv(connfd, &RecvBuf[RecvedTotallen], RecvStrlen, 0);
                        if (len <= 0)
                        {
                            FD_CLR(clientfd[loop], &RecvdFd);
                            closesocket(clientfd[loop]);
                            break;
                        }
                        RecvedTotallen += len;
    
                        if (RecvedTotallen == RecvStrlen)
                        {
                            break;
                        }
                    }
                    printf("%s
    ", RecvBuf);
                }
            }
        }
        return;
    }
    
    /************************************************************
    函数名称:main
    函数功能:主函数
    参数:
    数据类型        输入/输出描述
    argc        int            输入的数据argc
    argv        char*        输入的数组argv[]
    说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
    ************************************************************/
    void    main(int argc, char* argv[])
    {
        argc = 2;//是2是为了保证我们设定参数
        argv[1] = "1";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
        int type;
    
        socket_init();
        if (argc != 2)
        {
            printf("Usage:< s% [Mode 1|2]>
    ", argv[0]);
            return;
        }
        type = strtol(argv[1], NULL, 10);
        if (1 == type)
        {
            printf("Server  Mode Run!
        ");
            tcp_server();
            return;
        }
        if (2 == type)
        {
            printf("Client     Mode Run!
    ");
            tcp_client();
            return;
        }
    
        return;
    }

    同样的方法进行调用结果如下:

    当第二个客户端连接到服务器时,有如下输出

     3、通过之前的编程和优化,基本上已经实现了大多数TCP服务的应用场景,但是此处还有一个问题,就是当服务器正在接收数据时,另一个客户端又发来数据了该如何处理,这里就设计到TCP的并发处理了,每当accept一个连接时就创建一个线程去单独处理这个连接,下面对前面的程序进行修改如下

    /*
    文件名称:socket.c
    功能说明:通过selct实现TCP服务器的非阻塞模式
    使用说明:复制此文件,对argc赋值2,argv[1]先赋值1运行启动服务器,再赋值2启动客户端发送数据
    编写日期:2017-11-18
    修改历史:
    2017-11-18  修改发送数据,使client发送字符串,优化服务器,使服务器可处理多个客户端数据
    */
    //包含文件申明
    #include <stdio.h>
    #include<io.h>
    #include <string.h>
    #include <WinSock2.h>
    #include <conio.h>
    #include <process.h>
    
    
    #pragma comment(lib, "ws2_32.lib")
    
    
    //全局变量定义初始化
    #define S_ADDR S_un.S_addr
    #define CLOSE closesocket
    struct socketaddr_in
    {
        short sin_family;
        u_short sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
    };
    
    
    /************************************************************
    函数名称:socket_init
    函数功能:打开windows下的socket,UNIX下不用
    参数:
    数据类型        输入/输出描述
    
    说明:socket设置失败返回1,socket设置成功返回0
    ************************************************************/
    void socket_init()//为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。
    {
        WSADATA wsa;
    
        printf("
    初始化中Initialising Winsock...
    ");
        if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
            printf("Failed. Error Code : %d", WSAGetLastError());
            return 1;
        }
        printf("初始化成功Initialised.
    ");
    }
    
    /************************************************************
    函数名称:tcp_client
    函数功能:创建一个tcp客户端,给IP为"192.168.191.1"的服务器7777端口发送数据Hi,TCP
    参数:
    数据类型        输入/输出描述
    
    说明:IP地址0"192.168.191.1"是本机现在的网络环境,不同的网络环境的IP不同。可用IPCONFIG在windows的cmd下查询
    修改历史:
    2017-11-18     将字符发送优化为字符串发送
    ************************************************************/
    void tcp_client()
    {
        int sockfd;
        struct sockaddr_in    ServAddr;
        int ret;
        char* SendStr[] =
        { 
        "Hi,Tcp",
        "HI,TCP,nice to meet you",
        "HI,TCP,are you ok???"
    
        };//定义一个发送的指针,指针指向要发送的数据的地址
        unsigned int SendStrlen = 0;
        unsigned int SendLen = 0;
        int loop = 0;
        int len;
    
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = inet_addr("192.168.191.1");//配置ServAddr结构体参数
    
        ret = connect(sockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr));//connect有TCP客户端用来和TCP服务器建立连接的
    
        if (ret != 0)
        {
            return;
        }
        for (loop = 0; loop < 5; loop++)
        {
            SendStrlen = strlen(SendStr[loop]);
            SendLen = htonl(SendStrlen);
            len = send(sockfd, (char*)&SendLen, sizeof(SendLen), 0);
            if (len <= 0)
            {
                printf("send data error
    ");
                break;
            }
            len = send(sockfd, SendStr[loop], SendStrlen, 0);
            if (len <= 0)
            {
                printf("send data error
    ");
                break;
            }
        }
        CLOSE(sockfd);//关闭socket函数
        return;
    }
    /************************************************************
    函数名称:tcp_server
    函数功能:创建一个tcp服务器,接收任何IP地址的客户端给其7777端口发送的数据并显示
    参数:
    数据类型        输入/输出描述
    
    说明:无
    ************************************************************/
    void tcp_server()
    {
        int listensockfd;
        int connfd;
        struct sockaddr_in  ServAddr;
        fd_set RecvdFd;
        struct timeval timeout;
        int ret;
        char RecvBuf[64];
        int len;
        struct sockaddr ConnAddr;
        int ConnAddrLen = sizeof(struct sockaddr);
    
        listensockfd = socket(AF_INET, SOCK_STREAM, 0);//打开网络通信端口,为了执行I/O操作,第一件事就是要调用socket函数
    
    
        ServAddr.sin_family = AF_INET;
        ServAddr.sin_port = htons(7777);
        ServAddr.sin_addr.S_ADDR = htonl(INADDR_ANY);//配置ServAddr数据,所有iP的设备都可以通过7777端口将数据传输到这个服务器
                                                     //
        if (bind(listensockfd, (struct sockaddr*)&ServAddr, sizeof(ServAddr)) != 0)//
                                                                                   //将本地的一个IP地址和套接字绑定在一起
        {
            printf("bind error
    ");
            return;
        }
        if (listen(listensockfd, 1) != 0)
            //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
        {
            printf("listen error
        ");
            return;
        }
        timeout.tv_sec = 60;
        timeout.tv_usec = 0;
    
    
        FD_ZERO(&RecvdFd);
        //清除监听select函数描述符的监听符
        for (;;)
        {
            FD_SET(listensockfd, &RecvdFd);
            //设置文件描述符集,将监听socket的描述符加入到select的监控中
            ret = select(listensockfd + 1, &RecvdFd, NULL, NULL, &timeout);
        //配置select函数监控设定的描述符,并且只对“读”事件关心,只监控监听描述符,不再监控客户连接进来的描述符。
            if (-1 == ret)
            {
                continue;
            }
    
            if (FD_ISSET(listensockfd, &RecvdFd))
                //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生
            {
                connfd = accept(listensockfd, &ConnAddr, &ConnAddrLen);
                //如果是,则调用accept函数进行事件处理,
                if (-1 == connfd)
                {
                    printf("accept error=%d
    ", GetLastError());
                    continue;
                }
            /*创建线程*/
                tcp_create_thread(connfd);
            }
        }
        return;
    }
    
    unsigned int _stdcall tcp_conn_process_thread(void *args)//window下用这个定义
    //unix下定义为void* tcp_conn_process_thread(void* args)
    {
        int connfd;
        fd_set RecvdFd;
        struct timeval timeout;
        char RecvBuf[64];
        unsigned int RecvStrlen = 0;
        unsigned int RecvedTotallen = 0;
        int ret;
        int len;
    
        timeout.tv_sec = 60;
        timeout.tv_usec = 0;
    
        connfd = *((int*)args);
        FD_ZERO(&RecvdFd);
        //清除监听select函数描述符的监听符
        for (;;)
        {
            FD_SET(connfd, &RecvdFd);
            //设置文件描述符集,将监听socket的描述符加入到select的监控中
            memset(RecvBuf, 0, sizeof(RecvBuf));
            //分配接受数据的内存
            ret = select(connfd + 1, &RecvdFd, NULL, NULL, &timeout);
            //配置select函数监控设定的描述符,并且只对“读”事件关心
            if (-1 == ret)
            {
                continue;
            }
    
            if (FD_ISSET(connfd, &RecvdFd))
                //select函数返回,通知有时间,判断是否是监听的socket描述符中的读事件发生
            {
                RecvStrlen = 0;
                len = recv(connfd, (char*)&RecvStrlen, sizeof(RecvStrlen), 0);
                //如果是,调用recv函数进行数据接收
                if (len <= 0)
                    //如果recv接受长度小于0,则表示对端关闭
                {
                    CLOSE(connfd);
                    //调用socket关闭函数,关闭本端socket
                    break;
                }
                memset(RecvBuf, 0, sizeof(RecvBuf));
                RecvStrlen = ntohl(RecvStrlen);//将接收到的长度进行字节转换,转换成本机序
                RecvedTotallen = 0;
                while (1)//循环接收每次发送的字符串,直到制定长度
                {
                    len = recv(connfd, &RecvBuf[RecvedTotallen], RecvStrlen, 0);
                    if (len <= 0)
                    {
                        CLOSE(connfd);
                        break;
                    }
                    RecvedTotallen += len;
    
                    if (RecvedTotallen == RecvStrlen)
                    {
                        break;
                    }
                }
                printf("%s
    ", RecvBuf);
            }
        }
        return 0;
    
    }
    
    int g_connfd;
    int tcp_create_thread(int sockfd)
    {
        g_connfd = sockfd;
        //声明一个全局变量,作为参数传递给线程,如果使用局部变量的话,由于主线程与新创线程运行的时间关系,在线创建线程需要
        //使用参数地址时,主线程函数调用已经推出,从而导致传递的局部变量的地址已经被释放。
        unsigned long threadId;
        threadId = _beginthreadex(NULL, 0, (unsigned(_stdcall*)(void*))tcp_conn_process_thread, &g_connfd, 0, NULL);
        //windows下创建一个线程
        /***********************************************************************
        UNIX下:
        pthread_t threadId;
        pthread_create(&threaId,NULL,tcp_conn_process_thread,(void*)&g_connfd);
        ***********************************************************************/
        return;
    }
    
    /************************************************************
    函数名称:main
    函数功能:主函数
    参数:
    数据类型        输入/输出描述
    argc        int            输入的数据argc
    argv        char*        输入的数组argv[]
    说明:argc=2,且argv[1]为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
    ************************************************************/
    void    main(int argc, char* argv[])
    {
        argc = 2;//是2是为了保证我们设定参数
        argv[1] = "1";//为1的时候为服务器,2的时候为客户端,打开两个程序,先编译运行1,再编译运行2
        int type;
    
        socket_init();
        if (argc != 2)
        {
            printf("Usage:< s% [Mode 1|2]>
    ", argv[0]);
            return;
        }
        type = strtol(argv[1], NULL, 10);
        if (1 == type)
        {
            printf("Server  Mode Run!
        ");
            tcp_server();
            return;
        }
        if (2 == type)
        {
            printf("Client     Mode Run!
    ");
            tcp_client();
            return;
        }
    
        return;
    }

    运行程序1、2后结果如下:

    到这里,一个功能完善的TCP服务器就搭建完成了。

     下一篇文章:

    基于visual studio的UDP编程

  • 相关阅读:
    ES6 变量的解构赋值
    【js重学系列】new
    【js面试系列】手写常见js方法
    【js重学系列】this
    js-继承
    【js重学系列】数组高阶函数
    【js面试系列】数组去重
    云服务器部署项目-基本使用流程
    mongodb-基本使用
    移动端适配
  • 原文地址:https://www.cnblogs.com/noticeable/p/7786608.html
Copyright © 2020-2023  润新知