socket.error: [Errno 99] Cannot assign requested address
網上你去搜,基本都是說bind的時候,地址已經被用了,都是胡扯。地址被用報的錯誤應該是:
Address already in use才對
然后我看得都是英文的,說明外國人也不是想象中的那么一絲不苟,
言歸正傳。socket發起connect請求的時候會隨機分配一個端口給你。這個分配的端口是有范圍的,記錄在:
/proc/sys/net/ipv4/ip_local_port_range
這個文件里面(fedora 17).當你用多個進程發起過多的請求的時候,端口用完了就會報這個錯誤。比如我就開了4個進程,一下發起了40000個請求。
你可以做個實驗試試,切換到root用戶,敲一下這條命令:
echo 32768 32769 > /proc/sys/net/ipv4/ip_local_port_range
這下你打開人人,微博就會發現很多圖片加載不出來了。因為圖片加載在瀏覽器里面就是並行加載的,由於你沒有足夠的端口數,所以圖片加載都失敗了。別當心,這個修改是臨時的(是不是臨時的我也不知道,聽別人說的)
修改過來用下面這條命令:
echo 32768 61000 > /proc/sys/net/ipv4/ip_local_port_range
32768 到 61000 是系統默認的隨機分配端口范圍(再次聲明,fedora 17版本)
(3) errno = 99的原因;
至於connect系統調用為什么返回失敗,就只能看系統調用的實現了。
a) connect系統調用
connect系統調用在net/socket.c中實現,Sys_connect系統調用的調用棧如下:
Sys_connect--->
sock->ops->connect // inet_stream_connect sk->sk_prot->connect // tcp_v4_connect
tcp_v4_connect的作用主要是完成TCP連接三次握手中的第一個握手,即向服務端發送SYNC = 1和一個32位的序號的連接請求包。要發送SYNC請求包,按照TCP/IP協議,就必須有源IP地址和端口,源IP地址的選擇和路由相關,需要查詢路由表,在ip_route_connect中實現,源端口的選擇在__inet_hash_connect中實現,而且如果找不到一個可用的端口,這個函數會返回-EADDRNOTAVAIL,因此基本上可以確定是這個函數返回錯誤導致connect失敗;
b) __inet_hash_connect
這個函數的主要作用是選擇一個可用的端口,其主要的實現步驟如下:
i. 調用inet_get_local_port_range(&low, &high);獲取可用的端口鏈表;
- 調用read_seqbegin(&sysctl_local_ports.lock);得到順序鎖;
- 得到可用端口的low和high:
*low = sysctl_local_ports.range[0];
*high = sysctl_local_ports.range[1];
ii. 對於每一個端口,進行下面的步驟:
- 在inet_hashinfo *hinfo中查找這個端口inet_hashinfo用於保存已經使用的端口信息,每個使用的端口在這個hash表中有一個entry;
- 對端口做hash得到鏈表頭(使用鏈表解決hash沖突)
- 遍歷鏈表中的每一個entry:
a) 判斷是否與這個要使用的端口相同,如果相同轉到步驟b,如果不相同則遍歷下一個entry
b) 找到這個端口,調用check_established(__inet_check_established)判斷這個端口是否可以重用(TIME_WAIT狀態下的端口並且net.ipv4.tcp_tw_recycle = 1是端口可以重用)
- 如果在鏈表中沒有找到這個端口,表示端口沒有被使用,調用inet_bind_bucket_create在hash表中插入一個entry;
iii. 如果到最后都沒有找到一個可用的端口就返回EADDRNOTAVAIL;
從這個函數的實現可以看出,主要是由於可用的端口被占滿了,所以找不到一個可用的端口,導致連接失敗。運行netstat可以發現確實存在很多TIME_WAIT狀態的socket,這些socket將可用端口占滿了。
[root@test miuistorage-dev]# netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' TIME_WAIT 26837 ESTABLISHED 30
(4) 解決辦法:
要解決端口被TIME_WAIT狀態的socket占滿的問題,可以有以下的解決辦法:
a) 修改可用端口范圍
查看當前的端口范圍:
root@guojun8-desktop:/linux-2.6.34# sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768 61000
修改端口范圍:
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.ip_local_port_range="32768 62000" net.ipv4.ip_local_port_range = 32768 62000
這種辦法可能不能解決根本問題,因為如果使用短連接,即使增加可用端口還是會被占滿的。
b) 設置net.ipv4.tcp_tw_recycle = 1(本人采用這種解決方式)
這個參數表示系統的TIME-WAIT sockets是否可以快速回收
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.tcp_tw_recycle=1 net.ipv4.tcp_tw_recycle = 1
c) 設置net.ipv4.tcp_tw_reuse=1
這個參數表示是否可以重用TIME_WAIT狀態的端口;
root@guojun8-desktop:linux-2.6.34# [root@test thumbnail]# sysctl net.ipv4.tcp_tw_reuse=1 net.ipv4.tcp_tw_reuse = 1
(5) 更深入的探討:sysctl做了什么
可以用strace跟蹤一下sysctl的系統調用:
root@guojun8-desktop:linux-2.6.34# strace sysctl net.ipv4.tcp_tw_recycle=1 execve("/sbin/sysctl", ["sysctl", "net.ipv4.tcp_tw_recycle=1"], [/* 20 vars */]) = 0 brk(0) = 0x952f000 ….. open("/proc/sys/net/ipv4/tcp_tw_recycle", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000 write(3, "1\n", 2) = 2 close(3) = 0 munmap(0xb788e000, 4096) = 0 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000 write(1, "net.ipv4.tcp_tw_recycle = 1\n", 28net.ipv4.tcp_tw_recycle = 1 ) = 28 exit_group(0) = ?
可以看到這個程序打開/proc/sys/net/ipv4/tcp_tw_recycle並向文件中寫入1,但是這個設置時怎樣其作用的呢?在內核中對/proc/sys目錄下的文件的i_fop做了特殊的處理,在proc_sys_make_inode 中設置:inode->i_fop = &proc_sys_file_operationsproc_sys_file_operations的定義如下:
static const struct file_operations proc_sys_file_operations = { .read = proc_sys_read, .write = proc_sys_write, };
proc_sys_write中會修改對應的文件,並且修改內存中的內容,不同的文件有不同的proc_handler,如tcp_tw_recycle對應的處理函數是proc_dointvec,這個函數會修改下面的變量:
tcp_death_row.sysctl_tw_recycle
這個變量在內核中表示TIME_WIAT狀態的socket是否可以被快速回收。
(3) errno = 99的原因;
至於connect系統調用為什么返回失敗,就只能看系統調用的實現了。
a) connect系統調用
connect系統調用在net/socket.c中實現,Sys_connect系統調用的調用棧如下:
Sys_connect--->
sock->ops->connect // inet_stream_connect sk->sk_prot->connect // tcp_v4_connect
tcp_v4_connect的作用主要是完成TCP連接三次握手中的第一個握手,即向服務端發送SYNC = 1和一個32位的序號的連接請求包。要發送SYNC請求包,按照TCP/IP協議,就必須有源IP地址和端口,源IP地址的選擇和路由相關,需要查詢路由表,在ip_route_connect中實現,源端口的選擇在__inet_hash_connect中實現,而且如果找不到一個可用的端口,這個函數會返回-EADDRNOTAVAIL,因此基本上可以確定是這個函數返回錯誤導致connect失敗;
b) __inet_hash_connect
這個函數的主要作用是選擇一個可用的端口,其主要的實現步驟如下:
i. 調用inet_get_local_port_range(&low, &high);獲取可用的端口鏈表;
- 調用read_seqbegin(&sysctl_local_ports.lock);得到順序鎖;
- 得到可用端口的low和high:
*low = sysctl_local_ports.range[0];
*high = sysctl_local_ports.range[1];
ii. 對於每一個端口,進行下面的步驟:
- 在inet_hashinfo *hinfo中查找這個端口inet_hashinfo用於保存已經使用的端口信息,每個使用的端口在這個hash表中有一個entry;
- 對端口做hash得到鏈表頭(使用鏈表解決hash沖突)
- 遍歷鏈表中的每一個entry:
a) 判斷是否與這個要使用的端口相同,如果相同轉到步驟b,如果不相同則遍歷下一個entry
b) 找到這個端口,調用check_established(__inet_check_established)判斷這個端口是否可以重用(TIME_WAIT狀態下的端口並且net.ipv4.tcp_tw_recycle = 1是端口可以重用)
- 如果在鏈表中沒有找到這個端口,表示端口沒有被使用,調用inet_bind_bucket_create在hash表中插入一個entry;
iii. 如果到最后都沒有找到一個可用的端口就返回EADDRNOTAVAIL;
從這個函數的實現可以看出,主要是由於可用的端口被占滿了,所以找不到一個可用的端口,導致連接失敗。運行netstat可以發現確實存在很多TIME_WAIT狀態的socket,這些socket將可用端口占滿了。
[root@test miuistorage-dev]# netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' TIME_WAIT 26837 ESTABLISHED 30
(4) 解決辦法:
要解決端口被TIME_WAIT狀態的socket占滿的問題,可以有以下的解決辦法:
a) 修改可用端口范圍
查看當前的端口范圍:
root@guojun8-desktop:/linux-2.6.34# sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768 61000
修改端口范圍:
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.ip_local_port_range="32768 62000" net.ipv4.ip_local_port_range = 32768 62000
這種辦法可能不能解決根本問題,因為如果使用短連接,即使增加可用端口還是會被占滿的。
b) 設置net.ipv4.tcp_tw_recycle = 1
這個參數表示系統的TIME-WAIT sockets是否可以快速回收
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.tcp_tw_recycle=1 net.ipv4.tcp_tw_recycle = 1
c) 設置net.ipv4.tcp_tw_recycle = 1
這個參數表示是否可以重用TIME_WAIT狀態的端口;
root@guojun8-desktop:linux-2.6.34# [root@test thumbnail]# sysctl net.ipv4.tcp_tw_reuse=1 net.ipv4.tcp_tw_reuse = 1
(5) 更深入的探討:sysctl做了什么
可以用strace跟蹤一下sysctl的系統調用:
root@guojun8-desktop:linux-2.6.34# strace sysctl net.ipv4.tcp_tw_recycle=1 execve("/sbin/sysctl", ["sysctl", "net.ipv4.tcp_tw_recycle=1"], [/* 20 vars */]) = 0 brk(0) = 0x952f000 ….. open("/proc/sys/net/ipv4/tcp_tw_recycle", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000 write(3, "1\n", 2) = 2 close(3) = 0 munmap(0xb788e000, 4096) = 0 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000 write(1, "net.ipv4.tcp_tw_recycle = 1\n", 28net.ipv4.tcp_tw_recycle = 1 ) = 28 exit_group(0) = ?
可以看到這個程序打開/proc/sys/net/ipv4/tcp_tw_recycle並向文件中寫入1,但是這個設置時怎樣其作用的呢?在內核中對/proc/sys目錄下的文件的i_fop做了特殊的處理,在proc_sys_make_inode 中設置:inode->i_fop = &proc_sys_file_operationsproc_sys_file_operations的定義如下:
static const struct file_operations proc_sys_file_operations = { .read = proc_sys_read, .write = proc_sys_write, };
proc_sys_write中會修改對應的文件,並且修改內存中的內容,不同的文件有不同的proc_handler,如tcp_tw_recycle對應的處理函數是proc_dointvec,這個函數會修改下面的變量:
tcp_death_row.sysctl_tw_recycle
這個變量在內核中表示TIME_WIAT狀態的socket是否可以被快速回收。