前言:前面我们讲了多进程的并发服务端,只要有客服端连接请求就会创建新进程,这虽然也是一种解决方案,但创建进程是需要付出极大代价的,这需要大量运算和内存空间,而且每个进程间具有独立的内存空间,所以相互间的数据交换也相对复杂(管道)。
本章将讨论并发服务器的第二种实现方法——基于I/O复用的服务器端构建。
什么是I/O复用?通俗点讲,其实就是一个事件监听,只是这个监听的事件一般是I/O操作里的读(read)与写(write),只要发生了监听的事件它就会响应。注意与一般服务器的区别,一般服务器是连接请求先进入请求队列里,然后,服务端套接字一个个有序去受理。而I/O复用服务器是事件监听,只要对应监听事件发生就会响应,是属于并发服务器的一种。
I/O复用的使用
1,I/O复用的使用其实就是对select函数的使用,说select函数是I/O复用的全部内容也不为过。但这个函数与一般函数不同,它很难使用,我们先来看看它的调用顺序,分为3步:
步骤一:
步骤二:
步骤三:
2,再来讲讲select函数,首先,来看看它的定义:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
maxfd:监视范围
readset:监视对应文件描述符的接收事件,不监视这个事件则传0
writeset:监视对应文件描述符的传输事件,不监视则传0
exceptset:监视异常事件,不监视则传0
timeout:设置超时时间,不设置则传NULL
返回值:发生错误返回-1,超时返回0,发生参数2-4事件返回事件发生的对应文件描述符。
然后,再来看看fd_set数组,它是一个存有0和1的位数组,这个数组中的值0表不监视,1表监视。而每个值对应在数组中的位置与文件描述符一一对应。如Linux上的标准输入的文件描述符是0,则要监听这个文件描述符,就只需把fd_set数组的fd_set[0]设置为1即可。下面是对fd_set数组操作的一些宏:
FD_ZERO(fd_set *fdset):将fd_set变量的所有位初始化为0
FD_SET(int fd, fd_set *fdset):设置文件描述符fd为监听状态
FD_CLR(int fd, fd_set *fdset):取消文件描述符fd的监听状态
FD_ISSET(int fd, fd_set *fdset):文件描述符fd是否发生相应的监视事件,发生则返回真。
3,select函数调用示例:
//
// main.cpp
// hello_client
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, const char * argv[]) {
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
/*初始化状态设置*/
FD_ZERO(&reads); //将位数组reads初始化为0,即不监听任何文件描述符
FD_SET(0, &reads); //设置监听,监听文件描述符为0的对象(标准输入)
while (1)
{
//因为select调用后,timeout和初始化状态会发生变化,所以需要每次循环前再初始化一次
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
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;
}
用I/O复用的方式实现前面的回声程序
//
// main.cpp
// hello_server
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//
#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, const 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;
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;
//监听服务端套接字和与客服端连接的服务端套接字的read事件
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if(fd_num == 0)
continue;
if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端连接请求
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else//转发客服端数据
{
str_len = read(clnt_sock, buf, BUF_SIZE);
if (str_len == 0)//客服端发送的退出EOF
{
FD_CLR(clnt_sock, &reads);
close(clnt_sock);
printf("closed client: %d \n", clnt_sock);
}
else
{
write(clnt_sock, buf, str_len);
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
//
// main.cpp
// hello_client
//
// Created by app05 on 15-7-6.
// Copyright (c) 2015年 app05. All rights reserved.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, const char * argv[]) {
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("Connected ...............");
while (1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
str_len = write(sock, message, strlen(message));
/*这里需要循环读取,因为TCP没有数据边界,不循环读取可能出现一个字符串一次发送 但分多次读取而导致输出字符串不完整*/
recv_len = 0;
while (recv_len < str_len) {
recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}