12.1 基于 I/O 复用的服务器端
接下来讨论并发服务器端实现方法的延伸. 如果有读者已经跳过第10章和第11章, 那就只需把本章内容当作并发服务器实现的第一种方法即可. 将要讨论的内容中包括一部分与多进程服务器端的比较, 跳过第10章和第11章的读者简单浏览即可.
多进程服务器端的缺点和解决方法
为了构建并发服务器, 只要有客户端连接请求就会创建新进程. 这的确是实际操作中采用的一种方案, 但并非十全十美, 因为创建进程是需要付出极大的代价. 这需要大量的运算和内存空间, 由于每个进程都具有独立的内存空间, 所以相互的数据交换也要求采用相对复杂的方法(IPC属于相对复杂的通信方法). 各位应该也感到需要IPC时会提高编程难度.
当然能! 本节讲解的I/O复用就是这种技术. 大家听到有这种方法是否感到一阵兴奋? 但亲不要过于依赖该模型! 该方案并不适用于所有情况, 应当根据目标服务器端的特点采用不同实现方法. 下面先理解 “复用”(Muliplexing)的意义.
理解复用
“复用” 在电子及通信工程领域很常见, 向这些领域的专家询问其概念时, 他们会亲切的进行如下说明:
能理解吗? 不能的话就再看看"复用"的含义.
上述两种说法内容完全一致, 只是描述方式有所区别, 下面我根据自己的理解进行解析. 图12-1 中给出的是纸杯电话.
图12-1是远距离的3人通话的3方对话纸杯电话系统. 为使3人同时对话, 需准备图中所示系统. 另外, 为了完成3人对话, 说话时需要同时对着两个纸杯, 接听时也需要耳朵同时对准两个纸杯. 此时引入复用技术会使通话更加方便, 图12-2所示.
我们上小学时做过类似的系统(把线捆在中间并绷直). 构建这种系统就无需同时使用两个杯子, 可以说小学就学过"复用"的概念. 接下来讨论复用技术的优点.
即使减少了连接和纸杯的量仍能进行3人通话, 当然也有人在考虑如下这种情况:
实际上, 因为是在进行对话, 所以很少发生同时说话的情况. 也就是说, 上述系统采用的是"时(time)分复用技术". 而且, 因为说话人声高(频率)不同, 即使同时说话也能进行一定程度的区分(当然杂音也随之增多). 因此, 也可以说系统同时采用了 “频(frequency)分复用技术”. 这样大家就能理解之前讲的"复用"的定义了.
复用技术在服务器端的应用
纸杯电话系统引入复用技术后, 可以减少纸杯数和连接长度. 同时, 服务器端引入复用技术可以减少所需纸杯数和连线长度. 同样, 服务器端引入复用技术可以减少所需进程数. 为便于比较, 先给出第10章的多进程服务器端模型, 如图12-3所示.
图12-3的模型中引入复用技术, 可以减少进程数. 重要的是, 无论连接多少客户端, 提供服务的进程只有一个.
以上就是 I/O 复用服务器模型的讲解, 下面考虑通过1个进程向多个客户端提供服务的方法.
12.2 理解 select 函数并实现服务器端
运用 select 函数是最具代表性的实现复用服务器端方法. Windows 平台下也有同名函数提供相同功能, 因此具有良好的移植性.
select 函数的功能和调用顺序
使用 select 函数时可以将多个文件描述符集中到统一监视, 项目如下.
select 函数的使用方法与一般函数区别较大, 更准确地说, 它很难使用. 但为了实现I/O复用服务器端, 我们应掌握select 函数, 并运用到套接字编程中. 认为 “select 函数时I/O复用的全部内容” 也不为过. 接下来介绍 select 函数的调用方法和顺序, 如图12-5所示.
图12-5 给出从调用 select 函数到获取结果所经过程. 可以看到, 调用 select 函数前需要一些准备工作, 调用后还需查看结果. 接下来按照上述顺序逐一讲解.
设置文件描述符
利用select 函数可同时监视多个文件描述符. 当然, 监视文件描述符可以视为监视套接字. 此时首先需要将要监视的文件描述符集中到一起. 集中时也要按照监视项(接收, 传输, 异常)进行区分, 即按照上述3种监视分成3类.
使用fd_set 数组变量执行此项操作, 如图12-6所示. 该数组是存在有0和1的位数组.
图12-6中最左端的位表示文件描述符0(所在位置). 如果该位设置为1, 则表示该文件描述符是监视对象. 那么图中哪些文件描述符是监视对象呢? 很明显, 是文件描述符1和3.
当然不是! 针对fd_set 变量的操作是以位为单位进行的, 这也意味着直接操作该变量会比较繁琐. 难道要求各位自己完成吗? 实际上, 在fd_set 变量中注册或更改值的操作都由下列宏完成.
上述函数中, FD_ISSET 用于验证 select 函数的调用结果. 通过图12-7解析这些函数的功能, 简洁易懂, 无需
设置检查(监视)范围及超时
下面讲解图12-5中步骤一的剩余内容, 在此之前介绍 select 函数
如上所述, select 函数用来验证3种监视项变化情况. 根据监视项声明3个 fd_set 型变量, 分别向其注册文件描述符信息, 并把变量的地址值传递到上述函数的第二到第四个参数. 但在此之前(调用select 函数前)需要决定下面2件事.
第一, 文件描述符的监控范围与 select 函数的第一个参数有关. 实际上, select 函数要求通过第一个参数传递监视对象文件描述的数量. 因此, 需要得到注册在 fd_set 变量中的文件描述符数. 但每次新建文件描述符时, 其值都会增1, 故只需将最大的文件描述符值增加1再传递到 select 函数即可, 加1是因为文件描述符的值从0开始.
第二, select 函数的超时时间与 select 函数有关, 其中timeval 结构体定义如下.
本来 select 函数只有在监视的文件描述符发生变化时才返回. 如果未发生变化, 就会进入阻塞状态. 指定超时时间就是为了防止这种情况的发生. 通过声明上述结构体变量, 将秒数填入 tv_sec 成员, 将毫秒数填入 tv_usec 成员, 然后将结构体的地址传递到 select 函数的最后一个参数. 此时, 即使文件描述符中未发生变化, 只要过了指定时间, 也可以从函数中返回. 不过这种情况下, select 函数返回0, 因此, 可以通过了解返回原因. 如果不想设置超时, 则传递NULL 参数.
调用 select 函数后查看结果
虽未给出具体事例, 但图12-5中的步骤一 “select 函数调用前的所有准备工作”, 已讲解完毕, 同时也介绍了 select函数. 而函数调用后查看结果也同样重要. 我们已讨论过 select 函数的返回值, 如果返回大于0的整数, 说明相应的数量的文件描述符发送变化.
select 函数返回正整数时, 怎样获知哪些文件描述符发生变化? 向select 函数的第二到第四个参数的fd_set 变量中将产生如图12-8所示变化, 获知过程并不难.
由图12-8可知, select 函数调用完成后, 向其传递的fd_set 变量中将发生变化. 原来为1的所有位均变为0, 但发生变化的文件描述符对应位除外. 因此, 可以认为值仍为1的位置上的文件描述符发生变化.
select 函数调用实例
下面通过实例把 select 函数所有知识点进行整合, 希望各位通过如下示例完全理解之前的内容.
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads); /* 0 is standard input(console) */
/*
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
*/
while (1)
{
temps = reads;
timeout.tv_sec = 5;
result = select(1, &temps, 0, 0, &timeout);
if (result == -1)
{
puts("select() error");
break;
}
else if (result == 0)
{
puts("Time-out!");
}
else
{
if (FD_ISSET(0, &temps))
{
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
运行结果:
运行后若无任何输入, 经过5秒发生超时. 若通过键盘输入字符串, 则可看到相同的字符串输出.
实现 I/O 复用服务器端
下面通过select 函数实现 I/O 复用服务器端. 之前已给出关于 select 函数的所有说明, 各位只需通过实例掌握利用 select 函数实现服务器端的方法. 下列示例是基于 I/O 复用的回声服务器端.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1)
{
error_handling("listen() error");
}
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while (1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if ((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
{
break;
}
if (fd_num == 0)
{
continue;
}
for (i=0; i<fd_max+1; i++)
{
if (FD_ISSET(i, &cpy_reads))
{
if (i == serv_sock) /* connection request! */
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_sock, &adr_sz);
FD_SET(clnt_sock, &reads);
if (fd_max < clnt_sock)
{
fd_max = clnt_sock;
}
printf("connected clientt: %d \n", clnt_sock);
}
else /* read message! */
{
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0)
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len); /* echo */
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
服务器端:
客户端:
为了验证运行结果, 使用了第四章介绍的echo_client.c, 其实上述回声服务器端也可以与其他回声客户端配合运行.
12.3 基于 Windows 的实现
在 Window 平台使用 select 函数时需要注意一些细节, 本节主要补充这部分内容.
在 Windows 平台调用 select 函数
Windows 同样提供 select 函数, 而且所有参数与Linux 的select 函数完全相同. 只不过 Windows 平台 select 函数的第一个参数是为了保持与(包括Linux的)UNIX系列操作系统的兼容性而添加的, 并没有特殊意义.
返回值, 参数的顺序机含义与之前的 Linux 中的select 函数完全相同, 故省略. 下面给出timeval 结构体定义.
可以看到, 基本结构体与之前Linux 中的定义相同, 但 Windows 中使用的是 typede 声明. 接下来观察 fd_set 结构体. Windows 中实现是需要注意的地方再与此, 可以看到, Windows 的 fd_set 并非像 Linux 中那样采用了位数组.
Windows 的fd_set 由成员 fd_count 和 fd_array 构成, fd_count 用于套接字句柄数, fd_array 用于保存套接字句柄. 只要略加思考就能理解这样声明的原因. Linux 的文件描述符从0开始递增, 因此可以找到当前文件描述符数量和最后生成的文件描述符之间的关系. 但Windows 的套接字句柄并非从0 开始, 而且句柄的整数值之间并无规律可循, 因此需要直接保存句柄的数组和记录句柄数的变量. 幸好处理 fd_set 结构体的 FD_XXX型的4个宏的名称, 功能及使用方法与Linux 完全相同(故省略), 这也许是微软为了保证兼容性所做的考量.
基于 Windows 实现I/O复用服务器端
下面将示例echo_selectserv.c 改为在 Windows 平台运行. 如果各位掌握之前的内容, 那理解起来并不难, 故省略源代码的讲解.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
void ErrorHandling(const char* message);
void ShowSocketBufSize(SOCKET sock);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSock;
int sndBuf, rcvBuf, state;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
hSock = socket(PF_INET, SOCK_STREAM, 0);
ShowSocketBufSize(hSock);
sndBuf = 1024 * 3, rcvBuf = 1024 * 3;
state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, sizeof(sndBuf));
if (state == SOCKET_ERROR)
{
ErrorHandling("setsockopt() error");
}
state = setsockopt(hSock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, sizeof(rcvBuf));
if (state == SOCKET_ERROR)
{
ErrorHandling("setsockopt() error");
}
ShowSocketBufSize(hSock);
closesocket(hSock);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void ShowSocketBufSize(SOCKET sock)
{
int sndBuf, rcvBuf, state, len;
len = sizeof(sndBuf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, &len);
if (state == SOCKET_ERROR)
{
ErrorHandling("getsockopt() error");
}
len = sizeof(rcvBuf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, &len);
if (state == SOCKET_ERROR)
{
ErrorHandling("getsockopt() error");
}
printf("Input buffer size: %d \n", rcvBuf);
printf("Output buffer size: %d \n", sndBuf);
}
运行结果:
上述代码可以结合第4章的echo_client_win.c 运行(当然也可以很好地与其他客户端结合运行). 最后, 我想再次前调, 本章的I/O 复用技术在理论和实际都非常重要.
结语:
你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/
时间: 2020-06-04