总结一下Linux下常见的几种并发编程方式。
这是出现的最早的也是写起来最简单的一种方式。大概可以总结成父进程接收客户端的连接请求,启动子进程负责与客户端通信。
多进程服务器代码:
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void client_handler(int fd)
{
char buf[32] = {
0};
int ret;
while (1)
{
ret = recv(fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
exit(1);
}
else if (0 == ret)
{
break;
}
if (!strcmp(buf, "bye"))
{
break;
}
printf("%s\n", buf);
memset(buf, 0, sizeof(buf));
}
close(fd);
kill(getppid(), SIGALRM);
}
void my_wait(int sig)
{
int status;
wait(&status);
}
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
printf("等待客户端的连接...\n");
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
signal(SIGALRM, my_wait);
while (1)
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受客户端的连接 %d\n", fd);
pid_t pid = fork();
if (0 == pid)
{
client_handler(fd);
exit(0);
}
}
close(sockfd);
return 0;
}
多进程服务器优点在于:
多进程并发的缺点也很明显:
为了解决以上两个缺点,于是就有了线程。
跟进程相比,线程的优点很多:
我们可以为每一个客户端创建一个线程,这样同样的并发量对服务器造成的压力会比进程小。
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
void *ClientHandler(void *arg)
{
int ret;
int fd = *(int *)arg;
char buf[32] = {
0};
pthread_detach(pthread_self()); //线程结束,自动释放资源
while (1)
{
ret = recv(fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
exit(1);
}
else if (0 == ret) //客户端异常退出
{
break;
}
printf("接收%d客户端%s\n", fd, buf);
memset(buf, 0, sizeof(buf));
}
printf("%d 客户端退出!\n", fd);
close(fd);
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
printf("等待客户端的连接...\n");
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
while (1)
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受客户端的连接 %d\n", fd);
//为每一个客户端创建新的线程
pthread_t tid;
ret = pthread_create(&tid, NULL, ClientHandler, &fd);
if (ret != 0)
{
perror("pthread_create");
exit(1);
}
}
close(sockfd);
return 0;
}
是不是使用多线程实现的服务器就是完美的呢?当然不是。
多线程因为是共享同一个地址空间,所以一个线程的奔溃会导致整个进程挂掉。同时线程的通信方式过于简单,只需要读取内存就行,会导致多个线程同时访问共享资源。程序员的大部分时间都消耗在了解决多线程并发的问题上。
因此,多线程解决并发的问题也并不是完美的方案。
并发服务器并不是只有进程和线程才能解决,还有一个目前比较流行的技术叫做多路复用。
什么是多路复用技术?
举个例子,假如你是一个老师(服务器),现在让班级里30个学生做一道数学题,做好之后你逐个检查,有这么几种方案。
第一种:从第一个人开始检查,顺序把30个人检查完。这种效率是最低的,一旦中间遇到某个学生解题没有思路,那么将会影响整个班的进度。这种方法都谈不上是并发服务器。
第二种:找来30个老师,每个老师负责检查一个学生,不难理解,效率是相当的高。这种方法就是多进程/多线程并发服务器。
第三种:你站在讲台上,告诉学生题目答完后叫一声,但是你又不知道是谁叫的,所以一听到声音你就逐个询问,直到找到这个学生并且检查他的答案。这种就是多路复用select的实现方案。
顺便讲一下第四种:你站在讲台上,告诉学生题目答完后举手,这种方法直接省去了你逐个询问,效率高于select,在服务器上使用epoll技术实现,也是多路复用的一种。
多路复用基本上可以归纳为两个因素:事件以及处理事件的函数。
程序在不断的循环,等待事件的到来,来了之后根据事件类型的不同调用不同的事件处理函数。
select并发服务器代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd[1024] = {
0};
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
fd_set readfd, tmpfd; //定义集合
FD_ZERO(&readfd); //清空集合
FD_SET(sockfd, &readfd); //添加到集合
int maxfd = sockfd, i = 0;
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
char buf[32] = {
0};
while (1)
{
tmpfd = readfd;
ret = select(maxfd + 1, &tmpfd, NULL, NULL, NULL);
if (-1 == ret)
{
perror("select");
exit(1);
}
if (FD_ISSET(sockfd, &tmpfd)) //判断sockfd是否还留在集合里面 判断是否有客户端发起连接
{
for (i = 0; i < 1024; i++) //选择合适的i
{
if (fd[i] == 0)
{
break;
}
}
fd[i] = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd[i])
{
perror("accept");
exit(1);
}
printf("接受来自%s的客户端的连接 fd = %d\n", inet_ntoa(client_addr.sin_addr), fd[i]);
FD_SET(fd[i], &readfd); //新的文件描述符加入到集合中
if (maxfd < fd[i])
{
maxfd = fd[i];
}
}
else //有客户端发消息
{
for (i = 0; i < 1024; i++)
{
if (FD_ISSET(fd[i], &tmpfd)) //判断是哪个fd可读
{
ret = recv(fd[i], buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
}
else if (0 == ret)
{
close(fd[i]); //关闭TCP连接
FD_CLR(fd[i], &readfd); //从集合中清除掉
printf("客户端%d下线!\n", fd[i]);
fd[i] = 0;
}
else
{
printf("收到%d客户端的消息%s\n", fd[i], buf);
}
memset(buf, 0, sizeof(buf));
break;
}
}
}
}
return 0;
}
select的缺点:
epoll并发服务器代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXSIZE 256
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
int epfd = epoll_create(MAXSIZE); //创建epoll对象
if (-1 == epfd)
{
perror("epoll_create");
exit(1);
}
struct epoll_event ev, events[MAXSIZE] = {
0};
ev.events = EPOLLIN; //监听sockfd可读
ev.data.fd = sockfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (-1 == ret)
{
perror("epoll_ctl");
exit(1);
}
struct sockaddr_in client_addr; //用于保存客户端的信息
int length = sizeof(client_addr), i;
char buf[32] = {
0};
while (1)
{
int num = epoll_wait(epfd, events, MAXSIZE, -1); //-1表示阻塞
if (-1 == num)
{
perror("epoll_wait");
exit(1);
}
for (i = 0; i < num; i++)
{
if (events[i].data.fd == sockfd) //有客户端发起连接
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受来自%s的连接fd=%d\n", inet_ntoa(client_addr.sin_addr), fd);
//为新的文件描述符注册事件
ev.data.fd = fd;
ev.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if (-1 == ret)
{
perror("epoll_ctl");
}
}
else
{
if (events[i].events & EPOLLIN) //如果事件是可读的
{
ret = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
}
else if (0 == ret)
{
ev.data.fd = events[i].data.fd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev); //客户端退出,注销事件
close(events[i].data.fd);
}
else
{
printf("收到%d客户端的消息%s\n", events[i].data.fd, buf);
}
memset(buf, 0, sizeof(buf));
}
}
}
}
return 0;
}
epoll的性能提升:
多路复用解决了多线程中同步的问题,确实省去了很多麻烦。
但是由于只有一个线程,如果在处理某个事件的时候,遇到了IO操作,比如读取大文件,那么程序将会被阻塞,此时其他的事件将不会被处理。解决这个问题可以使用异步IO,就是读取文件的时候,不管文件有没有读完都会立即返回,不影响处理其他事件。至于IO操作什么时候完成,操作系统会有其他办法去检测。
随着服务器负载的越来越大,高并发问题早已不是进程、线程或者多路复用能解决的,而是事件 + 线程 + 协程的组合,但是不管怎样,了解了历史才能更深刻的理解当下。以上就是给大家总结的几种比较经典的并发服务器实现方案。
以上有不足的地方欢迎指出讨论,最后,如果觉得学习资料难找的话,可以添加学习交流群:960994558 学习资料已经共享在群里了,期待你的加入~
这里推荐一个金牌大佬的免费课程,这个跟以往所见到的只会空谈理论的有所不同,正在学习的朋友可以体验一下
C/C++Linux服务器开发/后台架构师