技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 890|回复: 2
收起左侧

【C】socket编程之select()函数的例子,以及,一个超轻量级http服务器的实例

[复制链接]

993

主题

2190

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
197
威望
261 点
宅币
16161 个
贡献
31411 次
宅之契约
0 份
在线时间
1543 小时
注册时间
2014-1-26
发表于 2017-3-23 08:16:50 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
很多人在网上查select()函数怎么用的时候都感觉十分头大,一个是例子少之又少,另一个是微软的MSDN也搞得让人看不懂,这玩意儿要传3个看不懂的玩意儿进去!而且,最重要的,这个函数是干嘛的!许多人都搞不明白。

那么,现在你正在阅读的这篇帖子,将会让你知道,这个函数是如何使用和工作的。
转载请保留原文链接:https://www.0xaa55.com/thread-2079-1-1.html

首先我们先从网络编程开始说起。假设我写了个服务器程序,我要同时处理多个用户与服务器之间的会话,但研究过TCPIP协议的socket编程的人都知道,recv()accept()这俩函数,在没有收到数据或者用户的连接请求的时候,是不会返回的,它会一直卡在那,直到有了新的活动,才会返回数据给你。于是一个比较简单粗暴的解决方式就是直接使用多线程,我主线程等待新的连接,一旦建立连接,我就搞个线程专门处理这个连接的会话,非常直观。但这个方法有个弊端:通常每个线程都会为了建立栈而消耗1MB左右的RAM,而如果服务器有4GB内存,它就只能同时处理大约1000个左右的连接,因为用户程序在4GB内存的机器上能用上的RAM也就1.2G左右,况且这程序本身还有其它部分要占用RAM。

那么一个靠谱的高并发解决方案是啥呢,这里我列举三种:
  • 使用非阻塞IO套接字。
  • 使用select。
  • 学腾讯爸爸的处理办法:先自己基于UDP协议写个类似TCP那样的可靠连接,再在这个的基础上自己把不同会话的包裹区分开。这样还是需要多线程,只不过至少不是1线程1会话的那种模式了,而是一个线程负责所有的网络包裹的处理,另一个线程则处理全部的会话。


第一种方法是用非阻塞IO套接字我就不多说了,原理很简单,非阻塞的socket在你调用accept和recv等函数的时候,无论有没有数据或者有没有人发来连接请求,它都会立即返回。于是你要做的就是不停地轮询所有的socket,有数据就处理,就这样。



第二种方法,它之所以能实现“靠谱的高并发解决方案”,是因为它其实是用来查询socket的状态的。它的原型是这样的:
int select(
  _In_    int nfds,
  _Inout_ fd_set *readfds,
  _Inout_ fd_set *writefds,
  _Inout_ fd_set *exceptfds,
  _In_    const struct timeval *timeout
);

nfds这个参数在Windows上并没有什么卵用,其它平台上则依赖它来判断其中最大的那个数组里面你总共给了多少个socket进去,它应该填的值是最大的那个数组的元素个数+1。但Windows上你可以直接填0.
然后我们需要认识一下fd_set是一个什么样的结构体:
typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
} fd_set;

fd_array是socket的数组,其中的“fd”是“file descriptor”的意思,Unix上的socket是个文件描述符,Windows的不是,所以Windows用“SOCKET”变量类型而Unix用int。
顺带一提,不少人利用select来实现类似Sleep()usleep()之类的功能,作为一种跨平台的多线程资源管理的函数,用法是给3个空的数组,然后设置timeout,让timeout作为它的超时处理。
fd_count是socket数组中的socket的个数,事实上FD_SETSIZE这个宏是可以自定义的,你把它定义为1024的话,你的fd_set结构体的fd_array的预定容量就是1024。其实你可以不用理它,自己malloc一个fd_set也是可以的,长度自定义。不过要注意的是glibc里面的fd_set的大小是有限的,它内定了FD_SETSIZE大小为1024,所以你给2048个socket它也只检测1024个。Linux文档说建议使用poll()而非select(),去检测更多的套接字的状态。

所以select()是用来判断你给的3个数组里面,哪些socket有数据可读,哪些socket可写,哪些socket出现了异常。
用法就是,自己先制作这3个数组,把需要查询状态的套接字都弄进去,然后调用select();之后我们再看这3个数组里面还剩下哪些套接字。剩下的,就是要么可读要么可写要么有异常的(取决于它们在哪个数组里)
[C] 纯文本查看 复制代码
// 这是我们的套接字
SOCKET socks[50];

// 这是我们用来调用select进行查询的那几个数组
fd_set readfds; // 可读的套接字
fd_set writefds; // 可写的套接字
fd_set exceptfds; // 有问题的套接字


// 此处开始,判断我们的套接字的状态。

size_t i;

struct timeval check_time; // 查询时间,select会在没有查询结果的时候一直阻塞,直到超时
check_time.tv_sec = 0; // 我们不给它阻塞
check_time.tv_usec = 0;


// 初始化这些数组
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);

for(i = 0; i < 50; i++)
{
	// 保证不超出数组下标的状态下,我们把自己的套接字丢到这个数组里
	if(readfds.fd_count < FD_SETSIZE)
		readfds.fd_array[readfds.fd_count++] = socks[i];
	if(writefds.fd_count < FD_SETSIZE)
		writefds.fd_array[writefds.fd_count++] = socks[i];
	if(exceptfds.fd_count < FD_SETSIZE)
		exceptfds.fd_array[exceptfds.fd_count++] = socks[i];
}

// 设置好数组了,接下来调用select
if(select(51 /*我们放了50个socket到每个数组里,所以这里填50 + 1*/, &readfds, &writefds, &exceptfds, &check_time) < 0)
{
    fprintf(stderr, "select() 失败: %d\n", WSAGetLastError());
}

// 然后就是判断select返回的结果。
for(i = 0; i < 50; i++)
{
	if(FD_ISSET(socks[i], &readfds))
	{
		printf("第 %d 个套接字已经收到了数据或者连接请求。\n", i);
		// 然后你就可以用recv接收socks[i]的数据了,如果它是个被你调用过listen的套接字的话,你就可以accept了。
	}
	if(FD_ISSET(socks[i], &writefds))
	{
		printf("第 %d 个套接字现在是可写的状态。\n", i);
		// 然后你就可以用send通过socks[i]发送数据了。
	}
	if(FD_ISSET(socks[i], &exceptfds))
	{
		printf("第 %d 个套接字现在出现了异常。\n", i);
		// 然后就closesocket吧。
	}
}
看起来似乎挺麻烦的,不过我们其实可以直接用select来一个个查询套接字的状态,虽然这很慢。

方法就是每个数组的fd_count都设为1,然后把fd_array[0]设为你要查询的套接字。最后根据fd_count的值来判断你这套接字的状态咋样。

示例代码:
[C] 纯文本查看 复制代码
// 这是待查询状态的套接字
SOCKET sock;

// 这是我们用来调用select进行查询的那几个数组
fd_set readfds; // 可读的套接字
fd_set writefds; // 可写的套接字
fd_set exceptfds; // 有问题的套接字

struct timeval tv = {0}; // 我们不给它阻塞

// 初始化这些数组
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);

// 把我们的套接字丢进去
readfds.fd_count = 1;
readfds.fd_array[0] = sock;
writefds.fd_count = 1;
writefds.fd_array[0] = sock;
exceptfds.fd_count = 1;
exceptfds.fd_array[0] = sock;

// 设置好数组了,接下来调用select
if(select(2 /*因为只有1个套接字,所以填2*/, &readfds, &writefds, &exceptfds, &check_time) < 0)
{
    fprintf(stderr, "select() 失败: %d\n", WSAGetLastError());
}

if(readfds.fd_count)
{
	printf("这个套接字有数据要接收,或者有连接请求要接受。\n");
}

if(writefds.fd_count)
{
	printf("这个套接字可以发送数据。\n");
}

if(exceptfds.fd_count)
{
	printf("这个套接字出现了一些问题。\n");
}
你可以一遍遍地用这种提交只有一个元素的数组的方式来查询一个套接字的状态,但这样很慢,我们应该一批一批的处理。

这样你大概能明白select是怎么用的了吧?不过我觉得我还是写一个更加真实的例子比较好。这里,我就造了个轮子,它是个http服务器,有一个内置的网页。这玩意儿其实就是在用C写网页!
源码下载(不包括工程):
http_server.c (27.94 KB, 下载次数: 3)

本帖被以下淘专辑推荐:

34

主题

132

帖子

6832

积分

用户组: 管理员

UID
77
精华
11
威望
112 点
宅币
6292 个
贡献
129 次
宅之契约
0 份
在线时间
87 小时
注册时间
2014-2-22
发表于 2017-3-23 08:57:07 | 显示全部楼层
那么一个靠谱的高并发解决方案是啥呢,这里我列举三种:
使用非阻塞IO套接字。
使用select。
学腾讯爸爸的处理办法:先自己基于UDP协议写个类似TCP那样的可靠连接,再在这个的基础上自己把不同会话的包裹区分开。这样还是需要多线程,只不过至少不是1线程1会话的那种模式了,而是一个线程负责所有的网络包裹的处理,另一个线程则处理全部的会话。

我也说我了解的三点:
1、这个特别好,可惜一些限制环境下,使用不方便。
2、LIBEVENT也用的是SELECT。
3、据说是因为当年网络质量差,承受不起大量的TCP长连接。

0

主题

3

帖子

5

积分

用户组: 初·技术宅

UID
2349
精华
0
威望
0 点
宅币
2 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-3-23
发表于 2017-3-23 14:01:56 | 显示全部楼层
很好,很喜欢

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2018-7-18 15:05 , Processed in 0.096114 second(s), 30 queries , Gzip On, Memcache On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表