10.select函數可以檢測網絡異常嗎?
答:不可以。當網絡異常時,select函數可以檢測到可讀事件,這時候用read函數讀取資料,會傳回0.
詳情見UNP 卷1 6.3.1 select描述符就緒條件 第131頁和132頁
12. epoll的水準模式LT和邊緣模式ET
答:預設是LT。
水準模式指的是低電平到低電平,或者高電平到高電平。
邊緣模式指的是低電平到高電平,或者高電平到低電平。
水準模式:如果epoll_wait檢測到可讀事件,可以一次性把資料讀完,也可以分多次讀完。
邊緣模式:如果epoll_wait檢測到可讀事件,必須一次性把資料讀完,否則如果分兩次讀,第一次讀部分,第二次再讀時,沒有可讀觸發信号了,讀不到了。隻能使用非阻塞的網絡模型。
參考部落格:
http://blog.csdn.net/analogous_love/article/details/60761528用于windows或linux水準模式下收取資料,這種情況下收取的資料可以小于指定大小,總之一次能收到多少是多少:
bool TcpSession::Recv()
{
//每次隻收取256個位元組
char buff[256];
//memset(buff, 0, sizeof(buff));
int nRecv = ::recv(clientfd_, buff, 256, 0);
if (nRecv == 0)
return false;
inputBuffer_.add(buff, (size_t)nRecv);
return true;
}
如果是linux epoll邊緣模式(ET),則一定要一次性收完:
bool TcpSession::RecvEtMode()
while (true)
{
//memset(buff, 0, sizeof(buff));
int nRecv = ::recv(clientfd_, buff, 256, 0);
if (nRecv == -1)
{
if (errno == EWOULDBLOCK || errno == EINTR)
return true;
return false;
}
//對端關閉了socket
else if (nRecv == 0)
inputBuffer_.add(buff, (size_t)nRecv);
}
13. 如何将socket設定成非阻塞的(建立時設定與建立完成後設定),非阻塞socket與阻塞的socket在收發資料上的差別
14. send/recv(read/write)傳回值大于0、等于0、小于0的差別
答:
recv:
阻塞與非阻塞recv傳回值沒有區分,都是 <0:出錯,=0:連接配接關閉,>0接收到資料大小,
特别:非阻塞模式下傳回 值 <0時并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情況 下認為連接配接是正常的,繼續接收。
隻是阻塞模式下recv會阻塞着接收資料,非阻塞模式下如果沒有資料會傳回,不會阻塞着讀,是以需要 循環讀取。
write:
阻塞與非阻塞write傳回值沒有區分,都是 <0:出錯,=0:連接配接關閉,>0發送資料大小,
特别:非阻塞模式下傳回值 <0時并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情況下認為連接配接是正常的, 繼續發送。
隻是阻塞模式下write會阻塞着發送資料,非阻塞模式下如果暫時無法發送資料會傳回,不會阻塞着 write,是以需要循環發送。
read:
阻塞與非阻塞read傳回值沒有區分,都是 <0:出錯,=0:連接配接關閉,>0接收到資料大小,
隻是阻塞模式下read會阻塞着接收資料,非阻塞模式下如果沒有資料會傳回,不會阻塞着讀,是以需要 循環讀取。
send:
阻塞與非阻塞send傳回值沒有區分,都是 <0:出錯,=0:連接配接關閉,>0發送資料大小,
隻是阻塞模式下send會阻塞着發送資料,非阻塞模式下如果暫時無法發送資料會傳回,不會阻塞着 send,是以需要循環發送。
15.如何編寫正确的收資料代碼與發資料代碼
答:都需要考慮緩沖區的設計,發資料如果失敗先緩存起來,等待下一次的機會再發。
bool TcpSession::Send()
int n = ::send(clientfd_, buffer_, buffer_.length(), 0);
if (n == -1)
//tcp視窗容量不夠, 暫且發不出去,下次再發
if (errno == EWOULDBLOCK)
break;
//被信号中斷,繼續發送
else if (errno == EINTR)
continue;
//對端關閉了連接配接
else if (n == 0)
buffer_.erase(n);
//全部發送完畢
if (buffer_.length() == 0)
break;
tcp是流協定,應用層要自己來區分包的邊界,就是加標頭包尾。但是tcp有個滑動視窗,假如是10,我第一個資料包占了6,然後在伺服器還沒讀取的時候,我發送第二個,也是6大小,此時會先隻發4過去,剩下的6-4=2就得等服務端騰出空間後并提示後再發了.
16.發送資料緩沖區與接收資料緩沖區如何設計
答:參考學習muduo的buffer設計
17.socket選項SO_SNDTIMEO和SO_RCVTIMEO
答:阻塞模式時需要設定逾時時間,否則會卡死。
18.socket選項TCP_NODELAY
答:一般來說,應用層send函數不會立刻把資料發出去,而是先給到網卡緩沖區。網卡緩沖區需要等待資料積累到一定量之後才會發送資料,這樣會導緻一定的延遲。
預設情況下,發送資料采用Nagle算法。這樣雖然提高了網絡吞吐量,但是實時性卻降低了,在一些互動性很強的應用程式來說是不允許的,使用TCP_NODELAY選項可以禁止Nagle算法,避免連續發包出現延遲,這對低延遲網絡服務很重要。 此時,應用程式向核心遞交的每個資料包都會立即發送出去。需要注意的是,雖然禁止了Nagle 算法,但網絡的傳輸仍然受到TCP确認延遲機制的影響。
19.socket選項SO_REUSEADDR和SO_REUSEPORT(Windows平台與linux平台的差別)
答:說白了當伺服器程序關閉時,想立刻再複用原來的ip和端口需要等待2MSL的時間。舉個例子,伺服器監聽了127.0.0.1和8001端口,如果此時結束掉程序,再立刻重新開機,是不可以再監聽成功的。因為TCP四次揮手最後一步TIME_WAIT需要等待應答,如果等不到需要重連。
MSL的時間一般是1min~4min不等。MSL是資料包的最大存活時間,最後一步的ack需要考慮去和回,是以周期是2*MSL。
Linux是所有程序在2MSL的時間内不能複用剛才使用的ip和port,bind會失敗;Windows是除了本程序可以,其他程序不可以。作業系統這麼設計的。
結論:一般為了友善重新開機伺服器或調試,會設定這兩個選項,REUSE就是複用的意思,讓程序立刻可以複用位址和端口。
20.socket選項SO_LINGER
答:當調用closesocket關閉套接字時,SO_LINGER将決定系統如何處理殘存在套接字發送隊列中的資料。處理方式無非兩種:丢棄或者将資料繼續發送至對端,優雅關閉連接配接。事實上,SO_LINGER并不被推薦使用,大多數情況下我們推薦使用預設的關閉方式(即下方表格中的第一種情況)。
21.shutdown與優雅關閉
答:socket 多程序中的shutdown, close使用
當所有的資料操作結束以後,你可以調用close()函數來釋放該socket,進而停止在該socket上的任何資料操作:close(sockfd);
你也可以調用shutdown()函數來關閉該socket。該函數允許你隻停止在某個方向上的資料傳輸,而一個方向上的資料傳輸繼續進行。如你可以關閉某socket的寫操作而允許繼續在該socket上接受資料,直至讀入所有資料。
int shutdown(int sockfd,int how);
Sockfd是需要關閉的socket的描述符。參數 how允許為shutdown操作選擇以下幾種方式:
SHUT_RD:關閉連接配接的讀端。也就是該套接字不再接受資料,任何目前在套接字接受緩沖區的資料将被丢棄。程序将不能對該套接字發出任何讀操作。對TCP套接字該調用之後接受到的任何資料将被确認然後無聲的丢棄掉。
SHUT_WR:關閉連接配接的寫端,程序不能在對此套接字發出寫操作。
SHUT_RDWR:相當于調用shutdown兩次:首先是以SHUT_RD,然後以SHUT_WR。
伺服器如果要主動關閉連接配接,可以這麼執行:先關本地“寫”端,等對方關閉後,再關本地“讀”端。
伺服器如果要被動關閉連接配接,可以這麼執行:當read函數傳回值是0時,先關本地“寫”端,等對方關閉後,再關本地“讀”端。
23.socket選項SO_KEEPALIVE
答:一般來說不推薦使用預設的心跳機制,預設是2小時。預設心跳有兩個缺陷:
1、貌似設定之後會影響整個作業系統所有應用層的心跳時間;
2、每2小時發一次心跳,有時候會造成流量浪費。比如應用層如果有正常資料互動,不需要發心跳。
具體實作可以參考redis源碼anet.c裡的anetKeepAlive函數。
24.關于錯誤碼EINTR
答:EINTR是linux中函數的傳回狀态,在不同的函數中意義不同。表示某種阻塞的操作,被接收到的信号中斷,造成的一種錯誤傳回值。
write
表示:由于信号中斷,沒寫成功任何資料。
read
表示:由于信号中斷,沒讀到任何資料。
sem_wait
函數調用被信号處理函數中斷。
recv
由于信号中斷傳回,沒有任何資料可用。
25.如何解決tcp粘包問題
答:通過應用層自定義協定來解決
1、固定長度的包
2、每個包以"\r\n"結尾
3、定義結構體,包含固定標頭,包體長度等
26.信号SIGPIPE與EPIPE錯誤碼
答:在linux下寫socket的程式的時候,如果伺服器嘗試send到一個disconnected socket上,就會讓底層抛出一個SIGPIPE信号。 這個信号的預設處理方法是退出程序,大多數時候這都不是我們期望的。也就是說,當伺服器繁忙,沒有及時處理用戶端斷開連接配接的事件,就有可能出現在連接配接斷開之後繼續發送資料的情況,如果對方斷開而本地繼續寫入的話,就會造成伺服器程序意外退出。
根據信号的預設處理規則SIGPIPE信号的預設執行動作是terminate(終止、退出),是以client會退出。若不想用戶端退出可以把 SIGPIPE設為SIG_IGN 如:signal(SIGPIPE, SIG_IGN); 這時SIGPIPE交給了系統處理。 伺服器采用了fork的話,要收集垃圾程序,防止僵屍程序的産生,可以這樣處理: signal(SIGCHLD,SIG_IGN); 交給系統init去回收。 這裡子程序就不會産生僵屍程序了。
27.gethostbyname阻塞與錯誤碼擷取問題
答:Unix/Linux下的gethostbyname函數常用來向DNS查詢一個域名的IP位址。 由于DNS的遞歸查詢,常常會發生gethostbyname函數在查詢一個域名時嚴重逾時。而該函數又不能像connect和read等函數那樣通過setsockopt或者select函數那樣設定逾時時間,是以常常成為程式的瓶頸。有人提出一種解決辦法是用alarm設定定時信号,如果逾時就用setjmp和longjmp跳過gethostbyname函數(這種方式我沒有試過,不知道具體效果如何)。
gethostbyname确實是阻塞的,但應該可以設定一個time_out免得DNS Server出問題時老是執行,關于設定Time_out,參閱一下code:
int timeout = TIMEOUT_VALUE;
int err;
SOCKET s;
s = socket( ... );
err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout).
在使用 gethostbyname() 的時候,你不能用perror() 列印錯誤資訊 (因為 errno 沒有使用),你應該調用 herror()。herror()函數簽名如下:
void herror(const char *s);
舉例如下:參考部落格
http://blog.csdn.net/analogous_love/article/details/53433994bool Connect(const char* pszIp, int nPort)
struct hostent* pHostent = NULL;
char* host = NULL;
struct sockaddr_in addrSrv;
memset(&addrSrv, 0, sizeof(addrSrv));
addrSrv.sin_addr.s_addr = ::inet_addr(pszIp);
if (addrSrv.sin_addr.s_addr == INADDR_NONE)
pHostent = ::gethostbyname(pszIp);
if (pHostent != NULL)
host = inet_ntoa(*((struct in_addr *)pHostent->h_addr));
std::cout << pszIp << "=" << host << std::endl;
else
herror("gethostbyname error");
std::cout << std::endl;
else
host = (char*)pszIp;
if (Socket.Connect(host, nPort))
return true;
std::cout << "Unable to connect to server " << pszIp << ":" << nPort << std::endl;
return false;
}
28.心跳包的設計技巧(保活心跳包與業務心跳包)
http://blog.csdn.net/analogous_love/article/details/7838818729.用戶端斷線重連機制如何設計
答:用戶端先2s連接配接一次伺服器,如果失敗,再4s連接配接一次,如果失敗,再8s連接配接一次,如果失敗再16s連接配接一次。。。。。
補充一個情況,當網絡狀況突變時,立刻連接配接一次。例如,使用者從地鐵站出來,手機信号滿格了,此時手機app立刻連接配接伺服器。
《Linux多線程服務端程式設計:使用muduo C++網絡庫》P333有這麼描述:
用戶端連接配接斷開後初次重試的延遲應該有随機性,比如說服務端奔潰,它所有的客戶連接配接同時斷開,然後0.5s之後再次發起連接配接,這樣既可能造成SYN丢包,也可能給伺服器帶來短期大負載,影響其服務品質。是以每個用戶端應該等待一段随機的時間(0.5~2s),再重試,避免擁塞。
30.如何檢測對端已經關閉socket
答:根據ERRNO和recv結果進行判斷
在UNIX/LINUX下,非阻塞模式SOCKET可以采用recv+MSG_PEEK的方式進行判斷,其中MSG_PEEK保證了僅僅進行狀态判斷,而不影響資料接收
對于主動關閉的SOCKET, recv傳回-1,而且errno被置為9(#define EBADF 9 /* Bad file number */)或104 (#define ECONNRESET 104 /* Connection reset by peer */)
對于被動關閉的SOCKET,recv傳回0,而且errno被置為11(#define EWOULDBLOCK EAGAIN /* Operation would block */)
對正常的SOCKET, 如果有接收資料,則傳回>0, 否則傳回-1,而且errno被置為11(#define EWOULDBLOCK EAGAIN /* Operation would block */)
是以對于簡單的狀态判斷(不過多考慮異常情況):
recv傳回>0, 正常
31.如何清除無效的死鍊(端與端之間的線路故障)
答:TCP四次揮手時産生的TIME_WAIT或CLOSE_WAIT,造成死鍊。或者伺服器A<->路由器B<->路由器C<->用戶端D,鍊路中的路由器發生了故障,造成死鍊。需要采取定時器/心跳檢測來清理死鍊。
32.定時器的不同實作及優缺點
答:Windows可以使用OnTimer函數,Linux網絡庫定時器需要自己實作。
例如libevent的小根堆,libuv的紅黑樹,muduo的二叉搜尋樹,nginx的紅黑樹,redis的升序連結清單等
學習redis網絡庫,muduo,優先隊列std:priority_queue
34.http協定的具體格式
35.http head、get與post方法的細節
GET /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
content-length: 8
User-Agent: Mozilla/5.0\r\n
\r\n
abcdefgh
POST /index.php HTTP/1.1\r\n
36.http代理、socks4代理與socks5代理如何編碼實作
答:代碼如下
BOOL UMySocket::Connect(PNETCONN_INFO pInfo,LPSTR lpMessage) //連接配接伺服器
{
wsysplus_memory vMemory;
long noDelay(1),tmSend(1800*1000L),tmRecv(1800*1000L);
LPSTR lpBuffer=vMemory.GetBuf(8001);
//關閉連接配接先
UMySocket::Close();
//初始化
if(!lpBuffer)
{
if(lpMessage) strcpy(lpMessage,"MALLOC DATA");
return(FALSE);
}
_hSocket=socket(PF_INET,SOCK_STREAM,0);
if(_hSocket==INVALID_SOCKET)
{
if(lpMessage) strcpy(lpMessage,"INVALID_SOCKET");
return(FALSE);
}
//設定連接配接屬性
setsockopt(_hSocket,IPPROTO_TCP,TCP_NODELAY,(LPSTR)&noDelay,sizeof(long));
setsockopt(_hSocket,SOL_SOCKET,SO_SNDTIMEO,(LPSTR)&tmSend,sizeof(long));
setsockopt(_hSocket,SOL_SOCKET,SO_RCVTIMEO,(LPSTR)&tmRecv,sizeof(long));
//連接配接伺服器
WORD wPortConn=(WORD)pInfo->proxyport;
char *lpServer=pInfo->proxyurl;
sockaddr_in remote={0};
remote.sin_family = AF_INET;
if(!(pInfo->conntype&0x000F)) //未使用代理
{
lpServer = pInfo->srvurl;
wPortConn = (WORD)pInfo->srvport;
}
remote.sin_port = htons(wPortConn);
LPHOSTENT lphost=NULL;
if((remote.sin_addr.s_addr=inet_addr(lpServer))==INADDR_NONE)
{
if(lphost=gethostbyname(lpServer))
remote.sin_addr.s_addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;
}
do
{
if(connect(_hSocket,(sockaddr*)&remote,sizeof(remote))
==SOCKET_ERROR&&WSAGetLastError()!=WSAEWOULDBLOCK)
{
if(lpMessage) sprintf(lpMessage,"伺服器:%s:%d連接配接失敗",lpServer,wPortConn);
break;
}
if(pInfo->conntype&PROXY_HTTP)//http proxy
{
sprintf(lpBuffer,"CONNECT %s:%d HTTP/1.0\r\nUser-Agent:rmtcmd/0.1\r\n\r\n",
pInfo->srvurl,pInfo->srvport);
send(_hSocket,lpBuffer,strlen(lpBuffer),0);
if(recv(_hSocket,lpBuffer,8000,0)<1)
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
if(strstr(lpBuffer,"Connection established")==NULL)
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
}
else if(pInfo->conntype&PROXY_SOCK5) //sock5 proxy
{
PSOCK5REQ req = (PSOCK5REQ)lpBuffer;
PSOCK5ANS ans = (PSOCK5ANS)(lpBuffer+1024);
req->ver = 5;
req->lmethods = 2;
req->methods[0] = 0;
req->methods[1] = 2;
send(_hSocket,(LPSTR)req,4,0);
if(!Recv(ans,sizeof(SOCK5ANS))||
ans->ver!=5||(ans->method&&ans->method!=2))
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
if(ans->method==2) //need user & passwd
{
PAUTHREQ reqa = (PAUTHREQ)lpBuffer;
PAUTHANS ansa = (PAUTHANS)(lpBuffer+1024);
memset(reqa,0,sizeof(AUTHREQ));
reqa->ver = 1;
reqa->ulen = strlen(strcpy(reqa->user,pInfo->proxyuser));
reqa->plen = strlen(strcpy(reqa->passwd,pInfo->proxypasswd));
send(_hSocket,lpBuffer,sizeof(AUTHREQ),0);
if(!Recv(ansa,sizeof(AUTHANS))||ansa->ver!=1||ansa->status)
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
}
PSOCK5REQEX reqex = (PSOCK5REQEX)lpBuffer;
PSOCK5ANSEX ansex = (PSOCK5ANSEX)(lpBuffer+1024);
memset(reqex,0,sizeof(SOCK5REQEX));
reqex->ver = 5;
reqex->cmd = 1;
reqex->rsv = 0;
reqex->atyp = 1;
if((reqex->addr=inet_addr(pInfo->srvurl))==INADDR_NONE)
{
if(lphost=gethostbyname(pInfo->srvurl))
reqex->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;
}
reqex->port = ntohs((WORD)pInfo->srvport);
send(_hSocket,(LPSTR)reqex,sizeof(SOCK5REQEX),0);
if(!Recv(ansex,sizeof(SOCK5ANSEX))||ansex->ver!=5||ansex->rep)
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
}
else if(pInfo->conntype&PROXY_SOCK4) //sock4 proxy
{
PSOCK4REQ req=(PSOCK4REQ)lpBuffer;
PSOCK4ANS ans=(PSOCK4ANS)(lpBuffer+1024);
req->vn = 4;
req->cd = 1;
req->port = ntohs((WORD)pInfo->srvport);
if((req->addr=inet_addr(pInfo->srvurl))==INADDR_NONE)
{
if(lphost=gethostbyname(pInfo->srvurl))
req->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;
}
send(_hSocket,lpBuffer,sizeof(SOCK4REQ),0);
if(!Recv(ans,sizeof(SOCK4ANS)))
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
if(ans->vn||ans->cd!=90)
{
if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);
break;
}
}
return(TRUE);
}while(0);
UMySocket::Close();
return(FALSE);
}
//代理伺服器連接配接
#pragma pack(push,1)
//sock4 req & ans
typedef struct tagSock4Req
{
char vn;
char cd;
WORD port;
DWORD addr;
char other[1];
}SOCK4REQ,*PSOCK4REQ;
typedef struct tagSock4Ans
{
char vn;
char cd;
}SOCK4ANS,*PSOCK4ANS;
//sock5 req & ans
typedef struct tagSock5Req
{
char ver;
char lmethods;
char methods[255];
}SOCK5REQ,*PSOCK5REQ;
typedef struct tagSock5Ans
{
char ver;
char method;
}SOCK5ANS,*PSOCK5ANS;
//sock5 check user
typedef struct tagAuthReq
{
char ver;
char ulen;
char user[255];
char plen;
char passwd[255];
}AUTHREQ,*PAUTHREQ;
typedef struct tagAuthAns
{
char ver;
char status;
}AUTHANS,*PAUTHANS;
typedef struct tagSock5ReqEx
{
char ver;
char cmd;
char rsv;
char atyp;
long addr;
WORD port;
}SOCK5REQEX,*PSOCK5REQEX;
typedef struct tagSock5AnsEx
{
char ver;
char rep;
char rsv;
char atyp;
char other[1];
}SOCK5ANSEX,*PSOCK5ANSEX;
#pragma pack(pop)
37.ping
38.telnet
39.close函數,fork
答:參考《UNP》卷1,第94頁。close是引用計數-1,在沒有到0的時候是不會關閉套接字的。fork調用會使父程序打開的socket引用計數+1,。是以一般多程序裡面,父程序在fork子程序之後,父程序可以關閉accept套接字,子程序可以關閉listen套接字。這個時候兩個套接字計數都從2減到1,是以不會關閉。是以父程序可以隻做監聽,子程序隻做通信。
40.Linux終端調試指令
netstat -nalp|grep 8011 #檢視8011端口的連接配接情況,觀察TCP狀态圖
netstat -nalp|grep 8011|wc -l #檢視8011端口的用戶端連接配接數
ulimit -n 102400 #修改目前程序的最大檔案數
tcpdump -i any 'tcp port 80'
lsof -i -Pn #lsof是list opened fd的單詞縮寫
netstat -anip
41.Windows cmd指令
netstat -ano|findstr "8011"#檢視8011端口的連接配接情況,觀察TCP狀态圖
-------
https://leetcode.com/tcp/ip詳解 第1卷,UNP,APUE,TCP/IP協定族
《程式設計珠玑第2版·修訂版》
《程式設計珠玑(續)(修訂版)》
《程式設計之美——微軟技術面試心得》
《劍指OFFER:名企面試官精講典型程式設計題(第2版)》
《程式員代碼面試指南:IT名企算法與資料結構題目最優解》
《程式員面試寶典(第5版)》