天天看點

TCP常見的網絡通信問題

本次 Live 主要包括以下内容 • TCP/IP協定棧層次與三次握手、四次揮手需要知道的細節 • TCP與UDP适用場景 • linux網絡模型 • epoll_event結構中epoll_data_t的fd與ptr使用場景 •Windows網絡模型 •異步connect •select可以檢測網絡異常嗎 •epoll的水準模式和邊緣模式 •阻塞與非阻塞socket的設定與差別 •send/recv傳回值問題 •如何編寫正确的收與發資料代碼 •收發緩沖區如何設計 •SO_SNDTIMEO、SO_RCVTIMEO、TCP_NODELAY、SO_REUSEADDR和SO_REUSEPORT、SO_LINGER選項 •shutdown與優雅關閉 •錯誤碼EINTR •tcp粘包問題 •信号SIGPIPE與EPIPE錯誤碼 •gethostbyname阻塞與錯誤碼 •SO_KEEPALIVE選項與心跳包設計技巧 •如何設計斷線重連機制 •如何清除無效的死鍊 •網絡架構中定時器不同實作 •http協定格式、head、get與post方法細節 •http、socks4與socks5代理編碼實作 •你問我答互動環節 •總結 

技術面試中常見的網絡通信細節問題解答

1. TCP/IP協定棧層次結構

2. TCP三向交握需要知道的細節點

3. TCP四次揮手需要知道的細節點(CLOSE_WAIT、TIME_WAIT、MSL)

4. TCP與UDP的差別與适用場景

5. linux常見網絡模型詳解(select、poll與epoll)

6. epoll_event結構中的epoll_data_t的fd與ptr的使用場景

7. Windows常見的網絡模型詳解(select、WSAEventSelect、WSAAsyncSelect)

8. Windows上的完成端口模型(IOCP)

9. 異步的connect函數如何編寫

10.select函數可以檢測網絡異常嗎?

11.你問我答環節一

12. epoll的水準模式和邊緣模式

13. 如何将socket設定成非阻塞的(建立時設定與建立完成後設定),非阻塞socket與阻塞的socket在收發資料上的差別

14. send/recv(read/write)傳回值大于0、等于0、小于0的差別

15.如何編寫正确的收資料代碼與發資料代碼

16.發送資料緩沖區與接收資料緩沖區如何設計

17.socket選項SO_SNDTIMEO和SO_RCVTIMEO

18.socket選項TCP_NODELAY

19.socket選項SO_REUSEADDR和SO_REUSEPORT(Windows平台與linux平台的差別)

20.socket選項SO_LINGER

21.shutdown與優雅關閉

22.你問我答環節二

23.socket選項SO_KEEPALIVE

24.關于錯誤碼EINTR

25.如何解決tcp粘包問題

26.信号SIGPIPE與EPIPE錯誤碼

27.gethostbyname阻塞與錯誤碼擷取問題

28.心跳包的設計技巧(保活心跳包與業務心跳包)

29.斷線重連機制如何設計

30.如何檢測對端已經關閉

31.如何清除無效的死鍊(端與端之間的線路故障)

32.定時器的不同實作及優缺點

33.你問我答環節三

34.http協定的具體格式

35.http head、get與post方法的細節

36.http代理、socks4代理與socks5代理如何編碼實作

37.ping

38.telnet

39.你問我答環節四

40.總結

--------------------------------------------------------------------------------------

技術面試中常見的網絡通信細節問題解答

1. TCP/IP協定棧層次結構

TCP常見的網絡通信問題

2. TCP三向交握需要知道的細節點

答:1、三次握手,如果前兩次有某一次失敗,會重新從第一次開始,重來三次。

2、三次握手,如果最後一次失敗,伺服器并不會重傳ack封包,而是直接發送RTS封包段,進入CLOSED狀态。這樣做的目的是為了防止SYN洪泛攻擊。詳情參見http://blog.csdn.net/libaineu2004/article/details/79020031

3、發起連接配接時如果發生TCP SYN丢包,那麼系統預設的重試間隔是3s,這期間不會傳回錯誤碼。

4、如何模拟tcp揮手失敗?答案是iptables指令可以過濾資料包,丢棄所有的連接配接請求,緻使用戶端無法得到任何ack封包。

TCP 的那些事兒(上)

TCP 的那些事兒(下)

3. TCP四次揮手需要知道的細節點(CLOSE_WAIT、TIME_WAIT、MSL)

答:http://blog.csdn.net/libaineu2004/article/details/78886213 再談應用環境下的TIME_WAIT和CLOSE_WAIT

http://blog.csdn.net/libaineu2004/article/details/78886182 CLOSE_WAIT狀态的原因與解決方法

http://blog.csdn.net/libaineu2004/article/details/78803068 TCP面試常見題:time_wait狀态産生的原因,危害,如何避免

4. TCP與UDP的差別與适用場景

答:TCP協定棧本身是可靠,不會丢包,不會亂序,失敗會重發。UDP需要應用層做協定來保證可靠性。視訊可以用UDP。

必須使用udp的場景:廣播。

5. linux常見網絡模型詳解(select、poll與epoll)

答:select和poll本質上沒有差別,都是輪詢,但是poll沒有最大裝置描述符數量的限制。

6. epoll_event結構中的epoll_data_t的fd與ptr的使用場景

答:在epoll模型中使用了一個struct epoll_event的結構體: 

typedef union epoll_data 

{ void *ptr;

 int fd; 

 uint32_t u32;

 uint64_t u64;

 } epoll_data_t; 

struct epoll_event 

{ uint32_t events;  

 epoll_data_t data;

 };

7. Windows常見的網絡模型詳解(select、WSAEventSelect、WSAAsyncSelect)

8. Windows上的完成端口模型(IOCP)

9. 異步的connect函數如何編寫

參考部落格:http://blog.csdn.net/analogous_love/article/details/60761528

TCP常見的網絡通信問題
  1. //關于tcp連接配接的異步connect實作流程如下:

  2. //(1)設定socket連接配接為非阻塞.

  3. //(2)調用connect函數.傳回0表明連接配接成功.如果傳回-1,同時errno為EINPROGRESS表明正在建立連接配接.

  4. //(3)使用select , epoll等 , 當描述符可寫的時候檢查連接配接狀态.

  5. #include <stdio.h>

  6. #include <fcntl.h>

  7. #include <sys/types.h>

  8. #include <sys/socket.h>

  9. #include <errno.h>

  10. #include <sys/epoll.h>

  11. #include <netinet/in.h>

  12. #include <string.h>

  13. void setnonblock(int fd)

  14. {

  15. int flags = fcntl(fd, F_GETFL);

  16. fcntl(fd, F_SETFL, flags | O_NONBLOCK);

  17. }

  18. int main(){

  19. const char* ip = "127.0.0.1";

  20. short port = 9999;

  21. //設定連接配接位址

  22. struct sockaddr_in addr;

  23. socklen_t socklen = sizeof(struct sockaddr_in);

  24. memset(&addr , 0 , sizeof(struct sockaddr_in));

  25. addr.sin_family = AF_INET;

  26. addr.sin_addr.s_addr = inet_addr(ip);

  27. addr.sin_port = htons(port);

  28. //建立描述符

  29. int fd = socket( AF_INET , SOCK_STREAM , 0);

  30. if (fd < 0){

  31. printf("socket() error\n");

  32. return -1;

  33. }

  34. //設定描述符為非阻塞

  35. setnonblock(fd);

  36. //連接配接

  37. int res;

  38. res = connect(fd , (struct sockaddr*)&addr , socklen);

  39. if (res == 0){

  40. printf("connect ok(1)\n");

  41. } else if (res == -1 && errno != EINPROGRESS){

  42. printf("connect err(1)\n");

  43. close(fd);

  44. return -1;

  45. } else {

  46. int epfd;

  47. //建立epoll描述符

  48. epfd = epoll_create(1024);

  49. if ( (epfd = epoll_create(1024) ) == -1){

  50. printf("epoll_create() err\n");

  51. close(fd);

  52. return -1;

  53. }

  54. //添加關注事件

  55. struct epoll_event ev;

  56. ev.events = EPOLLOUT;

  57. ev.data.fd = fd;

  58. epoll_ctl( epfd , EPOLL_CTL_ADD , fd , &ev);

  59. //編寫網絡程式的時候,epoll是程式的主循環.我們這裡為了測試,連接配接上或connect逾時(75秒)就break掉.

  60. //正常的流程是寫一個處理connect結果的回調函數.

  61. int event_max = 1;

  62. struct epoll_event events[event_max];

  63. int i;

  64. while (1){

  65. res = epoll_wait( epfd , events , event_max , -1);

  66. if (res > 0){

  67. for ( i = 0 ; i < res ; i++){

  68. if ( events[i].data.fd == fd && ( events[i].events & EPOLLOUT) ){ //29(EPOLLOUT|EPOLLERR|EPOLLHUP) //4(EPOLLOUT)

  69. //檢查是否連接配接成功

  70. int optval;

  71. socklen_t optlen = sizeof(optval);

  72. int res1 = getsockopt(fd, SOL_SOCKET, SO_ERROR, &optval, &optlen);

  73. if ( res1 < 0 || optval){

  74. close(fd);

  75. close(epfd);

  76. printf("connect err(2)\n");

  77. return -1;

  78. } else {

  79. printf("connect ok(2)\n");

  80. }

  81. }

  82. }

  83. break;

  84. }

  85. }

  86. close(fd);

  87. close(epfd);

  88. }

  89. }

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水準模式下收取資料,這種情況下收取的資料可以小于指定大小,總之一次能收到多少是多少:

  1. bool TcpSession::Recv()

  2. {

  3. //每次隻收取256個位元組

  4. char buff[256];

  5. //memset(buff, 0, sizeof(buff));

  6. int nRecv = ::recv(clientfd_, buff, 256, 0);

  7. if (nRecv == 0)

  8. return false;

  9. inputBuffer_.add(buff, (size_t)nRecv);

  10. return true;

  11. }

如果是linux epoll邊緣模式(ET),則一定要一次性收完:

  1. bool TcpSession::RecvEtMode()

  2. {

  3. //每次隻收取256個位元組

  4. char buff[256];

  5. while (true)

  6. {

  7. //memset(buff, 0, sizeof(buff));

  8. int nRecv = ::recv(clientfd_, buff, 256, 0);

  9. if (nRecv == -1)

  10. {

  11. if (errno == EWOULDBLOCK || errno == EINTR)

  12. return true;

  13. return false;

  14. }

  15. //對端關閉了socket

  16. else if (nRecv == 0)

  17. return false;

  18. inputBuffer_.add(buff, (size_t)nRecv);

  19. }

  20. return true;

  21. }

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接收到資料大小,

特别:非阻塞模式下傳回 值 <0時并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情況 下認為連接配接是正常的,繼續接收。

隻是阻塞模式下read會阻塞着接收資料,非阻塞模式下如果沒有資料會傳回,不會阻塞着讀,是以需要 循環讀取。

send:

阻塞與非阻塞send傳回值沒有區分,都是 <0:出錯,=0:連接配接關閉,>0發送資料大小,

特别:非阻塞模式下傳回值 <0時并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情況下認為連接配接是正常的, 繼續發送。

隻是阻塞模式下send會阻塞着發送資料,非阻塞模式下如果暫時無法發送資料會傳回,不會阻塞着 send,是以需要循環發送。

15.如何編寫正确的收資料代碼與發資料代碼

答:都需要考慮緩沖區的設計,發資料如果失敗先緩存起來,等待下一次的機會再發。

參考部落格:http://blog.csdn.net/analogous_love/article/details/60761528

  1. bool TcpSession::Send()

  2. {

  3. while (true)

  4. {

  5. int n = ::send(clientfd_, buffer_, buffer_.length(), 0);

  6. if (n == -1)

  7. {

  8. //tcp視窗容量不夠, 暫且發不出去,下次再發

  9. if (errno == EWOULDBLOCK)

  10. break;

  11. //被信号中斷,繼續發送

  12. else if (errno == EINTR)

  13. continue;

  14. return false;

  15. }

  16. //對端關閉了連接配接

  17. else if (n == 0)

  18. return false;

  19. buffer_.erase(n);

  20. //全部發送完畢

  21. if (buffer_.length() == 0)

  22. break;

  23. }

  24. return true;

  25. }

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:

  1. int timeout = TIMEOUT_VALUE;

  2. int err;

  3. SOCKET s;

  4. s = socket( ... );

  5. 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/53433994

  1. bool Connect(const char* pszIp, int nPort)

  2. {

  3. struct hostent* pHostent = NULL;

  4. char* host = NULL;

  5. struct sockaddr_in addrSrv;

  6. memset(&addrSrv, 0, sizeof(addrSrv));

  7. addrSrv.sin_addr.s_addr = ::inet_addr(pszIp);

  8. if (addrSrv.sin_addr.s_addr == INADDR_NONE)

  9. {

  10. pHostent = ::gethostbyname(pszIp);

  11. if (pHostent != NULL)

  12. {

  13. host = inet_ntoa(*((struct in_addr *)pHostent->h_addr));

  14. std::cout << pszIp << "=" << host << std::endl;

  15. }

  16. else

  17. {

  18. herror("gethostbyname error");

  19. std::cout << std::endl;

  20. return false;

  21. }

  22. }

  23. else

  24. host = (char*)pszIp;

  25. if (Socket.Connect(host, nPort))

  26. return true;

  27. std::cout << "Unable to connect to server " << pszIp << ":" << nPort << std::endl;

  28. return false;

  29. }

28.心跳包的設計技巧(保活心跳包與業務心跳包)

答:http://blog.csdn.net/analogous_love/article/details/78388187

29.用戶端斷線重連機制如何設計

答:用戶端先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 )或104 (#define ECONNRESET 104 )

對于被動關閉的SOCKET,recv傳回0,而且errno被置為11(#define EWOULDBLOCK EAGAIN )

 對正常的SOCKET, 如果有接收資料,則傳回>0, 否則傳回-1,而且errno被置為11(#define EWOULDBLOCK EAGAIN )

是以對于簡單的狀态判斷(不過多考慮異常情況):

    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方法的細節

答:

  1. GET /index.php HTTP/1.1\r\n

  2. Host: www.hootina.org\r\n

  3. Connection: keep-alive\r\n

  4. Cache-Control: max-age=0\r\n

  5. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,**;q=0.8\r\n

  6. content-length: 8

  7. User-Agent: Mozilla/5.0\r\n

  8. \r\n

  9. abcdefgh

36.http代理、socks4代理與socks5代理如何編碼實作

答:代碼如下

  1. BOOL UMySocket::Connect(PNETCONN_INFO pInfo,LPSTR lpMessage) //連接配接伺服器

  2. {

  3. wsysplus_memory vMemory;

  4. long noDelay(1),tmSend(1800*1000L),tmRecv(1800*1000L);

  5. LPSTR lpBuffer=vMemory.GetBuf(8001);

  6. //關閉連接配接先

  7. UMySocket::Close();

  8. //初始化

  9. if(!lpBuffer)

  10. {

  11. if(lpMessage) strcpy(lpMessage,"MALLOC DATA");

  12. return(FALSE);

  13. }

  14. _hSocket=socket(PF_INET,SOCK_STREAM,0);

  15. if(_hSocket==INVALID_SOCKET)

  16. {

  17. if(lpMessage) strcpy(lpMessage,"INVALID_SOCKET");

  18. return(FALSE);

  19. }

  20. //設定連接配接屬性

  21. setsockopt(_hSocket,IPPROTO_TCP,TCP_NODELAY,(LPSTR)&noDelay,sizeof(long));

  22. setsockopt(_hSocket,SOL_SOCKET,SO_SNDTIMEO,(LPSTR)&tmSend,sizeof(long));

  23. setsockopt(_hSocket,SOL_SOCKET,SO_RCVTIMEO,(LPSTR)&tmRecv,sizeof(long));

  24. //連接配接伺服器

  25. WORD wPortConn=(WORD)pInfo->proxyport;

  26. char *lpServer=pInfo->proxyurl;

  27. sockaddr_in remote={0};

  28. remote.sin_family = AF_INET;

  29. if(!(pInfo->conntype&0x000F)) //未使用代理

  30. {

  31. lpServer = pInfo->srvurl;

  32. wPortConn = (WORD)pInfo->srvport;

  33. }

  34. remote.sin_port = htons(wPortConn);

  35. LPHOSTENT lphost=NULL;

  36. if((remote.sin_addr.s_addr=inet_addr(lpServer))==INADDR_NONE)

  37. {

  38. if(lphost=gethostbyname(lpServer))

  39. remote.sin_addr.s_addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;

  40. }

  41. do

  42. {

  43. if(connect(_hSocket,(sockaddr*)&remote,sizeof(remote))

  44. ==SOCKET_ERROR&&WSAGetLastError()!=WSAEWOULDBLOCK)

  45. {

  46. if(lpMessage) sprintf(lpMessage,"伺服器:%s:%d連接配接失敗",lpServer,wPortConn);

  47. break;

  48. }

  49. if(pInfo->conntype&PROXY_HTTP)//http proxy

  50. {

  51. sprintf(lpBuffer,"CONNECT %s:%d HTTP/1.0\r\nUser-Agent:rmtcmd/0.1\r\n\r\n",

  52. pInfo->srvurl,pInfo->srvport);

  53. send(_hSocket,lpBuffer,strlen(lpBuffer),0);

  54. if(recv(_hSocket,lpBuffer,8000,0)<1)

  55. {

  56. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  57. break;

  58. }

  59. if(strstr(lpBuffer,"Connection established")==NULL)

  60. {

  61. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  62. break;

  63. }

  64. }

  65. else if(pInfo->conntype&PROXY_SOCK5) //sock5 proxy

  66. {

  67. PSOCK5REQ req = (PSOCK5REQ)lpBuffer;

  68. PSOCK5ANS ans = (PSOCK5ANS)(lpBuffer+1024);

  69. req->ver = 5;

  70. req->lmethods = 2;

  71. req->methods[0] = 0;

  72. req->methods[1] = 2;

  73. send(_hSocket,(LPSTR)req,4,0);

  74. if(!Recv(ans,sizeof(SOCK5ANS))||

  75. ans->ver!=5||(ans->method&&ans->method!=2))

  76. {

  77. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  78. break;

  79. }

  80. if(ans->method==2) //need user & passwd

  81. {

  82. PAUTHREQ reqa = (PAUTHREQ)lpBuffer;

  83. PAUTHANS ansa = (PAUTHANS)(lpBuffer+1024);

  84. memset(reqa,0,sizeof(AUTHREQ));

  85. reqa->ver = 1;

  86. reqa->ulen = strlen(strcpy(reqa->user,pInfo->proxyuser));

  87. reqa->plen = strlen(strcpy(reqa->passwd,pInfo->proxypasswd));

  88. send(_hSocket,lpBuffer,sizeof(AUTHREQ),0);

  89. if(!Recv(ansa,sizeof(AUTHANS))||ansa->ver!=1||ansa->status)

  90. {

  91. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  92. break;

  93. }

  94. }

  95. PSOCK5REQEX reqex = (PSOCK5REQEX)lpBuffer;

  96. PSOCK5ANSEX ansex = (PSOCK5ANSEX)(lpBuffer+1024);

  97. memset(reqex,0,sizeof(SOCK5REQEX));

  98. reqex->ver = 5;

  99. reqex->cmd = 1;

  100. reqex->rsv = 0;

  101. reqex->atyp = 1;

  102. if((reqex->addr=inet_addr(pInfo->srvurl))==INADDR_NONE)

  103. {

  104. if(lphost=gethostbyname(pInfo->srvurl))

  105. reqex->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;

  106. }

  107. reqex->port = ntohs((WORD)pInfo->srvport);

  108. send(_hSocket,(LPSTR)reqex,sizeof(SOCK5REQEX),0);

  109. if(!Recv(ansex,sizeof(SOCK5ANSEX))||ansex->ver!=5||ansex->rep)

  110. {

  111. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  112. break;

  113. }

  114. }

  115. else if(pInfo->conntype&PROXY_SOCK4) //sock4 proxy

  116. {

  117. PSOCK4REQ req=(PSOCK4REQ)lpBuffer;

  118. PSOCK4ANS ans=(PSOCK4ANS)(lpBuffer+1024);

  119. req->vn = 4;

  120. req->cd = 1;

  121. req->port = ntohs((WORD)pInfo->srvport);

  122. if((req->addr=inet_addr(pInfo->srvurl))==INADDR_NONE)

  123. {

  124. if(lphost=gethostbyname(pInfo->srvurl))

  125. req->addr = ((LPIN_ADDR)lphost->h_addr)->s_addr;

  126. }

  127. send(_hSocket,lpBuffer,sizeof(SOCK4REQ),0);

  128. if(!Recv(ans,sizeof(SOCK4ANS)))

  129. {

  130. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  131. break;

  132. }

  133. if(ans->vn||ans->cd!=90)

  134. {

  135. if(lpMessage) sprintf(lpMessage,"代理伺服器:%s:%d通訊失敗",pInfo->srvurl,pInfo->srvport);

  136. break;

  137. }

  138. }

  139. return(TRUE);

  140. }while(0);

  141. UMySocket::Close();

  142. return(FALSE);

  143. }

  144. //代理伺服器連接配接

  145. #pragma pack(push,1)

  146. //sock4 req & ans

  147. typedef struct tagSock4Req

  148. {

  149. char vn;

  150. char cd;

  151. WORD port;

  152. DWORD addr;

  153. char other[1];

  154. }SOCK4REQ,*PSOCK4REQ;

  155. typedef struct tagSock4Ans

  156. {

  157. char vn;

  158. char cd;

  159. }SOCK4ANS,*PSOCK4ANS;

  160. //sock5 req & ans

  161. typedef struct tagSock5Req

  162. {

  163. char ver;

  164. char lmethods;

  165. char methods[255];

  166. }SOCK5REQ,*PSOCK5REQ;

  167. typedef struct tagSock5Ans

  168. {

  169. char ver;

  170. char method;

  171. }SOCK5ANS,*PSOCK5ANS;

  172. //sock5 check user

  173. typedef struct tagAuthReq

  174. {

  175. char ver;

  176. char ulen;

  177. char user[255];

  178. char plen;

  179. char passwd[255];

  180. }AUTHREQ,*PAUTHREQ;

  181. typedef struct tagAuthAns

  182. {

  183. char ver;

  184. char status;

  185. }AUTHANS,*PAUTHANS;

  186. typedef struct tagSock5ReqEx

  187. {

  188. char ver;

  189. char cmd;

  190. char rsv;

  191. char atyp;

  192. long addr;

  193. WORD port;

  194. }SOCK5REQEX,*PSOCK5REQEX;

  195. typedef struct tagSock5AnsEx

  196. {

  197. char ver;

  198. char rep;

  199. char rsv;

  200. char atyp;

  201. char other[1];

  202. }SOCK5ANSEX,*PSOCK5ANSEX;

  203. #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狀态圖

繼續閱讀