UNIX网络套接字相关总结
文章目錄
- 網絡協議
- RFC 相關文檔
- 網絡 ip 層
- ip 頭部
- 傳輸層
- tcp
- 相關概念
- MSL是Maximum Segment Lifetime,“報文最大生存時間”
- MSS:Maximum Segment Size
- RTT:Round Trip Time
- RTO:Retransmission TimeOut
- /proc/sys/net/ipv4/
- tcp 頭部
- tcp 狀態轉換圖
- tcp 定時器
- 流量控制和滑動窗口
- 擁塞控制和擁塞窗口
- tcp 延時 ACK
- Time-wait狀態(2MSL)
- 1. 為什么需要TIME_WAIT狀態
- linux網絡編程
- 相關概念
- 套接字地址結構
- struct in_addr
- struct sockaddr和struct sockaddr_in
- 字節排序函數
- 地址轉換函數:
- 1. in_addr_t inet_addr(const char *cp)函數轉換標準的ASCII以點分十進制的地址值返回為網絡字節序二進制值
- 2. int inet_aton(const char *__cp, in_addr *__inp)轉換標準的ASCII以點分十進制的地址值返回網絡字節序二進制值
- 3. char *inet_ntoa (struct in_addr __in)函數轉換網絡字節序二進制值返回標準的ASCII以點分十進制的地址值
- 4. 代碼示例:
- 1. int inet_pton(int domain, const char *str, void *addr)將標準的ASCII以點分十進制的地址值轉化為網絡傳輸的二進制數值格式
- 2. const char *inet_ntop(int domain, const void *addr, char *str, socklen_t size)將網絡傳輸的二進制數值轉化標準的ASCII以點分十進制的地址值格式
- 3. 代碼示例:
- TCP通信相關函數
- 1. int socket(family, type, protocol):創建套接字
- 2. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):綁定套接字
- 3. int listen(int fd,int backlog):設置同時通信的最大套接字數量
- 4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)阻塞式監聽客戶端連接
- 5. int connect(int sockfd, const strcut sockaddr *addr, socklen_t addrlen)用來客戶端和tcp服務器建立連接
- 5. ssize_t read(int fd,void *ptr,size_t nbytes)一次讀取指定字節長度數據
- 6. ssize_t write(int fd,const void *ptr,size_t nbytes)一次寫入指定字節長度數據
- 7. ssize_t Readn(int fd,void *vptr,size_t n);循環讀取n個字節數據
- 8. ssize_t Writen(int fd,const void*vptr,size_t n)循環寫入n個字節數據
- 9. ssize_t Readline(int fd, void *vptr, size_t maxlen) 讀到'\n'或者讀滿緩沖區才返回
- 10. int close(int fd) 關閉套接字
- 11. shutdown()函數切斷進程共享的套接字的所有連接
- 12. recv()和send()函數
- UDP通信相關函數
- 1. recvfrom()函數
- 2. sendto()函數
- UDP組播通信相關函數
- 常見的錯誤碼
- 參考文獻
網絡協議
RFC 相關文檔
- RFC官網
- RFC 791:INTERNET PROTOCOL
- RFC 793:Transmission Control Protocol
網絡 ip 層
ip 頭部
ip消息頭可分為 20 個字節的固定頭部和最多40字節可擴展頭:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Version| IHL |Type of Service| Total Length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Identification |Flags| Fragment Offset |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Time to Live | Protocol | Header Checksum |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Source Address |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Destination Address |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Options | Padding |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+Example Internet Datagram Header| Version | 4bit | 值為4時代表IPV4;值為6時代表IPV6 |
| IHL | 4bit | ip消息頭可分為20個字節的固定頭部40字節可擴展頭 |
| Type of Service | 8bit | 服務類型,只有在有QoS差分服務要求時這個字段才起作用 |
| Total Length | 16bit | 代表總長度,整個IP數據報的長度,包括首部和數據之和,單位為字節,最長65535,總長度必須不超過最大傳輸單元MTU |
| Identification | 16bit | 標識,主機每發一個報文值會加1,分片重組時會用到該字段 |
| Flags | 3bit | 分片重裝時使用:第一位,為0,第二位,DF(Don’t Fragment),能否分片位,0表示可以分片,1表示不能分片;第三位MF(More Fragment),表示是否該報文為最后一片,0表示最后一片,1代表后面還有 |
| Fragment Offset | 13bit | 片偏移:分片重組時會用到該字段。表示較長的分組在分片后,某片在原分組中的相對位置 |
| Time to Live | 8bit | 生存時間可經過的最多路由數,即數據包在網絡中可通過的路由器數的最大值 |
| Protocol | 8bit | 標識下一層協議 |
| Header Checksum | 16bit | 首部校驗和,只檢驗數據包的首部,不檢驗數據部分 |
| Source Address | 32bi | 源IP地址 |
| Destination Address | 32bit | 目的IP地址。 |
| Options | 長度可變 | 選項字段,用來支持排錯,測量以及安全等措施。 |
| Padding | 長度可變 | 填充字段,全為0 |
傳輸層
tcp
相關概念
MSL是Maximum Segment Lifetime,“報文最大生存時間”
它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄。
因為TCP報文(segment)是IP數據報(datagram)的數據部分,而IP頭中有一個TTL域,TTL是time to live的縮寫,中文可以譯為“生存時間”,這個生存時間是由源主機設置初始值但不是存的具體時間,而是存儲了一個IP數據報可以經過的最大路由數,每經過一個處理他的路由器此值就減1,當此值為0則數據報將被丟棄,同時發送ICMP報文通知源主機。
RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等
2MSL即兩倍的MSL,TCP的TIME_WAIT狀態也稱為2MSL等待狀態,當TCP的一端發起主動關閉,在發出最后一個ACK包后,即第3次握手完成后發送了第四次握手的ACK包后就進入了TIME_WAIT狀態,必須在此狀態上停留兩倍的MSL時間。
等待2MSL時間主要目的是怕最后一個ACK包對方沒收到,那么對方在超時后將重發第三次握手的FIN包,主動關閉端接到重發的FIN包后可以再發一個ACK應答包。
在TIME_WAIT狀態時兩端的端口不能使用,要等到2MSL時間結束才可繼續使用。
當連接處于2MSL等待階段時任何遲到的報文段都將被丟棄。不過在實際應用中可以通過設置SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此端口。
MSS:Maximum Segment Size
對于IPv4,為了避免IP分片,主機一般默認MSS為536字節 (576IP最大字節數-20字節TCP協議頭-20字節IP協議頭=536字節)。同理,IPv6的主機默認MSS為1220字節(1280IP最大字節數-20字節TCP協議頭-40字節IP協議頭=1220字節)。
當發送方主機想要調整MSS時,應注意以下幾點:
- MSS不包含TCP及IP的協議頭長度。
- MSS選項只能在初始化連接請求(SYN=1)使用。
- 發送方與接收方的MSS不一定相等
最大報文段長度(MSS)與最大傳輸單元(Maximum Transmission Unit, MTU)均是協議用來定義最大長度的。不同的是,MTU應用于OSI模型的第二層數據鏈接層,并無具體針對的協議。MTU限制了數據鏈接層上可以傳輸的數據包的大小,也因此限制了上層(網絡層)的數據包大小。例如,如果已知某局域網的MTU為1500字節,則在網絡層的因特網協議(Internet Protocol, IP)里,最大的數據包大小為1500字節(包含IP協議頭)。MSS針對的是OSI模型里第四層傳輸層的TCP協議。因為MSS應用的協議在數據鏈接層的上層,MSS會受到MTU的限制
RTT:Round Trip Time
發送一個數據包收到對應的ACK,所花費的時間
RTO:Retransmission TimeOut
重傳時間間隔
/proc/sys/net/ipv4/
參考:/proc/sys/net/ipv4/下網絡參數的理解以及sysctl命令修改內核參數
tcp 頭部
TCP Header Format0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Source Port | Destination Port |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Sequence Number |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Acknowledgment Number |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Data | |U|A|P|R|S|F| || Offset| Reserved |R|C|S|S|Y|I| Window || | |G|K|H|T|N|N| |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Checksum | Urgent Pointer |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Options | Padding |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| data |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+TCP Header Format1)源端口:源端口和IP地址的作用是標識報文的返回地址。
2)目的端口:端口指明接收方計算機上的應用程序接口。
TCP報頭中的源端口號和目的端口號同IP數據報中的源IP與目的IP唯一確定一條TCP連接
序號和確認號:是TCP可靠傳輸的關鍵部分
1)序號是本報文段發送的數據組的第一個字節的序號。在TCP傳送的流中,每一個字節一個序號。e.g.一個報文段的序號為300,此報文段數據部分共有100字節,則下一個報文段的序號為400。所以序號確保了TCP傳輸的有序性。
2)確認號,即ACK,指明下一個期待收到的字節序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標志為1時才有效。比如建立連接時,SYN報文的ACK標志位為0。
數據偏移/首部長度:4bits
由于首部可能含有可選項內容,因此TCP報頭的長度是不確定的,報頭不包含任何任選字段則長度為20字節,4位首部長度字段所能表示的最大值為1111,轉化為10進制為15,15*32/8 = 60,故報頭最大長度為60字節。首部長度也叫數據偏移,是因為首部長度實際上指示了數據區在報文段中的起始偏移值。
保留:為將來定義新的用途保留,現在一般置0。
控制位:URG ACK PSH RST SYN FIN,共6個,每一個標志位表示一個控制功能。
1)URG:緊急指針標志,為1時表示緊急指針有效,為0則忽略緊急指針。
2)ACK:確認序號標志,為1時表示確認號有效,為0表示報文中不含確認信息,忽略確認號字段。
3)PSH:push標志,為1表示是帶有push標志的數據,指示接收方在接收到該報文段以后,應盡快將這個報文段交給應用程序,而不是在緩沖區排隊。
4)RST:重置連接標志,用于重置由于主機崩潰或其他原因而出現錯誤的連接。或者用于拒絕非法的報文段和拒絕連接請求。
5)SYN:同步序號,用于建立連接過程,在連接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,而連接應答捎帶一個確認,即SYN=1和ACK=1。
6)FIN:finish標志,用于釋放連接,為1時表示發送方已經沒有數據發送了,即關閉本方數據流。
窗口:滑動窗口大小,用來告知發送端接受端的緩存大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小時一個16bit字段,因而窗口大小最大為65535。
校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,并由接收端進行驗證。
緊急指針:只有當 URG標志置1時緊急指針才有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最后一個字節的序號。TCP的緊急方式是發送端向另一端發送緊急數據的一種方式。傳輸層協議使用帶外數據(out-of-band,OOB)來發送一些重要的數據,如果通信一方有重要的數據需要通知對方時,協議能夠將這些數據快速地發送到對方.為了發送這些數據,協議一般不使用與普通數據相同的通道,而是使用另外的通道.linux系統的套接字機制支持低層協議發送和接受帶外數據.但是TCP協議沒有真正意義上的帶外數據.為了發送重要協議,TCP提供了一種稱為緊急模式(urgentmode)的機制.TCP協議在數據段中設置URG位,表示進入緊急模式.接收方可以對緊急模式采取特殊的處理.很容易看出來,這種方式數據不容易被阻塞,可以通過在我們的服務器端程序里面捕捉SIGURG信號來及時接受數據或者使用帶OOB標志的recv函數來接受
選項和填充:最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。
數據部分: TCP 報文段中的數據部分是可選的。在一個連接建立和一個連接終止時,雙方交換的報文段僅有 TCP 首部。如果一方沒有數據要發送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多情況中,也會發送不帶任何數據的報文段
可選項:
- 選項的第一個字段kind說明選項的類型。有的TCP選項沒有后面兩個字段,僅包含1字節的kind字段
- 第二個字段length(如果有的話)指定該選項的總長度,該長度包括kind字段和length字段占據的2字節
- 第三個字段info(如果有的話)是選項的具體信息。常見的TCP選項有7種,
-
kind=0是選項表結束選項。
-
kind=1是空操作(nop)選項,沒有特殊含義,一般用于將TCP選項的總長度填充為4字節的整數倍。
-
kind=2是最大報文段長度選項。TCP連接初始化時,通信雙方使用該選項來協商最大報文段長度(Max Segment Size,MSS)。TCP模塊通常將MSS設置為(MTU-40)字節(減掉的這40字節包括20字節的TCP頭部和20字節的IP頭部)。這樣攜帶TCP報文段的IP數據報的長度就不會超過MTU(假設TCP頭部和IP頭部都不包含選項字段,并且這也是一般情況),從而避免本機發生IP分片。對以太網而言,MSS值是1460(1500-40)字節。
-
kind=3是窗口擴大因子選項。TCP連接初始化時,通信雙方使用該選項來協商接收通告窗口的擴大因子。在TCP的頭部中,接收通告窗口大小是用16位表示的,故最大為65535字節,但實際上TCP模塊允許的接收通告窗口大小遠不止這個數(為了提高TCP通信的吞吐量)。窗口擴大因子解決了這個問題。假設TCP頭部中的接收通告窗口大小是N,窗口擴大因子(移位數)是M,那么TCP報文段的實際接收通告窗口大小是N乘2M,或者說N左移M位。注意,M的取值范圍是0~14。我們可以通過修改/proc/sys/net/ipv4/tcp_window_scaling內核變量來啟用或關閉窗口擴大因子選項。和MSS選項一樣,窗口擴大因子選項只能出現在同步報文段中,否則將被忽略。但同步報文段本身不執行窗口擴大操作,即同步報文段頭部的接收通告窗口大小就是該TCP報文段的實際接收通告窗口大小。當連接建立好之后,每個數據傳輸方向的窗口擴大因子就固定不變了。關于窗口擴大因子選項的細節,可參考標準文檔RFC 1323。
-
kind=4是選擇性確認(Selective Acknowledgment,SACK)選項。TCP通信時,如果某個TCP報文段丟失,則TCP模塊會重傳最后被確認的TCP報文段后續的所有報文段,這樣原先已經正確傳輸的TCP報文段也可能重復發送,從而降低了TCP性能。SACK技術正是為改善這種情況而產生的,它使TCP模塊只重新發送丟失的TCP報文段,不用發送所有未被確認的TCP報文段。選擇性確認選項用在連接初始化時,表示是否支持SACK技術。我們可以通過修改/proc/sys/net/ipv4/tcp_sack內核變量來啟用或關閉選擇性確認選項。
-
kind=5是SACK實際工作的選項。該選項的參數告訴發送方本端已經收到并緩存的不連續的數據塊,從而讓發送端可以據此檢查并重發丟失的數據塊。每個塊邊沿(edge of block)參數包含一個4字節的序號。其中塊左邊沿表示不連續塊的第一個數據的序號,而塊右邊沿則表示不連續塊的最后一個數據的序號的下一個序號。這樣一對參數(塊左邊沿和塊右邊沿)之間的數據是沒有收到的。因為一個塊信息占用8字節,所以TCP頭部選項中實際上最多可以包含4個這樣的不連續數據塊(考慮選項類型和長度占用的2字節)。
-
kind=8是時間戳選項。該選項提供了較為準確的計算通信雙方之間的回路時間(Round Trip Time,RTT)的方法,從而為TCP流量控制提供重要信息。我們可以通過修改/proc/sys/net/ipv4/tcp_timestamps內核變量來啟用或關閉時間戳選項。
tcp 狀態轉換圖
+---------+ ---------\ active OPEN | CLOSED | \ ----------- +---------+<---------\ \ create TCB | ^ \ \ snd SYN passive OPEN | | CLOSE \ \ ------------ | | ---------- \ \ create TCB | | delete TCB \ \ V | \ \ +---------+ CLOSE | \ | LISTEN | ---------- | | +---------+ delete TCB | | rcv SYN | | SEND | | ----------- | | ------- | V +---------+ snd SYN,ACK / \ snd SYN +---------+| |<----------------- ------------------>| || SYN | rcv SYN | SYN || RCVD |<-----------------------------------------------| SENT || | snd ACK | || |------------------ -------------------| |+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+| -------------- | | ----------- | x | | snd ACK | V V | CLOSE +---------+ | ------- | ESTAB | | snd FIN +---------+ | CLOSE | | rcv FIN V ------- | | ------- +---------+ snd FIN / \ snd ACK +---------+| FIN |<----------------- ------------------>| CLOSE || WAIT-1 |------------------ | WAIT |+---------+ rcv FIN \ +---------+| rcv ACK of FIN ------- | CLOSE | | -------------- snd ACK | ------- | V x V snd FIN V +---------+ +---------+ +---------+|FINWAIT-2| | CLOSING | | LAST-ACK|+---------+ +---------+ +---------+| rcv ACK of FIN | rcv ACK of FIN | | rcv FIN -------------- | Timeout=2MSL -------------- | | ------- x V ------------ x V \ snd ACK +---------+delete TCB +---------+------------------------>|TIME WAIT|------------------>| CLOSED |+---------+ +---------+TCP Connection State DiagramFigure 6. LISTEN:偵聽來自遠方的TCP端口的連接請求SYN-SENT:再發送連接請求后等待匹配的連接請求(客戶端)SYN-RECEIVED:再收到和發送一個連接請求后等待對方對連接請求的確認(服務器)ESTABLISHED:代表一個打開的連接FIN-WAIT-1:等待遠程TCP連接中斷請求,或先前的連接中斷請求的確認FIN-WAIT-2:從遠程TCP等待連接中斷請求CLOSE-WAIT:等待從本地用戶發來的連接中斷請求CLOSING:等待遠程TCP對連接中斷的確認LAST-ACK:等待原來的發向遠程TCP的連接中斷請求的確認TIME-WAIT:等待足夠的時間以確保遠程TCP接收到連接中斷請求的確認CLOSED:沒有任何連接狀態 主動端可能出現的狀態:FIN_WAIT1、FIN_WAIT2、CLOSING、TIME_WAIT? 被動端可能出現的狀態:CLOSE_WAIT LAST_ACK-
SYN_RCVD: 這個狀態表示接收到了SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫,基本上用netstat你是很難看到這種狀態的,除非你特意寫了一個客戶端測試程序,故意將三次TCP握手過程中最后一個ACK報文不予發送。因此這種狀態時,當收到客戶端的ACK報文后,它會進入到ESTABLISHED狀態。如果收到一個RST信號,則返回到LISTEN狀態
-
SYN_SENT: 這個狀態與SYN_RCVD遙相呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,因此也隨即它會進入到了SYN_SENT狀態,并等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。
-
FIN_WAIT_1: 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是:FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連接,向對方發送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方回應ACK報文后,則進入到FIN_WAIT_2狀態,當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1狀態一般是比較難見到的,而FIN_WAIT_2狀態還有時常常可以用netstat看到。
-
FIN_WAIT_2:上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半連接,也即有一方要求close連接,但另外還告訴對方,我暫時還有點數據需要傳送給你,稍后再關閉連接。
-
TIME_WAIT: 表示收到了對方的FIN報文,并發送出了ACK報文,就等2MSL后即可回到CLOSED可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。
-
CLOSING: 這種狀態比較特殊,實際情況中應該是很少見,屬于一種比較罕見的例外狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,并沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?那就是如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。另外一種情況就是,ACK丟失了。
-
CLOSE_WAIT: 這種狀態的含義其實是表示在等待關閉。怎么理解呢?當對方close一個SOCKET后發送FIN報文給自己,你系統毫無疑問地會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正需要考慮的事情是察看你是否還有數據發送給對方,如果沒有的話,那么你也就可以close這個SOCKET,發送FIN報文給對方,也即關閉連接。所以你在CLOSE_WAIT狀態下,需要完成的事情是等待你去關閉連接。
-
LAST_ACK: 這個狀態還是比較容易好理解的,它是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,也即可以進入到CLOSED可用狀態了
tcp 定時器
- 超時重傳
- 堅持定時器:
- keepalive
- time_wait
流量控制和滑動窗口
滑動窗口協議是傳輸層進行流控的一種措施,接收方通過通告發送方自己的可以接受緩沖區大小(這個字段越大說明網絡吞吐量越高),從而控制發送方的發送速度,不過如果接收端的緩沖區一旦面臨數據溢出,窗口大小值也會隨之被設置一個更小的值通知給發送端,從而控制數據發送量(發送端會根據接收端指示,進行流量控制)。
發送端:
- 已發送被確認
- 已發送未確認
- 允許發送未發送
- 暫不允許發送
接收端:
- 已確認消息
- 允許接收
- 接收未發送確認消息
- 不允許接收
擁塞控制和擁塞窗口
發送方維持一個擁塞窗口 cwnd ( congestion window )的狀態變量。擁塞窗口的大小取決于網絡的擁塞程度,并且動態地在變化。發送方讓自己的發送窗口等于擁塞。
發送方控制擁塞窗口的原則是:只要網絡沒有出現擁塞,擁塞窗口就再增大一些,以便把更多的分組發送出去。但只要網絡出現擁塞,擁塞窗口就減小一些,以減少注入到網絡中的分組數
- 慢開始( slow-start )
- 擁塞避免( congestion avoidance )
- 快重傳( fast retransmit )
- 快恢復( fast recovery )
tcp 延時 ACK
參考:TCP/IP卷一:80—TCP數據流與窗口管理之(延時確認(延遲ACK)、Nagle算法
ACK延遲確認機制
接收方在收到數據后,并不會立即回復ACK,而是延遲一定時間。一般ACK延遲發送的時間低于500ms,但這個時間并非收到數據后需要延遲的時間。系統有一個固定的定時器會來檢查是否需要發送ACK包。這樣做有兩個目的。
- 這樣做的目的是ACK是可以合并的,也就是指如果連續收到兩個TCP包,并不一定需要ACK兩次,只要回復最終的ACK就可以了,可以降低網絡流量。
- 如果接收方有數據要發送,那么就會在發送數據的TCP數據包里,帶上ACK信息。這樣做,可以避免大量的ACK以一個單獨的TCP包發送,減少了網絡流量。
不同操作系統對延遲確認的實現
- 采用延時ACK的方法會減少ACK傳輸數目,可以一定程度地減輕網絡負載。對于批量數據傳輸通常為 2:1 的比例。基于不同的主機操作系統,延遲發送ACK的最大時延可以動態配置
- Linux使用了一種動態調節算法,可以在每個報文段返回一個ACK (稱為“快速 確認”模式)與傳統延時ACK模式間相互切換
- Mac OS X中,可以改變系統變量net.inet. tcp.delayed_ack值?來設置延時ACK。可選值如下:禁用延時(設為0),始終延時(設為1),每隔一個包回復一個ACK(設為2),自動檢測確認時間(設為3)。默認值為3
- 最新的 Windows版本中,?注冊表項中,每個接口的全局唯一標識(GUID)都不同(IG表示被引用的特定網絡接口的GUID)。TcpAckFrequency值(需要被添加)可以設為0-255,默認為2。它?代表延時ACK計時器超時前在傳的ACK數目?。將其設為1表明對每個收到的報文段都生成相應的ACK。ACK計時器值可以通過TcpDelAckTicks注冊表項控制。該值可設為2 - 6,默認為2。它以百毫秒為單位,表明在發送延時ACK前要等待百毫秒數
Time-wait狀態(2MSL)
1. 為什么需要TIME_WAIT狀態
假設最后的ACK丟失,server將重發FIN,client必須維護TCP狀態信息以便可以重發最后的ACK,否則將會發送RST,結果server認為發生錯誤。TCP實現必須可靠地終止連接的兩個方向,所以client必須進入TIME_WAIT狀態。
此外,考慮一種情況,TCP實現可能面臨著先后兩個相同的五元組。如果前一個連接處于TIME_WAIT狀態,而允許另一個擁有相同五元組連接出現,可能處理TCP報文時,兩個連接互相干擾。所以使用SO_REUSEADDR選項就需要考慮這種情況。
linux網絡編程
相關概念
同步異步
同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程觸發IO 操作并等待或者輪詢的去查看IO 操作是否就緒,而異步是指用戶進程觸發IO 操作以后便開始做自己的事情,而當IO 操作已經完成的時候會得到IO 完成的通知。
阻塞非阻塞
阻塞和非阻塞是針對于進程在訪問數據的時候,根據IO操作的就緒狀態來采取的不同方式,說白了是一種讀取或者寫入操作方法的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入方法會立即返回一個狀態值。
套接字地址結構
struct in_addr
字節順序為網絡順序(network byte ordered),即該無符號整數采用大端字節序
//ipv4套接字地址結構,在<netinet/in.h>中聲明 typedef uint32_t in_addr_t; //32位(unsigned int)的ip地址, struct in_addr {in_addr_t s_addr; };struct sockaddr和struct sockaddr_in
struct sockaddr是通用的套接字地址,而struct sockaddr_in則是internet環境下套接字的地址形式,
二者長度一樣,都是16個字節。二者是并列結構,指向sockaddr_in結構的指針也可以指向sockaddr。
一般情況下,需要把sockaddr_in結構強制轉換成sockaddr結構再傳入系統調用函數中
字節排序函數
首先解釋一下字節序的概念,所謂字節序是指多字節數據的存儲順序,比如0x1234要放在0000H和0001H兩存儲單元,有兩種存儲方式:大端格式為[0000H]=12,[0001H]=34和小端格式為[0000H]=34,[0001H]=12。
大端格式:將高位字節數據存儲在低地址,低位字節數據存儲在高地址
小端格式:將高位字節數據存儲在高地址,低位字節數據存儲在低地址
網際協議采取的是大端字節序,我們在編程的時候才需要考慮網絡字節許和主機字節序之間的轉換。下面是四個轉換函數
#include <netinet/in.h>uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); //均返回網絡字節序uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue); //均返回主機字節序地址轉換函數:
BSD網絡軟件中包含了inet_addr、inet_aton和inet_ntoa,用來在二進制地址格式和點分十進制字符串格式之間相互轉換,但是這三個函數僅僅支持IPv4。(廢棄,不建議使用)
1. in_addr_t inet_addr(const char *cp)函數轉換標準的ASCII以點分十進制的地址值返回為網絡字節序二進制值
如果參數 char *cp 無效則返回-1(INADDR_NONE),但這個函數有個缺點:在處理地址為255.255.255.255時也返回-1,雖然它是一個有效地址,但inet_addr()無法處理這個地址。
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp); //in_addr_t-->uint32_t輸入是點分的IP地址格式(如A.B.C.D)的字符串,從該字符串中提取出每一部分,轉換為ULONG,假設得到4個ULONG型的A,B,C,D,
ulAddress(ULONG型)是轉換后的結果,
ulAddress = D<<24 + C<<16 + B<<8 + A(網絡字節序),即inet_addr(const char *)的返回結果
另外,我們也可以得到把該IP轉換為主機序的結果,轉換方法一樣
A<<24 + B<<16 + C<<8 + D
2. int inet_aton(const char *__cp, in_addr *__inp)轉換標準的ASCII以點分十進制的地址值返回網絡字節序二進制值
如果這個函數成功,函數的返回值非零。如果輸入地址不正確則會返回零。使用這個函數并沒有錯誤碼存放在errno中,所以他的值會被忽略。
#include <arpa/inet.h> /*** @brief inet_aton* @param __cp 輸入參數包含ASCII表示的IP地址* @param __inp 輸出參數將要用新的IP地址更新的結構* @return */ extern int inet_aton (const char *__cp, struct in_addr *__inp);3. char *inet_ntoa (struct in_addr __in)函數轉換網絡字節序二進制值返回標準的ASCII以點分十進制的地址值
該函數返回值指向保存點分十進制的字符串地址的指針,該字符串的空間為靜態分配 的,所以在第二次調用這個函數時,意味著上一次調用并保存的結果將會被覆蓋(重寫)
#include <arpa/inet.h>extern char *inet_ntoa (struct in_addr __in);4. 代碼示例:
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> int main(int argc, char *argv[]) {char ip1[] = "192.168.0.74";char ip2[] = "211.100.21.179";struct in_addr addr1, addr2;long l1, l2;l1 = inet_addr(ip1); //IP字符串——》網絡字節l2 = inet_addr(ip2);printf("IP1: %s\nIP2: %s\n", ip1, ip2);printf("Addr1: %ld\nAddr2: %ld\n", l1, l2);memcpy(&addr1, &l1, 4); //復制4個字節大小memcpy(&addr2, &l2, 4);printf("%s <--> %s\n", inet_ntoa(addr1), inet_ntoa(addr2)); //注意:printf函數自右向左求值、覆蓋printf("%s\n", inet_ntoa(addr1)); //網絡字節 ——》IP字符串printf("%s\n", inet_ntoa(addr2));return 0; } IP1: 192.168.0.74 IP2: 211.100.21.179 Addr1: 1241557184 Addr2: 3004523731 192.168.0.74 <--> 192.168.0.74 192.168.0.74 211.100.21.179功能相似的兩個函數同時支持IPv4和IPv6,p代表presentation表達,n代表numeric數值
1. int inet_pton(int domain, const char *str, void *addr)將標準的ASCII以點分十進制的地址值轉化為網絡傳輸的二進制數值格式
返回值:若成功則為1,若輸入不是有效的表達式則為0,若出錯則為-1
#include <arpa/inet.h>int inet_pton(int family, const char *strptr, void *addrptr) { //這兩個函數的family參數既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)。 //如果,以不被支持的地址族作為family參數,這兩個函數都返回一個錯誤,并將errno置為EAFNOSUPPORT.if (family == AF_INET) {struct in_addr in_val;if (inet_aton(strptr, &in_val)) {memcpy(addrptr, &in_val, sizeof(in_val));return (1);}}errno = EAFNOSUPPOPT;return (-1); }2. const char *inet_ntop(int domain, const void *addr, char *str, socklen_t size)將網絡傳輸的二進制數值轉化標準的ASCII以點分十進制的地址值格式
inet_ntop函數的strptr參數不可以是一個空指針。調用者必須為目標存儲單元分配內存并指定其大小,返回值:若成功則為指向結構的指針,若出錯則為NULL
#include <arpa/inet.h>const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len) {const u_char *p = (const u_char*)addrptr;if (family == AF_INET) {char temp[INET_ADDRSTRLEN];snprintf(temp, sizeof(temp), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);if (strlen(temp) >= len) {errno = ENOSPC;rturn (NULL);}strcpy(strptr, temp);return (strptr);}errno = EAFNOSUPPOPT;return (NULL); }3. 代碼示例:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>int main() {char ip[] = "192.168.0.74"; struct in_addr addr;int ret = inet_pton(AF_INET, ip, (void *)&addr); //IP字符串 ——》網絡字節流if(0 == ret){printf("inet_pton error, return 0\n");return -1;}else{printf("inet_pton ip: %ld\n", addr.s_addr);printf("inet_pton ip: 0x%x\n", addr.s_addr);}const char *pstr = inet_ntop(AF_INET, (void *)&addr, ip, 128); //網絡字節流 ——》IP字符串if(NULL == pstr){printf("inet_ntop error, return NULL\n");return -1;}else{printf("inet_ntop ip: %s\n", ip);}return 0; } inet_pton ip: 1241557184 inet_pton ip: 0x4a00a8c0 inet_ntop ip: 192.168.0.74TCP通信相關函數
1. int socket(family, type, protocol):創建套接字
/*** #include <sys/types.h>* #include <sys/socket.h>* @brief Socket 創建一個套接字用于通信* @param family* AF_INET IPv4地址協議AF_INET6 IPv6地址協議AF_LOCAL UNIX域協議AF_ROUTE 路由套接字AF_KEY 密鑰套接字* @param type 指定socket類型,SOCK_STREAM 流式套接字SOCKDGRAM 數據報套接字SOCK_SEQPACKET 有序分組套接字SOCKRAW 原始套接字,提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使用該協議)* @param protocol 協議類型 If PROTOCOL 為0,內核將會自動進行選擇,可以默認填0IPPROTO_TCP TCP傳輸協議IPPROTO_UDP UDP傳輸協議IPPROTO_SCTP SCTP傳輸協議* @return 成功返回非負整數套接字描述符;失敗返回-1*/ int socket(int family,int type,int protocol);2. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):綁定套接字
#include <sys/types.h> #include <sys/socket.h> /*** @brief bind* @param fd 綁定套接子* @param addr 要綁定的地址* @param addrlen 地址長度* @return 成功返回 0 失敗返回 -1*/ int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);3. int listen(int fd,int backlog):設置同時通信的最大套接字數量
#include <sys/types.h> #include <sys/sock.h>/*** @brief listen* (1)一般來說,listen函數應該在調用socket和bind函數之后,調用accept函數之前調用* (2)對于給定的監聽套接字接口,內核要維護兩個隊列* <1>已由客戶發送并到達服務器,服務器正在等待完成對應的TCP三次握手過程* <2>已經完成連接的隊列* @param fd socket函數返回的套接字* @param backlog 規定內核為此套接字排隊的最大的連接個數* @return 成功返回 0 失敗返回 -1*/ int listen(int fd,int backlog);4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)阻塞式監聽客戶端連接
#include <sys/types.h> #include <sys/scoket.h>/*** @brief accept 從已經完成連接隊列返回第一個連接,如果已經完成連接隊列為空,則阻* @param sockfd 服務器套接字* @param addr 將返回對等待的套接字地址* @param addrlen 返回對等方的套接字地址長度* @return 成功返回非負整數:對應和客戶點連接的新套接字 ,失敗返回-1*/ int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);5. int connect(int sockfd, const strcut sockaddr *addr, socklen_t addrlen)用來客戶端和tcp服務器建立連接
#include <sys/types.h> #include <sys/socket.h>/*** @brief connect 用于建立與指定socket的連接* @param sockfd 標識一個未連接的socket* @param addr 指定要連接套接字的sockaddr結構體的指針* @param addrlen sockaddr結構體的字節長度* @return 0 on success, -1 for errors*/ int connect(int sockfd, const strcut sockaddr *addr, socklen_t addrlen);5. ssize_t read(int fd,void *ptr,size_t nbytes)一次讀取指定字節長度數據
#include <unistd.h>/*** @brief read* @param fd 將要讀取數據的文件描述符* @param ptr 所讀取到的數據的內存緩沖* @param nbytes 需要讀取的數據量* @return 成功執行時,返回所讀取的數據量;* 如果返回0, 表示已到達文件尾或是無可讀取的數據* 失敗返回-1,errno被設為以下的某個值EAGAIN:打開文件時設定了O_NONBLOCK標志,并且當前沒有數據可讀取EBADF:文件描述詞無效,或者文件不可讀EFAULT:參數buf指向的空間不可訪問EINTR:數據讀取前,操作被信號中斷EINVAL:一個或者多個參數無效EIO:讀寫出錯EISDIR:參數fd索引的時目錄*/ ssize_t read(int fd,void *ptr,size_t nbytes);6. ssize_t write(int fd,const void *ptr,size_t nbytes)一次寫入指定字節長度數據
/*** @brief write* @param fd 將要寫入數據的文件描述符* @param ptr 所寫入到的數據的內存緩沖* @param nbytes 需要寫入的數據量* @return 成功執行時,返回所寫入的數據量。失敗返回-1,錯誤代碼存入errno中*/ ssize_t write(int fd,const void *ptr,size_t nbytes);7. ssize_t Readn(int fd,void *vptr,size_t n);循環讀取n個字節數據
/*** @brief Readn 從描述符fd中讀取n個字節,存入vptr指針的位置1. 當剩余長度大于0的時候就一直讀啊讀2. 當read的返回值小于0的時候,做異常檢測3. 當read的返回值等于0的時候,退出循環4. 當read的返回值大于0的時候,拿剩余長度減read的返回值,拿到新的剩余長度,讀的入口指針加上read的返回值,進入步15. 返回參數n減去剩余長度,即實際讀取的總長度* @param fd* @param vptr* @param n* @return*/ /* Read "n" bytes from a descriptor. */ ssize_t Readn(int fd, void *vptr, size_t n) {size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0; /* and call read() again */elsereturn(-1);} else if (nread == 0)break; /* EOF */nleft -= nread;ptr += nread;}return(n - nleft); /* return >= 0 */ } /* end readn */8. ssize_t Writen(int fd,const void*vptr,size_t n)循環寫入n個字節數據
/*** @brief Writen 向描述符fd中寫入n個字節,從vptr位置開始寫1. 當要寫入的剩余長度大于0的時候就一直寫啊寫2. 當write的返回值小于0的時候,做異常檢測3. 當write的返回值等于0的時候,出錯退出程序4. 當write的返回值大于0的時候,拿剩余長度減去write的返回值,拿到新的剩余長度,寫的入口指針加上write的返回值,進入步驟15. 返回參數n的值,即期望寫入的總長度* @param fd* @param vptr* @param n* @return */ /* Write "n" bytes to a descriptor. */ ssize_t Writen(int fd, const void *vptr, size_t n) {size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0; /* and call write() again */elsereturn(-1); /* error */}nleft -= nwritten;ptr += nwritten;}return(n); } /* end writen */9. ssize_t Readline(int fd, void *vptr, size_t maxlen) 讀到’\n’或者讀滿緩沖區才返回
static ssize_t readch(int fd, char *ptr) {static int read_cnt;static char *read_ptr;static char read_buf[100];if(read_cnt <= 0){ again:if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){if(errno == EINTR){goto again;}else{return -1;}}else if(read_cnt == 0){return 0;}read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1; }ssize_t Readline(int fd, void *vptr, size_t maxlen) {ssize_t n, rc;char c, *ptr;ptr = vptr;for(n = 1; n < maxlen; n++){if((rc = readch(fd, &c)) == 1){*ptr++ = c;if(c == '\n'){break;}}else if(rc == 0){*ptr = 0;return n - 1;}else{return (n - 1);}}*ptr = 0;return n; }10. int close(int fd) 關閉套接字
close函數會關閉套接字ID,如果有其他的進程共享著這個套接字,那么它仍然是打開的,這個連接仍然可以用來讀和寫,并且有時候這是非常重要的 ,特別是對于多進程并發服務器來說
//一般不會立即關閉而經歷TIME_WAIT的過程 #include<unistd.h> int close(int sockfd); //返回成功為0,出錯為-111. shutdown()函數切斷進程共享的套接字的所有連接
#include<sys/socket.h> /*** @brief shutdown shutdown會切斷進程共享的套接字的所有連接,不管這個套接字的引用計數是否為零,* 那些試圖讀得進程將會接收到EOF標識,那些試圖寫的進程將會檢測到SIGPIPE信號,* 同時可利用shutdown的第二個參數選擇斷連的方式* @param sockfd 文件描述符* @param howto1.SHUT_RD:值為0,關閉連接的讀這一半。2.SHUT_WR:值為1,關閉連接的寫這一半。3.SHUT_RDWR:值為2,連接的讀和寫都關閉。* @return 成功為0,出錯為-1.*/ int shutdown(int sockfd,int howto);12. recv()和send()函數
int recv(int sockfd,void *buf,int len,int flags); int send(int sockfd,void *buf,int len,int flags);| 0 | 相當于read和write函數 |
| MSG_DONTROUTE | 不查找表 |
| MSG_OOB | 接受或者發送帶外數據 |
| MSG_PEEK | 查看數據,并不從系統緩沖區移走數據 |
| MSG_WAITALL | 等待所有數據 |
-
MSG_DONTROUTE:是send函數使用的標志。這個標志告訴IP,目的主機在本地網絡上面,沒有必要查找表。這個標志一般用網絡診斷和路由程序里面。
-
MSG_OOB:表示可以接收和發送帶外的數據。關于帶外數據我們以后會解釋的。
-
MSG_PEEK:是recv函數的使用標志。表示只是從系統緩沖區中讀取內容,而不清除系統緩沖區的內容,這樣下次讀的時候仍然是一樣的內容。一般在有多個進程讀寫數據時可以使用這個標志。
-
MSG_WAITALL:是recv函數的使用標志。表示等到所有的信息到達時才返回。使用這個標志的時候recv會一直阻塞,直到指定的條件滿足或者是發生了錯誤。
1)當讀到了指定的字節時,函數正常返回。返回值等于len
2)當讀到了文件的結尾時,函數正常返回。返回值小于len
3)當操作發生錯誤時返回-1,且設置錯誤為相應的錯誤號(errno)
UDP通信相關函數
1. recvfrom()函數
/*** @brief recvfrom* @param sockfd 套接字* @param buf UDP數據報緩存區(包含所接收的數據* @param nbytes 緩沖區長度* @param flags 調用操作方式(一般設置為0)* @param from 指向發送數據的客戶端地址信息的結構體(sockaddr_in需類型轉換)* @param fromlen 指針,指向from結構體長度值* @return 成功則返回實際接收到的字符數,失敗返回-1,錯誤原因會存于errno 中*/int recvfrom(int sockfd, const void *buf, size_t nbytes,int flags,struct sockaddr *from, int *fromlen);2. sendto()函數
sendto函數專用與UDP連接
/*** @brief sendto* @param sockfd 套接字* @param buf 帶發送數據存儲緩沖區* @param nbytes 要發送數據的字節數* @param flags 可選標志* @param destaddr (目標地址)數據接收方* @param destlen 目標地址結構長度* @return */ssize_t sendto(int sockfd,const void * buf,size_t nbytes,int flags,const struct sockaddr_in * destaddr,socklen_t destlen );UDP組播通信相關函數
組播組可以是永久的也可以是臨時的。組播組地址中,有一部分由官方分配的,稱為永久組播組。永久組播組保持不變的是它的ip地址,組中的成員構成可以發生變化。永久組播組中成員的數量都可以是任意的,甚至可以為零。那些沒有保留下來供永久組播組使用的ip組播地址,可以被臨時組播組利用。
| IP_MULTICAST_TTL | 設置多播組數據的TTL值 |
| IP_ADD_MEMBERSHIP | 在指定接口上加入組播組 |
| IP_DROP_MEMBERSHIP | 退出組播組 |
| IP_MULTICAST_IF | 獲取默認接口或設置接口 |
| IP_MULTICAST_LOOP | 禁止組播數據回送 |
常見的錯誤碼
參考文獻
總結
以上是生活随笔為你收集整理的UNIX网络套接字相关总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 人生之路1.20代码 第一部分
- 下一篇: php面试题目100及最佳答案,2020