- 多进程并发
- 多线程并发
- 多路I\O转接
select
poll
epoll
多进程并发服务器
- 设计思路:
1)服务器阻塞接收若干个客户端的连接请求,一旦有一个客户端connect
,则服务器端fork
一个子进程与之连接。
2)在服务器端,子进程的任务是接收并处理客户端的数据,而父进程的任务是监听新的客户端请求(accept
),创建子进程,并负责回收子进程(可通过处理SIGCHLD
信号的方式)。 - 示例程序如下:
//server.c
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <strings.h>
#include <stdlib.h>
#include <sys/wait.h>
#include "wrap.h"
#define SERV_PORT 6666
//#define SERV_IP "127.0.0.1"
void handle_child(int signo) {
while (waitpid(0, NULL, WNOHANG) > 0);
return ;
}
int main() {
int lfd, cfd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
pid_t pid;
int n, i;
char buf[BUFSIZ], clie_IP[BUFSIZ];
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT); //convert to net bytes order
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_pton(AF_INET, (const char *)SERV_IP, &serv_addr.sin_addr.s_addr);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
while(1) {
clie_addr_len = sizeof(clie_addr);
cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
printf("client IP: %s, port: %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), //inet_ntop returns the third argument
ntohs(clie_addr.sin_port)); //convert net bytes order to host bytes order
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
//child
close(lfd); //lfd is useless for child now
break; //get out of the loop to go on
} else if (pid > 0) {
//parent
close(cfd); //cfd is useless for parent now
signal(SIGCHLD, handle_child); //wait for children's end
}
}
if (pid == 0) {
//child
while (1) {
n = Read(cfd, buf, sizeof(buf));
if (n == -1) {
//other errors except for signal interruption
perror("read error");
exit(1);
} else if (n == 0) {
//client close, read over
close(cfd);
return 0;
} else {
//loop to handle data
for(i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(cfd, buf, n);
Write(STDOUT_FILENO, buf, n); //print to screen
}
}
}
return 0;
}
多线程并发服务器
- 思路与多进程并发相同,区别仅在于将多进程中的创建子进程改为多线程中的创建子线程。
- 示例程序如下:
//server.c
#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include "wrap.h"
#define SERV_PORT 7777
#define SERV_IP "127.0.0.1"
#define MAXLINE 8192
struct s_info {
//bind a client's sockaddr and its file descriptor
struct sockaddr_in clie_addr;
int cfd;
};
void *child_thrd(void *arg) {
//start routine of child thread
int n, i;
struct s_info *ts = (struct s_info *)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; //cmd:"[+d": #define INET_ADDRSTRLEN 16
while (1) {
n = Read(ts->cfd, buf, MAXLINE);
if (n == 0) {
//client close, read over
printf("the client %d closed ...\n", ts->cfd);
break; //get out of the loop and close cfd
}
printf("received from client IP: %s at port: %d, thread: %lu\n",
inet_ntop(AF_INET, &(*ts).clie_addr.sin_addr, str, sizeof(str)),
ntohs((*ts).clie_addr.sin_port),
pthread_self());
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(ts->cfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
Close(ts->cfd); //if n == 0
return (void *)0;
}
int main() {
int lfd, cfd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
pthread_t tid;
int i = 0;
struct s_info ts[256]; //max number of threads to be created
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
//serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET, (const char *)SERV_IP, &serv_addr.sin_addr.s_addr);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
printf("Accepting client connect ...\n");
while (1) {
bzero(&clie_addr, sizeof(clie_addr));
clie_addr_len = sizeof(clie_addr);
cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
ts[i].clie_addr = clie_addr;
ts[i].cfd = cfd;
// error handling if numbers of new threads is up to the limit
pthread_create(&tid, NULL, child_thrd, (void *)&ts[i]);
pthread_detach(tid);
//child thread detachs(in case that a zombie thread is created), no need for pthread_join
i++;
}
return 0;
}
- 但是,不论是多进程版还是多线程版的并发服务器模型,它们的问题都是无法处理大量客户端的连接和数据处理的请求,因为每当有一个新的客户端发起连接请求,服务器端都要创建一个子进程或子线程与之对应,来响应这个客户端。而一个进程或一个线程均要占用一定的CPU资源,具体地说,当有大量的子进程或子线程存在时,CPU要不停地在它们之间切换,这要消耗大量的CPU资源。因此,更有效的方法是借助多路I\O转接。
多路I\O转接
多路I\O转接服务器的主旨思想是,不再由应用程序服务器端自己监听客户端的连接请求,取而代之由内核代替监听。一旦内核监听到来自客户端的事件,它就会通知服务器端来处理,从而将服务器端从阻塞监听中解放出来。
select
- 一旦调用了
select
函数,该函数会驱使内核帮助服务器监听客户端请求,因此可以笼统地说是使用select
函数来监听客户端。 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
:
参数1:nfds
为监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符
参数2:readfds
为监控有读数据到达的文件描述符集合,传入传出参数
参数3:writefds
为监控写数据到达的文件描述符集合,传入传出参数
参数4:exceptfds
为监控异常发生到达的文件描述符集合,传入传出参数
上述三个监控事件集中不关心的事件集可传NULL
参数5:timeout
为定时阻塞监控时间:NULL
,永远等下去;设置timeval
,等待固定时间;设置timeval
里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- 另有四个配套函数:
void FD_CLR(int fd, fd_set *set);//把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set);//测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set);//把文件描述符集合里fd数据位置1
void FD_ZERO(fd_set *set);//把文件描述符集合里所有位清0,常用于初始化
- 示例程序:
//server.c
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <strings.h>
#include <stdio.h>
#include "wrap.h"
#define SERV_PORT 7777
#define SERV_IP "127.0.0.1"
#define MAXLINE 4096
int main() {
int lfd, cfd, maxfd, sockfd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
int client[FD_SETSIZE]; //restore fds having connection with server
fd_set rset; //read file descriptor set
fd_set allset; //listening set of select function(backup set of rset)
int nready; //return value of select
char buf[MAXLINE], str[INET_ADDRSTRLEN];
int maxi, i, j;
int n