服务器模型选择
服务器模型选择
- 一、循环服务器
- 1.UDP循环服务器
- 2.TCP循环服务器
- 二、简单并发服务器
- 1.并发服务器的模型
- 2.UDP并发服务器
- 3.TCP并发服务器
- 三、TCP的高级并发服务器模型
- 1.单客户端单进程,统一accept()
- 2.单客户端单线程,统一accept()
- 3.单客户端单线程,各线程独自accept(),使用互斥锁
- 四、IO复用循环服务器
- 1.IO复用循环服务器模型介绍
- 2.IO复用循环服务器模型的例子
一、循环服务器
循环服务器指的是对于客户端的请求和连接,服务器在处理完毕一个后在处理另一个,即串行处理客户端的请求。
服务器又叫迭代服务器。循环服务器常用于UDP服务程序。
1.UDP循环服务器
UDP协议的循环服务器如下所示,在recv()函数和处理数据这两种业务之间轮询处理。
/*服务器端*/
//客户端发送请求,内容为TIME,服务器端判断客户端发送的字符串是否正确再进行响应
//若客户端的请求正确,服务器发送主机的本地时间,将一个字符串形式的时间反馈给客户端
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
int main(int argc, char *argv[])
{
int s; /*服务器套接字文件描述符*/
struct sockaddr_in local, to; /*本地地址*/
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
socklen_t len = sizeof(to);
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立UDP套接字*/
/*初始化地址*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s, (struct sockaddr*)&local, sizeof(local));
/*主处理过程*/
//每次仅仅处理一个客户端的数据请求
while(1)
{
memset(buff, 0, BUFFLEN); /*清零*/
n = recvfrom(s, buff, BUFFLEN,0,(struct sockaddr*)&to, &len);
/*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now));/*将时间复制入缓冲区*/
sendto(s, buff, strlen(buff),0, (struct sockaddr*)&to, len);
/*发送数据*/
}
}
close(s);
return 0;
}
/*客户端*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
int main(int argc, char *argv[])
{
int s; /*服务器套接字文件描述符*/
struct sockaddr_in server; /*本地地址*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0; /*接收字符串长度*/
socklen_t len = 0; /*地址长度*/
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立UDP套接字*/
/*初始化地址初始化*/
memset(&server, 0, sizeof(server)); /*清零*/
server.sin_family = AF_INET; /*AF_INET协议族*/
server.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
server.sin_port = htons(SERVER_PORT); /*服务器端口*/
memset(buff, 0, BUFFLEN); /*清零*/
strcpy(buff, "TIME"); /*复制发送字符串*/
/*发送数据到服务器*/
sendto(s, buff, strlen(buff), 0, (struct sockaddr*)&server,
sizeof(server));
memset(buff, 0, BUFFLEN); /*清零*/
/*接收数据*/
len = sizeof(server);
n = recvfrom(s, buff, BUFFLEN, 0, (struct sockaddr*)&server, &len);
/*打印消息*/
if(n >0){
printf("TIME:%s",buff);
}
close(s);
return 0;
}
2.TCP循环服务器
相当于UDP,TCP协议的循环服务器的主处理过程中多一个accept()的过程,服务器在此等待客户端的连接,由于accept()函数为阻塞函数,所以通常情况,服务器会在此处等待。对accept()函数的不同处理是区别各种服务器类型的一个重要依据。
①.TCP循环服务器介绍
-
socket()函数建立套接字文件描述符后,地址和套接字文件描述符使用bind()函数进行绑定,使用listen()函数设定侦听的队列长度,然后进入循环服务器的主处理过程。
-
当**客户端的连接connect()**到来时,accept()返回客户端的主要连接信息,此时服务器可以进行数据的接收,或者直接发送数据给客户端。
-
当需要接收客户端的数据的时候,需要对数据进行处理,然后判定下一 步服务器的处理方法和响应数据。
/*服务器端*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
int main(int argc, char *argv[])
{
int s_s, s_c; /*服务器套接字文件描述符*/
struct sockaddr_in local, from; /*本地地址*/
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
socklen_t len = sizeof(from);
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*侦听*/
/*主处理过程*/
while(1)
{
/*接收客户端连接*/
//先将缓冲区清零,使用这个缓冲区接收发送方的数据,如果发送的数据正确
//先获取本地时间,将时间复制到缓冲区后向请求方发送数据。最后关闭套接字描述符。
s_c = accept(s_s, (struct sockaddr*)&from, &len);
memset(buff, 0, BUFFLEN); /*清零*/
n = recv(s_c, buff, BUFFLEN,0); /*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now));/*将时间复制入缓冲区*/
send(s_c, buff, strlen(buff),0); /*发送数据*/
}
close(s_c);
}
close(s_s);
return 0;
}
/*客户端*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
int main(int argc, char *argv[])
{
int s; /*服务器套接字文件描述符*/
struct sockaddr_in server; /*本地地址*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0; /*接收字符串长度*/
/*建立TCP套接字*/
s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址*/
memset(&server, 0, sizeof(server)); /*清零*/
server.sin_family = AF_INET; /*AF_INET协议族*/
server.sin_addr.s_addr = htonl(INADDR_ANY);/*任意本地地址*/
server.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*连接服务器*/
connect(s, (struct sockaddr*)&server,sizeof(server));
memset(buff, 0, BUFFLEN); /*清零*/
strcpy(buff, "TIME"); /*复制发送字符串*/
/*发送数据*/
send(s, buff, strlen(buff), 0);
memset(buff, 0, BUFFLEN); /*清零*/
/*接收数据*/
n = recv(s, buff, BUFFLEN, 0);
/*打印消息*/
if(n >0){
printf("TIME:%s",buff);
}
close(s);
return 0;
}
二、简单并发服务器
循环服务器的串行处理不同,并发服务器对客户端的服务请求进行并发处理。例如多个客户端同时发送请求的话,服务器可以同时进行处理,而不像循环服务器那样处理完一个客户端 的请求后再处理 另一 个请 求 。
1.并发服务器的模型
在服务器端,主程序提前构建
多个子进程,当客户端的请求到来的时候,系统从进程池
中选取一个子进程处理客户端的连接,每个子进程处理一个客户端的请求,在全部子进程的处理能力得到满足之前,服务器的网络负载是基本不变的。
此类并发服务器的一个难点是如何确定进程池中子进程的数量。因为子进程的数量在客户端连接之前已经构造好了,不能进行扩展。利用可动态增加的子进程
与事先分配好子进程相结合的方法是常用服务器中的基本策略。
2.UDP并发服务器
并发服务器模型在UDP协议的实现模式如下:
①.UDP并发服务器介绍
-
建立套接字文件描述符后,对描述符和本地的地址端口进行绑定。
-
然后fork()多个子进程,客户端请求的处理在子进程中进行。
-
例如,当客户端0发送请求的时候,某个子进程对此请求进行处理,此时当客户端l的请求到来时,另一个子进程对此客户端进行处理。
②.UDP并发服务器的例子
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define PIDNUMB 2
static void handle_connect(int s)
{
struct sockaddr_in from; /*客户端地址*/
socklen_t len = sizeof(from);
int n = 0;
char buff[BUFFLEN];
time_t now; /*时间*/
/*主处理过程*/
while(1)
{
memset(buff, 0, BUFFLEN); /*清零*/
/*接收客户端连接*/
n = recvfrom(s, buff, BUFFLEN,0,(struct sockaddr*)&from, &len);
/*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now)); /*将时间复制入缓冲区*/
sendto(s, buff, strlen(buff),0, (struct sockaddr*)&from, len);/*发送数据*/
}
}
}
void sig_int(int num) /*SIGINT信号处理函数*/
{
exit(1);
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
signal(SIGINT, sig_int);
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_DGRAM, 0);
/*初始化地址*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
/*处理客户端连接*/
pid_t pid[PIDNUMB];
int i =0;
for(i=0;i<PIDNUMB;i++)
{
pid[i] = fork();
if(pid[i] == 0) /*子进程*/
{
handle_connect(s_s); //将客户端的请求放到一个进程中进行处理
}
}
while(1);
return 0;
}
3.TCP并发服务器
TCP模型相比较UDP,TCP协议的并发服务器的主处理多个accept()的过程,服务器在此等待客户端的连接,由于accept()函数为阻塞函数,所以通常情况下,服务器会在此等待。对accept()函数的不同处理是区别各种服务器类型的一个重要依据。
①.TCP并发服务器介绍
-
使用socket()函数建立套接字文件描述符,对地址和套接字文件描述符使用bind()函数进行绑定,使用listen()函数设定侦听的队列长度,然后进入并发服务器的主处理过程。
-
当**客户端的连接connect()**到来时,accept()返回客户端的主要连接信息,此时服务器可以进行数据的接收,或者直接发送数据给客户端。当需要接收客户端的数据时,需要对数据进行处理,然后判定下 一 步服务器的处理方法和响应数据。
②.TCP并发服务器例子
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define PIDNUMB 3 //进程
//实现客户端请求的实现,先接收客户端的请求数据,对数据进行分析判断是否合法
//获得当前的时间,将时间数据填入发送缓冲区发送给请求的客户端,然后等待下一个请求。
static void handle_connect(int s_s)
{
int s_c; /*客户端套接字文件描述符*/
struct sockaddr_in from; /*客户端地址*/
socklen_t len = sizeof(from);
/*主处理过程*/
while(1)
{
/*接收客户端连接*/
s_c = accept(s_s, (struct sockaddr*)&from, &len);
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
memset(buff, 0, BUFFLEN); /*清零*/
n = recv(s_c, buff, BUFFLEN,0); /*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now)); /*将时间复制入缓冲区*/
send(s_c, buff, strlen(buff),0); /*发送数据*/
}
/*关闭客户端*/
close(s_c);
}
}
void sig_int(int num)
{
exit(1);
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
signal(SIGINT,sig_int);
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址和端口*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*侦听*/
/*处理客户端连接*/
pid_t pid[PIDNUMB];
int i =0;
for(i=0;i<PIDNUMB;i++)
{
pid[i] = fork();
if(pid[i] == 0) /*子进程*/
{
handle_connect(s_s);
}
}
while(1);
close(s_s);
return 0;
}
三、TCP的高级并发服务器模型
1.单客户端单进程,统一accept()
前面介绍了并发服务器模型,在客户端到来之前就预分叉多个子进程用于处理客户端的请求。
-
下面的并发服务器模型并不预先分叉进程,而是由主进程统一处理客户端的连接,当客户端的连接请求到来时,才临时fork()进程,由子进程处理客户端的请求。种模型将客户端的连接请求和业务处理进行了分离,相比较来说条理更清晰。
-
如图所示主进程调用函数socket()建立套接字文件描述符,调用函数bind()绑定地址,调用listen()来设定侦听的队列长度。
-
然后主进程进入主处理过程,等待客户端连接的到来。
-
当客户端的连接请求到来时,服务器的accept()函数成功返回,此时服务器端进行进程分叉,父进程继续等待客户端的连接请求;
-
而子进程则处理客户端的业务请求,接收客户端的数据,分析数据并返回结果。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
//处理客户端请求
static void handle_request(int s_c)
{
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
memset(buff, 0, BUFFLEN); /*清零*/
n = recv(s_c, buff, BUFFLEN,0); /*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now)); /*将时间复制入缓冲区*/
send(s_c, buff, strlen(buff),0); /*发送数据*/
}
/*关闭客户端*/
close(s_c);
}
//处理客户端连接
static int handle_connect(int s_s)
{
int s_c; /*客户端套接字文件描述符*/
struct sockaddr_in from; /*客户端地址*/
socklen_t len = sizeof(from);
/*主处理过程*/
while(1)
{
/*接收客户端连接*/
s_c = accept(s_s, (struct sockaddr*)&from, &len);
if(s_c > 0) /*客户端成功连接*/
{
/*创建进程进行数据处理*/
if(fork() > 0){ /*父进程*/
close(s_c); /*关闭父进程的客户端连接套接字*/
}else{
handle_request(s_c); /*客户端的请求,处理连接请求*/
return(0);
}
}
}
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*侦听*/
/*处理客户端连接*/
handle_connect(s_s);
close(s_s);
return 0;
}
2.单客户端单线程,统一accept()
与进程比较,线程有很多优点,速度快、占用资源少、数据可以共享。如下图所示为单客户端线程,统一accept()的TCP并发服务器模型。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
static void handle_request(void *argv)
{
int s_c = *((int*)argv);
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
memset(buff, 0, BUFFLEN); /*清零*/
n = recv(s_c, buff, BUFFLEN,0); /*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now)); /*将时间复制入缓冲区*/
send(s_c, buff, strlen(buff),0); /*获得本机时间值发送客户端,发送数据*/
}
/*关闭客户端*/
close(s_c);
}
static void handle_connect(int s_s)
{
int s_c; /*客户端套接字文件描述符*/
struct sockaddr_in from; /*客户端地址*/
socklen_t len = sizeof(from);
pthread_t thread_do;
/*主处理过程*/
while(1)
{
/*接收客户端连接*/
s_c = accept(s_s, (struct sockaddr*)&from, &len);
if(s_c > 0) /*客户端成功连接*/
{
/*创建线程处理连接*/
pthread_create(&thread_do,
NULL,
(void*)handle_request,
&s_c);
}
}
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址和端口*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*侦听*/
/*处理客户端连接*/
handle_connect(s_s);
close(s_s);
return 0;
}
3.单客户端单线程,各线程独自accept(),使用互斥锁
下面是预先分配线程,在线程的accept()函数中,多个线程都可以使用此函数处理客户端的连接。为了防止冲突,使用了线程互斥锁
。在调用函数之前锁定,调用函数accept()之后,释放锁。实现框架如下所示:
-
首先调用
socket()
函数建立一个套接字文件描述符 ,调用bind()
函数将套接字文件描述符与本地地址进行绑定,调用listen()
函数设置侦听的队列长度。 -
然后使用
pthread_ create()
函数建立多个线程组成的线程池
,主程序等待线程结束。 -
各个线程中按照接收客户端连接accept()、接收数据 recvf()和处理数据发送响应 的数据进行。为了防止冲突,各个线程在调用accept()函数的时候,要调用一个线程互斥锁。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define CLIENTNUM 2
/*互斥量*/
pthread_mutex_t ALOCK = PTHREAD_MUTEX_INITIALIZER;
static void *handle_request(void *argv)
{
int s_s = *((int*)argv);
int s_c; /*客户端套接字文件描述符*/
struct sockaddr_in from; /*客户端地址*/
socklen_t len = sizeof(from);
for(;;)
{
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
//采用多个线程独立accept()。防止多个线程造成accept()竞争,使用一个互斥进行了accept()的锁定,每次仅允许一个。
pthread_mutex_lock(&ALOCK); /*进入互斥区*/
s_c = accept(s_s, (struct sockaddr*)&from, &len);
/*接收客户端的请求*/
pthread_mutex_unlock(&ALOCK); /*离开互斥区*/
memset(buff, 0, BUFFLEN); /*清零*/
n = recv(s_c, buff, BUFFLEN,0); /*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4)) /*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now)); /*将时间复制入缓冲区*/
send(s_c, buff, strlen(buff),0); /*发送数据*/
}
/*关闭客户端*/
close(s_c);
}
return NULL;
}
static void handle_connect(int s)
{
int s_s = s;
pthread_t thread_do[CLIENTNUM]; /*线程ID*/
int i = 0;
for(i=0;i<CLIENTNUM;i++) /*建立线程池*/
{
/*创建线程*/
pthread_create(&thread_do[i], /*线程ID*/
NULL, /*属性*/
handle_request, /*线程回调函数*/
(void*)&s_s); /*线程参数*/
}
/*等待线程结束*/
for(i=0;i<CLIENTNUM;i++)
pthread_join(thread_do[i], NULL);
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址和端口*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*侦听*/
/*处理客户端连接*/
handle_connect(s_s);
close(s_s); /*关闭套接字*/
return 0;
}
四、IO复用循环服务器
-
并发服务器有个重大的缺陷,需要建立多个并行的处理单元。客户端增加时,随着处理单元的增加,系统的负载会逐渐地转移到并行单元的现场切换。为了解决不必要的开支,需要降低并发单元的的数量,因此程序了IO复用循环服务器。
-
这种服务器模型,在系统开始的时候,建立多个不同工作类型的处理单元,例如处理连接的单元、处理业务的单元等。
-
在客户端连接到来的时候,将客户端的连接放到一个状态池中,对所有客户端的连接状态在 一 个处理单元中进行轮询处理。
-
与前面的并发服务器相比,客户端的增加不会造成系统并行处理单元的增加,而处理能力与 CPU 和内存的速度直接相关。
-
-
1.IO复用循环服务器模型介绍
-
服务器首先要调用 socket()函数建立个套接字文件描述符,调用 bind()函数将套接字文件描述符与本地地址进行绑定,调用listen()函数设置侦听的队列长度。然后建立两个线程,一 个用于处理客户端的连接,另一个线程用于处理客户端的请求。
-
连接业务处理线程接收客户端的连接,当客户端的连接到来时,函数 accept()成功时,会得到客户端连接的套接字文件描述符,连接线程将客户端的描述符放入客户端连接状态表。这个状态表与请求业务处理线程共享。
-
请求业务处理线程用于处理客户端的业务请求,例如请求的分析、IO数据的读取等。这个线程根据连接线程获得的客户端的套接字文件描述符,建立文件描述符集合,使用select() 函数对文件描述符集合进行超时等待。当有客户端的请求到来,请求线程接收客户端的数据,并对数据进行处理。当客户端的请求处理完毕,客户端退出的时候请求业务处理线程将此客户端的文件描述符从客户端连接状态表中取出。
-
从客户端的处理流程来看,客户端先与连接业务处理线程进行连接,即 connect()与服务器中的 accept()进行TCP 连接的三次握手。当连接成功后,客户端发送的数据被请求业务处理单元接收到,请求单元处理客户的数据,并发送响应给客户端。最后客户端关闭连接的时候,请求单元更新客户端的连接状态。
2.IO复用循环服务器模型的例子
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/select.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define CLIENTNUM 1024 /*最大支持客户端数量*/
/*可连接客户端的文件描述符数组*/
int connect_host[CLIENTNUM];
int connect_number = 0;
//客户端请求线程
static void *handle_request(void *argv)
{
time_t now; /*时间*/
char buff[BUFFLEN]; /*收发数据缓冲区*/
int n = 0;
int maxfd = -1; /*最大侦听文件描述符*/
fd_set scanfd; /*侦听描述符集合*/
struct timeval timeout; /*超时*/
timeout.tv_sec = 1; /* 阻塞1s后超时返回 */
timeout.tv_usec = 0;
int i = 0;
int err = -1;
for(;;)
{
/*最大文件描述符值初始化为-1*/
maxfd = -1;
FD_ZERO(&scanfd); /*清零文件描述符集合*/
for(i=0;i<CLIENTNUM;i++) /*将文件描述符放入集合*/
{
if(connect_host[i] != -1) /*合法的文件描述符*/
{
FD_SET(connect_host[i], &scanfd); /*放入集合*/
if(maxfd < connect_host[i]) /*更新最大文件描述符值*/
{
maxfd = connect_host[i];
}
}
}
/*select等待*/
err = select(maxfd + 1, &scanfd, NULL, NULL, &timeout) ;
switch(err)
{
case 0: /*超时*/
break;
case -1: /*错误发生*/
break;
default: /*有可读套接字文件描述符*/
if(connect_number<=0)
break;
for(i = 0;i<CLIENTNUM;i++)
{
/*查找激活的文件描述符*/
if(connect_host[i] != -1)
if(FD_ISSET(connect_host[i],&scanfd))
{
memset(buff, 0, BUFFLEN);/*清零*/
n = recv(connect_host[i], buff, BUFFLEN,0);
/*接收发送方数据*/
if(n > 0 && !strncmp(buff, "TIME", 4))
/*判断是否合法接收数据*/
{
memset(buff, 0, BUFFLEN); /*清零*/
now = time(NULL); /*当前时间*/
sprintf(buff, "%24s\r\n",ctime(&now));
/*将时间复制入缓冲区*/
send(connect_host[i], buff, strlen(buff),0);
/*发送数据*/
}
/*更新文件描述符在数组中的值*/
connect_host[i] = -1;
connect_number --; /*客户端计数器减1*/
/*关闭客户端*/
close(connect_host[i]);
}
}
break;
}
}
return NULL;
}
//客户端连接处理
static void *handle_connect(void *argv)
{
int s_s = *((int*)argv) ; /*获得服务器侦听套接字文件描述符*/
struct sockaddr_in from;
socklen_t len = sizeof(from);
/*接收客户端连接*/
for(;;)
{
int i = 0;
int s_c = accept(s_s, (struct sockaddr*)&from, &len);
/*接收客户端的请求*/
printf("a client connect, from:%s\n",inet_ntoa(from.sin_addr));
/*查找合适位置,将客户端的文件描述符放入*/
for(i=0;i<CLIENTNUM;i++)
{
if(connect_host[i] == -1) /*找到*/
{
/*放入*/
connect_host[i]= s_c;
/*客户端计数器加1*/
connect_number ++;
/*继续轮询等待客户端连接*/
break;
}
}
}
return NULL;
}
int main(int argc, char *argv[])
{
int s_s; /*服务器套接字文件描述符*/
struct sockaddr_in local; /*本地地址*/
int i = 0;
memset(connect_host, -1, CLIENTNUM);
/*建立TCP套接字*/
s_s = socket(AF_INET, SOCK_STREAM, 0);
/*初始化地址*/
memset(&local, 0, sizeof(local)); /*清零*/
local.sin_family = AF_INET; /*AF_INET协议族*/
local.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
local.sin_port = htons(SERVER_PORT); /*服务器端口*/
/*将套接字文件描述符绑定到本地地址和端口*/
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG); /*由于TCP的服务器连接,因此对等待的队列进行了长度设置侦听*/
pthread_t thread_do[2];/*线程ID*/
/*创建线程处理客户端连接*/
pthread_create(&thread_do[0], /*线程ID*/
NULL, /*属性*/
handle_connect, /*线程回调函数*/
(void*)&s_s); /*线程参数*/
/*创建线程处理客户端请求*/
pthread_create(&thread_do[1], /*线程ID*/
NULL, /*属性*/
handle_request, /*线程回调函数*/
NULL); /*线程参数*/
/*等待线程结束*/
for(i=0;i<2;i++)
pthread_join(thread_do[i], NULL);
close(s_s);
return 0;
}