1、五種I/O模型
五種I/O模型分別是:
- 阻塞I/O
- 非阻塞I/O
- I/O複用(select/poll)
- 信號驅動I/O
- 異步I/O
0.1 阻塞I/O
連接完成,我們通常使用recv函數讀取數據,如果此時沒有數據,那麼該函數阻塞,直到對方發送的數據到達套接口緩衝區,此時該函數將數據複製到用戶緩衝區中,並返回!
0.2 非阻塞I/O
連接完成,調用recv函數,並將套接口設置爲非阻塞模式(fcntl(sk_fd,F_SETFL,flag|O_NONBLOCK));此時,即使沒有數據到來,該函數也不會阻塞,它會返回一個錯誤-1,errno爲EWOULDBLOCK,此時需要再次提交請求,一直這樣循環,直到有數據到來,才複製數據到用戶緩衝區!很少使用這種方式!
0.3 I/O 複用
連接完成後,使用select來管理所有的I/O,一旦其中的一個IO或這多個IO發生了我們感興趣的事件,select函數就返回,返回值爲檢測到的個數,並且會告訴我們哪些IO發生了事件,事件發生後我們再調用recv去複製數據到用戶緩衝區,就不會發生阻塞的情況!這種方法經常使用!
0.4 信號驅動I/O
在程序中建立一個處理SIGIO的處理程序,捕獲該信號;程序可繼續處理其他事情,一旦有數據到來,會以信號的方式通知程序,然後應用程序就跳轉到信號處理程序中,通過recv函數複製數據到用戶緩衝區!這種方式也較少使用!
0.5 異步I/O
這種方式效率是比較高的,它通過aio_read函數來實現,調用該函數,它會遞交一個用戶緩衝區給操作系統,就算沒有數據,也立即返回,一旦有數據到來,操作系統內核會自動將數據拷貝到用戶緩衝區中,然後通過一個信號通知應用程序。這種方式相當於內核在自動“推送”數據!
2、使用I/O 複用–select
select函數使用示例
客戶端程序
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <sys/select.h>
#define handle_error(msg) \
do{perror(msg);exit(EXIT_FAILURE);}while(0)
ssize_t readn(int fd, void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩餘字節數
ssize_t nread = 0; //已讀字節數
char *pbuf = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if (nread == 0)
return count - nleft;
pbuf += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩餘字節數
ssize_t nwritten = 0; //已發送字節數
char *pbuf = (char*)buf;
while (nleft > 0)
{
if((nwritten = write(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue;
pbuf += nwritten;
nleft -= nwritten;
}
return count;
}
//使用recv函數從套接字接收緩衝區中接收數據,但並不從緩衝去中清除數據
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while(true)
{
int iret = recv(sockfd, buf, len, MSG_PEEK);
if(iret == -1 && errno == EINTR) //如果失敗是因爲信號中端,那麼就重新再試
continue;
return iret;
}
}
ssize_t recvline(int sockfd, void *buf, size_t maxlen)
{
int iret = 0;
int nread = 0; //已讀數據
char *pbuf = (char*)buf;
int nleft = maxlen; //剩餘字符
while(true)
{
iret = recv_peek(sockfd, pbuf, nleft);
if(iret < 0)
return iret; //讀取失敗
else if(iret == 0)
return iret; //對方關閉套接口
nread = iret;
if(nread > nleft) //已經讀取到的數據只可能小於或者等於剩餘的數據
exit(EXIT_FAILURE);
for(int i = 0; i < nread; i++)
{
if(pbuf[i] == '\n')
{
iret = readn(sockfd, pbuf, i+1); //從緩衝區中讀走包括\n在內的數據
if(iret != i+1)
exit(EXIT_FAILURE); //沒有讀取都i+1個數據,說明失敗
return iret; //讀取都\n返回
}
}
//在當前讀到的數據中沒有發現\n,那麼先將這部分數據從緩衝區中讀走,然後接着偷窺後面的數據
nleft -= nread;
iret = readn(sockfd, pbuf, nread);
if(iret != nread)
exit(EXIT_FAILURE);
pbuf += nread;
}
return -1;
}
//select可看做一個管理者,可用它來管理多個IO,
//一旦其中的一個IO或這多個IO發生了我們感興趣的事件,select函數就返回,
//返回值爲檢測到的個數,並且會告訴我們哪些IO發生了事件
//參數:
//1、讀寫異常集合中的文件描述父的最大值+1
//讀集合,輸入輸出參數
//寫集合,輸入輸出參數
//異常集合,輸入輸出參數
//超時結構體,設置此結構體後,可設定讓select函數返回的超時時間
void client_handler(int sk_fd)
{
fd_set readset;
FD_ZERO(&readset);
int nready = 0;
int fd_stdin = fileno(stdin); //獲得標準輸入的描述符,防止輸入重定向
int fd_max = fd_stdin > sk_fd ? fd_stdin : sk_fd;
while(true)
{
FD_SET(fd_stdin, &readset);
FD_SET(sk_fd, &readset); //readset集合可能會改變,所以每次都要添加
nready = select(fd_max + 1, &readset, NULL, NULL, NULL);
if(nready == -1)
handle_error("select");
if(nready == 0)
continue;
if(FD_ISSET(sk_fd, &readset)) //套接口產生了讀事件
{
char recvbuf[1024] = {0};
int iret = recvline(sk_fd, recvbuf, sizeof(recvbuf)); //接收包數據長度
if(iret == -1)
handle_error("read");
else if(iret == 0)
{
printf("Server was closed!\n");
break;
}
fputs(recvbuf, stdout);
memset(&recvbuf, 0, sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin, &readset)) //標準輸入產生讀事件
{
char sendbuf[1024] = {0};
if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
break;
writen(sk_fd, sendbuf, strlen(sendbuf)); //發送數據
memset(&sendbuf, 0, sizeof(sendbuf));
}
}
}
int main(void)
{
int sk_fd;
sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
//sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);
if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("connect");
}
client_handler(sk_fd);
close(sk_fd);
return 0;
}
服務端程序
server.c
編譯命令:gcc -Wall -g -std=gnu99 server.c -o server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#define handle_error(msg) \
do{perror(msg);exit(EXIT_FAILURE);}while(0)
//忽略子進程的SIG_CHLD信號
void handle_SIGIGN(void)
{
struct sigaction act_chld;
act_chld.sa_handler = SIG_IGN;
act_chld.sa_flags = 0;
sigemptyset(&act_chld.sa_mask);
if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕獲終端中斷信號
handle_error("sigaction SIGCHLD");
}
void wait_sigchld(int sig)
{
wait(NULL);
}
//調用wait函數處理該信號
void handle_wait(void)
{
struct sigaction act_chld;
act_chld.sa_handler = wait_sigchld;
act_chld.sa_flags = 0;
sigemptyset(&act_chld.sa_mask);
if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕獲終端中斷信號
handle_error("sigaction SIGCHLD");
}
void waitpid_sigchld(int sig)
{
while( waitpid(-1,NULL,WNOHANG) >= 0); //不將執行掛起
}
//調用wait函數處理該信號
void handle_waitpid(void)
{
struct sigaction act_chld;
act_chld.sa_handler = waitpid_sigchld;
act_chld.sa_flags = 0;
sigemptyset(&act_chld.sa_mask);
if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕獲終端中斷信號
handle_error("sigaction SIGCHLD");
}
ssize_t readn(int fd, void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩餘字節數
ssize_t nread = 0; //已讀字節數
char *pbuf = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if (nread == 0)
return count - nleft;
pbuf += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩餘字節數
ssize_t nwritten = 0; //已發送字節數
char *pbuf = (char*)buf;
while (nleft > 0)
{
if((nwritten = write(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue;
pbuf += nwritten;
nleft -= nwritten;
}
return count;
}
//使用recv函數從套接字接收緩衝區中接收數據,但並不從緩衝去中清除數據
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while(true)
{
int iret = recv(sockfd, buf, len, MSG_PEEK);
if(iret == -1 && errno == EINTR) //如果失敗是因爲信號中端,那麼就重新再試
continue;
return iret;
}
}
//如果讀到的數據包含\n,則返回,表示一條完整的消息讀取完畢
ssize_t recvline(int sockfd, void *buf, size_t maxlen)
{
int iret = 0;
int nread = 0; //已讀數據
char *pbuf = (char*)buf;
int nleft = maxlen; //剩餘字符
while(true)
{
iret = recv_peek(sockfd, pbuf, nleft);
if(iret < 0)
return iret; //讀取失敗
else if(iret == 0)
return iret; //對方關閉套接口
nread = iret;
if(nread > nleft) //已經讀取到的數據只可能小於或者等於剩餘的數據
exit(EXIT_FAILURE);
for(int i = 0; i < nread; i++)
{
if(pbuf[i] == '\n')
{
iret = readn(sockfd, pbuf, i+1); //從緩衝區中讀走包括\n在內的數據
if(iret != i+1)
exit(EXIT_FAILURE); //沒有讀取都i+1個數據,說明失敗
return iret; //讀取都\n返回
}
}
//在當前讀到的數據中沒有發現\n,那麼先將這部分數據從緩衝區中讀走,然後接着偷窺後面的數據
nleft -= nread;
iret = readn(sockfd, pbuf, nread);
if(iret != nread)
exit(EXIT_FAILURE);
pbuf += nread;
}
return -1;
}
void do_work(int sock);
int main(void)
{
//三種殭屍進程處理方式
//handle_SIGIGN();
//handle_wait();
handle_waitpid();
int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
//使用REUSEADDR,不必等待TIME_WAIT 狀態消失,就可以重新使用端口
int on = 1;
if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
close(sk_fd);
handle_error("setsockopt");
}
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);
if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("bind");
}
//被動套接字
if(listen(sk_fd, SOMAXCONN) < 0) //內核爲此套接字排隊的最大連接數由SOMAXCONN宏指定
{
close(sk_fd);
handle_error("listen");
}
struct sockaddr_in cl_addr;
socklen_t cl_length = sizeof(cl_addr);
pid_t pid;
while (true)
{
memset(&cl_addr,0,sizeof(cl_addr));
int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
if(ac_sk < 0)
{
if(errno == EINTR)
continue;
close(sk_fd);
handle_error("accept");
}
printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));
//每個客戶端對應一個子進程
pid = fork();
if(pid == -1)
handle_error("fork");
if(pid == 0)
{
close(sk_fd);
do_work(ac_sk);
close(ac_sk);
exit(EXIT_SUCCESS);
}
else
close(ac_sk);
}
close(sk_fd);
return 0;
}
void do_work(int sock)
{
char recvbuf[1024];
while(true)
{
memset(recvbuf,0,sizeof(recvbuf));
int iret = recvline(sock,recvbuf,sizeof(recvbuf)); //獲取包數據長度
if(iret == -1)
handle_error("read");
else if(iret == 0)
{
printf("Client was closed!\n");
break;
}
fputs(recvbuf,stdout);
writen(sock, recvbuf, strlen(recvbuf)); //回傳數據
}
}