read_timeout函數封裝
//讀超時函數,但不包含讀操作 int read_timeout(int fd, long waitSec) { int returnValue = http://www.xuebuyuan.com/0; if (waitSec > 0) { fd_set readSet; FD_ZERO(&readSet); //清零 FD_SET(fd,&readSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == 0) //在waitTime時間段中一個事件也沒到達 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 { returnValue = 0; //返回0,表示成功 } } return returnValue; }
write_timeout函數封裝
//寫超時函數,但不包含讀操作 int write_timeout(int fd, long waitSec) { int returnValue = http://www.xuebuyuan.com/0; if (waitSec > 0) { fd_set writeSet; FD_ZERO(&writeSet); //清零 FD_SET(fd,&writeSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == 0) //在waitTime時間段中一個事件也沒到達 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 { returnValue = 0; //返回0,表示成功 } } return returnValue; }
accept_timeout函數封裝
/**超時accept函數 成功:返回已經建立鏈接的文件描述符 失敗:返回-1,errno=ETIMEDOUT */ int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec) { int returnValue = http://www.xuebuyuan.com/0; if (waitSec > 0) { fd_set acceptSet; FD_ZERO(&acceptSet); //清零 FD_SET(fd,&acceptSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == -1) //error { return -1; } else if (returnValue == 0) //在waitTime時間段中有事件產生 { errno = ETIMEDOUT; return -1; } } /**select正確返回: 表示有select所等待的事件發生:對等放完成了三次握手, 客戶端有新的鏈接建立,此時再調用accept就不會阻塞了 */ socklen_t socklen = sizeof(struct sockaddr_in); if (addr != NULL) { returnValue = accept(fd,(struct sockaddr *)addr,&socklen); } else { returnValue = accept(fd,NULL,NULL); if (returnValue == -1) { err_exit("accept error"); } } return returnValue; }
connect_timeout函數封裝
1.我們為什麽需要這個函數????
TCP/IP在客戶端連接服務器時,如果發生異常,connect(如果是在默認阻塞的情況下),返回的是1.5*RTT(相當於客戶端阻塞了這麽長的時間,客戶需要等待這麽長的時間,顯然這樣的客戶端用戶體驗並不好);會造成嚴重的軟件質量下降.
2.怎樣實現?
1)sockfd首先變成非阻塞的;然後試著進行connect,如果網絡狀況良好,則立刻建立鏈接並返回,如果網絡狀況不好,則鏈接不會馬上建立,這時需要我們的參與:調用select,設置等待時間,通過select管理者去監控sockfd,一旦能夠建立鏈接,則馬上返回,然後建立鏈接,這樣就會大大提高我們的軟件質量.
2)需要註意一點:select機制監控到sockfd可寫(也就是可以建立鏈接時),並不代表調用accept就一定能夠成功(造成sockfd可寫有兩種情況:1.真正的鏈接可以建立起來了;2.建立鏈接的過程中發生錯誤,然後錯誤會回寫錯誤信息,造成sockfd可寫);
通過調用sockoptret做一個容錯即可!
代碼實現:
//設置文件描述符fd為非阻塞模式 int setUnBlock(int fd) { int flags = fcntl(fd,F_GETFL); flags |= O_NONBLOCK; if (fcntl(fd,F_SETFL,flags) == -1) { err_exit("fcntl O_NONBLOCK error"); } return 0; } //設置文件描述符fd為阻塞模式 int setBlock(int fd) { int flags = fcntl(fd,F_GETFL); flags &= ~O_NONBLOCK; if (fcntl(fd,F_SETFL,flags) == -1) { err_exit("fcntl BLOCK error"); } return 0; } /**說明: fd:套接字 addr:要連接的對方的地址 waitSec:等待超時的秒數,如果為0,則為正常模式 返回值:成功(沒有超時)返回0,失敗/超時返回-1,errno=ETIMEDOUT */ int connect_timeout(int fd, struct sockaddr_in *addr, long waitSec) { socklen_t addrLen = sizeof(struct sockaddr_in); if (waitSec > 0) { //設置為非阻塞模式 setUnBlock(fd); } //首先嘗試著進行鏈接 int returnValue = http://www.xuebuyuan.com/connect(fd,(struct sockaddr *)addr,addrLen); if (returnValue < 0 && errno == EINPROGRESS) //如果首次嘗試失敗,則需要我們的介入 { fd_set connectSet; FD_ZERO(&connectSet); FD_SET(fd,&connectSet); struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { //一旦建立鏈接,則套接字可寫,所以將套接字放在了寫集合中 returnValue = select(fd+1,NULL,&connectSet,NULL,&waitTime); } while (returnValue < 0 && errno == EINTR); if (returnValue == 0) //超時 { returnValue = -1; errno = ETIMEDOUT; } else if (returnValue == -1) //error { return -1; } else if (returnValue == 1) //正確返回,有一個套接字可寫 { /**註意 套接字可寫有兩種情況: 1.連接建立成功 2.套接字產生錯誤,但是此時錯誤信息沒有保存在errno中,需要調用getsockopt獲取 */ int err; socklen_t errLen = sizeof(err); int sockoptret = 0; if ((sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&errLen)) == -1) { return -1; } if (err == 0) //確實是建立成功 { returnValue = 0; } else //連接產生了錯誤 { errno = err; returnValue = -1; } } } if (waitSec > 0) { setBlock(fd); } return returnValue; }
完整的server端調用代碼:
//server端完整代碼及解析 #include "commen.h" #include "timeout.h" int main() { signal(SIGCHLD,onCatchSIGCHLD); int serverSockfd = mkATCPServer(9001); long int waitSec = 10; //超時時間為10秒 struct sockaddr_in peerAddr; while (true) { //接受鏈接,只等待100秒,如果鏈接不來,我就停止服務(這兒只是為了演示,其實這是不合理的) int peerSockfd = accept_timeout(serverSockfd,&peerAddr,100); if (peerSockfd == -1) { err_exit("main: accept error"); } //打印客戶信息 cout << "Client:" << endl; cout << "\tsin_port: " << ntohs(peerAddr.sin_port) << endl; cout << "\tsin_addr: " << inet_ntoa(peerAddr.sin_addr) << endl; cout << "\tsocket: " << peerSockfd << endl; //每有一個客戶端連接進來,就fork一個子進程, //相應的業務處理由子進程完成,父進程繼續監聽 pid_t pid = fork(); if (pid == -1) { close(serverSockfd); close(peerSockfd); err_exit("fork error"); } else if (pid == 0) //子進程,處理業務 { close(serverSockfd); //子進程關閉監聽套接字,因為子進程不負責監聽任務 char recvBuf[BUFSIZ]; ssize_t readCount = 0; while (true) { /**設定讀等待的秒數: 1.如果在這段時間中有事件(socket 可讀)到來,則返回 2.如果一直都沒有事件到來,則select會一直等待,直到超時返回(-1) */ int ret = read_timeout(peerSockfd,waitSec); if (ret == 0) { memset(recvBuf,0,sizeof(recvBuf)); //此時再進行讀取socket就一定不會阻塞了O(∩_∩)O~ if ((readCount = read(peerSockfd,recvBuf,sizeof(recvBuf))) == -1) { err_exit("readn error"); } else if (readCount == 0) { peerClosePrint("client connect closed"); } } else if (ret == -1 && errno == ETIMEDOUT) //超時 { err_exit("read time out"); } /**設定寫等待的秒數: 1.如果在這段時間中有事件(socket 可寫)到來,則返回 2.如果一直都沒有事件到來,則select會一直等待,直到超時返回(-1) */ ret = write_timeout(peerSockfd,waitSec); if (ret == 0) { //將整體報文回寫回客戶端 if (writen(peerSockfd,recvBuf,strlen(recvBuf)) == -1) { err_exit("writen error"); } } else if (ret == -1 && errno == ETIMEDOUT) //超時 { err_exit("write time out"); } recvBuf[readCount] = 0; //寫至終端 fputs(recvBuf,stdout); } } else if (pid > 0) //父進程 { close(peerSockfd); } } close(serverSockfd); return 0; }
完整的client端調用代碼:
#include "commen.h" int main() { int sockfd = mkATCPClient(9001,"127.0.0.1"); char sendBuf[BUFSIZ]; char recvBuf[BUFSIZ]; while (true) { int fdCount = sockfd > STDIN_FILENO ? sockfd+1 : STDIN_FILENO+1; fd_set rdset; FD_ZERO(&rdset); FD_SET(sockfd,&rdset); FD_SET(STDIN_FILENO,&rdset); int nReady = select(fdCount,&rdset,NULL,NULL,NULL); if (nReady == -1) { err_exit("select error"); } //server端有數據可讀 if (FD_ISSET(sockfd,&rdset)) { //從socket中讀取數據 int readCount = read(sockfd,recvBuf,sizeof(recvBuf)); if ( readCount == -1) { err_exit("read socket error"); } else if (readCount == 0) //如果對端結束鏈接 { peerClosePrint(); } fputs(recvBuf,stdout); memset(recvBuf,0,sizeof(recvBuf)); } //標準輸入上有數據可讀:從鍵盤讀取數據 -> 發送至socket if (FD_ISSET(STDIN_FILENO,&rdset)); { if(fgets(sendBuf,sizeof(sendBuf),stdin) != NULL) { if (write(sockfd,sendBuf,strlen(sendBuf)) == -1) //發送到socket上 { err_exit("write error"); } memset(sendBuf,0,sizeof(sendBuf)); } } } close(sockfd); return 0; }
結果演示:
附-commen.h
#ifndef COMMEN_H_INCLUDED #define COMMEN_H_INCLUDED #include <unistd.h> #include <signal.h> #include <errno.h> #include <fcntl.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/msg.h> #include <sys/sem.h> #include <sys/socket.h> #include <sys/select.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <iostream> using namespace std; #include "timeout.h" void peerClosePrint(std::string str = "peer connect closed") { cout << str << endl; _exit(0); } //return a socket that have start listened. int mkATCPServer(int serverPort, int backlog = SOMAXCONN) { int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd == -1) { err_exit("socket error"); } //add address reused int on = 1; if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) == -1) { err_exit("setsockopt SO_REUSEADDR error"); } //band a local address and port struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(serverPort); serverAddr.sin_addr.s_addr = INADDR_ANY; //band an any IP address if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1) { err_exit("bind error"); } //start to listen. if (listen(sockfd,backlog) == -1) { err_exit("listen error"); } return sockfd; } //return a socket that have connected to server. int mkATCPClient(int serverPort, string serverIPAddr) { //first. create a socket int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd == -1) { err_exit("socket error"); } //second. connect to a server struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(serverPort); serverAddr.sin_addr.s_addr = inet_addr(serverIPAddr.c_str()); if (connect_timeout(sockfd,&serverAddr,10) == -1) { err_exit("In mkATCPClient -> connect error"); } return sockfd; } void onCatchSIGCHLD(int signalNumber) { int ret = 0; while ((ret = waitpid(-1,NULL,WNOHANG) != -1)) ; } ssize_t readn(int fd,void *buf,size_t count) { size_t nLeft = count; ssize_t nRead = 0; char *ptr = static_cast<char *>(buf); while (nLeft > 0) { if ((nRead = read(fd,ptr,nLeft)) < 0) { //一點東西都沒讀 if (nLeft == count) { return -1; //error } else { break; //error, return amount read so far } } else if (nRead == 0) { break; //EOF } nLeft -= nRead; ptr += nRead; } return count - nLeft; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nLeft = count; ssize_t nWritten; const char *ptr = static_cast<const char *>(buf); while (nLeft > 0) { if ((nWritten = write(fd,ptr,nLeft)) < 0) { //一點東西都沒寫 if (nLeft == count) { return -1; //error } else { break; //error, return amount write so far } } else if (nWritten == 0) { break; //EOF } nLeft -= nWritten; ptr += nWritten; } return count - nWritten; } //只是查看一下網絡中的數據,並不是將之真正取走:MSG_PEEK ssize_t recv_peek(int fd, void *buf, size_t count) { int nRead = 0; //如果讀取網絡數據出錯,則繼續讀取 while ((nRead = recv(fd,buf,count,MSG_PEEK)) == -1); return nRead; } ssize_t readline(int fd, void *buf, size_t maxline) { char *pBuf = (char *)buf; int nLeft = maxline; while (true) { //查看緩沖區中的數據,並不真正取走 int nTestRead = recv_peek(fd,pBuf,nLeft); //檢測這次讀來的數據中是否包含'\n'; //如果有,則將之全部讀取出來 for (int i = 0; i < nTestRead; ++i) { if (pBuf[i] == '\n') { //真正的從緩沖區中將數據取走 if (readn(fd,pBuf,i+1) != i+1) { err_exit("readn error"); } else { return i + 1; } } } //如果這次讀的緩沖區中沒有'\n' //如果讀超了:讀道德數目大於一行最大數,則做異常處理 if (nTestRead > nLeft) { exit(EXIT_FAILURE); } nLeft -= nTestRead; //若緩沖區沒有'\n',則將剩余的數據讀走 if (readn(fd,pBuf,nTestRead) != nTestRead) { exit(EXIT_FAILURE); } pBuf += nTestRead; } return -1; } #endif // COMMEN_H_INCLUDED
附-timeout.h
#ifndef TIMEOUT_H_INCLUDED #define TIMEOUT_H_INCLUDED static void err_exit(std::string str) { perror(str.c_str()); exit(EXIT_FAILURE); } /* struct timeval { long tv_sec; // seconds // long tv_usec; // microseconds // }; */ //讀超時函數,但不包含讀操作 int read_timeout(int fd, long waitSec) { int returnValue = http://www.xuebuyuan.com/0; if (waitSec > 0) { fd_set readSet; FD_ZERO(&readSet); //清零 FD_SET(fd,&readSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == 0) //在waitTime時間段中一個事件也沒到達 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 { returnValue = 0; //返回0,表示成功 } } return returnValue; } //寫超時函數,但不包含讀操作 int write_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set writeSet; FD_ZERO(&writeSet); //清零 FD_SET(fd,&writeSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == 0) //在waitTime時間段中一個事件也沒到達 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 { returnValue = 0; //返回0,表示成功 } } return returnValue; } /**超時accept函數 成功:返回已經建立鏈接的文件描述符 失敗:返回-1,errno=ETIMEDOUT */ int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set acceptSet; FD_ZERO(&acceptSet); //清零 FD_SET(fd,&acceptSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == -1) //error { return -1; } else if (returnValue == 0) //在waitTime時間段中有事件產生 { errno = ETIMEDOUT; return -1; } } /**select正確返回: 表示有select所等待的事件發生:對等放完成了三次握手, 客戶端有新的鏈接建立,此時再調用accept就不會阻塞了 */ socklen_t socklen = sizeof(struct sockaddr_in); if (addr != NULL) { returnValue = accept(fd,(struct sockaddr *)addr,&socklen); } else { returnValue = accept(fd,NULL,NULL); if (returnValue == -1) { err_exit("accept error"); } } return returnValue; } //設置文件描述符fd為非阻塞模式 int setUnBlock(int fd) { int flags = fcntl(fd,F_GETFL); flags |= O_NONBLOCK; if (fcntl(fd,F_SETFL,flags) == -1) { err_exit("fcntl O_NONBLOCK error"); } return 0; } //設置文件描述符fd為阻塞模式 int setBlock(int fd) { int flags = fcntl(fd,F_GETFL); flags &= ~O_NONBLOCK; if (fcntl(fd,F_SETFL,flags) == -1) { err_exit("fcntl BLOCK error"); } return 0; } /**說明: fd:套接字 addr:要連接的對方的地址 waitSec:等待超時的秒數,如果為0,則為正常模式 返回值:成功(沒有超時)返回0,失敗/超時返回-1,errno=ETIMEDOUT */ int connect_timeout(int fd, struct sockaddr_in *addr, long waitSec) { socklen_t addrLen = sizeof(struct sockaddr_in); if (waitSec > 0) { //設置為非阻塞模式 setUnBlock(fd); } //首先嘗試著進行鏈接 int returnValue = http://www.xuebuyuan.com/connect(fd,(struct sockaddr *)addr,addrLen); if (returnValue < 0 && errno == EINPROGRESS) //如果首次嘗試失敗,則需要我們的介入 { fd_set connectSet; FD_ZERO(&connectSet); FD_SET(fd,&connectSet); struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { //一旦建立鏈接,則套接字可寫,所以將套接字放在了寫集合中 returnValue = select(fd+1,NULL,&connectSet,NULL,&waitTime); } while (returnValue < 0 && errno == EINTR); if (returnValue == 0) //超時 { returnValue = -1; errno = ETIMEDOUT; } else if (returnValue == -1) //error { return -1; } else if (returnValue == 1) //正確返回,有一個套接字可寫 { /**註意 套接字可寫有兩種情況: 1.連接建立成功 2.套接字產生錯誤,但是此時錯誤信息沒有保存在errno中,需要調用getsockopt獲取 */ int err; socklen_t errLen = sizeof(err); int sockoptret = 0; if ((sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&errLen)) == -1) { return -1; } if (err == 0) //確實是建立成功 { returnValue = 0; } else //連接產生了錯誤 { errno = err; returnValue = -1; } } } if (waitSec > 0) { setBlock(fd); } return returnValue; } #endif // TIMEOUT_H_INCLUDED
附-Makefile
CC = g++ CPPFLAGS = -Wall -g -pthread BIN = server client SOURCES = $(BIN.=.cpp) .PHONY: clean all all: $(BIN) $(BIN): $(SOURCES) clean: -rm -rf $(BIN) bin/ obj/ core
附-RTT(Round-Trip Time):
往返時延。在計算機網絡中它是一個重要的性能指標,表示從發送端發送數據開始,到發送端收到來自接收端的確認(接收端收到數據後便立即發送確認),總共經歷的時延。
往返延時(RTT)由三個部分決定:即鏈路的傳播時間、末端系統的處理時間以及路由器的緩存中的排隊和處理時間。其中,前面兩個部分的值作為一個TCP連接相對固定,路由器的緩存中的排隊和處理時間會隨著整個網絡擁塞程度的變化而變化。所以RTT的變化在一定程度上反映了網絡擁塞程度的變化。簡單來說就是發送方從發送數據開始,到收到來自接受方的確認信息所經歷的時間。
Tags: Socket return 時間段
文章來源: