Windows Sockets网络编程(1)TCP select & thread

来源:互联网 发布:福建省应急管理网络 编辑:程序博客网 时间:2024/06/07 08:02

select在socket通信中有着举足轻重的地位,这要先从recv谈起。既然来到了本文,就默认你已经明白了send/recv阻塞式通信了,如果不太了解可以先阅读《Windows Sockets网络编程(0)TCP In Action》。这种阻塞式通信,存在一个很大的问题:“假设需要建立两条以上的TCP/UDP通信,那么recv该如何弄?”。很常见的方式是多线程?回答很正确。一般网络通信,肯定是要开启一个线程用来接受数据的,也就是在一个子线程中recv。那么,如果有N条TCP/UDP通道,就必须使用N个线程。开设线程本身需要一定的消耗(Windows一个线程,默认需要4M左右的线程栈),而更多的问题是线程间的互斥与同步。那么一个解决方案——select机制,出现了。


目录:

  • select有何作用
  • fd_set集合
  • create socket
  • select函数
  • 如何使用select
  • select是如何监视recv的
  • select是如何执行的
  • heart beat
  • notify event
  • 完整DEMO

select有何作用?

如果可以用一个不太准确的数学式子来表示的话,可以这么写:

  • select = recv + recv + … + recv.

当然,在不采取特殊手段的前提下,上式中的recv个数是<=64的。那么,select到底是什么?

有人回答:“select是recv监视器。”

这个比喻其实很恰当。

fd_set集合

要使用select,则必须知道fd_set集合。它是系统中的一个socket集合类型。由于你可能没接触过这个类型,看起来觉得有些奇怪。如果我在这里写个int或者long,你可能就觉得亲切了。

fd_set g_tcp_select_client; //global default fd_set all null.//long a[64];

虽然fd_set从字面来看“set”像是集合,但是它的内部构造却是一个数组。也就是说上面的g_tcp_select_client,其实完全可以当做一个数组使用。可以想象一下long a[64]如何使用,fd_set也莫过于此。

create socket

相信来到本文的你,已经会很熟练的创建socket了。那么,以前你是如何管理创建好的socket呢?

SOCKET tcp_client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  1. 你是使用一个数组SOCKET array[5],将所有的SOCKET存放进去?
  2. 还是前进一步,使用STL map将其存放进去?能够更方便的查找?

其实,有一种相对来说,更好的方式。你要学会使用select,要学会fd_set的使用。

//add into select setFD_SET(tcp_client_socket, &g_tcp_select_client);

FD_SET是一个宏,以上语句,就可以将我们创建的SOCKET添加到fd_set集合中了。而且,fd_set与select配合可以对SOCKET进行神奇的管理。

select函数

那么真正的select函数是什么样子呢?select到底是如何监视recv的呢?

int select( int nfds, fd_set FAR* readfds, fd_set * writefds, fd_set * exceptfds, const struct timeval * timeout);
  • nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
  • readfds:(可选)指针,指向一组等待可读性检查的套接口。
  • writefds:(可选)指针,指向一组等待可写性检查的套接口。
  • exceptfds:(可选)指针,指向一组等待错误检查的套接口。
  • timeout:select()最多等待时间,对阻塞操作则为NULL。

对于参数timeout来说,如果传入NULL,则表示无限阻塞。如果需要设置超时时间?那么你可以这样:

timeval tm;tm.tv_sec = 1L;   //stm.tv_usec = 100L;//ms

如何使用select?

select使用起来有不少注意事项:

 u_int select_ret = select(0, &tcp_select_client_temp, NULL, NULL, NULL);//select核心

假设tcp_select_client_temp中存放的是:[123][456][789][][][][][][].

也就是说,fd_set中目前已经放入了3个SOCKET。那么,使用上述一条select语句就可以对这3个SOCKET是否与数据到来进行监视了。(而不需要开启3个线程,然后每个线程中使用1个recv)。

for (u_int i = 0, count = 0; i < g_tcp_select_client.fd_count; ++i){   SOCKET tcp_client_socket = g_tcp_select_client.fd_array[i];}

fd_count存放着fd_set中SOCKET的个数,这里就是3。而使用fd_array[1]就可以将存放着的SOCKET取出来。

看到这里,感受到了fd_set是否和数组使用起来极为一致?

select是如何监视recv的?

那么select是怎么知道fd_set中放置的N个SOCKET有数据到来?如是如何知道是其中的哪个或者哪几个SOCKET有数据到来?

if (FD_ISSET(tcp_client_socket, &tcp_select_client_temp)){    //recv data}

伟大的设计师,计算机先驱肯定是设计好了的。魔法师给我们提供了FD_ISSET宏。它能很方便的知道,某个SOCKET是否有数据到来。一般for与FD_ISSET是结合使用的。

for (u_int i = 0, count = 0; i < g_tcp_select_client.fd_count; ++i){    SOCKET tcp_client_socket = g_tcp_select_client.fd_array[i];    //valid    if (FD_ISSET(tcp_client_socket, &tcp_select_client_temp)){        char recv_buffer[1025];        int recv_ret = recv(tcp_client_socket, recv_buffer, sizeof(recv_buffer)-1, 0);        if (recv_ret > 0){            recv_buffer[recv_ret] = '\0';            printf("%s\n",recv_buffer);        }   }}

对于以上代码中,需要注意的是:一般而言,recv返回值<=0时,这时候就认为对应的SOCKET已经异常了,需要及时清理。

FD_CLR(tcp_client_socket, &g_tcp_select_client);

FD_CLR宏,主要作用是将fd_set中的某个SOCKET移除。

select是如何执行的?

还是假设fd_set中(也就是g_tcp_select_client中)存放的是:[123][456][789]…,这3个SOCKET。

某时刻,select阻塞在线程中,一直监听着fd_set中的所有SOCKET。(直到超时,否则一直阻塞)。

假设,此时SOCKET = 456通道,有消息到来?select将执行以下操作:

  1. 将fd_set中的SOCKET重新排序:[456][456][789]….
  2. 将select函数返回值置为1(表示有一个SOCKET有数据到来)。

如果知道这个机制的话,其实如果不使用FD_ISSET宏,也是可以知道到底哪个SOCKET有数据到来的。(其实将fd_set中,前select_ret个SOCKET取出来即可)。

这里需要注意,由于select每次都会修改fd_set,所以需要一个备份。以备下次while循环时,让fd_set能够重新初始化为原来的。一般在线程中,是这么写的(以下代码中g_tcp_select_client就是一个备份):

DWORD WINAPI recv_server_data_forever_thread(LPVOID param){    fd_set tcp_select_client_temp;//temp fd_set    while (g_tcp_select_client.fd_count > 0){        FD_ZERO(&tcp_select_client_temp);        //reset        tcp_select_client_temp = g_tcp_select_client;//buffer        u_int select_ret = select(0, &tcp_select_client_temp, NULL, NULL, NULL);//select核心        if (select_ret  == SOCKET_ERROR){        }        if (select_ret == 0){            //time out        }        //....    }}

heart beat

为了让通信更加稳定,心跳技术一般是必不可少的。RFC并没有强制规定,TCP之类的一定要保持持久连接。但是,一般TCP会保持链路在3-5分钟之类不会被断开。(有时候,路由器会对长时间没有数据经过的TCP链路进行清理,这时候通道就断开了)。所以,对于TCP来说,一般3分钟左右发送一个极小的数据包至服务器,这种技术就是心跳技术。

notify event

在多线程通信中,WaitForSingleObject/WaitForMultipleObjects/SetEvent,是保持同步必不可少的技术。

完整DEMO

#include <windows.h>#include <stdio.h>#pragma comment(lib,"ws2_32.lib") fd_set g_tcp_select_client; //global default fd_set all null.HANDLE g_thread[2];HANDLE g_event_close, g_event_heartbeat;DWORD WINAPI recv_server_data_forever_thread(LPVOID param){    fd_set tcp_select_client_temp;    while (g_tcp_select_client.fd_count > 0){        FD_ZERO(&tcp_select_client_temp);        //reset        tcp_select_client_temp = g_tcp_select_client;        u_int select_ret = select(0, &tcp_select_client_temp, NULL, NULL, NULL);        for (u_int i = 0, count = 0; i < g_tcp_select_client.fd_count &&             (count < select_ret || select_ret < 0); ++i){            SOCKET tcp_client_socket = g_tcp_select_client.fd_array[i];            //valid            if (FD_ISSET(tcp_client_socket, &tcp_select_client_temp)){                ++count;                char recv_buffer[1025];                int recv_ret = recv(tcp_client_socket, recv_buffer, sizeof(recv_buffer)-1, 0);                if (recv_ret > 0){                    recv_buffer[recv_ret] = '\0';                    printf("%s\n",recv_buffer);                    Sleep(1000);                    char* get = "get!";                    ::send(tcp_client_socket, get, strlen(get), 0);                    continue;                }                int err_code = WSAGetLastError();                printf("close success, code = %d\n",err_code);                //remove                FD_CLR(tcp_client_socket, &g_tcp_select_client);                //notify finish                if (g_event_close){                    ::SetEvent(g_event_close);                }            }        }    }    //notify heart beat    if (g_event_heartbeat){        ::SetEvent(g_event_heartbeat);    }    //close thread    if (g_thread[0]){        ::CloseHandle(g_thread[0]);        g_thread[0] = NULL;    }    FD_ZERO(&g_tcp_select_client);//clear select set    return 0L;}DWORD WINAPI send_heart_beat_thread(LPVOID param){    while (g_tcp_select_client.fd_count > 0){        g_event_heartbeat = ::CreateEvent(NULL, TRUE, FALSE, NULL);        WaitForSingleObject(g_event_heartbeat, 3 * 1000);        if (g_event_heartbeat){            ::CloseHandle(g_event_heartbeat);            g_event_heartbeat = NULL;        }        if (g_tcp_select_client.fd_count <= 0){            break;        }        //send heart beat        for (u_int i = 0, count = 0; i < g_tcp_select_client.fd_count; ++i){            SOCKET tcp_client_socket = g_tcp_select_client.fd_array[i];            char data[1024] = "heart beat";            ::send(tcp_client_socket, data, strlen(data), 0);        }    }    //close thread    if (g_thread[1]){        ::CloseHandle(g_thread[1]);        g_thread[1] = NULL;    }    return 0L;}void close_socket(SOCKET tcp_client_socket){    if (FD_ISSET(tcp_client_socket, &g_tcp_select_client)){        ::closesocket(tcp_client_socket);        tcp_client_socket = INVALID_SOCKET;        g_event_close = ::CreateEvent(NULL, TRUE, FALSE, NULL);        //wait select to deal        ::WaitForSingleObject(g_event_close, INFINITE);        printf("close ok.\n");        if (g_event_close){            ::CloseHandle(g_event_close);            g_event_close = NULL;        }    }}void close_all_socket(){    //copy    fd_set close_select_temp = g_tcp_select_client;    for (u_int i = 0, count = 0; i < close_select_temp.fd_count; ++i){        SOCKET tcp_client_socket = close_select_temp.fd_array[i];        close_socket(tcp_client_socket);    }}SOCKET get_tcp_socket(char* addr, int port){    SOCKET tcp_client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    if (tcp_client_socket == INVALID_SOCKET){        perror("socket error !");        return INVALID_SOCKET;    }    SOCKADDR_IN remote_config;    remote_config.sin_port = htons(port);    remote_config.sin_family = AF_INET;    remote_config.sin_addr.S_un.S_addr = inet_addr(addr);    if (connect(tcp_client_socket, (sockaddr *)&remote_config, sizeof(remote_config)) == SOCKET_ERROR){        perror("connect error !");        closesocket(tcp_client_socket);        return INVALID_SOCKET;    }    //add into select set    FD_SET(tcp_client_socket, &g_tcp_select_client);    if (g_tcp_select_client.fd_count <= 1){        g_thread[0] = ::CreateThread(NULL, 0, recv_server_data_forever_thread, NULL, NULL, NULL);        g_thread[1] = ::CreateThread(NULL, 0, send_heart_beat_thread, NULL, NULL, NULL);    }    return tcp_client_socket;}void init_select(){    WSADATA wsa;    if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0){        perror("WSASartup error !");    }}void destroy_select(){    close_all_socket();    //wait recv & heartbeat thread    ::WaitForMultipleObjects(2, g_thread, TRUE, INFINITE);    ::WSACleanup();}int main(void){    SOCKET tcp_socket[10] = {NULL};    init_select();    tcp_socket[0] = get_tcp_socket("127.0.0.1", 8086);    tcp_socket[1] = get_tcp_socket("127.0.0.1", 8086);    Sleep(1000);    tcp_socket[2] = get_tcp_socket("127.0.0.1", 8086);    tcp_socket[3] = get_tcp_socket("127.0.0.1", 8086);    close_socket(tcp_socket[0]);    tcp_socket[4] = get_tcp_socket("127.0.0.1", 8086);    tcp_socket[5] = get_tcp_socket("127.0.0.1", 8086);    Sleep(2000);    tcp_socket[6] = get_tcp_socket("127.0.0.1", 8086);    close_socket(tcp_socket[4]);    tcp_socket[7] = get_tcp_socket("127.0.0.1", 8086);    Sleep(4000);    destroy_select();    return 0;}

读完本文意犹未尽?可以继续阅读《Windows Sockets网络编程(2)TCP Stream拆分、拼接》你将知晓,网络流式数据包是如何分割的。

阅读全文
1 0
原创粉丝点击