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("\n初始化中Initialising Winsock...\n");
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        printf("Failed. Error Code : %d", WSAGetLastError());
        return 1;
    }
    printf("初始化成功Initialised.\n");
}

/************************************************************
函数名称: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\n");
    }
    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\n");
        return;
    }
    if (listen(listensockfd, 1) != 0)
        //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
    {
        printf("listen error\n    ");
        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\n");
                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\n", 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]>\n", argv[0]);
        return;
    }
    type = strtol(argv[1], NULL, 10);
    if (1 == type)
    {
        printf("Server  Mode Run!\n    ");
        tcp_server();
        return;
    }
    if (2 == type)
    {
        printf("Client     Mode Run!\n");
        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("\n初始化中Initialising Winsock...\n");
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        printf("Failed. Error Code : %d", WSAGetLastError());
        return 1;
    }
    printf("初始化成功Initialised.\n");
}

/************************************************************
函数名称: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\n");
            break;
        }
        len = send(sockfd, SendStr[loop], SendStrlen, 0);
        if (len <= 0)
        {
            printf("send data error\n");
            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\n");
        return;
    }
    if (listen(listensockfd, 1) != 0)
        //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
    {
        printf("listen error\n    ");
        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\n", 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.\n");
            }
        }
        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\n", 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]>\n", argv[0]);
        return;
    }
    type = strtol(argv[1], NULL, 10);
    if (1 == type)
    {
        printf("Server  Mode Run!\n    ");
        tcp_server();
        return;
    }
    if (2 == type)
    {
        printf("Client     Mode Run!\n");
        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("\n初始化中Initialising Winsock...\n");
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        printf("Failed. Error Code : %d", WSAGetLastError());
        return 1;
    }
    printf("初始化成功Initialised.\n");
}

/************************************************************
函数名称: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\n");
            break;
        }
        len = send(sockfd, SendStr[loop], SendStrlen, 0);
        if (len <= 0)
        {
            printf("send data error\n");
            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\n");
        return;
    }
    if (listen(listensockfd, 1) != 0)
        //listen由TCP服务器调起,监听客户发起的connect,如果监听到connect,则和客户进行三次握手
    {
        printf("listen error\n    ");
        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\n", 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\n", 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]>\n", argv[0]);
        return;
    }
    type = strtol(argv[1], NULL, 10);
    if (1 == type)
    {
        printf("Server  Mode Run!\n    ");
        tcp_server();
        return;
    }
    if (2 == type)
    {
        printf("Client     Mode Run!\n");
        tcp_client();
        return;
    }

    return;
}

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

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

 下一篇文章:

基于visual studio的UDP编程

posted @ 2017-11-20 14:51  noticeable  阅读(1511)  评论(0编辑  收藏  举报