一、服務器
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){perror("connect fail");return 1;}pid_t pid = fork();if (pid > 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 < 0){perror("fail accept");return 1;}FD_SET(connfd, &rdfds);maxs = maxs > 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返回檢測到的事件結果;
3.在數據量比較小的時候,select的比epoll的性能差不多,甚至更好,更小的時候,優勢體現不明顯
對于IO:如果處理的任務有耗時任務,此時應該考慮增加進線程,把耗時的任務給進線程去做
并發服務器的性能對比
線程池+epoll?
對于IO:如果處理的任務有耗時任務,此時應該考慮增加進線程,把耗時的任務給進線程去做