在网络通信过程中,服务端通常需要处理多个客户端。由于多个客户端的请求可能会同时到来,服务器端可采用不同的方法来处理。总体上来说,服务器端可采用两种模型来实现:循环服务器模型和并发服务器模型。
循环服务器模型是指服务器端依次处理每个客户端,直到当前客户端的所有请求处理完毕,再处理下一个客户端。这类模型的优点是简单,缺点显而易见。特别是TCP循环服务器模型,由于必须先处理完当前客户端,容易造成其他客户端等待时间较长的情况。
为了提高服务器的并发处理能力,又引入了并发服务器模型。其基本思想是在服务器端此阿勇多任务机制(多进程或多线程),分别为每个客户端创建一个任务来处理,极大地提高了服务器的并发处理能力。
下面具体介绍循环服务器模型和并发服务器模型的流程及实现。为了更好的进行对比。本节均以TCP为例讨论相关模型。
一、循环服务器(TCP)
1、运行介绍
TCP循环服务器是一种常用的模型,其工作流程如下:
1)服务器端从连接请求队列中提取请求,建立连接并返回新的已连接套接字;
2)服务器端通过已连接套接字循环接收数据,处理并发送给客户端,知道客户端关闭连接;
3)服务器端关闭已连接套接字,返回步骤1;
2、特点分析
通过上面对服务器执行过程的介绍,可以得到以下结论。
1)服务器端采用循环嵌套来实现。外层循环依次提取每个客户端的连接请求,建立TCP连接。内层循环接受并处理当前客户端的所有数据,知道客户端关闭连接;
2)如果当前客户端没有处理结束,其他客户端必须一直等待。
注意:采用这种模型的服务器无法同时为多个客户端服务。
3、编程示例
下面实现 TCP ECHO 服务器端和客户端。服务器端接收到客户端数据后,原封不动发送回去(回射服务);客户端运行时,用户从键盘输入字符串,发送给服务器端并接受返回的数据,直到用户输入quit 后退出。
server.c
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <string.h>
-
#include <sys/types.h>
-
#include <sys/socket.h>
-
#include <unistd.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#define BUFFER_SIZE 128
-
#define PORT 8888
-
int main()
-
{
-
int listenfd, clientfd;
-
int n;
-
struct sockaddr_in serveraddr,clientaddr;
-
socklen_t peerlen;
-
char buffer[BUFFER_SIZE];
-
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
-
{
-
perror("socket error");
-
exit(-1);
-
}
-
else
-
{
-
printf("listenfd:%d\n",listenfd);
-
}
-
memset(&serveraddr,0,sizeof(serveraddr));
-
serveraddr.sin_family = AF_INET;
-
serveraddr.sin_port = htons(PORT);
-
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0) //绑定IP地址和端口号
-
{
-
perror("bind error");
-
exit(-1);
-
}
-
else
-
{
-
printf("bind successfully!\n");
-
}
-
if(listen(listenfd,10) == -1)
-
{
-
perror("listen error");
-
exit(-1);
-
}
-
else
-
{
-
printf("listening....\n");
-
}
-
peerlen = sizeof(clientaddr);
-
while(1)
-
{
-
if((clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&peerlen)) < 0) //循环等待客户端的连接
-
{
-
perror("accept error");
-
exit(-1);
-
}
-
else
-
{
-
printf("connection from [%s:%d]\n",inet_ntoa(clientaddr.sin_addr)
-
,ntohs(clientaddr.sin_port));
-
}
-
memset(buffer,0,sizeof(buffer));
-
while(1)
-
{
-
if((n = recv(clientfd,buffer,BUFFER_SIZE,0)) == -1) //循环接收客户端发送的数据
-
{
-
perror("recv error");
-
exit(-1);
-
}
-
else if(n == 0) //此时,客户端断开连接
-
{
-
break;
-
}
-
else
-
{
-
printf("Received message:%s\n",buffer);
-
if(send(clientfd, buffer, n, 0) == -1)
-
{
-
perror("send error");
-
exit(-1);
-
}
-
else
-
{
-
printf("sendmessage:%s\n",buffer);
-
}
-
}
-
}
-
close(clientfd); //客户端断开连接后,服务端也断开
-
}
-
close(listenfd);
-
return 0;
-
}
client.c
-
#include <stdio.h>
-
#include <string.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <sys/socket.h>
-
#include <sys/types.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#define BUFFER_SIZE 128
-
#define PORT 8888
-
int main()
-
{
-
int n;
-
int serverfd, clientfd;
-
struct sockaddr_in serveraddr;
-
char buffer[BUFFER_SIZE];
-
if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
-
{
-
perror("socket error");
-
exit(-1);
-
}
-
else
-
{
-
printf("clientfd:%d\n",clientfd);
-
}
-
//设置服务端的IP地址和端口号
-
memset(&serveraddr,0,sizeof(serveraddr));
-
serveraddr.sin_family = AF_INET;
-
serveraddr.sin_port = htons(PORT);
-
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
if(connect(clientfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
-
{
-
perror("connect error");
-
exit(-1);
-
}
-
else
-
{
-
printf("connect successfully!\n");
-
}
-
while(1)
-
{
-
printf("input > ");
-
fgets(buffer,sizeof(buffer),stdin);
-
if(strcmp(buffer,"quit\n") == 0) //遇到quit,退出
-
break;
-
buffer[strlen(buffer) - 1] = '\0'; //将'\n'去掉
-
if(send(clientfd,buffer,sizeof(buffer),0) == -1)
-
{
-
perror("send error");
-
exit(-1);
-
}
-
memset(buffer,0,sizeof(buffer)); //清空buffer
-
if((n = recv(clientfd,buffer,sizeof(buffer),0)) == -1)
-
{
-
perror("recv error");
-
exit(-1);
-
}
-
else if(n == 0)
-
{
-
break; //若服务端意外关闭
-
}
-
else
-
{
-
printf("echo:%s\n",buffer);
-
}
-
}
-
close(clientfd);
-
return 0;
-
}
运行结果如下:
server
-
fs@ubuntu:~/qiang/netprogram/2$ ./server
-
listenfd:3
-
bind successfully!
-
listening....
-
connection from [127.0.0.1:57366]
-
Received message:xiao
-
sendmessage:xiao
-
Received message:zhi
-
sendmessage:zhi
-
Received message:qiang
-
sendmessage:qiang
-
//继续等待下一个客户端的连接
client
-
fs@ubuntu:~/qiang/netprogram/2$ ./client
-
clientfd:3
-
connect successfully!
-
input > xiao
-
echo:xiao
-
input > zhi
-
echo:zhi
-
input > qiang
-
echo:qiang
-
input > quit
-
fs@ubuntu:~/qiang/netprogram/2$
二、并发服务器(TCP)
1、运行介绍
TCP并发服务器模型在网络通信中被广泛使用,既可以采用多进程也可以采用多线程来实现。以多进程为例,其工作流程如下:
1)服务器端父进程从连接请求队列中提取请求,建立连接并返回新的已连接套接字。
2)服务器端父进程创建子进程为客户端服务。客户端关闭连接时,子进程结束。
3)服务器端父进程关闭已连接套接字,返回步骤1;
2、特点分析
通过上面对服务器执行过程的介绍,可以得到以下结论。
1)服务器端父进程一旦接受到客户端的连接请求,建立好连接并创建新的子进程。这意味着每个客户端在服务器端有一个专门的子进程为其服务。
2)服务器端的多个子进程同时运行(宏观上),处理多个客户端。
3)服务器端的父进程不具体处理每个客户端的数据请求。
注意:采用这种模型的服务器端需要避免僵死进程。
3、编程示例
下面采用并发模型实现 TCP ECHO服务器端。
server.c
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <string.h>
-
#include <sys/types.h>
-
#include <sys/socket.h>
-
#include <unistd.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#include <signal.h>
-
#define BUFFER_SIZE 128
-
#define PORT 8888
-
#define IP "192.168.3.51"
-
void handler(int signo);
-
int main()
-
{
-
int listenfd, clientfd;
-
int n;
-
pid_t pid;
-
struct sockaddr_in serveraddr,clientaddr;
-
socklen_t peerlen;
-
char buffer[BUFFER_SIZE];
-
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
-
{
-
perror("socket error");
-
exit(-1);
-
}
-
else
-
{
-
printf("Socket successfully!\nlistenfd:%d\n",listenfd);
-
}
-
memset(&serveraddr,0,sizeof(serveraddr));
-
serveraddr.sin_family = AF_INET;
-
serveraddr.sin_port = htons(PORT);
-
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0)
-
{
-
perror("bind error");
-
exit(-1);
-
}
-
else
-
{
-
printf("bind successfully!\n");
-
printf("local IP:%s port:%d\n",IP,PORT);
-
}
-
if(listen(listenfd,10) == -1)
-
{
-
perror("listen error");
-
exit(-1);
-
}
-
else
-
{
-
printf("listening....\n");
-
}
-
signal(SIGCHLD,handler);//捕捉SIGCHLD信号,子进程死亡后,调用handler回收
-
peerlen = sizeof(clientaddr);
-
while(1)
-
{
-
if((clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&peerlen)) < 0)
-
{
-
perror("accept error");
-
exit(-1);
-
}
-
else
-
{
-
printf("connection from [%s:%d]\n",inet_ntoa(clientaddr.sin_addr)
-
,ntohs(clientaddr.sin_port));
-
}
-
memset(buffer,0,sizeof(buffer));
-
if((pid = fork()) < 0)
-
{
-
perror("fork error");
-
exit(-1);
-
}
-
else if(pid == 0)
-
{
-
close(listenfd);
-
while(1)
-
{
-
if((n = recv(clientfd,buffer,BUFFER_SIZE,0)) == -1)
-
{
-
perror("recv error");
-
exit(-1);
-
}
-
else if(n == 0)
-
{
-
break;
-
}
-
else
-
{
-
printf("Received message:%s\n",buffer);
-
}
-
if(send(clientfd, buffer, n, 0) == -1)
-
{
-
perror("send error");
-
exit(-1);
-
}
-
else
-
{
-
printf("sendmessage:%s\n",buffer);
-
}
-
}
-
printf("client is closed\n");
-
exit(0);
-
}
-
else
-
{
-
close(clientfd);
-
}
-
}
-
close(listenfd);
-
return 0;
-
}
-
//接收到SIFCHLD信号后,回收子进程
-
void handler(int signo)
-
{
-
pid_t pid;
-
while((pid = waitpid(-1, NULL, WNOHANG)) > 0)
-
{
-
printf("child(%d) is over!\n",pid);
-
}
-
}
client
-
#include <stdio.h>
-
#include <string.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <sys/socket.h>
-
#include <sys/types.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#define BUFFER_SIZE 128
-
#define PORT 8888
-
int main()
-
{
-
int n;
-
int serverfd, clientfd;
-
struct sockaddr_in serveraddr;
-
char buffer[BUFFER_SIZE];
-
if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
-
{
-
perror("socket error");
-
exit(-1);
-
}
-
else
-
{
-
printf("clientfd:%d\n",clientfd);
-
}
-
memset(&serveraddr,0,sizeof(serveraddr));
-
serveraddr.sin_family = AF_INET;
-
serveraddr.sin_port = htons(PORT);
-
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
if(connect(clientfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
-
{
-
perror("connect error");
-
exit(-1);
-
}
-
else
-
{
-
printf("connect successfully!\n");
-
}
-
while(1)
-
{
-
printf("input > ");
-
fgets(buffer,sizeof(buffer),stdin);
-
if(strcmp(buffer,"quit\n") == 0)
-
break;
-
buffer[strlen(buffer) - 1] = '\0';
-
if(send(clientfd,buffer,sizeof(buffer),0) == -1)
-
{
-
perror("send error");
-
exit(-1);
-
}
-
memset(buffer,0,sizeof(buffer));
-
if((n = recv(clientfd,buffer,sizeof(buffer),0)) == -1)
-
{
-
perror("recv error");
-
exit(-1);
-
}
-
else if(n == 0)
-
{
-
break;
-
}
-
else
-
{
-
printf("echo:%s\n",buffer);
-
}
-
}
-
close(clientfd);
-
return 0;
-
}
执行结果如下:
client1:
-
fs@ubuntu:~/qiang/netprogram/3$ ./client
-
clientfd:3
-
connect successfully!
-
input > I am client1!
-
echo:I am client1!
-
input > I am client1, how are you!
-
echo:I am client1, how are you!
-
input > quit
-
fs@ubuntu:~/qiang/netprogram/3$
clinet2:
-
fs@ubuntu:~$ telnet 192.168.3.51 8888
-
Trying 192.168.3.51...
-
Connected to 192.168.3.51.
-
Escape character is '^]'.
-
I am client2! I am telnet
-
I am client2! I am telnet
-
I am telnet, I am still alive!
-
I am telnet, I am still alive!
server:
-
fs@ubuntu:~/qiang/netprogram/3$ ./server
-
Socket successfully!
-
listenfd:3
-
bind successfully!
-
local IP:192.168.3.51 port:8888
-
listening....
-
connection from [127.0.0.1:45890]
-
Received message:I am client1!
-
sendmessage:I am client1!
-
connection from [192.168.3.51:35721]
-
Received message:I am client2! I am telnet
-
sendmessage:I am client2! I am telnet
-
Received message:I am client1, how are you!
-
sendmessage:I am client1, how are you!
-
client is closed
-
child(7216) is over!
-
Received message:I am telnet, I am still alive!
-
sendmessage:I am telnet, I am still alive!
可以看到实现了并发!
三、I/O多路复用并发服务器
I/O多路复用模型可以解决资源限制的问题。此模型实际上时将UDP循环模型用在了TCP上面。服务器用单进程循环处理请求(客户端有限的情况下)。
但是,其存在同样的问题:由于服务器是依次处理客户的请求,所以可能会导致有的客户等待时间过长。
server:
-
#include <stdio.h>
-
#include <string.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
#include <sys/socket.h>
-
#include <sys/select.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#define PORT 8888
-
#define MAXSIZE 128
-
int main()
-
{
-
int i,nbyte;
-
int listenfd, confd, maxfd;
-
char buffer[MAXSIZE];
-
fd_set global_rdfs, current_rdfs;
-
struct sockaddr_in addr,clientaddr;
-
int addrlen = sizeof(struct sockaddr_in);
-
int caddrlen = sizeof(struct sockaddr_in);
-
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
-
{
-
perror("socket error");
-
exit(-1);
-
}
-
else
-
{
-
printf("socket successfully!\n");
-
printf("listenfd : %d\n",listenfd);
-
}
-
memset(&addr, 0 ,addrlen);
-
addr.sin_family = AF_INET;
-
addr.sin_port = htons(PORT);
-
addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
if(bind(listenfd,(struct sockaddr *)&addr,addrlen) == -1)
-
{
-
perror("bind error");
-
exit(-1);
-
}
-
else
-
{
-
printf("bind successfully!\n");
-
printf("listen port:%d\n",PORT);
-
}
-
if(listen(listenfd,5) == -1)
-
{
-
perror("listen error");
-
exit(-1);
-
}
-
else
-
{
-
printf("listening...\n");
-
}
-
maxfd = listenfd;
-
FD_ZERO(&global_rdfs);
-
FD_SET(listenfd,&global_rdfs);
-
while(1)
-
{
-
current_rdfs = global_rdfs;
-
if(select(maxfd + 1,¤t_rdfs, NULL, NULL,0) < 0)
-
{
-
perror("select error");
-
exit(-1);
-
}
-
for(i = 0; i <= listenfd + 1; i++)
-
{
-
if(FD_ISSET(i, ¤t_rdfs)) //fd 就绪
-
{
-
if(i == listenfd) //有新的连接
-
{
-
if((confd = accept(listenfd,(struct sockaddr *)&clientaddr,&caddrlen)) == -1)
-
{
-
perror("accept error");
-
exit(-1);
-
}
-
else
-
{
-
printf("Connect from [IP:%s PORT:%d]\n",
-
inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
-
FD_SET(confd,&global_rdfs);
-
maxfd = (maxfd > confd ? maxfd : confd);
-
}
-
}
-
else
-
{
-
if((nbyte = recv(i, buffer, sizeof(buffer),0)) < 0)
-
{
-
perror("recv error");
-
exit(-1);
-
}
-
else if(nbyte == 0) //客户端close
-
{
-
close(i);
-
FD_CLR(i,&global_rdfs); //将其清出
-
}
-
else
-
{
-
printf("recv:%s\n",buffer);
-
send(i, buffer, sizeof(buffer),0);
-
}
-
}
-
}
-
}
-
}
-
return 0;
-
}
这里使用多路连接TCP服务器,执行结果如下:
-
fs@ubuntu:~/qiang/select$ ./select
-
socket successfully!
-
listenfd : 3
-
bind error: Address already in use
-
fs@ubuntu:~/qiang/select$ ./select2
-
socket successfully!
-
listenfd : 3
-
bind successfully!
-
listen port:8888
-
listening...
-
Connect from [IP:192.168.3.51 PORT:40075]
-
recv:xiao
-
Connect from [IP:192.168.3.51 PORT:40331]
-
Connect from [IP:192.168.3.84 PORT:36233]
-
Connect from [IP:192.168.3.33 PORT:2499]