vSocket模型详解及select应用详解

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编程

 

时间: 2024-10-13 11:19:35

vSocket模型详解及select应用详解的相关文章

mysql基础篇 - SELECT 语句详解

基础篇 - SELECT 语句详解 SELECT语句详解 一.实验简介 SQL 中最常用的 SELECT 语句,用来在表中选取数据,本节实验中将通过一系列的动手操作详细学习 SELECT 语句的用法. 二.实验准备 在正式开始本实验内容之前,需要先下载相关数据库表,搭建好一个名为mysql_shiyan 的数据库(有三张表:department,employee,project),并向其中插入数据. 具体操作如下,首先输入命令进入 /home/shiyanlou/Desktop 目录: cd /

MySQL之SELECT 语句详解

本文参考实验楼的SELECT 语句详解结合自己操作部分而写成. 注意:大多数系统中,SQL语句都是不区分大小写的,但是出于严谨和便于区分保留字和变量名,在书写的时,保留字应大写,而变量名应小写.所谓的保留字,即为:在高级语言中已定义过的字,使用者不能将这些字作为变量名和过程名使用. 1)SELECT语句的基本格式是:SELECT 查询的列名 FROM 表名 WHERE 限制条件:在上一篇博客创建一个简单的成绩管理系统 中,使用SELECT * FROM employee代表查询所有的列.例如,要

java爬取网页内容 简单例子(2)——附jsoup的select用法详解

http://www.cnblogs.com/xiaoMzjm/p/3899366.html [背景] 在上一篇博文java爬取网页内容 简单例子(1)——使用正则表达式 里面,介绍了如何使用正则表达式去解析网页的内容,虽然该正则表达式比较通用,但繁琐,代码量多,现实中想要想出一条简单的正则表达式 对于没有很好正则表达式基础的人——比如说我T_T——是一件蛮困难的事.这一篇,我们改用jsoup,一个强大的解析html工具,去解析html,你会发现,一切都变得很容易. [准备工作] 下载:jsou

IE8“开发人员工具”使用详解上(各级菜单详解)

来源: http://www.cnblogs.com/JustinYoung/archive/2009/03/24/kaifarenyuangongju.html IE8“开发人员工具”使用详解上(各级菜单详解) IE8正式版已经发布了.本篇文章不会非常扯蛋地去进行什么评测,然后给出什么“Chrome运行JavaScript能力是IE8的15倍”.什么“IE8页面渲染速度是Safari的2.456倍”.什么“IE8的抗强暴能力比FireFox高出1.235倍” 这样的操蛋的结论.我管谁比谁强多少

iOS 开发之照片框架详解之二 —— PhotoKit 详解(下)

这里接着前文<iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)>,主要是干货环节,列举了如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法. 三. 常用方法的封装 虽然 PhotoKit 的功能强大很多,但基于兼容 iOS 8.0 以下版本的考虑,暂时可能仍无法抛弃 ALAssetLibrary,这时候一个比较好的方案是基于 ALAssetLibrary 和 PhotoKit 封装出一系列模拟系统 Asset 类的自定义类,然后在其中封装好兼容 A

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

一. 概况 本文接着 iOS 开发之照片框架详解,侧重介绍在前文中简单介绍过的 PhotoKit 及其与 ALAssetLibrary 的差异,以及如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法. 这里引用一下前文中对 PhotoKit 基本构成的介绍: PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源 PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值 PHAssetCo

android WebView详解,常见漏洞详解和安全源码

这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析. 由于博客内容长度,这次将分为上下两篇,上篇详解 WebView 的使用,下篇讲述 WebView 的漏洞和坑,以及修复源码的解析. 下篇:android WebView详解,常见漏洞详解和安全源码(下) 转载请注明出处:http://blog.csdn.net/self_study/article/details/54928371. 对技术感兴趣的同鞋加群 54

【Git使用详解】EGit使用详解

此系列文章写给那些打算使用Git或正在使用Git,但对Git还不是很理解的程序猿们,希望能帮助大家在学习和使用Git的过程中少走弯路,并以最少的时间和代价来熟悉Git,让Git能够辅助更多的开发者提高开发效率. Ps.使用Git已经很久了,回想当初使用Git的时候可谓是雾里看花,懵懵懂懂,没少犯错误,但我从未畏惧过错误,每一次错误的解决都是对我技术的提升和经验的积累. 下面是此系列文章的目录: [Git使用详解]Egit插件的安装图解 [Git使用详解]使用Egit克隆项目到本地图解 [Git使

android WebView详解,常见漏洞详解和安全源码(下)

上篇博客主要分析了 WebView 的详细使用,这篇来分析 WebView 的常见漏洞和使用的坑. 上篇:android WebView详解,常见漏洞详解和安全源码(上) 转载请注明出处:http://blog.csdn.net/self_study/article/details/55046348 对技术感兴趣的同鞋加群 544645972 一起交流. WebView 常见漏洞 WebView 的漏洞也是不少,列举一些常见的漏洞,实时更新,如果有其他的常见漏洞,知会一下我-- WebView