前言:
本文分為三個章節,第一個章節主要是翻譯總結匯總一位國外的老兄在Stack Overflow上的回答,但實際上Linux發展這么多年,文中的知識點已經過時且不准確了,
在第二章中通過實驗,有更加准確的描述。但是,第一章節也不是全然無用,至少在了解SO_REUSEPORT和SO_REUSEADDR的發展上是有幫助的。
在第三章節中,做實驗過程中需要驗證一些其他的知識點,因此在這里做一個匯總。
wxy:其實就是我研究完才發現文章寫的不對,又不想浪費自己的研究成果,哈哈哈哈哈,hianghiang.....
一:讀書筆記
關於Socket基本用法的總結
1,無論tcp還是udp,最終的目的都是為了建立連接(互聯網嘛),只不過tcp屬於長連接(我自己理解的長連接),udp是短連接
所謂tcp是面向連接的是指,得先有個連接,我才能干活
而udp是面向非連接的是指,沒有現成的連接,我直接沖出去
2,關於bind,無論是tcp還是udp,其實都可以省略,tcp在listen的時候自動又內核指定本機地址和端口(未驗證)
udp的話,作為客戶端更不需要bind,sendto的時候都會自動綁定,即將這個socket類型的文件描述符fd綁定到本機一個地址+端口上 之后在這個socket上蹲守(recvfrom)就可以接收到應答(經過實驗)
wxy:關於udp,想要接收到應答其實並不一定要bind,因為發出去的時候內核已經有記錄了,只要你recv着(相當於告訴內核我在這個socket上守候着),一旦內核在這個socket上有接收到數據,則就會通知上層守候的那個
如果綁定,那么就意味着我可以不用守着,當服務端回應答的時候,內核自然回上報通知我-----使用select實驗下
3,關於系統綁定,
如果指定端口號是"any port"(0),表示,表示的含義是“choose a specific port itself in that case“,即由內核指定的一個在他看來,當前情況下最合適的"一個"
如果指定的ip使用的是"any address"(ipv4通配符為0.0.0.0,ipv6通配符為::),表示的意思是"all source IP addresses of all local interfaces",即由內核將這個socket綁定到本地的所有接口ip上。一旦這個socket被connect了,則Depending on the destination address and the content of the routing table, the system will pick an appropriate source address and replace the "any" binding with a binding to the chosen source IP address.
BSD:
5,關於SO_REUSEADDR
1)如果在為socket進行綁定操作之前設置該標志位,那么這個socket就可以成功綁定到一個地址(ip:port)上,除非另一個socket已經被綁定到和這個地址exactly same的地址上。。
進一步來說,對於綁定到wildcard addresses(ipv4為0.0.0.0, ipv6為::)上,表示的含義是本機的所有ip地址,所以如果該地址已經被綁定上一個socket了,那么其他socket想綁定到設備的某個獨立的ip地址上也是會fail (with error EADDRINUSE
),除非這個socket先有設置下SO_REUSEADDR標記。
但是,一個socekt綁定到了一個地址無論是0.0.0.0還是某個特定地址,如果另一socket也綁定到這個一模一樣的地址即0.0.0.0或這個特定地址,則會失敗,即對exactly the same的地址即使設置了SO_REUSEADDR標記也不好用。
即0.0.0.0就是表示所有ip,如果先有個socketA綁定到某個地址即xxx:port_n,然后socketB想要綁定到0.0.0.0:portt_n則會失敗,並不是說會選擇"剩下"的地址
以上的描述都是針對端口號相同的,對於端口號不相同的那么不屬於沖突,根本沒SO_REUSEADDR啥事
2)socket都有一個send buf,當調用send()函數后,數據實際上是被放到了這個buffer中,並沒有真正被發送出去。
udp的話,數據接着會很快被發送出去
tcp的話,數據可能會有很長的延遲才會被發送出去,所以如果close tcp socket,很可能遺留一些數據在send buffer中,直接扔了吧不符合tcp宣稱的"可靠的連接",所以盡管上層代碼以為自己結束了,但是協議棧還在盡力的嘗試將這些數據繼續發送出去,此時socket的狀態就是TIME_WAIT。直到數據被全部發送出去或者超時( Linger Time,一般是2min)了,此時socket才真正的發送出去....
3)內核如果對待處於TIME_WAIT狀態的socket
如果沒有設置SO_REUSEADDR,那么認為該狀態的socket仍然被綁定在之前指定的端口上,或者說這個地址仍然被該socket占用着,所以新socket就不能再綁定在這個地址上,除非這個socket真正closed了
如果有設置,那么即使是exactly the same的地址,也是可以綁定成功的。
6,關於SO_REUSEPORT
1)一旦之前綁定到該ip:port上的socket設置了這個參數,那么再綁定這個地址也沒問題,如果這次綁定也設置了SO_REUSEPORT,則還可以繼續無限綁定下去...
如果第一個綁定該地址的socket沒有設置 SO_REUSEPORT,意思是說我占用了,之后的socket崩想跟老子搶
不同於SO_REUESADDR使用的場景,SO_REUSEPORT參數是給前一個綁定的socket用,表示我與這個地址綁定后,之后的socket是否還能復用
SO_REUESADDR參數是給當前socket使用的,表示我想和這個地址上的其他socket分享該地址
另外,這是一個功能更強大的參數,這個參數對 exactly the same source address都是適用的
2)SO_REUSEPORT不等同於SO_REUSEADDR,也就是說如果一個沒有設置SO_REUSEPORT的socket被綁定到一個地址上,然后又一個socket設置了SO_REUSEPORT再綁定,那么必然,是失敗的
而且不僅如此,即使之前的那個socket已經正在dying並且處於TIME_WAIT,一樣是綁定失敗的
那么此時,如果就想能綁定上這個地址怎么辦,當然是指在TIME_WAIT狀態,則辦法有二:
方法一:參考上一節中的內容,在本次綁定的時候配置SO_REUSEADDR選項
方法二:那就讓之前綁定到這個地址上的端口都設置成SO_REUSEPORT選項
3)關於Connect()
在前面的描述中,地址復用都是在調用bind()函數的時候失敗,實際上在地址復用的情況下,調用Connect()函數也會失敗,具體來說
首先,再描述下Connect()函數做了什么?
答:在前面說過,一個連接是由五元組(tuple)組成(協議,源地址,源端口號,目的地址,目的端口號),那么既然地址可以重用,那么就是說可以將兩個socket綁定到同一個協議族的同源地址和端口號,
然后如果用這些socket去connect相同的目的地址IP:Port,那么他們的tuple是完全相同的。
這是行不通的,至少對於TCP連接來說這是不行的,對於UDP里來說因為步是真正建立了連接所以勉強可以。
那么如果就真的存在這種連接會怎樣呢?答:如果有數據過來,那么系統無法識別這個數據屬於哪一個連接,所以要求這種古時候至少目的地址或端口號必須不同才行。這樣系統才能正確識別數據到底屬於那個連接的。
那么總結來說,為了避免以上的錯誤,一旦bind的時候是協議,源地址,源port一定要相同,那么在connect的時候如果目的地址也重復了,則后面那個就會報錯,報EADDRINUSE,表示你不能再訪問這個地址啦,因為相應的五元組已經屬於某個存在的連接了....
7,關於Multicast Addresses
所謂多播,就是一對多的連接,如果對於多播地址設置了SO_REUSEADDR選項,那么就可以將多個socket綁定到完全相同的多播地址中,換言之SO_REUSEADDR的作用等同於單播地址的SO_REUSEPORT的作用。實際上,在綁定多播地址的時候,寫代碼的時候可以將二者看作是一回事,也就是說我們可以用SO_REUSEADDR去實現SO_REUSEPORT的作用
wxy:這里有個問題需要之后驗證,即他們的用法是不是還是單播時候的用法,即分別用在當前的socket和之前的socket,然后只不過都達到了可以綁定exactly the same地址的效果?
還是說連用法都一樣?
Linux中這兩個參數的用法說明
一:Linux版本<3.9
此時的Linux系統只有SO_REUSEADDR選項存在,他的用法和BSD基本上是相同的,除了以下兩個重要的區別
.
區別1:一旦某個端口號被一個listen tcp socket(服務器)綁定上了,那么這個端口就不能再被其他socket綁定了,即使設置了SO_REUSEADDR也不能綁定到上。
也就是說,在BSD中,如果第一個socket綁定到了wildcard addresses上,那么只要后面的socket設置了SO_REUSEADDR就可以綁定到一個特定的地址上了
但是在LInux<3.9中,只要前面的socket綁定的是wildcard addresses,就表示所有地址都被我占用了,那么就算這時候是單獨的一個特定地址也不行,相同的端口號也不行了
這一點上LInux<3.9要比BSD更嚴格
wxy:在這里,listening (server) TCP socket該如何理解呢?
有一種翻譯是指那種綁定到了wildcard addresses上的,並且啟動了Listen了,這不就是server的情況么...
如果上面成立的話,那么是不是就可以理解而非監聽(客戶)TCP socket則無此限制????
區別2: 對於client sockets,這個選項的效果很像BSD中的SO_REUSEPORT,甚至是使用方式都是在綁定socket之前設置的。為什么會這樣呢?
因為為了能讓多個socket綁定到 exactly to the same UDP socket address for various protocols,SO_REUSEADDR就相當於代替SO_REUSEPORT的工作了
Linux >= 3.9
此時的Linux系統已經增加了SO_REUSEPORT選項,該選項的作用完全和BSD相同,也就是說只要大家都在綁定之前設置該參數,那么就可以一直一直綁定到exactly the same地址和端口號
當然,還是有兩個區別需要說明下
1,為了放置"端口劫持",這里有一個限制:想要共享地址:port,必須是那些具有相同User ID的進程范圍內。這樣一個用戶就不能把其他用戶的的端口偷走了。
另外,This is some special magic to somewhat compensate for the missing SO_EXCLBIND
/SO_EXCLUSIVEADDRUSE
flags.
2,除此之外,Linux系統還有一個特別功能對於SO_REUSEPORT
sockets ,那就是:
對於UDP socket,內核會盡量為這些共享地址的socket平分數據報
對於TCP listenning socket, 內核會盡量為這些共享地址的socket平分進來的連接請求(即通過調用accept()得到的連接請求),或者說
這些listen中的socket都在等待着,基本上大家accept到的連接是平均的。
所以,有一些應用常常創建多個子進程,然后利用SO_REUSEPORT選項復用相同的port,這樣依托於內核的這種平分機制實現一個簡單的負載均衡。
二:SO_REUSEPORT和SO_REUSEADDR用法實操
我的實驗
0,環境
# uname -a
Linux one1-asm-hx 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
一:針對Bind socket:
int on=1; //這個非常重要,因為他表示開關,1表示設置這個類型的參數,0表示關閉?這個參數
//1首先,創建一個socket,並且綁定到指定端口號上
fd= socket(PF_INET, SOCK_STREAM, 0); setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); //我告訴別人可以復用我所綁定的端口號 res=tcpBind(fd,ip,i); //ip地址可以是任何,包括0.0.0.0,即INADDR_ANY
//2然后,直接又創建一個socket並且綁定到相同的端口號上 int fd2= socket(PF_INET, SOCK_STREAM, 0); setsockopt(fd2, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); //我要復用這個端口號,盡管這個端口號可能被別人占用了 setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); res=tcpBind(fd2,ip,i);
結果:
測試1: 同一進程中的兩次綁定:
若,均不設置任何參數,順序執行1和2,則2綁定失敗;
若,1和2全部設置SO_REUSEADDR/SO_REUSEPORT,則2可以綁定成功;
若,任意設置一個也沒用(2種參數都是),2還是綁定不成功,都是98
測試2:殺死進程,立即重新啟動,無需設置,沒問題,可以綁定成功
測試3:同時啟動2個進程,綁定相同的端口號,
若,不設置參數,綁定失敗,錯誤碼98,
若,設置SO_REUSEADDR或SO_REUSEPORT,則第二個進程可以綁定成功
小小結:在綁定階段,SO_REUSEADDR和SO_REUSEPORT在用法上沒有區別,想要達到復用的效果,必須所有socket在綁定之前就設置該參數
二:針對Listen socket:
測試1:
//1)創建,綁定,監聽socket,此時查看該端口號,處於"LISTEN "態 fd= socket(PF_INET, SOCK_STREAM, 0); res=tcpBind(fd,ip,i); res=listen(fd,1024);
測試1: 同一進程中的2次綁定+ 監聽,
若,1和2全部只設置SO_REUSEADDR,則2監聽失敗:[Error]Listen on this socket2 error:98;
若,1和2全部只設置SO_REUSEPORT,則1和2均監聽成功:[All Success]Listen on this socket 1 and socket2 all success;
若,1全部設置,2只設置 SO_REUSEADDR(保證綁定是可以成功),則2監聽失敗
2只設置SO_REUSEPORT(保證綁定是可以成功),則2監聽可以成功 --這個自然是,同第二個若...
測試2: 無需設置參數,殺死進程,立刻重起,沒問題,可以綁定成功
測試3: 同時啟動2個進程,綁定相同的端口號
同測試1;
小小結:對於為socket設置監聽的話,SO_REUSEADDR和SO_REUSEPORT在用法上就有區別了,設置前者則不能復用端口去監聽,設置后者則可復用端口號去監聽了
並且要求所有復用的socket都要設置該參數,並不是向那篇文章所說的,只要前一個設置了SO_REUSEPORT,就表示后面那個可以去用了...
三:針對派生出連接的socket:
1)創建,綁定,監聽,並且還接收了一個客戶端的連接
fd= socket(PF_INET, SOCK_STREAM, 0); //setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); //setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); res=tcpBind(fd,ip,i); res=listen(fd,1024); int accept_fd=accept(fd,(struct sockaddr*)&client_addr,&len);
此時查看端口號的狀態可以發現,30000仍然處於Listen狀態,但是派生出來一個新的連接,處於ESTABLISHED狀態
# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 0.0.0.0:* LISTEN 19260/tcp_server
tcp 0 0 192.168.1.158:30000 192.168.1.181:61003 ESTABLISHED 19260/tcp_server
測試1:斷開客戶端的連接,此時
[root@localhost ~]# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 0.0.0.0:* LISTEN 19260/tcp_server
tcp 1 0 192.168.1.158:30000 192.168.1.181:61003 CLOSE_WAIT 19260/tcp_server
wxy:在這系列的測試中,斷開客戶端服務端的端口直接進入CLOSE_XX狀態,所以端口號可以立即使用,基本並不會影響server端,頂多是派生出來的那個連接狀態會有所改變,所以無需繼續測試。
測試2:客戶端還在連接着的時候,關閉服務端進程,於是
[root@localhost ~]# date
Fri Dec 13 20:43:23 CST 2019
[root@localhost ~]# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 192.168.1.181:61129 TIME_WAIT -
此時重起服務進程,則報錯:Bind 192.168.1.158:30000 error:98
經過一段時間后,大約1min,2MSL:
[root@localhost ~]# netstat -auntp|grep 30000 ---表示已經釋放端口了
[root@localhost ~]# date
Fri Dec 13 20:44:24 CST 2019
此時再重起服務進程,則成功。
測試3:在測試2的基礎所上
若,增加SO_REUSEADDR,則可以瞬間重新建立監聽,並接收連接;
1)如果前一次有設置,后一次沒有設置,則后一次綁定失敗
2)如果前一次沒有設置,后一次有設置,則后一次綁定失敗
若,增加SO_REUSEPORT選項也可以瞬間重新建立監聽,並接收連接,但還有三個和SO_REUSEADDR不同的點
1)還可以同時啟動多個進程,隨即接收客戶端的連接,一旦一個進程結束,連接斷開,重新連的話就會和另一個進程連接上。這個是ADDR做不到的
2)如果前一次沒有設置或者設置的是SO_REUSEADDR選項,則在2MSL期間,即使第二次代碼已經設置了SO_REUSEPORT,同樣連接是失敗的。
3)如果前一次設置的是SO_REUSEPORT,進入TIME_WAIT后,若第二次沒有設置(設置了SO_REUSEADDR也不行),則都會連接失敗
小小結:SO_REUSEADDR主要是用在當端口進入TIME_WAIT后,可以復用的場景,要求前后兩次都要設置了該參數才行
SO_REUSEPORT則是直接可以復用一個端口,同樣要求前后或同時都設置該標記。
總的來說,SO_REUSEPORT是后來新增的選項,功能幾乎覆蓋了SO_REUSEADDR更強大,貌似只又一點不同,針對不同的userid的進程,復用時有額外的要求,見下面。
源碼解析:
SO_REUSEADDR
Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses.
For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address.
When the listening socket is bound to INADDR_ANY with a spe‐cific port then it is not possible to bind to this port for any local address.
解析:1)這個參數是用於bind(),即想要讓不同的socket復用local address(ip:port),除了2)種約定,也就是說socket處於初始狀態和Time_Wait狀態都可以綁定成功(和實驗一致)
2)如果這個地址已經被某個處於listening態的socket綁定了,那就不能再復用了,或者說綁定在上面的(和實驗一致)
3)這里面有一個特殊的地址0.0.0.0:port,他可以認為是和任何一個local address 相同,所以是否能夠復用也要遵循1)和2)
另外,wxy認為,man7中其實說的很准確,也就是說在客戶端只要設置了這個參數就可以復用了,因為客戶端不存在listen,盡管我還沒有做實驗....
SO_REUSEPORT
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve
the performance of multithreaded network server applications running on top of multicore systems.
解析:用於多線程的
源碼(3.10.0-693.el7內核版本,即CentOS 7.4):
int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax) { struct sock *sk2; int reuse = sk->sk_reuse; //本次要綁定的socket的地址可重用標志 int reuseport = sk->sk_reuseport; //本次要綁定的socket的端口可重用標志 kuid_t uid = sock_i_uid((struct sock *)sk);//該連接的用戶id sk_for_each_bound(sk2, &tb->owners) { //遍歷綁定在本端口上的所有socket:sk2就做為每一次檢查的參照物
//1.第一關的檢查:如果不符合,就跳過本輪取得下一個參照物再來;如果符合進入第二關 if (sk != sk2 && !inet_v6_ipv6only(sk2) && //1.1:這次綁定的socket和參照物不是一個; 並且參照物不是ipv6only??? (!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if || //1.3:並且本socket和參照物如果有一個沒設置SO_BINDTODEVICE,或者都設置了但相等(即綁定了同一個網絡設備) sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
//2.第二.1關的檢查:如果不符合轉如二.2關檢查;如果符合進入第三關 if ((!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) && //2.1:當前socket沒有設置addrreuse; 或者參照物沒有設置addrreuse; 或者參照物處於LISTEN態 (!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT && //2.2:並且也沒有設置portreuse;或者參照物沒有設置portreuse;或者和參照物屬於不同用於id,而參照物的狀態又不是TIME_WAIT態 !uid_eq(uid, sock_i_uid(sk2))))) { //第三關:如果符合則證明沖突了,不再檢查其他參照物;否則重新進入二.2關檢查 if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || //3.1參照物的socket還沒有rcv地址(還沒有和某端建立連接);或者當前socket也沒有rcv地址;或者都有並且相同 sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break;//沖突了 //綜上,如果1,2,3組條件都滿足了,那么沖突無疑了 } //2.第二.2關的檢查:relax為false,且當前連接和表中連接都允許地址重用且表中連接狀態不為listen if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { //2.1:relax為false並且當前端口和參照物都設置了addr reuse,並且參照物也沒有處於LISTEN狀態 //第三關:如果符合就證明沖突了,不再檢查其他參照物;否則進行下一輪參照物的檢查 if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || //3.1同上 sk2->sk_rcv_saddr == sk->sk_rcv_saddr) break;//沖突了 } } } return sk2 != NULL; //表示在輪循的過程中,已經綁定到本端口上的所有socket中存在一個滿足條件的socket,於是沖突了 }
根據源碼,可以總結如下情況不會沖突,即地址可以復用
1,同一個socket,重復綁定
2,綁定的不是用一個interface,即ip
3,都設置了SO_REUSEADDR標志,且早前的socket並沒有進入LISTEN態(當然處於TIME_WAIT態也算沒有進入LISTEN狀態,所以可以重用)
4,都設置了SO_REUSEPORT標志,且早前的socket已經進入TIME_WAIT態或者這些socket屬於一個uid的
wxy:從上面的代碼也可以看出來,不像早前那篇文章所描繪的,SO_REUSEADDR用在當前要綁定的socket上,而SO_REUSEPORT用在之前的socket上,作用於當前socekt....
小小結:socekt是否阻塞模式和epoll一點關系沒有,epoll只管看是否有事件發生,有則調用相應的"處理邏輯"處理。為什么有的時候要求設置非阻塞模式,那是為了"處理邏輯"能順利執行下去,因為在epoll模型中,一般都是同事檢測着多路socket,所以發生wait出來的往往是好幾個時間,而"處理邏輯"肯定得是一個一個處理,因此如果因為一個socket阻塞在那里,就影響其他socket了,所以根據各種場景按需設置socket為非阻塞模式。
三:其他知識點
1,關於tcp的四次揮手
A --------tcp---------------B
A說:我要斷開連接,所以應用程序調用close()函數,內核得知后有如下行為
首先發送FIN=1,seq=u報文給B,A進入--->FIN-WAIT-1態
B聽:內核接收消息后,回應ACK=1,seq=v,ack=u+1,並進入--->ClOSE-WAIT態,同時
A聽:收到B的ack后,狀態從FIN-WAIT-1--->FIN-WAIT-2
B說: 內核回應A的同時還會通知上層應用層,即發EOF(end of file)消息,於是上層也應該調用close()函數
同時發送FIN=1,ACK=1,seq=w,ack=u+1給A,B變成--->LAST-ACK態;
A聽:內核接收到消息后,回應ACK=1,seq=u+1,ack=w+1給B,同時進入--->TIME-WAIT態
經過2MSL時間后,最終進入--->CLOSED態
B聽:收到A的ack后,轉態從LAST-ACK--->CLOSED
wxy:關於這個經過實驗可以發現:
Server端代碼: ip="0.0.0.0"; port=0; //1.綁定時 sockaddr_in server_addr,local_addr; server_addr.sin_addr.s_addr = inet_addr(ip.c_str()); server_addr.sin_port = htons(port); int res=bind(fd, (struct sockaddr*)&server_addr, addrlen); getsockname(fd, (struct sockaddr*)&local_addr, &addrlen); printf("After bind to any adress, the fact address which socket use is:[%s:%d]\n",inet_ntoa(local_addr.sin_addr),ntohs(local_addr.sin_port)); //2,接收到客戶端的連接后(通過epoll) int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len); //3.調用完接收函數后 getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len); printf(" After recv from client, Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
Client:
sock = socket(AF_INET,SOCK_DGRAM, 0);
getsockname(sock, (struct sockaddr*)&local_addr, &len);
printf("***creat the socket:%d and Local[%s:%d]\n",sock,inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
int res=sendto(sock, Data, sizeof(Data), 0 , (struct sockaddr*)&server_addr, len);
getsockname(sock, (struct sockaddr*)&local_addr, &len);
printf("***after send to server:%d and Local[%s:%d]\n",sock,inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
res = recvfrom(sock,recvBuf,PACKAGE_LEN,0, (struct sockaddr*)&server_recv,&len); printf(" Get echo From[%s:%d] by[:%d] success\n",inet_ntoa(server_recv.sin_addr), ntohs(server_recv.sin_port), ntohs(local_addr.sin_port));
結果:
# ./udp_server2 After bind to any adress, the socket use the fact address is:[0.0.0.0:53366] //第一次客戶端通過182網段的接口發送請求 After epoll event happend, Local address[0.0.0.0:53366]. After recv from client [182.168.1.246:48410], Local address[0.0.0.0:53366]. //第二次客戶端通過192網斷的接口發送請求 After epoll event happend, Local address[0.0.0.0:53366]. After recv from client [192.168.1.246:22119], Local address[0.0.0.0:53366].
#./udp_client 182.168.1.245 53366
Get echo From[182.168.1.245:53366] by[:20937] success //此時的客戶端獲取的地址確實是相應的動態變化
# ./udp_client 192.168.1.245 53366
Get echo From[192.168.1.245:39964] by[:19950] success
另外,客戶端使用的端口號再發送報文的時候就被指定了:
***creat the socket:5 and Local[0.0.0.0:0]
***after send to server:5 and Local[0.0.0.0:56648]
***after recv to server:5 and Local[0.0.0.0:56648]
抓包:
服務端確實會根據訪問接口調整發送包的接收及ip,且並不會在所有接口上洪泛....
結論:
服務端綁定的時候是0.0.0.0,表示確實被綁定到本機的所有接口上,但是真正數據報從哪個接口出去,則是由內核根據情況界定,即在被連接后確實根據客戶端的請求指定相應的接口及ip去回應,但是此時無論是在接收請求之后還是數據報都sendto出去了,getsockname時,仍然獲取的是0.0.0.0
對於客戶端:
並不需要綁定,創建socket的時候相當於socket還沒有指向哪個具體的地址,一旦報文發送出去,端口號就自動指定了(當然地址應該也指定了,但是直接獲取是獲取不到的)
再這個socket上阻塞recvfrom,則如果有應答回來,則必然會從這個端口號/fd中進來
wxy:如果發現client再send之后端口還是0,或者sever接收后得到客戶端的端口為0,那么不用懷疑,一定是你的代碼中有寫錯了,因為端口0只能是人為指定用來告訴內核去指定一個可用的端口號作為真正使用的....
4,關於udp的connect
Server端:
... //實驗1:從客戶端接收到報文后之,向client發送應答之前,將這個socket固定給這個客戶端
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len); connect(events[i].data.fd,(struct sockaddr*)&client_addr,len); bzero(&local_addr, sizeof(local_addr)); getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len); printf(" After connect client, Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
//實驗2:同樣還實驗了綁定之后直接就connect操作(當然,這么做沒什么意義,因為作為服務端怎么能固定客戶端的地址和端口號呢)
bind(fd, (struct sockaddr*)&server_addr, addrlen);
connect(fd,(struct sockaddr*)&client_addr,len); //此時可以獲取到系統給socket綁定的地址
結果:沒意義,客戶端除非綁定,否則無法剛好使用server端connect的地址(端口號)
//實驗3: 接收報文后返回應答,再用這個socket向其他設備且是不同網段的發送數據
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len);
int send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&client_addr,len);
send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&tmp_addr,len); //"192.168.1.181:50000"
bzero(&local_addr, sizeof(local_addr));
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
結果:初次,數據從182網段接口接收,然后從該接口回送應答后,再向192.168.1.181發送報文,也是正常的。接收初次的第二個報文也都是正常
第二次,即客戶端會使用的端口號會變,同樣收發正常.
第n次,OK...
//實驗4:接收到報文后,connect到發起者,再回送應答,最后向其他設備發送數據
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len);
connect(events[i].data.fd,(struct sockaddr*)&client_addr,len);
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
//int send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&client_addr,len);
int send_cnt=send(events[i].data.fd, echoBuf, sizeof(echoBuf),0); //注意,在實驗4中,使用send()和sendto實驗效果相同
send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&tmp_addr,len);
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
printf(" After connect and send echo to client, Send to other(192.168.1.181:50000), Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
結果:效果等同關於實驗1,並且向另一個設備181也是正常的,無論客戶端使用192網段還是182網段發送報文
結果(實驗1):
客戶端執行: # ./udp_client 182.168.1.245 40122 2 //初次發送,2次 Send 182.168.1.245:40122 by [:27275]成功. Get echo From[182.168.1.245:40122] by[:27275] success Send182.168.1.245:40122 by [:27275]成功. Get echo From[182.168.1.245:40122] by[:27275] success # ./udp_client 192.168.1.245 40122 40122 2 //再次發送,無論是原來的ip還是新的ip,服務端都不再接收 Send 192.168.1.245:40122 by [:16009]成功. //沒有接收到服務端的echo 服務端效果: # ./udp_server2 //初次接收客戶端的連接時,獲取的本地地址同樣是0,但一旦connect,初次接收的第二次發送就編程有地址了 After bind to any adress, the socket use the fact address is:[0.0.0.0:40122] After epoll event happend, Local address[0.0.0.0:40122]. After recv from client, Local address[0.0.0.0:40122]. After connect client, Local address[182.168.1.245:40122]. After epoll event happend, Local address[182.168.1.245:40122]. //初次接收時的第二次發送 After recv from client, Local address[182.168.1.245:40122]. After connect client, Local address[182.168.1.245:40122].
//之后就在也無法接收別人的連接
結論:udp,如果接口進行了connect操作,相當於提前為socket指定了方向,所以對於那些綁定到0.0.0.0的socket,無需等到被連接或者主動連接內核才會為其指定具體的ip。
一旦一個socket被綁定到一個指定地址(ip:port), 那么這個socket就在也無法接收來自其他的連接了; 但是他向其他地址發送報文是不受影響的
注:關於inet_ntoa函數
"The return value is a pointer into a statically-allocated buffer. Subsequent calls will overwrite the same buffer, so you should copy the string if you need to save it."
解析:經過轉化后的地址是放在一塊靜態內存中,即所有的轉化結果都放置在一塊內存中,如果轉化后不立刻打印或者使用,則如果接下來還有轉化操作,就會覆蓋掉之前的轉化
-----------------------------------------------------
---------------------------------------------------
-----------------------------------------------------------------------------------------------