day33:零基础学嵌入式之网络——TCP并发服务器
一、服务器
1.服务器分类
- 单循环服务器:只能处理一个客户端任务的服务器
- 并发服务器:可同时处理多个客户端任务的服务器
二、TCP并发服务器的构建
1.如何构建?
(1)多进程(每一次创建都非常耗时耗空间,但是安全)
#include \"head.h\"int init_tcp(const char *ip, unsigned short port){ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror(\"socket fail\"); return 1; } struct sockaddr_in seraddr; seraddr.sin_family = AF_INET; seraddr.sin_port = htons(port); seraddr.sin_addr.s_addr = inet_addr(ip); int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) { perror(\"bind fail\"); return 1; } ret = listen(sockfd, 100); if (ret < 0) { perror(\"lisen fail\"); return 1; } return sockfd;}void do_wait(int signo){ wait(NULL);}int main(int argc, char const *argv[]){ int sockfd = init_tcp(\"192.168.1.138\", 50000); if (sockfd < 0) { return 1; } signal(SIGCHLD, do_wait); char buf[1024] = {0}; struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); while (1) { int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd 0) { } else if (0 == pid) { while (1) { memset(buf, 0, sizeof(buf)); ssize_t size = recv(connfd, buf, sizeof(buf), 0); if (size < 0) { perror(\"recv fail\"); break; } else if (0 == size) { printf(\"client connet offline\"); break; } printf(\"[%s] [%d] : %s\\n\", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buf); strcat(buf, \"------ok\"); size = send(connfd, buf, sizeof(buf), 0); if (size < 0) { perror(\"fail send\"); break; } } close(connfd); exit(1); } else { perror(\"fork fail\"); return 1; } } close(sockfd); return 0;}
(2)多线程(并发程度高、不太安全)
#include \"head.h\"int init_tcp(const char *ip, unsigned short port){ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror(\"socket fail\"); return 1; } struct sockaddr_in seraddr; seraddr.sin_family = AF_INET; seraddr.sin_port = htons(port); seraddr.sin_addr.s_addr = inet_addr(ip); int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) { perror(\"bind fail\"); return 1; } ret = listen(sockfd, 100); if (ret < 0) { perror(\"lisen fail\"); return 1; } return sockfd;}typedef struct{ int connfd; struct sockaddr_in cliaddr;} XIN;void do_thurance(void *arg){ XIN xi = *(XIN *)arg; char buf[1024] = {0}; while (1) { memset(buf, 0, sizeof(buf)); ssize_t size = recv(xi.connfd, buf, sizeof(buf), 0); if (size < 0) { perror(\"recv fail\"); break; } else if (0 == size) { printf(\"client connet offline\"); break; } printf(\"[%s] [%d] : %s\\n\", inet_ntoa(xi.cliaddr.sin_addr), ntohs(xi.cliaddr.sin_port), buf); strcat(buf, \"------ok\"); size = send(xi.connfd, buf, sizeof(buf), 0); if (size < 0) { perror(\"fail send\"); break; } } close(xi.connfd); pthread_exit(NULL);}int main(int argc, char const *argv[]){ int sockfd = init_tcp(\"192.168.1.138\", 50000); if (sockfd < 0) { return 1; } char buf[1024] = {0}; pthread_t tid; struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); while (1) { int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { perror(\"connect fail\"); return 1; } printf(\"client getline\\n\"); XIN xi; xi.connfd = connfd; xi.cliaddr = cliaddr; pthread_create(&tid, NULL, do_thurance, &xi); pthread_detach(tid); //设置分离属性,线程结束,操作系统自动会回收; } close(sockfd); return 0;}
(3)线程池
- 主要解决:程序运行过程中,线程被反复创建和销毁带来的耗时问题;
(4)IO多路复用
理解:不创建进程和线程的情况下,对多个文件描述符监测复用一个进程;
二、IO多路复用
1.阻塞IO方式:
(1)多个IO之间是同步关系;
(2)多个IO之间相互影响;
2.IO多路复用
(1)步骤
1)创建文件描述符集合(数组、链表、树形结构.......);
2)添加关注的文件描述符带集合中;
3)通过函数接口,把集合传递给内核,并开始检测IO事件(输入输出、读写事件);
4)当内核检测到事件时,通过相关函数返回,做具体的相关操作;
(2)select
1)创建文件描述符集合表:fd_set
2)清楚集合表
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);//把集合表整体清空
3)把文件描述符加入到集合表中
4)select:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:通知内核检测的集合表并开始检测
参数:
nfds:关注的最大描述符+1
readfds:关注的读事件的我文件描述符的地址
writefds:关注的写事件的我文件描述符的地址
exceptfds:其他事件
timeout:超时事件的地址;设置一个时间结点,如果都没有事件来,就直接返回;NULL:不设置超时时间
返回值:
成功:返回到达事件的个数
失败:-1
超时时间到达没有事件时:0
位图在内核中,保持最小未被使用原则
#include \"head.h\"int init_tcp(const char *ip, unsigned short port){ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror(\"socket fail\"); return 1; } struct sockaddr_in seraddr; seraddr.sin_family = AF_INET; seraddr.sin_port = htons(port); seraddr.sin_addr.s_addr = inet_addr(ip); int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) { perror(\"bind fail\"); return 1; } ret = listen(sockfd, 100); if (ret < 0) { perror(\"lisen fail\"); return 1; } return sockfd;}int main(int argc, char const *argv[]){ int sockfd = init_tcp(\"192.168.1.138\", 50002); if (sockfd < 0) { return 1; } struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); int maxs; fd_set rdfds; fd_set tmprdfds; FD_ZERO(&rdfds); FD_SET(sockfd, &rdfds); int i = 0; maxs = sockfd; char buf[1024]={0}; while (1) { tmprdfds = rdfds; int cnt = select(maxs + 1, &tmprdfds, NULL, NULL, NULL); if (cnt < 0) { perror(\"fail select\"); return 1; } if (FD_ISSET(sockfd, &tmprdfds)) { int connfd = accept(sockfd,(struct sockaddr*)&cliaddr, &clilen); if (connfd connfd ? maxs : connfd; } // for(i=sockfd;i<maxs+1;++i) // { // printf(\"%d\\n\",i); // } // sleep(3); for (i = sockfd + 1; i < maxs + 1; ++i) { if (FD_ISSET(i, &tmprdfds)) { memset(buf, 0, sizeof(buf)); ssize_t size = recv(i, buf, sizeof(buf), 0); if (size < 0) { perror(\"recv fail\"); continue; } if(0==size) { printf(\"client offlink\\n\"); return 1; } printf(\"[%s] [%d] : %s\\n\", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buf); strcat(buf, \"------ok\"); size = send(i, buf, sizeof(buf), 0); if (size < 0) { perror(\"fail send\"); continue; } } } } close(sockfd); return 0;}
缺陷:
- 限制了最多只能检测1024个文件描述符(底层使用数组的机制储存);
- 在应用层每次需要遍历才可找到到达的事件的文件描述符,效率不高,还耗时;
- 集合表存在于应用层,内核存在应用层和内核层的数据表的反复拷贝;
- select只能工作在水平触发模式(低速模式),不能工作在边沿触发模式(高速模式);
边沿触发:数据从无变有,从低电平到高电平,触发一次,称读数据的上升沿触发;
数据一次收不完,但是下一次继续读,
水平触发:数据从无到有,先触发一次读,没读完,再触发读,一直到读完了,才不触发;优势在反复把数据读完;缺点:耗时,低俗模式
(3)poll
1)解决的问题:检测的文件描述符个数不受1024限制;底层对于集合表的方式改变,变成了链表,时间复杂度O(n),其他问题未被改善,仍然需要反复拷贝、遍历、只可工作在水平触发模式;
(4)epoll
1)解决的问题:检测的文件描述符是树形结构;时间复杂度是O(log(n)【红黑树】,也不受1024限制;将检测的文件描述符集合创建在内核,解决了内核和用户层的数据拷贝;直接返回到达事件的文件描述符集合,不需要遍历寻找;epoll可以工作在水平触摸式,也可工作在边沿触发模式;
2)步骤
a)创建文件描述符集合表;
int epoll_create(int size);
功能:创建文件描述符集合表到内核
参数:
size:最多监测的文件描述符的个数
返回值:
成功返回非负的文件描述符,代表了内核的集合;
失败返回-1
b)添加关注的文件描述符到集合;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:对文件描述符进行操作;
参数:
epfd:文件描述符集合表的文件描述符
op:
EPOLL_CTL_ADD 新增事件
EPOLL_CTL_MOD 修改事件
EPOLL_CTL_DEL 删除事件
fd:要操作的文件描述符
events:事件相关结构体
EPOLLIN 读事件
EPOLLOUT 写事件
EPOLLET 边沿触发
LT 水平触发
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
返回值:
成功返回0;
失败返回-1;
c)epoll通知内核开始检测;
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能:监听事件表中的事件,并将产生的事件存放到结构体数组中
参数:
epfd:事件表文件描述符
events:存放结果事件结构体数组空间首地址
maxevents:最多存放事件个数
timeout:超时时间
-1:阻塞等待直到有事件发生
返回值:
成功返回产生事件个数
失败返回-1
d)epoll返回检测到的事件结果;