1.物種I/O模型
- (1)阻塞IO
1)一遞交讀操作,讀操作就阻塞了,直到對方有數據到來;
2)將套接口接收緩衝區拷貝到用戶空間緩衝區buff中,拷貝完成,recv函數就返回了;
- (2)非阻塞IO
1)recv函數即使沒有數據到來,也不會阻塞,因爲把他設置爲了非阻塞模式了
2)EMOULDBLOCK:改正爲:EWOULDBLOCK
3)忙等待:想等待一定的數據,而這些數據又沒有到來,又需要佔用CPU時間片,這種等待稱之爲忙等待
4)不推薦使用這種非阻塞IO模型,應用很窄
5)將套接口設置非阻塞方式
fcntl(fd, F_SETFL, flag|O_NONBLOCK)
-
(3)I/O複用(select和poll)
1)select管理多個文件描述符
2)阻塞的位置提前到了select,一旦監測到事件,recv則不再阻塞了
-
(4)信號驅動IO
1)不經常用
2)調用sigaction來安裝信號處理程序,安裝完畢後,程序可以做其它的事情
3)當數據到來時,以信號的方式通知應用進程,應用進程要在信號處理程序中處理接收數據的細節,使得異步處理得到可能(因爲信號是異步處理的方式,當提交信號後,應用進程可以處理其它事情,直到有數據到來,通過信號處理程序來處理到來的事件)
-
(5)異步IO
1)效率最高
2)會提交一個應用層的緩衝區buf,即使內核沒有數據,也會立刻返回,一旦返回,應用進程就處理其它的事情,達到了異步處理的能力;
當有數據到來,內核會將數據自動拷貝到應用層buff緩衝區,一旦複製完成,會通過信號等方式來通知應用進程的程序,這取決於aio_read的實現(大多數的系統實現,或多或少會有些問題,與windows的完成端口IOCP基本一樣)
3)異步IO與信號驅動IO的區別
異步IO是推的機制,
一旦內核接收到數據,會主動的將數據從內核返還給用戶的buf緩衝區,用戶空間沒有必要調用recv方法來接收他
信號驅動IO是拉的機制。
一旦有數據到來,僅僅是通過SIGIO信號通知應用進程,應用進程還得調用recv函數將數據從內核空間拉到用戶空間中
拉比推,可能多一些延遲
2.select
- select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);將文件描述符從集合set中移除;
int FD_ISSET(int fd, fd_set *set);判斷fd是否在集合set中?
void FD_SET(int fd, fd_set *set);將fd添加到集合set中
void FD_ZERO(fd_set *set);清空fd集合
nfds:讀,寫,異常集合中的文件描述符的最大值+1
可讀集合:檢測到有數據可讀的套接口,放到readfds集合中,一旦有數據可讀,select就可以返回。
IO中的緩衝區有數據了,就會產生可讀事件。
若關心某個IO的可讀事件,就將其fd放到readfds集合中。
是輸入輸出參數。
可寫集合:是輸入輸出參數。
異常集合:NULL;
是輸入輸出參數。
超時時間:若有超時時間,在超時時間到來時,若沒有監測到任何一個事件也可以返回,返回事件個數爲0,select失敗返回-1;
NULL表示不會超時,表示一定要檢測到某個事件纔會返回
是輸入輸出參數。
- select可以管理多個文件描述符,也就是管理多個IO。
eg:select會監測1,2,3,4,5。。等io,如果select監測到了其中的某些IO發生了事件,select就會立刻返回,並將感興趣的事件填充到readfds中,並且返回事件的個數,通過輪詢這些事件,一個個來處理,這時候處理基本不會阻塞了,因爲select提前阻塞了,他的返回意味着事件已經到來 - select是個管理者,是個中心
(1)用select來管理多個IO,一旦其中的一個I/O或者多個I/O檢測到我們所感興趣的事件,select函數會返回,返回值爲檢測到的事件的個數
(2)並且返回哪些I/O發生了事件,遍歷這些事件,進而處理事件
(3)與信號集的集合操作函數類似
3.用select改進回射客戶端程序
- eg:客戶端:NetworkProgramming-master (1)\LinuxNetworkProgramming\P13echocli6.cpp
服務端還是:NetworkProgramming-master (1)\LinuxNetworkProgramming\P11echo_srv.c
//
// Created by wangji on 19-8-6.
//
// socket編程 8 select模型
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
struct packet
{
int len;
char buf[1024];
};
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; // 剩餘字節數
ssize_t nread;
char *bufp = (char*) buf;
while (nleft > 0)
{
nread = read(fd, bufp, nleft);
if (nread < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
} else if (nread == 0)
{
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
{
continue;
}
return -1;
}
else if (nwritten == 0)
{
continue;
}
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK); // 查看傳入消息
if (ret == -1 && errno == EINTR)
{
continue;
}
return ret;
}
}
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = (char*)buf; // 當前指針位置
int nleft = maxline;
while (1)
{
ret = recv_peek(sockfd, buf, nleft);
if (ret < 0)
{
return ret;
}
else if (ret == 0)
{
return ret;
}
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if (ret != i+1)
{
exit(EXIT_FAILURE);
}
return ret;
}
}
if (nread > nleft)
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void ehco_cli(int sockfd)
{
// char recvbuf[1024];
// char sendbuf[1024];
// // struct packet recvbuf;
// // struct packet sendbuf;
// memset(recvbuf, 0, sizeof recvbuf);
// memset(sendbuf, 0, sizeof sendbuf);
// int n = 0;
// while (fgets(sendbuf, sizeof sendbuf, stdin) != NULL) // 鍵盤輸入獲取
// {
// writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
//
// int ret = readline(sockfd, recvbuf, sizeof recvbuf); // 服務器讀取
// if (ret == -1)
// {
// ERR_EXIT("readline");
// }
// if (ret == 0)
// {
// printf("server close\n");
// break;
// }
//
// fputs(recvbuf, stdout); // 服務器返回數據輸出
//
// // 清空
// memset(recvbuf, 0, sizeof recvbuf);
// memset(sendbuf, 0, sizeof sendbuf);
// }
fd_set rset;
FD_ZERO(&rset);
int nready;//檢測到的事件個數
int maxfd;
int fd_stdin = fileno(stdin);//fileno:獲取標準輸入的文件描述符
if (fd_stdin > sockfd)
{
maxfd = fd_stdin;
} else {
maxfd = sockfd;
}
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
//標準輸入IO和網絡IO
while (1)//檢測套接口是否有可讀事件,檢測標準輸入是否有可讀事件
{
FD_SET(fd_stdin, &rset);//將fd_stdin放到讀的集合rset中,關心fd_stdin文件描述符的事件
FD_SET(sockfd, &rset);//將sockfd放到讀的集合rset中
nready = select(maxfd+1, &rset, NULL, NULL, NULL);//讀集合中最大文件描述+1
//到底是哪一個套接口檢測到事件?rset集合是會改變的!!返回的是哪一些io或者套接口檢測到了事件
//說明rset是輸入輸出參數
if (nready == -1)
{
ERR_EXIT("select");
}
if (nready == 0)
{
continue;
}
//判斷標準輸入產生的可讀事件,還是套接口產生的可讀事件?
if (FD_ISSET(sockfd, &rset)) // 套接口產生的可讀事件
{
int ret = readline(sockfd, recvbuf, sizeof(recvbuf)); // 服務器讀取
if (ret == -1)
{
ERR_EXIT("readline");
}
if (ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout); // 服務器返回數據輸出
memset(recvbuf, 0, sizeof(recvbuf));
}
if (FD_ISSET(fd_stdin, &rset))//判斷標準輸入產生的可讀事件
{
if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) //等於NULL,說明客戶端已經得到一個EOF結束符
{
break;
}
writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
memset(sendbuf, 0, sizeof(sendbuf));
}
}
close(sockfd);
};
void handle_sigchld(int sig)
{
// wait(NULL);
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char** argv) {
// signal(SIGCHLD, SIG_IGN);
signal(SIGCHLD, handle_sigchld);
// 1. 創建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
// 3. 請求鏈接
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
ERR_EXIT("connect");
}
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof localaddr;
if (getsockname(sockfd, (struct sockaddr*)&localaddr, &addrlen) < 0)
{
ERR_EXIT("getsockname");
}
printf("id = %s, ", inet_ntoa(localaddr.sin_addr));
printf("port = %d\n", ntohs(localaddr.sin_port));
// 4. 數據交換
ehco_cli(sockfd);
// 5. 斷開連接
close(sockfd);
return 0;
}
- 測試:
不再有FIN_WAIT2和CLOSE_WAIT狀態了,服務器段的FIN_WAIT狀態已經變成了TIME_WAIT狀態了,客戶端的CLOSE_WAIT狀態向前推進,變成了cloes的狀態,其結束了,就不再顯示出來了
- select優點
本質上用單進程的方式處理IO比較方便,單進程可以輪詢事件,然後對這些事件進行處理