前言
本文将讨论同时向多个客户端提供服务的并发服务器端的模型和方法,将分别介绍:
- 多进程服务器:通过创建多个进程提供服务
- 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务
- 多线程服务器:通过生成与客户端等量的线程提供服务
1. 多进程并发服务器
1.1 多进程并发服务器模型
每当有客户端请求服务时,服务器创建子进程以提供服务。
- 父进程通过调用accept函数受理连接请求
- 将获取的套接字文件描述符创建并传递给子进程
- 子进程利用传递来的文件描述符提供服务
1.2 多进程并发服务器的示例代码
该示例是一个回声服务器,即将客户端发送给服务器的内容返回给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAX_SIZE 30
void read_childproc(int sig){
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[MAX_SIZE];
if(argc != 2){
perror("argv");
exit(0);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
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){
perror("bind");
exit(0);
}
if(listen(serv_sock, 5) == -1){
perror("listen");
exit(0);
}
while(1){
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock == -1)
continue;
else
puts("new client connected...");
pid = fork();
if(pid == -1){
close(clnt_sock);
continue;
}
if(pid == 0){
close(serv_sock);
while((str_len = read(clnt_sock, buf, MAX_SIZE)) != 0){
write(clnt_sock, buf, str_len);
}
close(clnt_sock);
puts("client disconnected...");
return 0;
}else{
close(clnt_sock);
}
}
close(serv_sock);
return 0;
}
1.3 客户端的示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFF_SIZE 1024
int main(int argc, char *argv[])
{
int sock;
char message[BUFF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if(argc != 3){
perror("argv");
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
perror("socket");
exit(1);
}
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){
perror("connect");
exit(1);
}else{
puts("Connecting......");
}
while(1){
fputs("Input message(Q to quit):", stdout);
fgets(message, BUFF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")){
break;
}
str_len =write(sock, message, strlen(message));
recv_len = 0;
while(recv_len < str_len){
recv_cnt = read(sock, &message[recv_len], BUFF_SIZE - 1);
if(recv_cnt == -1){
perror("read() error");
}
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
2. select函数实现并发服务器
2.1 select函数优点
- 将多个文件描述符集中到一起监视。
- 程序兼容性好。
2.2 select函数调用过程
- 步骤一:设置文件描述符 -> 指定监视范围 -> 设置超时
- 步骤二:调用select函数
- 步骤三:查看调用结果
2.3设置文件描述符
fd_set数组变量用来监视文件描述符。
FD_ZERO(fd_set * fdset)
:将fdset变量的所有位初始化为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的信息,则返回“真”。
2.4 调用select函数
int select(int maxfd, fd_set * readset, fd_set *writeset, fd_set * exceptset, const struct timeval * timeout);
- maxfd:监视文件描述符的数量
- readset:所有关注“是否存在待读取数据”的文件描述符
- writeset:所有关注“是否可传输无阻塞数据”的文件描述符
- exceptset:所有关注“是否发生异常”的文件描述符
- timeout:防止无线阻塞,传递超时信息
- 返回值:发生错误返回-1,超时返回0,返回大于0的值为发生事件的文件描述符
select函数调用完成后,向其传递的fd_set将发生变化,除了发生变化的文件描述符对应的位,所有位将变为0。
2.5 select函数实现并发服务器的示例代码
客户端示例代码使用1.3的客户端示例代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#include <arpa/inet.h>
#define BUF_SIZE 100
int main(int argc, 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 str_len, fd_max, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2){
perror("argv");
exit(0);
}
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){
perror("bind");
exit(0);
}
if(listen(serv_sock, 5) == -1){
perror("listen");
exit(0);
}
//初始化reads变量
FD_ZERO(&reads);
//注册服务端套接字。服务器端的套接字有接收的数据,说明有新的客户端的连接请求
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while(1){
//调用select函数后,fd_set将发生变化,要将准备好的reads变量复制到cpy_reads变量
cpy_reads = reads;
//设置超时
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//循环调用select函数
//根据监视目的传递必要参数,第三,第四个参数为0
if((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1){
break;
}
if(fd_num == 0){
continue;
}
//select返回值大于1时的循环
for(i = 0; i < fd_max + 1; i++){
if(FD_ISSET(i, &cpy_reads)){
//连接请求
if(i == serv_sock){
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(i, buf, BUF_SIZE);
//确认是否为EOF
if(str_len == 0){
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}else{
//执行回声服务
write(i, buf, str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
3. epoll函数实现并发服务器
3.1 基于 select 的 I/O 复用技术速度慢的原因
- 调用select函数后对所有文件描述符进行循环。
- 每次调用select函数都需要向操作系统传递监视对象信息。对程序造成很大负担,并且无法通过优化代码解决。
3.2 epoll函数优点
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
- 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。
3.3 epoll函数
int epoll_create(int size);
- 创建保存epoll文件描述符的空间,由操作系统保存监视对象文件描述符
- size:epoll实例大小
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
- 向空间注册并注销文件描述符
- epfd:用于注册监视对象的epoll实程的文件描述符
- op:用于指定监视对象的添加,删除或更改
- EPOLL_CTL_ADD:将文件描述符注册到epoll例程
- EPOLL_CTL_DEL:从epoll例程中删除文件描述符
- EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况
- fd:需要注册的监视对象的文件描述符
- event:监视对象的事件类型
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- 等待文件描述符发生变化
- epfd:表示事件发生监视对象的epoll实程的文件描述符
- events:保存发生事件的文件描述符集合的结构体地址值
- maxevents:第二个参数中可保存的最大事件数
- timeout:等待时间
3.4 epoll函数实现并发服务器的示例代码
客户端示例代码使用1.3的客户端示例代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc != 2){
perror("argv");
exit(0);
}
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){
perror("bind");
exit(0);
}
if(listen(serv_sock, 5) == -1){
perror("listen");
exit(0);
}
epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1){
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt == -1){
puts("epoll_wait() error");
break;
}
for(i = 0; i < event_cnt; i++){
if(ep_events[i].data.fd == serv_sock){
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}else{
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len == 0){
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}else{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
3.5 条件触发和边缘触发
- 条件触发中,只要输入缓冲有数据就会一直通知该事件。
- 边缘触发中,输入缓冲中收到数据时仅注册1次该事件。即使缓冲中还有数据,也不会再进行注册。
4. 多线程服务器
4.1 多进程服务器的缺点
- 创建进程的过程会带来一定开销
- 进程间数据交换需要特殊技术
- 进程切换时的上下文切换需要大量开销
4.2 多线程服务器的优点
- 线程的创建和上下文切换比进程更快
- 线程间数据交换时无需特殊技术
4.3 用多线程技术实现聊天程序
下面的示例代码是使用多线程技术写的多个客户端之间的可以交换信息的简单的聊天程序。
- 多线程服务器的示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <pthread.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int clnt_adr_sz;
pthread_t t_id;
if(argc != 2){
perror("argv");
exit(0);
}
pthread_mutex_init(&mutx, NULL);
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){
perror("bind");
exit(0);
}
if(listen(serv_sock, 5) == -1){
perror("listen");
exit(0);
}
while (1){
clnt_adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutx);
pthread_create(&t_id, NULL, handle_clnt,(void*)&clnt_sock);
pthread_detach(t_id);
printf("Connected client IP :%s \n", inet_ntoa(clnt_adr.sin_addr));
}
close(serv_sock);
return 0;
}
void * handle_clnt(void * arg){
int clnt_sock = *((int*)arg);
int str_len = 0, i;
char msg[BUF_SIZE];
while((str_len = read(clnt_sock, msg, sizeof(msg))) != 0){
send_msg(msg, str_len);
}
pthread_mutex_lock(&mutx);
for(i = 0; i < clnt_sock; i++){
if(clnt_sock == clnt_socks[i]){
while(i++ < clnt_cnt - 1){
clnt_socks[i] = clnt_socks[i + 1];
}
break;
}
}
clnt_cnt--;
pthread_mutex_unlock(&mutx);
close(clnt_sock);
return NULL;
}
void send_msg(char * msg, int len){
int i;
pthread_mutex_lock(&mutx);
for(i = 0; i < clnt_cnt; i++){
write(clnt_socks[i], msg, len);
}
pthread_mutex_unlock(&mutx);
}
- 多线程客户端的示例代码
客户端使用两个线程分别负责收取信息和发送信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <pthread.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void * send_msg(void * arg);
void * recv_msg(void * arg);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in serv_adr;
pthread_t snd_thread, rcv_thread;
void * thread_return;
if(argc != 4){
perror("argv");
exit(1);
}
sprintf(name, "[%s]", argv[3]);
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 = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
perror("connect");
exit(1);
}else{
puts("Connecting......");
}
pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);
pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
void * send_msg(void * arg){
int sock = *((int*)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
while(1){
fgets(msg, BUF_SIZE, stdin);
if(!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")){
close(sock);
exit(0);
}
sprintf(name_msg, "%s %s", name, msg);
write(sock, name_msg, strlen(name_msg));
}
return NULL;
}
void * recv_msg(void * arg){
int sock = *((int*)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while(1){
str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
if(str_len == -1){
return (void*) - 1;
}
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
示例代码运行结果如下。
总结
非常荣幸能够参加孟宁老师的这门课程。在这次的学习过程中,无论是对JavaScript网络编程,前端应用,还是socket网络编程,高并发服务器,亦或是网络协议设计,RPC,我都有了新的理解与认知。
孟宁老师的教学风格轻松幽默,能够把复杂的概念以简单明了的方式呈现出来,让我们这些初学者也能够轻松理解。互动式的教学方式让我们更加深入地理解课程内容。
课程内容丰富多样,涵盖了网络编程各个方面各个层次的知识。老师对于课程的每一个章节都进行了详细的讲解,并配以实例和案例分析,使我们能够更好地理解和掌握。同时,老师还为我们提供了大量的学习资源和实践机会,帮助我们巩固所学知识,提高实际操作能力。