Socket學習四
阿新 • • 發佈:2018-12-30
Socket 的五種I/O模型
- 阻塞I/O、非阻塞I/O、I/O複用、訊號驅動I/O、非同步I/O
- 現在用的最多的是I/O複用和非同步I/O
- 阻塞I/O
- 在這之前我們所用的套介面I/O模型都是阻塞I/O的方式來進行通訊的。一旦套介面接收完成後,我們就可以接收資料。此時像系統提交一個recv請求接收資料,即阻塞在這裡,直到對等方傳送資料填充了recv的接收緩衝區,阻塞解除。然後這些資料就會從緩衝區被複制到使用者空間中即 buf中,此時recv返回,這是侯我們就可以對返回得到的資料進行處理了。
- 非阻塞I/O
- 將套介面設定為非阻塞可以用 fcntl(fd, F_SETFD, flag|O_NONBLOCK),此時即使recv沒有收到資料也不會阻塞,它會返回一個錯誤,返回值為 -1,但是需要不斷地判斷是否有資料到來,這種等待我們稱之為忙等待。後面的處理和阻塞的處理是一樣的。這種最不推薦使用,極大的浪費了CPU資源
- 這兩種模式走了兩種極端,前者死等,後者直接不等,但是在不斷地輪詢等待著資料。
- 那麼有沒有這麼一種機制,集中管理檔案描述符。一旦檔案描述符的狀態發生變化,就是某一個socket的資料到來的時候,這個機制會告訴我們它的狀態發生變化了,然後才取讀資料。這樣就不用輪詢或者阻塞了。
- 答案是必定的。I/O複用就是這樣的機制。
- 這種模型主要是通過 select 函式來實現的。
- 思想:用 select 函式管理多個描述符。每當其中的一個檔案描述符的狀態發生變化時,即資料到來時,select就返回,這時候再呼叫rev函式時就不會阻塞了,就可以把資料從核心空間複製到使用者空間。其實時將阻塞提前到了select函式這裡。
- 訊號驅動I/O
- 非同步I/O
- 非同步 I/O, 呼叫 aio_read 函式 ,並提交一個緩衝區buf,即使核心中沒有資料到來,這個函式也會立刻返回,一旦返回,應用程序就可以處理其他的事情。當有資料到來時,核心會自動地將資料複製到使用者空間,複製完成後,會通過訊號來通知應用程序的程式來處理資料。但是這也需要一定的機制來來通知上層應用,比如 aio_read 中指定的訊號 SIGIO 也可能是其他的機制,這取決與 aio_read 的內部實現。因為不同的 aio_read 實現的方式可能不太一樣,而且非同步 I/O在大部分系統中或多或少都有一定的問題,所以非同步 I/O也沒有得到很好的推廣。
- 與訊號驅動的區別時:前者時拉訊號,後者是核心主動將訊號複製給使用者
- select模型:
- 函式原型:int select(int n, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout);
- 返回值:失敗返回-1,成功返回檔案描述符的個數,超時沒檢測到檔案描述符返回0
- 引數解析:
-
fd:讀、寫、異常集合描述符的最大值+1
- readfds 讀集合:監視readfds來檢視是否read的時候會被堵塞,注意,即便到了end-of-file,fd也是可讀的。
- writefds 寫集合:監視writefds看寫的時候會不會被堵塞。
- 監視exceptfd是否出現了異常。主要用來讀取OOB資料,異常並不是指出錯。
- 注意當一個套接口出錯時,它會變得既可讀又可寫。
如果有了狀態改變,會將其他fd清零,只有那些發生改變了的fd保持置位,以用來指示set中的哪一個改變了狀態。引數n是所有set裡所有fd裡,具有最大值的那個fd的值加1 - fd_set 異常集合
四個巨集用來對fd_set進行操作:
FD_CLR(int fd, fd_set set); //將檔案描述從集合中移除
FD_ISSET(int fd, fd_set set); //判斷檔案描述符是否在集合中
FD_SET(int fd, fd_set set); //將檔案描述符新增到集合中
FD_ZERO(fd_set set); //清空集合 -
time_out 超時結構體
- timeout是從呼叫開始到select返回前,會經歷的最大等待時間。
- 兩種特殊情況:如果為值為0,會立刻返回。
- 如果timeout是NULL,會阻塞式等待。
-
struct timeval {
long tv_sec; / seconds /
long tv_usec; / microseconds /
}; -
一些呼叫使用3個空的set, n為zero, 一個非空的timeout來達到較為精確的sleep.
- Linux中, select函式改變了timeout值,用來指示還剩下的時間,但很多實現並不改timeout。
-
為了較好的可移植性,timeout在迴圈中需要被重新賦初值。
-
timeout== NULL
- 無限等待
- 被訊號打斷時返回1, errno 設定成 EINTR
- timeout->tv_sec == 0 && tvptr->tv_usec == 0
- 不等待立即返回
- timeout->tv_sec != 0 || tvptr->tv_usec != 0
- 等待特定時間長度, 超時返回0
- 通過前面的程式碼我們可以知道,客戶端斷開會阻塞在TIME_WAIT_2,無法繼續向下推進到 TIME_WAIT 狀態???在下面我們用select模型來解決這個問題。
- 思想:
- select 作為管理者
- 用 select 管理多個I/O;一旦其中的一個或者多個I/O檢測到我們所感興趣的事件,select 函式返回,返回值為檢測到的事件個數。並且返回了那些I/O。
- 遍歷這些事件,進而處理這些事件
客戶端
伺服器程式碼見前面socket學習三
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
ssize_t readn(int fd,void *buf,size_t count)
{
//ssize_t=int,size_t=unsigned int
//接收count個位元組數
size_t nleft=count;//剩餘位元組數
ssize_t nread;//已經接收位元組數
char *bufp=(char *)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)//被中斷
continue;
return -1;
}
else if(nread==0)//對等方關閉
return count-nleft;//讀到EOF,對方關閉
bufp+=nread;
nleft-=nread;
}
return count;
}
ssize_t writen(int fd,void *buf,size_t count)
{
size_t nleft=count;//剩餘傳送位元組數
ssize_t nwritten;//已經發送位元組數
char *bufp=(char *)buf;
while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)//被中斷
continue;
return -1;
}
else if(nwritten==0)//對等方關閉
continue;//讀到EOF,對方關閉
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)//操作被訊號中斷,recv認為連結正常,繼續偷窺
continue;
return ret;//返回讀取的位元組數
}
}
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
int ret;
int nread;
char *bufp=buf;
int nleft=maxline;//讀取遇到\n返回,不會超過maxline
while(1)
{
ret=recv_peek(sockfd,bufp,nleft);
if(ret<=0)
return ret;
nread=ret;
//接下來判斷接收的緩衝區是否有\n
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) //偷窺到的資料不能大於maxline
{
exit(EXIT_FAILURE);
}
nleft-=nread;//剩餘位元組數
ret=readn(sockfd,bufp,nread);//將nread資料從緩衝區移除
if(ret!=nread)
exit(EXIT_FAILURE);
bufp+=nread;//繼續偷窺
}
return 1;
}
void echo_client(int sock)
{
/* char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//writen(sock,&sendbuf,strlen(sendbuf));//將接收到的資料傳送出去
//為檢驗 SIGPIPE 訊號
writen(sock, sendbuf, 1);
writen(sock,sendbuf+1,strlen(sendbuf)-1);
int ret=readline(sock,recvbuf,sizeof(recvbuf));//函式從開啟的檔案,裝置中讀取資料
if(ret==-1)
{
printf("readline err");
}
else if(ret==0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//傳送資料到檔案
memset(sendbuf,0,sizeof(sendbuf));
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock);
*/
fd_set rset; //建立一個集合
FD_ZERO(&rset); //初始化集合
int nready; //表示見到的檔案描述符個數
int maxfd;
int fd_stdin = fileno(stdin);//為什麼這裡不用STD_FILENO,因為這個巨集的值為0;但是我們不能保證這個標準輸入是否被重定向,導致這個檔案描述符可能部位0
if(fd_stdin > sock)
maxfd = fd_stdin;
else
maxfd = sock;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(1)
{
FD_SET(fd_stdin, &rset);
FD_SET(sock, &rset);
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1)
printf("select err\n");
if(nready == 0)
continue;
if(FD_ISSET(sock, &rset)) //判斷檢測到sock(可讀)事件是否在集合中
{
int ret=readline(sock,recvbuf,sizeof(recvbuf));//函式從開啟的檔案,裝置中讀取資料
if(ret==-1)
{
printf("readline err");
}
else if(ret==0)
{
printf("server is closed\n");
break;
}
fputs(recvbuf,stdout);//傳送資料到檔案
memset(recvbuf,0,sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin, &rset)) //判斷檢測到fd_ISSET(寫入)事件是否在集合中
{
if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
writen(sock,&sendbuf,strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
else
break;
}
}
close(sock);
}
void handle_sigpipe(int sig)
{
printf("recv a sig=%d\n", sig);
}
int main()
{
//signal(SIGPIPE, handle_sigpipe);
signal(SIGPIPE, SIG_IGN);
int sock;
/*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
printf("socket err\n");
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(8001);//埠,主機轉網路
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
printf("connect err\n");
//獲取本地埠號、IP
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
printf("getsockname err\n");
printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
echo_client(sock);
return 0;
}
- 執行伺服器與客戶端,然後將斷掉伺服器與客戶端的連線,結果如下:
- 可以看到,客戶端直接進入到了TIME_WAIT 狀態,並沒有阻塞在TIME_WAIT_2狀態。
-
讀、寫、異常事件發生的條件
- 可讀:
- 套介面緩衝區有資料可讀(對等方傳送資料過來,填充了本地套介面緩衝區,導致了套介面緩衝區有資料可讀)
- 連線的讀一半關閉,即接收到FIN段,讀操作將返回0 (這個時候也能通知select表示某個套介面產生了可讀事件,這時候執行讀操作,返回值為 0,表示對等方關閉)
- 如果是監聽套介面,已完成連線佇列不為空時。(就是說對等方在connect連線完成時,則已完成連線對列就不為空了,監聽套介面就會產生可讀事件通知select檢測到)
- 套介面上發生了一個錯誤待處理(同樣會產生可讀事件,通知select),錯誤可以通過getsockopt 指定SO_ERROR選項來獲取。
- 可讀:
-
可寫:
- 套介面傳送緩衝區有空間容納資料(在緩衝區寫入產生可寫事件,在緩衝區沒有滿的時候,會頻繁的產生可寫事件)
- 連線的寫一半關閉。及收到的RST段之後,再次呼叫write操作。
- 套介面上發生了一個錯誤待處理,錯誤可以通過getsockopt 指定SO_ERROR選項來獲取。
-
異常
- 套介面存在帶外資料。
-
用select改進伺服器
/*********************************************************************************
* Copyright: (C) 2018 anzhihong<[email protected]>
* All rights reserved.
*
* Filename: socket_server.c
* Description: This file
*
* Version: 1.0.0(2018年08月05日)
* Author: anzhihong <[email protected]>
* ChangeLog: 1, Release initial version on "2018年08月05日 08時23分10秒"
*
********************************************************************************/
#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#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)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)//read是可中斷的,所以當被外來訊號中斷打斷時,不算錯誤,任然繼續執行
continue;
return -1;
}
else if(nread==0)//對等方關閉
return count-nleft;//讀到EOF,對方關閉
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd,void *buf,size_t count)
{
size_t nleft=count;//剩餘傳送位元組數
ssize_t nwritten;//已經發送位元組數
char *bufp=(char *)buf;
while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)//被中斷
continue;
return -1;
}
else if(nwritten==0)//對等方關閉
continue;//讀到EOF,對方關閉
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)//操作被訊號中斷,recv認為連結正常,繼續執行。
continue;
return ret;
}
}
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
int ret = 0;
int nread;
char *bufp = buf;
int nleft = maxline; //讀取遇到\n返回,不會超過maxline
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
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) //偷窺到的資料不能大於maxline
exit(EXIT_FAILURE);
nleft -= nread; //剩餘位元組數
ret = readn(sockfd, bufp, nread); //將已經讀到資料nread從緩衝區移除
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //繼續偷窺
}
return 1;
}
void echo_service(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
if(ret == -1)
ERR_EXIT("readline");
if(ret == 0)
{
printf("client close");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
while(waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(void)
{
//處理殭屍程序
signal(SIGCHLD, handle_sigchld);
int listenfd;
/* if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0) //第一次開啟 listenfd
ERR_EXIT("socket err");
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(8001);//埠,主機轉網路
/*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&servaddr.sin_addr);*/
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
//地址複用
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt err");
//繫結埠號,ip地址
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //第二次開啟 listenfd
ERR_EXIT("bind err");
//監聽
if(listen(listenfd,SOMAXCONN)<0) //第三次開啟 listenfd
ERR_EXIT("bind err");
struct sockaddr_in peeraddr;
// socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
socklen_t peerlen;
int conn;//已連線套接字
/* pid_t pid;
while(1)
{
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept err");
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pid = fork();
if(pid==-1)
ERR_EXIT("fork err");
if(pid==0)
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);//將子程序退出,要不它還會fork()
}
else
close(conn);
}
*/
int nready; //select返回的監測個數
int maxfd = listenfd; //所以這裡的 listenfd 的值為3
fd_set allset; //建立集合
fd_set rset;
FD_ZERO(&allset); //清除集合
FD_ZERO(&rset);
FD_SET(listenfd, &allset); //將接聽套介面放入 allset 中
/* 當有新的客戶端連線過來時,conn 會被新的客戶端覆蓋,所以定義一個數組來存放客戶端的資訊,為什麼使用 fork()時 conn 沒有被覆蓋呢?這是因為每個 fork() 中的 conn 時獨立的,而我們現在採用的是單程序來實現,一個單程序只有一個 conn,所以需要一個套介面陣列集合來儲存客戶端 conn 的連線資訊 */
int client[FD_SETSIZE]; //select最多可以處理這麼多的描述符
int i;
for(i=0;i<FD_SETSIZE;i++)
{
client[i] = -1; //表示空閒
}
while(1)
{
/*在程式執行以後,allset 當中會有兩個套介面,一個是 listenfd, 一個是 conn。此時就會有多種事件發生。一種時listenfd套介面產生的事件,一種是conn套介面產生的事件,另一種是兩種套介面產生的事件。這裡需要allset的原因是這裡rset的套介面陣列集的的內容會變化,它只會儲存套介面數族當前的內容,在下一次select時,只會監聽當前的套介面,而不會監聽所有的套介面,所以需要allset來臨時儲存rset以前的內容*/
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1) //如果 nready 等於-1,監聽失敗
{
if(errno == EINTR) //還有一種情況就是被中斷訊號打斷,可能導致 select不能重啟
continue; //繼續監聽
ERR_EXIT("select");
}
if(nready == 0) //如果超時,繼續執行
continue;
/* 監聽套介面發生可讀事件意味著對方連線三次握手已經成功,我們這邊已完成連線佇列中的條目不為空了,那麼此時再呼叫 accept 函式將不會再阻塞*/
if(FD_ISSET(listenfd, &rset)) //判斷是否是監聽套介面產生了可讀事件
{
peerlen = sizeof(peeraddr);
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i=0; i<FD_SETSIZE;i++) //將客戶端的連線資訊儲存在一個空閒的位置
{
if(client[i] < 0) //說明找到了空閒位置,如果client小於0
{
client[i] = conn;
break;
}
}
if(i == FD_SETSIZE) //如果i等於select設定的長度,表明client的連線數量超出了我們的連線範圍
{
fprintf(stderr, "too many clients\n");
exit(EXIT_FAILURE);
}
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
/* 現在我們已經得到了一個已經連線的套介面conn,下一次我們仍然需要關心 conn 的可讀事件的發生,也就是說下一次呼叫select時我們也要把它放入 select 中處理 */
FD_SET(conn, &allset);
/* 因為不斷地往陣列集合中新增套介面,所以maxfd可能不是恆大於以前的maxfd,所以要更新maxfd的值 */
if(conn > maxfd)
maxfd = conn;
/* 因為程式的不斷執行,導致新增到陣列中的套介面越來越多,從而導致select的返回值可能大於1,所以我們需要一個一個的處理 */
//首先時conn產生的可讀事件
if(--nready <= 0) //證明這裡檢測到的事件已經處理完畢,然後繼續監聽
continue;
}
//處理已連線套介面conn的事件
for(i=0; i<FD_SETSIZE; i++)
{
conn = client[i];
if(conn == -1) //表示空閒的位置,繼續執行
continue;
if(FD_ISSET(conn, &rset)) //檢測讀集合中是否有conn
{
char recvbuf[1024] = {0};
memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
if(ret == -1)
ERR_EXIT("readline");
if(ret == 0)
{
printf("client close\n");
FD_CLR(conn, &allset); //當客戶端關閉,那麼就不需要再關心套介面conn產生的事件了
printf("conn already delete\n");
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0) //這裡意味著所有的事件都已經處理完了,就跳出迴圈。
break;
}
}
}
return 0;
}
- 如圖所示集合總的容量是 FD_SETSIZE ,下標為 0 的表示沒有將其放入集合當中,也就說檔案描述符0,1,2都沒有放入。因為我們的監聽套介面的檔案描述符為3,所以將其放入集合中。接下來如果套介面產生了可讀事件,accept返回一個新的套介面,此時套介面的文字描述符為4,然後將新的套介面加入空閒的陣列中。如第一個,client[0],文字描述符為5的套介面放入client[1]中,以此類推。當套介面為4的關閉client[0]的值又要重新置為1,並且將其從集合中的值置為0,即去除套介面為4的那個。但是這個時候不會更新maxfd的值,意味著select函式要從0開始遍歷所有的套介面(這裡是檢測到所有的可讀事件才返回,並不是一檢測到可讀事件就返回),然後返回發生事件的個數。一旦產生了可讀事件,要遍歷所有已連線的套介面,其範圍是0~FD_SETSIZE,那麼我們能不能縮小一下範圍呢?我們可以記錄一個最大的不空閒的位置 maxi。
- 程式碼
/*********************************************************************************
* Copyright: (C) 2018 anzhihong<[email protected]>
* All rights reserved.
*
* Filename: socket_server.c
* Description: This file
*
* Version: 1.0.0(2018年08月05日)
* Author: anzhihong <[email protected]>
* ChangeLog: 1, Release initial version on "2018年08月05日 08時23分10秒"
*
********************************************************************************/
#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#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)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)//read是可中斷的,所以當被外來訊號中斷打斷時,不算錯誤,任然繼續執行
continue;
return -1;
}
else if(nread==0)//對等方關閉
return count-nleft;//讀到EOF,對方關閉
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd,void *buf,size_t count)
{
size_t nleft=count;//剩餘傳送位元組數
ssize_t nwritten;//已經發送位元組數
char *bufp=(char *)buf;
while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)//被中斷
continue;
return -1;
}
else if(nwritten==0)//對等方關閉
continue;//讀到EOF,對方關閉
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)//操作被訊號中斷,recv認為連結正常,繼續執行。
continue;
return ret;
}
}
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
int ret = 0;
int nread;
char *bufp = buf;
int nleft = maxline; //讀取遇到\n返回,不會超過maxline
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
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) //偷窺到的資料不能大於maxline
exit(EXIT_FAILURE);
nleft -= nread; //剩餘位元組數
ret = readn(sockfd, bufp, nread); //將已經讀到資料nread從緩衝區移除
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //繼續偷窺
}
return 1;
}
void echo_service(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
if(ret == -1)
ERR_EXIT("readline");
if(ret == 0)
{
printf("client close");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
while(waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(void)
{
//處理殭屍程序
signal(SIGCHLD, handle_sigchld);
int listenfd;
/* if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0) //第一次開啟 listenfd
ERR_EXIT("socket err");
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(8001);//埠,主機轉網路
/*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&servaddr.sin_addr);*/
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
//地址複用
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt err");
//繫結埠號,ip地址
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //第二次開啟 listenfd
ERR_EXIT("bind err");
//監聽
if(listen(listenfd,SOMAXCONN)<0) //第三次開啟 listenfd
ERR_EXIT("bind err");
struct sockaddr_in peeraddr;
// socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
socklen_t peerlen;
int conn;//已連線套接字
/* pid_t pid;
while(1)
{
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept err");
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pid = fork();
if(pid==-1)
ERR_EXIT("fork err");
if(pid==0)
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);//將子程序退出,要不它還會fork()
}
else
close(conn);
}
*/
int nready; //select返回的監測個數
int maxfd = listenfd; //所以這裡的 listenfd 的值為3
fd_set allset; //建立集合
fd_set rset;
FD_ZERO(&allset); //清除集合
FD_ZERO(&rset);
FD_SET(listenfd, &allset); //將接聽套介面放入 allset 中
/* 當有新的客戶端連線過來時,conn 會被新的客戶端覆蓋,所以定義一個數組來存放客戶端的資訊,為什麼使用 fork()時 conn 沒有被覆蓋呢?這是因為每個 fork() 中的 conn 時獨立的,而我們現在採用的是單程序來實現,一個單程序只有一個 conn,所以需要一個套介面陣列集合來儲存客戶端 conn 的連線資訊 */
int client[FD_SETSIZE]; //select最多可以處理這麼多的描述符
int maxi = 0; //新增一個遍歷範圍,初始值為0
int i;
for(i=0;i<FD_SETSIZE;i++)
{
client[i] = -1; //表示空閒
}
// int count = 0; //測試伺服器接受客戶端連線數量
while(1)
{
/*在程式執行以後,allset 當中會有兩個套介面,一個是 listenfd, 一個是 conn。此時就會有多種事件發生。一種時listenfd套介面產生的事件,一種是conn套介面產生的事件,另一種是兩種套介面產生的事件。這裡需要allset的原因是這裡rset的套介面陣列集的的內容會變化,它只會儲存套介面數族當前的內容,在下一次select時,只會監聽當前的套介面,而不會監聽所有的套介面,所以需要allset來臨時儲存rset以前的內容*/
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1) //如果 nready 等於-1,監聽失敗
{
if(errno == EINTR) //還有一種情況就是被中斷訊號打斷,可能導致 select不能重啟
continue; //繼續監聽
ERR_EXIT("select");
}
if(nready == 0) //如果超時,繼續執行
continue;
/* 監聽套介面發生可讀事件意味著對方連線三次握手已經成功,我們這邊已完成連線佇列中的條目不為空了,那麼此時再呼叫 accept 函式將不會再阻塞*/
if(FD_ISSET(listenfd, &rset)) //判斷是否是監聽套介面產生了可讀事件
{
peerlen = sizeof(peeraddr);
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i=0; i<FD_SETSIZE;i++) //將客戶端的連線資訊儲存在一個空閒的位置
{
if(client[i] < 0) //說明找到了空閒位置,如果client小於0
{
client[i] = conn;
if(i > maxi)
{
maxi = i; //最大不空閒的位置發生了改變
printf("maxi is:%d\n", maxi);
}
break;
}
}
if(i == FD_SETSIZE) //如果i等於select設定的長度,表明client的連線數量超出了我們的連線範圍
{
fprintf(stderr, "too many clients\n");
exit(EXIT_FAILURE);
}
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
/* 現在我們已經得到了一個已經連線的套介面conn,下一次我們仍然需要關心 conn 的可讀事件的發生,也就是說下一次呼叫select時我們也要把它放入 select 中處理 */
FD_SET(conn, &allset);
/* 因為不斷地往陣列集合中新增套介面,所以maxfd可能不是恆大於以前的maxfd,所以要更新maxfd的值 */
if(conn > maxfd)
maxfd = conn;
/* 因為程式的不斷執行,導致新增到陣列中的套介面越來越多,從而導致select的返回值可能大於1,所以我們需要一個一個的處理 */
//首先時conn產生的可讀事件
if(--nready <= 0) //證明這裡檢測到的事件已經處理完畢,然後繼續監聽
continue;
}
//處理已連線套介面conn的事件
//for(i=0; i<FD_SETSIZE; i++)
for(i=0; i<=maxi; i++) //遍歷的範圍從FD_SETSIZE縮小到maxi
{
conn = client[i];
if(conn == -1) //表示空閒的位置,繼續執行
continue;
if(FD_ISSET(conn, &rset)) //檢測讀集合中是否有conn
{
char recvbuf[1024] = {0};
memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
if(ret == -1)
ERR_EXIT("readline\n");
if(ret == 0)
{
printf("client close\n");
FD_CLR(conn, &allset); //當客戶端關閉,那麼就不需要再關心套介面conn產生的事件了
printf("conn already delete\n");
client[i] = -1; //重新將client[i]置為空閒。
close(conn);
}
//printf("count is %d\n", ++count);
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0) //這裡意味著所有的事件都已經處理完了,就跳出迴圈。
break;
}
}
}
return 0;
}
- 注意這裡的套介面不只受FD_SETSIZE的限制,還受到一個程序中能夠開啟的I/O的數目的限制。I/O的數目是可以更改的。通過命令ulimit -n來更改程序數(臨時的)。FD_SETSIZE需要更改核心才行。
程式碼更改
/*********************************************************************************
* Copyright: (C) 2018 anzhihong<[email protected]>
* All rights reserved.
*
* Filename: socket_test1024.c
* Description: This file
*
* Version: 1.0.0(2018年08月05日)
* Author: anzhihong <[email protected]>
* ChangeLog: 1, Release initial version on "2018年08月05日 08時23分10秒"
*
********************************************************************************/
#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/resource.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("getrlimit");
printf("%d\n",(int)rl.rlim_max);
rl.rlim_cur = 2048;
rl.rlim_max = 2048;
if(setrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("setrlimit");
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("getrlimit");
printf("%d\n",(int)rl.rlim_max);
return 0;
}
- 但是這個只能更改當前程序的最大限制,並不能更改父程序的。
- select主要限制是其套介面數量的限制。
- 測試程式碼
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
int count = 0;
while(1)
{
int sock;
if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
{
sleep(4);
ERR_EXIT("socket");
}
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(8001);//埠,主機轉網路
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("connect");
//獲取本地埠號、IP
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
printf("getsockname err\n");
printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
printf("count = %d\n", ++count);
}
return 0;
}
- 測試伺服器的程式碼見上面服務程式碼的註釋。最後伺服器的結果是1020。也許有人會注意到上面有一行 sleep(4); 當客戶端呼叫socket準備建立第1022個套接字時,如上所示也會提示錯誤,此時socket函式返回-1出錯,如果沒有睡眠4s後再退出程序會有什麼問題呢?如果直接退出程序,會將客戶端所開啟的所有套接字關閉掉,即向伺服器端傳送了很多FIN段,而此時也許伺服器端還一直在accept ,即還在從已連線佇列中返回已連線套接字,此時伺服器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連線的套接字的可讀事件,read 返回0,所以會有很多 client close 欄位 參雜在條目的輸出中,還有個問題就是,因為read 返回0,伺服器端會將自身的已連線套接字關閉掉,那麼也許剛才說的客戶端某一個連線會被accept 返回,即測試不出伺服器端真正的併發容量。
- 客戶端結果:
- 伺服器結果:
- 將 sleep(4); 註釋掉,可以看到輸出參雜著client close,且這次的count 達到了1021,原因就是伺服器端前面已經有些套接字關閉了,所以accept 建立套接字不會出錯,伺服器程序也不會因為出錯而退出,可以看到最後接收到的一個連線埠是33742,即不一定是客戶端的最後一個連線。觀察伺服器端的輸出如下:
- 套接字I/超時設定方法
- 用select實現超時
- read_timeout 函式封裝
- write_timeout 函式封裝
- accept_timeout 函式封裝
- connect__timeout 函式封裝
-
三種設定方法:
- 鬧鐘方式設定alarm(但是這種方法可能會與其他的鬧鐘產生衝突,不推薦使用)
- 超時之後產生一個SIG_ALRM訊號
-
套接字選項(也不推薦使用,移植性不太好)
- SO_SNDTIMEO
- SO_RCVTIMEO
-
select (主要使用select來設定超時)
- read_timeout 函式封裝
- 鬧鐘方式設定alarm(但是這種方法可能會與其他的鬧鐘產生衝突,不推薦使用)
read_timeout - 讀超時檢測函式,不含讀操作
* fd:檔案描述符
* wait_seconds:等待超時秒數, 如果為0表示不檢測超時;
* 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
*/
int read_timeout( int fd, unsigned int wait_seconds)
{
int ret = 0 ;
if (wait_seconds > 0 )
{
fd_set read_fdset;
struct timeval timeout;
FD_ZERO(&read_fdset);
FD_SET(fd, &read_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0 ;
do
{
ret = select(fd + 1 , &read_fdset, NULL , NULL , &timeout); //select會阻塞直到檢測到事件或者超時
// 如果select檢測到可讀事件傳送,則此時呼叫read不會阻塞
}
while (ret < 0 && errno == EINTR);
if (ret == 0 )
{
ret = - 1 ;
errno = ETIMEDOUT;
}
else if (ret == 1 )
return 0 ;
}
return ret;
}
- write_timeout 函式封裝
/* write_timeout - 寫超時檢測函式,不含寫操作
* fd:檔案描述符
* wait_seconds:等待超時秒數, 如果為0表示不檢測超時;
* 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
*/
int write_timeout( int fd, unsigned int wait_seconds)
{
int ret = 0 ;
if (wait_seconds > 0 )
{
fd_set write_fdset;
struct timeval timeout;
FD_ZERO(&write_fdset);
FD_SET(fd, &write_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0 ;
do
{
ret = select(fd + 1 , NULL , &write_fdset, NULL , &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == 0 )
{
ret = - 1 ;
errno = ETIMEDOUT;
}
else if (ret == 1 )
return 0 ;
}
return ret;
}
- accept_timeout 函式封裝
/* accept_timeout - 帶超時的accept
* fd: 套接字
* addr: 輸出引數,返回對方地址
* wait_seconds: 等待超時秒數,如果為0表示正常模式
* 成功(未超時)返回已連線套接字,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
*/
int accept_timeout( int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
int ret;
socklen_t addrlen = sizeof ( struct sockaddr_in);
if (wait_seconds > 0 )
{
fd_set accept_fdset;
struct timeval timeout;
FD_ZERO(&accept_fdset);
FD_SET(fd, &accept_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0 ;
do
{
ret = select(fd + 1 , &accept_fdset, NULL , NULL , &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == - 1 )
return - 1 ;
else if (ret == 0 )
{
errno = ETIMEDOUT;
return - 1 ;
}
}
if (addr != NULL )
ret = accept(fd, ( struct sockaddr *)addr, &addrlen);
else
ret = accept(fd, NULL , NULL );
if (ret == - 1 )
ERR_EXIT( "accpet error" );
return ret
- connect__timeout 函式封裝
/* activate_nonblock - 設定IO為非阻塞模式
* fd: 檔案描述符
*/
void activate_nonblock( int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == - 1 )
ERR_EXIT( “fcntl error” );
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == - 1 )
ERR_EXIT( "fcntl error" );
}
/* deactivate_nonblock - 設定IO為阻塞模式
* fd: 檔案描述符
*/
void deactivate_nonblock( int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == - 1 )
ERR_EXIT( “fcntl error” );
flags &= ~O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == - 1 )
ERR_EXIT( "fcntl error" );
}
/* connect_timeout - 帶超時的connect
* fd: 套接字
* addr: 輸出引數,返回對方地址
* wait_seconds: 等待超時秒數,如果為0表示正常模式
* 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT
*/
int connect_timeout( int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
int ret;
socklen_t addrlen = sizeof ( struct sockaddr_in);
if (wait_seconds > 0 )
activate_nonblock(fd);
ret = connect(fd, ( struct sockaddr *)addr, addrlen);
if (ret < 0 && errno == EINPROGRESS)
{
fd_set connect_fdset;
struct timeval timeout;
FD_ZERO(&connect_fdset);
FD_SET(fd, &connect_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0 ;
do
{
/* 一旦連線建立,套接字就可寫 */
ret = select(fd + 1 , NULL , &connect_fdset, NULL , &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == 0 )
{
errno = ETIMEDOUT;
return - 1 ;
}
else if (ret < 0 )
return - 1 ;
else if (ret == 1 )
{
/* ret返回為1,可能有兩種情況,一種是連線建立成功,一種是套接字產生錯誤
* 此時錯誤資訊不會儲存至errno變數中(select沒出錯),因此,需要呼叫
* getsockopt來獲取 */
int err;
socklen_t socklen = sizeof (err);
int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
if (sockoptret == - 1 )
return - 1 ;
if (err == 0 )
ret = 0 ;
else
{
errno = err;
ret = - 1 ;
}
}
}
if (wait_seconds > 0 )
deactivate_nonblock(fd);
return ret;
}