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);
- 你是使用一个数组SOCKET array[5],将所有的SOCKET存放进去?
- 还是前进一步,使用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将执行以下操作:
- 将fd_set中的SOCKET重新排序:[456][456][789]….
- 将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拆分、拼接》你将知晓,网络流式数据包是如何分割的。
- Windows Sockets网络编程(1)TCP select & thread
- Windows Sockets网络编程(2)TCP select & thread
- Windows Sockets网络编程(0)TCP In Action
- Windows Sockets网络编程(0)TCP In Action
- Windows Sockets网络编程(2)TCP Stream拆分、拼接
- Windows Sockets 网络编程
- Windows sockets网络开发-基本TCP套接字编程
- Windows sockets 网络编程(1) — 综述
- Windows sockets 网络编程(1) — 综述 .
- Windows Sockets 网络编程(三)
- Windows Sockets 网络编程(三)
- Windows sockets 网络编程(一)
- Windows sockets网络开发-select模型
- Windows Sockets 网络编程 WINDOWS SOCKETS 1.1 程序设计
- Windows Sockets网络编程(3)WSAEventSelect模型开发
- Windows Sockets 网络编程——第三章 TCP/IP协议服务
- IOCP框架之Windows Sockets网络编程
- Windows网络编程之(二)Socket通信非阻塞模式Select(TCP和UDP)
- java学习路线参考
- Python探索记(14)——字符串、列表、元组、字典与运算符相关的操作
- Vuejs2.0 小应用
- 动态规划算法在不同阶楼梯中的走法
- Hibernate怎么修改ehcache.xml文件的存放位置
- Windows Sockets网络编程(1)TCP select & thread
- SSM框架中Mybatis向映射文件(XML)的<insert>元素传入多个数组参数的问题
- Linux-Flash驱动(2)-块设备驱动实例分析
- NSS管脚信号
- bzoj 3671 [Noi2014]随机数生成器
- nginx释放请求与tcp连接
- nginx基于用户认证
- java中super的作用及子类对象的创建过程
- 美团-字符串计数-Java