【C】socket编程之select()函数的例子,以及,一个超轻量级http服务器的实例
很多人在网上查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_intfd_count;
SOCKET fd_array;
} 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个数组里面还剩下哪些套接字。剩下的,就是要么可读要么可写要么有异常的(取决于它们在哪个数组里)// 这是我们的套接字
SOCKET socks;
// 这是我们用来调用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 = socks;
if(writefds.fd_count < FD_SETSIZE)
writefds.fd_array = socks;
if(exceptfds.fd_count < FD_SETSIZE)
exceptfds.fd_array = socks;
}
// 设置好数组了,接下来调用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, &readfds))
{
printf("第 %d 个套接字已经收到了数据或者连接请求。\n", i);
// 然后你就可以用recv接收socks的数据了,如果它是个被你调用过listen的套接字的话,你就可以accept了。
}
if(FD_ISSET(socks, &writefds))
{
printf("第 %d 个套接字现在是可写的状态。\n", i);
// 然后你就可以用send通过socks发送数据了。
}
if(FD_ISSET(socks, &exceptfds))
{
printf("第 %d 个套接字现在出现了异常。\n", i);
// 然后就closesocket吧。
}
}看起来似乎挺麻烦的,不过我们其实可以直接用select来一个个查询套接字的状态,虽然这很慢。
方法就是每个数组的fd_count都设为1,然后把fd_array设为你要查询的套接字。最后根据fd_count的值来判断你这套接字的状态咋样。
示例代码:// 这是待查询状态的套接字
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 = sock;
writefds.fd_count = 1;
writefds.fd_array = sock;
exceptfds.fd_count = 1;
exceptfds.fd_array = 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写网页!
源码下载(不包括工程):
暂时没丢Gayhub,这玩意儿我还没有对它做跨平台处理。
好,接下来放VS2012工程。如果你自己会配置工程的话直接下载上面3个源码就好,不需要再下载这个工程了。
那么接下来我们来看一下我写的这个实例里面到底都有啥。
首先这是个控制台程序,它打开的时候监听80端口,这80端口就是我们的http网站常用的端口了。直接用浏览器,输入自身IP或者localhost,就能看到页面了。
这上面的英文都是粗话,翻译过来就是“你好世界!欢迎来到我这日狗的服务器!请先登录。默认用户是admin,密码是2b2b2b2b。你不能修改密码,因为这玩意儿根本没有数据库。”
如果你密码输错了,你登录进去的话,会看到它说“我不是都告诉过你密码了吗!”
然后我们也能在控制台看到服务器与客户端之间的交互。
这里我对HTTP协议的解析也只是做了简单的处理,具体看代码。代码写得比较粗糙,就不要吐槽了,关键是你可以看到我用select进行多个会话的非阻塞管理,实现了高并发IO。
最后……
C语言大法好!看我如何用C写网页://=============================================================================
//作者:0xAA55
//网站:http://www.0xaa55.com
//-----------------------------------------------------------------------------
#include<stdio.h>
#include<errno.h>
#include<stdlib.h>
#include<stdint.h>
#include<signal.h>
#include<sys/stat.h>
#include<time.h>
#include"http_server.h"
#include<WinSock2.h>
// 服务器控制标识,为1时服务器需要退出循环
static volatile int g_Server_Go_Die = 0;
static void _cdecl _sigproc(int sig)
{
switch(sig)
{
case SIGINT:
case SIGTERM:
g_Server_Go_Die = 1;
break;
}
}
//=============================================================================
//函数:_the_Server_Side
//描述:服务端程序
//-----------------------------------------------------------------------------
void _cdecl _the_Server_Side(http_server_p p)
{
while(!g_Server_Go_Die)
{
hs_run_inst(p);
Sleep(1);
}
g_Server_Go_Die = 0;
}
//=============================================================================
//函数:_load_file
//描述:加载一整个文件到内存里
//-----------------------------------------------------------------------------
static void*_load_file(const char*psz)
{
FILE *fp = fopen(psz, "rb");
char *buf = NULL;
long lf;
if(!fp)
return NULL;
fseek(fp, 0, SEEK_END);
lf = ftell(fp); // 只最多给用户传输2G的文件
rewind(fp);
buf = malloc(lf);
if(!buf)
{
fclose(fp);
return NULL;
}
fread(buf, 1, lf, fp); // 也不检查读取是否成功。
fclose(fp);
return buf;
}
//=============================================================================
//函数:on_request
//描述:浏览器请求页面时调用的函数
//-----------------------------------------------------------------------------
static void on_request(session_p ps, void*userdata)
{
if(!ps->req.url && !ps->req.proto)
return;
// 假装自己是“全世界最好的编程语言”,内部钦定index.php
if(!strcmp(ps->req.url,"/index.php") || !strcmp(ps->req.url,"/"))
{
if(!ps->responsed)
{
size_t i;
const char*ch;
int logged_in = 0;
int user_id = 0;
ch = hs_session_get_cookie_field(ps, "username");
if(ch && !strcmp(ch,"admin"))
{
ch = hs_session_get_cookie_field(ps, "password");
if(ch && !strcmp(ch,"2b2b2b2b")) // 直接用cookie明文存密码
{
logged_in = 1; // 登入状态
user_id = 1; // 这个用户是admin
}
}
if(logged_in)
{
ch = hs_session_get_POST_field(ps, "logout");
if(ch && !strcmp(ch, "logout"))
{
logged_in = -2; // 登出
}
}
if(!logged_in)
{
ch = hs_session_get_POST_field(ps, "username");
if(ch)
{
if(strcmp(ch,"admin"))
{
// 没找到用户,用户不是admin
user_id = -1;
}
else
{
ch = hs_session_get_POST_field(ps, "password");
if(ch)
{
if(!strcmp(ch,"2b2b2b2b")) // post也是明文存密码
{
logged_in = 2; // 登入状态,显示欢迎
user_id = 1; // 这个用户是admin
}
else
{
logged_in = -1; // 密码输错。
}
}
else
{
logged_in = -1; // 密码输错。
}
}
}
}
hs_session_response(ps, 200, "OK");
hs_session_printf(ps, "Date: ");
hs_session_print_date_time(ps, time(NULL));
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Server: SimpleHTTP/1.0.0\r\n");
hs_session_printf(ps, "Content-Type: text/html\r\n");
hs_session_printf(ps, "Last-Modified: ");
hs_session_print_date_time(ps, 0 /* 咱的index.php是1970年1月1日的! */);
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Connection: close\r\n");
if(logged_in == 2 && user_id == 1) // 登入成功
{
hs_session_printf(ps, "Set-Cookie: username=admin; Expires=");
hs_session_print_date_time(ps, time(NULL) + 3600); // 1小时过期
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Set-Cookie: password=2b2b2b2b; Expires=");
hs_session_print_date_time(ps, time(NULL) + 3600); // 1小时过期
hs_session_printf(ps, "\r\n");
}
else if(logged_in == -2) // 登出
{
// 让两个cookie过期
hs_session_printf(ps, "Set-Cookie: username=admin; Expires=");
hs_session_print_date_time(ps, 0); // 设置过期
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Set-Cookie: password=2b2b2b2b; Expires=");
hs_session_print_date_time(ps, 0); // 设置过期
hs_session_printf(ps, "\r\n");
}
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "<html>\r\n");
hs_session_printf(ps, "<head>\r\n");
hs_session_printf(ps, "<title>Simple HTTP server written in C</title>\r\n");
hs_session_printf(ps, "</head>\r\n");
hs_session_printf(ps, "<body>\r\n");
hs_session_printf(ps, "Hello World!<br>\r\n");
if(ps->req.num_GET_fields)
{
hs_session_printf(ps, "GET fields:<br>\r\n");
for(i = 0; i < ps->req.num_GET_fields; i++)
{
hs_session_printf(ps, "%s: %s<br>\r\n",
ps->req.GET_fields.name, ps->req.GET_fields.value);
}
}
if(user_id == -1)
{
hs_session_printf(ps, "No such user!<br>\r\n");
hs_session_printf(ps, "Default user is admin, and the password is 2b2b2b2b.<br>\r\n");
}
if(logged_in > 0)
{
if(logged_in == 2)
hs_session_printf(ps, "Welcome, my lord!<br>\r\n");
else if(logged_in == 1)
hs_session_printf(ps, "Hello! Now you can do nothing here.<br>\r\n");
hs_session_printf(ps, "<form method = \"post\" action=\"index.php\" name=\"guit_form\" >\r\n");
hs_session_printf(ps, "<input type=\"hidden\" name=\"logout\" value=\"logout\"><br>\r\n");
hs_session_printf(ps, "<input type=\"submit\" value=\"Log out\"><br>\r\n");
hs_session_printf(ps, "</form>\r\n");
}
else if(logged_in == -1)
{
hs_session_printf(ps, "Wrong password!<br>\r\n");
hs_session_printf(ps, "Default user is admin, and the password is 2b2b2b2b.<br>\r\n");
hs_session_printf(ps, "I've told you the whole shit!<br>\r\n");
}
else if(logged_in == -2)
{
hs_session_printf(ps, "Log out successful!<br>\r\n");
logged_in = 0; // 显示登陆表单
}
if(!logged_in)
{
hs_session_printf(ps, "Welcome to my fucking server!<br>\r\n");
hs_session_printf(ps, "Please login first.<br>\r\n");
hs_session_printf(ps, "Default user is admin, and the password is 2b2b2b2b.<br>\r\n");
hs_session_printf(ps, "You can't change the password, because there's no database now.<br>\r\n");
hs_session_printf(ps, "<form method = \"post\" action=\"index.php\" name=\"login_form\" >\r\n");
hs_session_printf(ps, "Username:<input type=\"text\" name=\"username\"><br>\r\n");
hs_session_printf(ps, "Password:<input type=\"password\" name=\"password\"><br>\r\n");
hs_session_printf(ps, "<input type=\"submit\" value=\"Login\"><br>\r\n");
hs_session_printf(ps, "</form>\r\n");
}
hs_session_printf(ps, "</body>\r\n");
hs_session_printf(ps, "</html>\r\n");
hs_session_close(ps);
}
}
else
{
if(!ps->responsed)
{
struct _stat filestat;
int result;
// 注意用户可能会用“/../”来取咱这程序当前目录的上一级目录偷各种文件
result = _stat(ps->req.url, &filestat);
if(result) // 有错误
{
if(errno == ENOENT)
{
FILE *fp = fopen(ps->req.url, "w");
if(fp)
{
fprintf(fp, "asd");
fclose(fp);
}
hs_session_response(ps, 404, "Not found");
hs_session_printf(ps, "Date: ");
hs_session_print_date_time(ps, time(NULL));
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Server: SimpleHTTP/1.0.0\r\n");
hs_session_printf(ps, "Content-Type: text/html\r\n");
hs_session_printf(ps, "Connection: close\r\n");
hs_session_printf(ps, "\r\n<html>\r\n");
hs_session_printf(ps, "<head>\r\n");
hs_session_printf(ps, "<title>Error!</title>\r\n");
hs_session_printf(ps, "</head>\r\n");
hs_session_printf(ps, "<body>\r\n");
hs_session_printf(ps, "Request is: %s<br>\r\n", ps->req.url);
hs_session_printf(ps, "404 Not found!<br>\r\n");
hs_session_printf(ps, "</body>\r\n");
hs_session_printf(ps, "</html>\r\n");
hs_session_close(ps);
}
else
{
hs_session_response(ps, 500, "Server fault");
hs_session_printf(ps, "Date: ");
hs_session_print_date_time(ps, time(NULL));
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Server: SimpleHTTP/1.0.0\r\n");
hs_session_printf(ps, "Content-Type: text/html\r\n");
hs_session_printf(ps, "Connection: close\r\n");
hs_session_printf(ps, "\r\n<html>\r\n");
hs_session_printf(ps, "<head>\r\n");
hs_session_printf(ps, "<title>Error!</title>\r\n");
hs_session_printf(ps, "</head>\r\n");
hs_session_printf(ps, "<body>\r\n");
hs_session_printf(ps, "Request is: %s<br>\r\n", ps->req.url);
hs_session_printf(ps, "500 Server fault,<br>\r\n");
hs_session_printf(ps, "</body>\r\n");
hs_session_printf(ps, "</html>\r\n");
hs_session_close(ps);
}
}
else
{
void *cont;
cont = _load_file(ps->req.url);
if(cont)
{
hs_session_response(ps, 200, "OK");
hs_session_printf(ps, "Date: ");
hs_session_print_date_time(ps, time(NULL));
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Server: SimpleHTTP/1.0.0\r\n");
hs_session_printf(ps, "Content-Type: text/html\r\n");
hs_session_printf(ps, "Connection: close\r\n");
hs_session_printf(ps, "Last-Modified: ");
hs_session_print_date_time(ps, filestat.st_mtime);
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "%s", cont);
hs_session_close(ps);
free(cont);
}
else
{
hs_session_response(ps, 500, "Server fault");
hs_session_printf(ps, "Date: ");
hs_session_print_date_time(ps, time(NULL));
hs_session_printf(ps, "\r\n");
hs_session_printf(ps, "Server: SimpleHTTP/1.0.0\r\n");
hs_session_printf(ps, "Content-Type: text/html\r\n");
hs_session_printf(ps, "\r\n<html>\r\n");
hs_session_printf(ps, "<head>\r\n");
hs_session_printf(ps, "<title>Error!</title>\r\n");
hs_session_printf(ps, "</head>\r\n");
hs_session_printf(ps, "<body>\r\n");
hs_session_printf(ps, "Request is: %s<br>\r\n", ps->req.url);
hs_session_printf(ps, "500 Server fault,<br>\r\n");
hs_session_printf(ps, "</body>\r\n");
hs_session_printf(ps, "</html>\r\n");
hs_session_close(ps);
}
}
}
}
}
//=============================================================================
//函数:main
//描述:程序入口点
//-----------------------------------------------------------------------------
int main(int argc,char**argv)
{
http_server_p phs = NULL;
WSADATA wsaData;
WSAStartup(WINSOCK_VERSION, &wsaData);
signal(SIGINT, _sigproc);
signal(SIGTERM, _sigproc);
phs = hs_new_v4(INADDR_ANY, 80, on_request, NULL);
if(phs)
{
_the_Server_Side(phs);
hs_delete(phs);
}
WSACleanup();
return 0;
}其中的http_server.h和http_server.c在本帖的附件里有了,请自行下载。 那么一个靠谱的高并发解决方案是啥呢,这里我列举三种:
使用非阻塞IO套接字。
使用select。
学腾讯爸爸的处理办法:先自己基于UDP协议写个类似TCP那样的可靠连接,再在这个的基础上自己把不同会话的包裹区分开。这样还是需要多线程,只不过至少不是1线程1会话的那种模式了,而是一个线程负责所有的网络包裹的处理,另一个线程则处理全部的会话。
我也说我了解的三点:
1、这个特别好,可惜一些限制环境下,使用不方便。
2、LIBEVENT也用的是SELECT。
3、据说是因为当年网络质量差,承受不起大量的TCP长连接。 很好,很喜欢 谢谢分享!!!!!!
页:
[1]