六十七、 TCP的并发服务器模型
67.1 循环服务器模型
- 一次只能处理一个客户端,当上一个客户端退出后,才能处理下一个客户端
- 缺点:无法同时处理多个客户端
代码模型
sfd = socket();
bind();
listen();
while(1){newfd = accept();while(1){recv();send(); }close(newfd);
}
close(sfd);
67.2 并发服务器模型
- 目的:可以同时处理多个客户端的请求。
- 实现:创建多进程或者创建多线程实现
- 父进程 / 主线程 只负责连接(accept)
- 子进程 / 分支线程只负责与客户端交互(recv / send);
67.2.1 多进程并发服务器
67.2.1.1 代码模型
void handler(int sig){while(waitpid(-1, NULL, WNOHANG) > 0);
}signal(17, handler);
sfd = socket();
bind();
listen();
while(1){newfd = accept();cpid = fork();if(0 == cpid){close(sfd);while(1){recv();send(); }close(newfd);exit(0); //退出子进程 }close(newfd);
}
close(sfd);
67.2.1.2 代码示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>#define ERR_MSG(msg) do{\fprintf(stderr, "__%d__ ", __LINE__);\perror(msg);\
}while(0)#define PORT 8888 //端口号的网络字节序,1024~49151
#define IP "192.168.125.55" //本机IP,ifconfigint deal_cli_msg(int newfd, struct sockaddr_in cin);void handler(int sig)
{//循环回收僵尸进程//有子进程,没有僵尸进程 == 0//没有子进程,也没有僵尸进程 ==-1、while(waitpid(-1, NULL, WNOHANG) > 0); return ;
}int main(int argc, const char *argv[])
{//捕获17号 SIGCHLD信号if(signal(SIGCHLD, handler) == SIG_ERR){ ERR_MSG("signal");return -1; } //创建流式套接字int sfd = socket(AF_INET, SOCK_STREAM, 0); if(sfd < 0){ ERR_MSG("socket");return -1; } printf("socket create success sfd=%d\n", sfd);//允许端口快速复用int reuse = 1;if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0){ ERR_MSG("setsockopt");return -1; } printf("允许端口快速复用成功\n");//填充地址信息结构体给bind函数绑定使用;//真实的地址信息结构体根据地址族指定,AF_INET:man 7 ipstruct sockaddr_in sin;sin.sin_family = AF_INET; //必须填AF_INET;sin.sin_port = htons(PORT); //端口号的网络字节序,1024~49151sin.sin_addr.s_addr = inet_addr(IP); //本机IP的网络字节序,ifconfig//绑定服务器的地址信息---》必须绑定if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0){ ERR_MSG("bind");return -1; } printf("bind success\n");//将套接字转换成被动监听状态if(listen(sfd, 128) < 0){ ERR_MSG("listen"); return -1; } printf("listen success\n");struct sockaddr_in cin; //存储客户端的地址信息socklen_t addrlen = sizeof(cin);int newfd = -1; pid_t cpid = -1; while(1){ //父进程只负责连接newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen);if(newfd < 0){ERR_MSG("accept");return -1; }printf("[%s:%d] newfd=%d 客户端连接成功__%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);//能运行到当前位置,则代表有客户端连接成功,//此时需要创建一个子进程,专门用于与客户端交互cpid = fork();if(0 == cpid){close(sfd); //子进程只负责交互,sfd没有用deal_cli_msg(newfd, cin);close(newfd);exit(0); //退出子进程,子进程只负责交互,不允许回到accept函数。}else if(cpid < 0){ERR_MSG("fork");;return -1; }close(newfd); //在父进程中newfd没有用} //关闭套接字close(sfd);return 0;
}//子进程负责与客户端交互的函数
int deal_cli_msg(int newfd, struct sockaddr_in cin)
{char buf[128] = ""; ssize_t res = 0;while(1){ bzero(buf, sizeof(buf));//接收数据res = recv(newfd, buf, sizeof(buf), 0); if(res < 0){ERR_MSG("recv");return -1; }else if(0 == res){printf("[%s:%d] newfd=%d 客户端下线__%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);break;}printf("[%s:%d] newfd=%d : %s __%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, buf, __LINE__);if(strcmp(buf, "quit") == 0)break;//发送数据strcat(buf, "*_*"); //数据可以选择冲终端获取if(send(newfd, buf, sizeof(buf), 0) < 0){ERR_MSG("send");return -1; }printf("send success\n");} return 0;
}
67.2.2 多进程并发服务器
67.2.2.1 代码模型
sfd = socket();
bind();
listen();
while(1){newfd = accept();pthread_create( , , deal_cli_msg, );pthread_detach(tid);
}
close(sfd);void* deal_cli_msg(void* arg){while(1){recv();send(); }close(newfd);pthread_exit();
}
67.2.2.1 代码示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>#define ERR_MSG(msg) do{\fprintf(stderr, "__%d__ ", __LINE__);\perror(msg);\
}while(0)#define PORT 8888 //端口号的网络字节序,1024~49151
#define IP "192.168.125.55" //本机IP,ifconfig//传递给线程执行体的数据封装成结构体
struct Climsg
{int newfd;struct sockaddr_in cin;
};void* deal_cli_msg(void* arg) ; //void* arg = &infoint main(int argc, const char *argv[])
{//创建流式套接字int sfd = socket(AF_INET, SOCK_STREAM, 0);if(sfd < 0){ERR_MSG("socket");return -1;}printf("socket create success sfd=%d\n", sfd);//允许端口快速复用int reuse = 1;if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0){ERR_MSG("setsockopt");return -1;}printf("允许端口快速复用成功\n");//填充地址信息结构体给bind函数绑定使用;//真实的地址信息结构体根据地址族指定,AF_INET:man 7 ipstruct sockaddr_in sin;sin.sin_family = AF_INET; //必须填AF_INET;sin.sin_port = htons(PORT); //端口号的网络字节序,1024~49151sin.sin_addr.s_addr = inet_addr(IP); //本机IP的网络字节序,ifconfig//绑定服务器的地址信息---》必须绑定if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0){ERR_MSG("bind");return -1;}printf("bind success\n");//将套接字转换成被动监听状态if(listen(sfd, 128) < 0){ERR_MSG("listen");return -1;}printf("listen success\n");struct sockaddr_in cin; //存储客户端的地址信息socklen_t addrlen = sizeof(cin);int newfd = -1;pthread_t tid; //存储线程tid号struct Climsg info;while(1){//代码先阻塞在accept函数,再断开连接关闭的文件描述符时//accept函数每次在阻塞的时候,会先预选一个没有被占用的文件描述符//当解除阻塞的时候,若预选的文件描述符没有被占用,则直接返回预选的文件描述符//若预选的文件描述符被占用,则会重新遍历一个没有被使用的文件名描述符返回//主线程只负责连接(accept)newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen);if(newfd < 0){ERR_MSG("accept");return -1;}printf("[%s:%d] newfd=%d 客户端连接成功__%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);info.newfd = newfd;info.cin = cin;//能运行到当前位置,则代表有客户端连接成功,//此时需要创建一个分支线程,专门用于处理客户端的交互if(pthread_create(&tid, NULL, deal_cli_msg, (void*)&info) != 0){fprintf(stderr, "pthread_create failed __%d__\n", __LINE__);return -1;}pthread_detach(tid); //分离线程}//关闭套接字close(sfd);return 0;
}//线程执行体 ---> 分支线程只负责交互
void* deal_cli_msg(void* arg) //void* arg = &info
{//newfd和cin必须另存,每个客户端都有自己独立的通信文件描述符和地址信息。//如果使用全局变量,或者指针方式间接访问,会导致所有线程共用一份newfd和cin,//那么newfd和cin会被覆盖int newfd = ((struct Climsg*)arg)->newfd;struct sockaddr_in cin = ((struct Climsg*)arg)->cin;char buf[128] = "";ssize_t res = 0;while(1){bzero(buf, sizeof(buf));//接收数据res = recv(newfd, buf, sizeof(buf), 0);if(res < 0){ERR_MSG("recv");break;}else if(0 == res){printf("[%s:%d] newfd=%d 客户端下线__%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);break;}printf("[%s:%d] newfd=%d : %s __%d__\n", \inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, buf, __LINE__);if(strcmp(buf, "quit") == 0)break;//发送数据strcat(buf, "*_*"); //数据可以选择冲终端获取if(send(newfd, buf, sizeof(buf), 0) < 0){ERR_MSG("send");break;}printf("send success\n");}close(newfd);pthread_exit(NULL);
}
六十八、IO模型
68.1 阻塞IO
- 最常用,最简单,效率最低的。
- 创建套接字文件描述符后,默认处于阻塞IO模式;
- read, write, recv, send, recvfrom ,sendto,accept
68.2 非阻塞IO
- .防止进程阻塞在IO函数上,当一个程序使用了非阻塞IO模式的套接字,那么它需要使用一个循环来不停的判断该文件描述符是否有数据可读,称之为polling;
- 应用程序不停的polling内核监测IO事件是否产生,cpu消耗率高;
- IO中导致函数阻塞的原因是因为文件描述符有阻塞属性。
read阻塞:文件描述符有阻塞属性:0号文件描述符有阻塞属性 + 读属性---》0号文件描述符加上非阻塞属性
- 修改 IO 为非阻塞方式
1. 先获取0号文件描述符原有属性
2. 在原有属性的基础上将阻塞 设置为 非阻塞
3. 将修改后的属性重新设置回0号文件描述符中
fcntl函数
功能:获取/设置文件描述属性;
原型:#include <unistd.h>#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );
参数:int fd:指定要设置或者获取属性的文件描述符int cmd:F_GETFL (void):获取属性,第三个参数不用填,获取到的属性在返回值返回;F_SETFL (int):设置属性,第三个参数是int类型;
68.3 信号驱动IO
- 异步通信方式;
- 信号驱动IO是指预先告诉内核,使得某个文件描述符发生IO事件的时候,内核会通知相关进程
SIGIO; - 对于TCP而言,信号驱动IO对TCP没有用。因为信号产生过于频繁,而且不能区分是哪个文件描述符发生的。
68.4 IO多路复用(重点!!!)
- 进程中如果同时需要处理多路输入输出流,
- 在无法用多进程多线程,可以选择用IO多路复用;
- 由于不需要创建新的进程和线程,减少系统的资源开销,减少上下文切换的次数。
- 上下文:运行一个进程所需要的所有资源
- 上下文切换:从A进程切换到B进程,A进程的资源要完全替换成B进程的,是一个耗时操作。
- 增加并发量的时候可以使用IO多路复用
- 允许同时对多个IO进行操作,内核一旦发现进程执行一个或多个IO事件,会通知该进程。
68.4.1 select
68.4.1.1 select函数
功能:阻塞函数,让内核监测集合中是否有文件描述符准备就绪,若准备就绪则解除阻塞;当函数解除阻塞后,集合中会只剩下产生事件的文件描述符;例如:0号准备就绪,则集合中只剩下0号sfd准备就绪,则集合中只能下sfd;0和sfd均准备就绪,则0和sfd均存在若不将数据从触发事件的文件描述符对应的空间中取出,此时该文件描述符一直处于就绪状态。
原型:#include <sys/select.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:int nfds:需要填充三个集合中最大的文件描述符编号+1;fd_set *readfds, fd_set *writefds,fd_set *exceptfds:读集合,写,其他集合,若集合不使用,填NULL; 一般只用读集合;struct timeval *timeout:设置超时时间; 1. 若不想设置超时时间,填NULL,则当前函数会一直阻塞,直到集合中有文件描述符准备就绪。2. 设置超时时间; 若时间到后依然没有事件产生,则该函数解除阻塞,且返回失败情况。struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */};
返回值:>0, 成功返回成功触发事件的文件描述符个数;=0, 超时了;失败,返回-1,更新errno;操作集合的函数 void FD_CLR(int fd, fd_set *set); //将fd从集合中删除int FD_ISSET(int fd, fd_set *set); //判断fd是否在集合中void FD_SET(int fd, fd_set *set); //将fd添加到结合中void FD_ZERO(fd_set *set); //清空集合
68.4.1.2 select的TCP模型
sfd = socket();
bind();
listen();
while(1){tempfds = readfds;select(maxfd+1, &tempfds, NULL, NULL, NULL);for(int i=0; i<=maxfd; i++){if(FD_ISSET(i, &tempfds) == 0) continue;if(0 == i){fgets(); }else if(sfd == i){newfd = accept()FD_SET(newfd, &readfds);maxfd = maxfd>newfd?maxfd:newfd; } else{res = recv(i, );if(0 == res){close(i); FD_CLR(i, &readfds);while(!FD_ISSET(maxfd, &readfds) && maxfd-->=0); }send(); }}
}
close(sfd);
68.4.1.3 select的TCP服务器代码
68.4.1.4 select的TCP客户端代码
这俩的代码都在这个链接里面,再放进来就太长了,写文档都在卡
IO多路复用实现TCP客户端与TCP并发服务器
68.4.2 poll
68.4.2.1 poll函数
功能:阻塞函数,阻塞等待集合中有文件描述符准备就绪,若准备就绪,则立即解除阻塞。
原型:#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:struct pollfd *fds:指定要监测的集合struct pollfd {int fd; /* file descriptor */ 指定要监测的文件描述符short events; /* requested events */ 指定要监测的事件short revents; /* returned events */ 实际产生的事件};事件:POLLIN 这里有数据可读POLLOUT 可写POLLERR 错误事件,只有在revents中有效nfds_t nfds:指定要监测的文件描述符的个数;int timeout:超时时间,以ms为单位>0, 设置超时时间,以ms为单位;=0, 不阻塞,即使没有文件描述符准备就绪,该函数不阻塞;<0, 不设置超时时间,一直阻塞,直到当集合中有文件描述符准备就绪,该函数解除阻塞;
返回值:>0, 实际产生事件的文件描述符个数;=0, 超时了=-1,更新errno;
68.4.2.2 poll的TCP客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>#define ERR_MSG(msg) do{\fprintf(stderr, "__%d__ ", __LINE__);\perror(msg);\
}while(0)#define SER_PORT 8888 //服务器绑定的端口号
#define SER_IP "192.168.125.55" //服务器绑定的IP int main(int argc, const char *argv[])
{//创建流式套接字int cfd = socket(AF_INET, SOCK_STREAM, 0);if(cfd < 0){ERR_MSG("socket");return -1;}printf("socket create success cfd=%d\n", cfd);//允许端口快速复用int reuse = 1;if(setsockopt(cfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0){ERR_MSG("setsockopt");return -1;}printf("允许端口快速复用成功\n");//绑定客户端的地址信息---》非必须绑定//客户端若不绑定,则操作系统会自动给客户端绑定本机IP及随机端口//填充服务器的地址信息结构体给connect函数使用;//真实的地址信息结构体根据地址族指定,AF_INET:man 7 ipstruct sockaddr_in sin;sin.sin_family = AF_INET; //必须填AF_INET;sin.sin_port = htons(SER_PORT); //服务器绑定的端口号sin.sin_addr.s_addr = inet_addr(SER_IP); //服务器绑定的IP//连接服务器,想要连接哪个服务器就需要填充哪个服务器绑定的地址信息if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) < 0){ERR_MSG("connect");return -1;}printf("connect server success\n");//创建集合struct pollfd fds[2] = {0};//将需要的文件描述符添加到集合中fds[0].fd = 0; //指定要监测0号文件描述符fds[0].events = POLLIN; //指定要监测读事件fds[1].fd = cfd; //指定要监测cfdfds[1].events = POLLIN;int p_res = 0;char buf[128] = "";ssize_t res = 0;while(1){p_res = poll(fds, sizeof(fds)/sizeof(struct pollfd), -1);if(p_res < 0){ERR_MSG("poll");return -1;}else if(0 == p_res){printf("time out");break;}//能运行到当前位置,则代表集合中有文件描述符准备就绪//即revents成员中有数据了,//判断revents中是否有POLLIN事件if(fds[0].revents & POLLIN){printf("触发键盘输入事件\n");bzero(buf, sizeof(buf));fgets(buf, sizeof(buf), stdin);buf[strlen(buf)-1] = '\0';//发送数据if(send(cfd, buf, sizeof(buf), 0) < 0)//if(write(cfd, buf, sizeof(buf)) < 0){ERR_MSG("send");return -1;}printf("send success\n");}if(fds[1].revents & POLLIN){bzero(buf, sizeof(buf));//接收数据res = recv(cfd, buf, sizeof(buf), 0);//res = read(cfd, buf, sizeof(buf));if(res < 0){ERR_MSG("recv");return -1;}else if(0 == res){printf("[%s:%d] cfd=%d 服务器下线__%d__\n", \SER_IP, SER_PORT, cfd, __LINE__);break;}printf("[%s:%d] cfd=%d :%s __%d__\n", \SER_IP, SER_PORT, cfd, buf, __LINE__);}}//关闭套接字close(cfd);return 0;
}