并发服务器三种实现方式之进程、线程和select
发布日期:2021-08-11 10:05:24 浏览次数:2 分类:技术文章

本文共 8709 字,大约阅读时间需要 29 分钟。

  前言:刚开始学网络编程,都会先写一个客户端和服务端,不知道你们有没有试一下:再打开一下客户端,是连不上服务端的。还有一个问题不知道你们发现没:有时启服务器,会提示“Address already in use”,过一会就好了,想过为啥么?在这篇博客会解释这个问题。

  但现实的服务器都会连很多客户端的,像阿里服务器等,所以这篇主要介绍如何实现并发服务器,主要通过三种方式:进程、线程和select函数来分别实现。

  一、进程实现并发服务器

  先说下什么是并发服务器吧?不是指有多个服务器同时运行,而是可以同时连接多个客户端。

  先简单说下原理吧,先画个图,如下:  PS:全博客园最丑图,不接受反驳!哈哈哈

  

  先要搞清楚通信的流程,图上参数说明:

  lfd:socket函数的返回值,就是监听描述符

  cfd1/cfd2/cfd3:accept函数的返回值,用通信的套接字

  server:服务器

  client:客户端

  socket通信过程中,总共有几个套接字呢?答:三个,客户端一个,服务器两个。

  根据上图来大致说明一下流程:

  客户端创建一个套接字描述符,用于通信,服务器先用socket函数创建套接字,用于监听客户端,然后调用accept函数,会返回一个套接字,用于通信的。图上就是,client1先通过cfd与server建立连接,然后与cfd1建立连接通信,这时lfd就空闲了,再监听客户端,client2再与lfd连接,再跟cfd2通信。client3也是如此。

  现在问题就是。如何创建多个进程与客户端通信呢?通过循环创建子进程就可以实现这个问题,可以参考我的这篇博客:https://www.cnblogs.com/liudw-0215/p/9667686.html

  服务端程序,如下:

  

#include 
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"#define MAXLINE 8192#define SERV_PORT 8000void do_sigchild(int num){ while (waitpid(0, NULL, WNOHANG) > 0) ;}int main(void){ struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); // int opt = 1; //setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0;}
View Code

  客户端程序如下:

  

/* client.c */#include 
#include
#include
#include
#include
#include "wrap.h"#define MAXLINE 8192#define SERV_PORT 8000int main(int argc, char *argv[]){ struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0;}
View Code

  演示效果,服务器可以同时处理两个客户端,如下:

  

  但我想再测试一下程序,执行./server,发现有个bind error,如下:

  

  下面来解释一下这个问题:

  先来一张图片(出自UNP),如下:

  

  这张图将三次握手、四次挥手和TCP状态转换图,这些在我的这篇博客都由介绍,可以参考一下:https://www.cnblogs.com/liudw-0215/p/9661583.html

注意最后有一个TIME_WAIT状态,主动关闭一端会经历2MSL时长等待(大约40秒),再变为最开始的状态CLOSED。

  复现上面的“bind error”,只需退出服务器,在启服务器,就会报出此错。因为主动关闭一端,会经历2MSL时长,端口IP会被占用,所以会报“bind error”。

但可能会问:为啥先退出客户端没有此问题?因为客户端没有调用bind函数地址结构,会“隐式”生成端口。

  有没有方法可以解决这个问题呢?当然有的,调用函数setsockopt即可,服务端程序如下:

  

#include 
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"#define MAXLINE 8192#define SERV_PORT 8000void do_sigchild(int num){ while (waitpid(0, NULL, WNOHANG) > 0) ;}int main(void){ struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0;}
View Code

  二、线程实现并发服务器

  理解了进程的方式,就是创建多个线程来实现,就不过多解释了,程序需要对线程有一定了解,之后我还会写篇博客来介绍线程,敬请期待哦。

  服务器代码如下,有有详细的解释:

  

#include 
#include
#include
#include
#include
#include
#include
#include "wrap.h"#define MAXLINE 8192#define SERV_PORT 8000struct s_info { //定义一个结构体, 将地址结构跟cfd捆绑 struct sockaddr_in cliaddr; int connfd;};void *do_work(void *arg){ int n,i; struct s_info *ts = (struct s_info*)arg; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看 while (1) { n = Read(ts->connfd, buf, MAXLINE); //读客户端 if (n == 0) { printf("the client %d closed...\n", ts->connfd); break; //跳出循环,关闭cfd } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)), ntohs((*ts).cliaddr.sin_port)); //打印客户端信息(IP/PORT) for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //小写-->大写 Write(STDOUT_FILENO, buf, n); //写出至屏幕 Write(ts->connfd, buf, n); //回写给客户端 } Close(ts->connfd); return (void *)0;}int main(void){ struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; pthread_t tid; struct s_info ts[256]; //根据最大线程数创建结构体数组. int i = 0; listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建一个socket, 得到lfd bzero(&servaddr, sizeof(servaddr)); //地址结构清零 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP servaddr.sin_port = htons(SERV_PORT); //指定端口号 8000 Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定 Listen(listenfd, 128); //设置同一时刻链接服务器上限数 printf("Accepting client connect ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求 ts[i].cliaddr = cliaddr; ts[i].connfd = connfd; /* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */ pthread_create(&tid, NULL, do_work, (void*)&ts[i]); pthread_detach(tid); //子线程分离,防止僵线程产生. i++; } return 0;}
View Code

  客户端代码如下:

  

/* client.c */#include 
#include
#include
#include
#include
#include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(int argc, char *argv[]){ struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0;}
View Code

  三、select实现并发服务器

  select和进程主要区别在于,进程是阻塞的,而select是交给内核自己来实现的,由于select比较复杂,参考我的另一篇博客:https://www.cnblogs.com/liudw-0215/p/9661583.html

  总结:有不懂的,欢迎及时评论。

  

 

 

 

  

  

 

转载于:https://www.cnblogs.com/liudw-0215/p/9664204.html

转载地址:https://blog.csdn.net/weixin_30621711/article/details/94768360 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【译】学习JavaScript中提升、作用域、闭包的终极指南
下一篇:任务十一:移动Web页面布局实践

发表评论

最新留言

很好
[***.229.124.182]2024年04月17日 19时27分07秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

!!! 2019-05-08
sql 小东西 2019-05-08
mysql 2019-05-08
删除 .svn 文件夹 2019-05-08
ssh 乱码 2019-05-08
数字及大写 2019-05-08
2pic 2019-05-08
巧记单词 2019-05-08
asp.net之状态管理分类 2019-05-08
巧记词汇2 2019-05-08
巧记单词6 2019-05-08
巧计单词6 2019-05-08
听力词汇2 2019-05-08
Html学习 2019-05-08
巧记单词9 2019-05-08
巧记单词8 2019-05-08
作文1 2019-05-08
viewState的应用 2019-05-08
操作系统 2019-05-08
操作系统04 2019-05-08