何为复用
简单来说,复用就是在1个通信频道中传递多个数据的技术。
常见的复用方式有时分复用和频分复用。
时分复用:即在某一时间段内容,只允许传输一个数据。
频分复用:指的是在某一时间段可以传输多个“频率”不同的数据。
之前的回声服务器只能服务一个客户端,本章将使用IO复用技术实现一个服务端向多个客户端提供回声服务。
select函数
select函数是实现IO复用服务器的关键,使用select函数可将多个套接字集中到一起统一监视,监视项目如下:
①是否存在套接字接收数据?
②无需阻塞传输数据的套接字有哪些?
③哪些套接字发生了异常?
原型如下:
#include <winsock2.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* excepfds, const struct timeval* timeout);
nfds: 该参数是为了保持与Linux系统的同名函数兼容而添加的,在windows系统的select函数中无实际意义,使用时传0即可。
readfds:将所有关注“是否存在待读取数据”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
writefds:将所有关注“是否可传输无阻塞数据”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
excepfds:将所有关注“是否发生异常”的文件描述符(套接字)注册到fd_set型变量,并传递其地址值
timeout:调用select函数后,为防止陷入无限阻塞状态,传递超时(time-out)信息
返回值:发生错误时返回-1,超时返回0.因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符(套接字)
这里重点关注readfds、writefds、exceptfds三个参数,对应着三个监视项,类型为fd_set,下面将详细介绍该类型如何使用。
fd_set的使用
首先看一下fd_set的定义:
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
可以看到该类型有两个成员,一个是统一监视的套接字数组,一个是监视的套接字的数量。
注意:针对fd_set变量的操作都是以位为单位进行的,因此不能直接将套接字的值直接写的fd_set的fd_array成员中。需要借助相关设置接口完成
fd_set相关设置接口
提供操作fd_set的相关接口如下:
- FD_ZERO(fd_set* fdset) : 将fd_set变量的所有位初始化为0
- FD_SET(int fd, fd_set* fdset) : 在fdset指向的变量中注册套接字fd的信息
- FD_CLR(int fd, fd_set* fdset) : 在参数fdset指向的变量中清除套接字fd的信息
- FD_ISSET(int fd, fd_set* fdset) : 若参数fdset指向的变量中包含文件描述符fd的信息,则返回TRUE,不包含则返回FALSE
示例如下:
timeval结构体
select函数的最后一个参数为timeval结构体,定义如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
本来select函数只有监视的套接字发送相应事件时才返回,如果未发生变化,就会进入阻塞状态。而该参数就是给开发人员提供设置阻塞事件的机会。
通过声明上述结构体变量,将秒数填入tv_sec 成员, 将微秒填入tv_usec成员,然后将结构体地址传入select函数的最后一个参数。这样,当指定时间内监视的套接字没有变化,select函数也会返回,只不过这时的返回值为0,表示超时。
若不行设置超时,则设置该参数为NULL即可。
select返回结果
前面已经提到,调用失败返回-1, 超时返回0,若返回大于0的整数,则说明相应数量的套接字有发生对应监视项目的变化。
PS:上述的套接字发生变化是指监视的套接字发生了相应的监视事件。例如:通过select函数的第二个参数传递的套接字集合中存在需要读数据的套接字时,就意味着这些套接字发送了变化,然后select函数返回发生变化的套接字数量。
变化规则如下:
此次,可以知道select函数的使用步骤如下:
①设置套接字
②设置监视范围
③设置超时
④调用select函数
⑤查看调用结果
这里给出一个简单的select使用示例模板:
fd_set reads, temps;
FD_ZERO(&reads);
//0表示标准输入
SOCKET hSock = socket(PF_INET, SOCK_STREAM, 0);
FD_SET(hSock , &reads);
timeval timeout;
int nSelRet = -1;
char buf[BUF_SIZE];
int nRecvLen = 0;
while (true)
{
temps = reads; //因为每次调用select后,fd_set中所有位都会置0,因此为了下次调用能够正常监视,这里使用拷贝的fd_set来调用select
timeout.tv_sec = 5; //设置5s超时
timeout.tv_usec = 0;
nSelRet = select(0, &temps, nullptr, nullptr, &timeout); //使用拷贝fd_set调用select,第一个参数无意义传0
if (nSelRet == -1)
{
puts("select() error!");
break;
}
else if (nSelRet == 0)
{
puts("time out!");
}
else
{
for (int i = 0; i < temps.fd_count; i++)
{
if (FD_SET(temps.fd_array[i], &temps)
{
if (temps.fd_array[i] == hSock)
{
//hSock套接字有数据可读取
}
}
}
}
}
实现IO复位服务器
基于上面的介绍,可以使用select函数实现IO复用服务器。代码如下:
// echo_select_server.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "select.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
printf("argc error!\n");
return -1;
}
WSADATA wsaData;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
{
printf("WSAStartup error!");
return -1;
}
SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == srvSock)
{
printf("socket error!\n");
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("bind error!\n");
return -1;
}
if (SOCKET_ERROR == listen(srvSock, 5))
{
printf("listen error!\n");
return -1;
}
fd_set reads, temps;
FD_ZERO(&reads);
FD_SET(srvSock, &reads);
timeval timeout;
int nFDNum;
SOCKADDR_IN cltAddr;
memset(&cltAddr, 0, sizeof(cltAddr));
int nCltAddrLen = 0;
int nRecvLen = 0;
char Msg[BUF_SIZE];
while (true)
{
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if ((nFDNum = select(0, &temps, nullptr, nullptr, &timeout)) == SOCKET_ERROR)
{
printf("select error!\n");
break;
}
printf("nFDNum: %d \n", nFDNum);
if (nFDNum == 0)
{
printf("select time out!\n");
continue;
}
//temps只是一个拷贝集合,只有添加或关闭新的套接字时,需对原始reads集合操作,其余都可使用temps完成
for (int i = 0; i < temps.fd_count; i++)
{
//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i].
//可以
if (FD_ISSET(temps.fd_array[i], &temps))
{
if (temps.fd_array[i] == srvSock)
{
nCltAddrLen = sizeof(cltAddr);
SOCKET cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrLen);
if (INVALID_SOCKET == cltSock)
{
printf("accept error!\n");
continue;
}
FD_SET(cltSock, &reads);
printf("connected client: %d \n", cltSock);
}
else
{
//读取客户端发来信息
//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i]
//可用temps
nRecvLen = recv(temps.fd_array[i], Msg, BUF_SIZE, 0);
if (nRecvLen == 0)
{
//断开连接
FD_CLR(temps.fd_array[i], &reads);
closesocket(temps.fd_array[i]);
printf("closed client: %d \n", temps.fd_array[i]);
}
else
{
//回发
//这里为什么要用reads.fd_array[i],而不能用temps.fd_array[i]
//可用temps
Msg[nRecvLen] = 0;
printf("echo to client: %s\n", Msg);
send(temps.fd_array[i], Msg, nRecvLen, 0);
}
}
}
}
}
closesocket(srvSock);
WSACleanup();
fputs("任意键继续...", stdout);
getchar();
getchar();
return 0;
}
这里也在给出客户端代码:
// echo_client.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
printf("arg error!");
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup error!");
return -1;
}
SOCKET srvSock = socket(PF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == srvSock)
{
printf("socket error!");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
srvAddr.sin_port = htons(_ttoi(argv[2]));
if (SOCKET_ERROR == connect(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("connect error!");
closesocket(srvSock);
WSACleanup();
return -1;
}
char Msg[BUF_SIZE];
int strLen = 0;
int sendLen = 0;
while (true)
{
fputs("Input Msg(Q to quit): ", stdout);
fgets(Msg, BUF_SIZE, stdin);
if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n"))
{
break;
}
sendLen = 0;
sendLen += send(srvSock, Msg, strlen(Msg), 0);
strLen = 0;
while (strLen < sendLen)
{
int recvLen = recv(srvSock, &Msg[strLen], BUF_SIZE - 1, 0);
if (recvLen == -1)
{
closesocket(srvSock);
WSACleanup();
return -1;
}
strLen += recvLen;
}
Msg[strLen] = 0;
printf("Msg From Server: %s \n", Msg);
}
closesocket(srvSock);
WSACleanup();
fputs("任意键继续...", stdout);
getchar();
return 0;
}
总结
select函数是实现IO复用服务器的关键,因此需要熟练掌握。这里也总结了select函数的使用步骤及示例模板,后续也可参考在实际开发时使用。
步骤:
①设置套接字
②设置监视范围
③设置超时
④调用select函数
⑤查看调用结果