高併發網絡連接數因端口數量受限問題

 高

併發網絡連接數因端口數量受限問題

遇到的問題:端口數量受限

一般來說,單獨對外提供請求的服務不用考慮端口數量問題,監聽某一個端口即可。但是向提供代理服務器,就不得不考慮端口數量受限問題了。當前的1M併發連接測試,也需要在客戶端突破6萬可用端口的限制。

單機端口上限爲65536

端口爲16進制,那麼2的16次方值爲65536,在Linux系統裏面,1024以下端口都是超級管理員用戶(如root)纔可以使用,普通用戶只能使用大於1024的端口值。 
系統提供了默認的端口範圍:

cat /proc/sys/net/ipv4/ip_local_port_range 
32768 61000

大概也就是共61000-32768=28232個端口可以使用,單個IP對外只能發送28232個TCP請求。 
以管理員身份,把端口的範圍區間增到最大:

echo "1024 65535"> /proc/sys/net/ipv4/ip_local_port_range

現在有64511個端口可用. 
以上做法只是臨時,系統下次重啓,會還原。 更爲穩妥的做法是修改/etc/sysctl.conf文件,增加一行內容

net.ipv4.ip_local_port_range= 1024 65535

保存,然後使之生效:

sysctl -p

現在可以使用的端口達到64510個(假設系統所有運行的服務器是沒有佔用大於1024的端口的,較爲純淨的centos系統可以做到),要想達到50萬請求,還得再想辦法。

增加IP地址

一般假設本機網卡名稱爲 eth0,那麼手動再添加幾個虛擬的IP:

ifconfig eth0:1 192.168.190.151 
ifconfig eth0:2 192.168.190.152 ......

或者偷懶一些:

for i in `seq 1 9`; do ifconfig eth0:$i 192.168.190.15$i up ; done

這些虛擬的IP地址,一旦重啓,或者 service network restart 就會丟失。

爲了模擬較爲真實環境,在測試端,手動再次添加9個vmware虛擬機網卡,每一個網卡固定一個IP地址,這樣省去每次重啓都要重新設置的麻煩。

192.168.190.134 
192.168.190.143
192.168.190.144
192.168.190.145
192.168.190.146
192.168.190.147
192.168.190.148
192.168.190.149
192.168.190.150
192.168.190.151

在server服務器端,手動添加橋接網卡和NAT方式網卡

192.168.190.230
192.168.190.240
10.95.20.250

要求測試端和服務器端彼此雙方都是可以ping通。

網絡四元組/網絡五元組

四元組是指的是

{源IP地址,源端口,目的IP地址,目的端口}

五元組指的是(多了協議)

{源IP地址,目的IP地址,協議號,源端口,目的端口}

在《UNIX網絡編程卷1:套接字聯網API(第3版)》一書中,是這樣解釋:
一個TCP連接的套接字對(socket pari)是一個定義該連接的兩個端點的四元組,即本地IP地址、本地TCP端口號、外地IP地址、外地TCP端口號。套接字對唯一標識一個網絡上的每個TCP連接。 

...... 

標識每個端點的兩個值(IP地址和端口號)通常稱爲一個套接字。

以下以四元組爲準。在測試端四元組可以這樣認爲:

{本機IP地址,本機端口,目的IP地址,目的端口}

請求的IP地址和目的端口基本上是固定的,不會變化,那麼只能從本機IP地址和本機端口上考慮,端口的範圍一旦指定了,那麼增加IP地址,可以增加對外發出的請求數量。假設系統可以使用的端口範圍已經如上所設,那麼可以使用的大致端口爲64000個,系統添加了10個IP地址,那麼可以對外發出的數量爲 64000 * 10 = 640000,數量很可觀。

只有{源IP地址,源端口}確定對外TCP請求數量

經測試,四元組裏面,只有{源IP地址,源端口}才能夠確定對外發出請求的數量,跟{目的IP地址,目的端口}無關。

測試環境

在server端,並且啓動./server兩次,分別綁定8000端口和9000端口

./server -p 8000
./server -p 9000

本機IP、端口綁定測試程序

這裏寫一個簡單的測試綁定本機IP地址和指定端口的客戶端測試程序。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
#include <sys/types.h>
#include <sys/time.h>
#include <sys/queue.h>
#include <stdlib.h>
#include <err.h>
#include <event.h>
#include <evhttp.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <pthread.h>
#include <errno.h>
 
#define BUFSIZE 4096
#define SLEEP_MS 10
 
char buf[BUFSIZE];
 
int bytes_recvd = 0;
int chunks_recvd = 0;
 
void chunkcb(struct evhttp_request *req, void *arg) {
int s = evbuffer_remove( req->input_buffer, &buf, BUFSIZE );
bytes_recvd += s;
chunks_recvd++;
printf(">Chunks: %d\tBytes: %d\n", chunks_recvd, bytes_recvd);
}
 
void reqcb(struct evhttp_request *req, void *arg) {
fprintf(stderr, ">Now closed\n");
exit(-1);
}
 
void err_cb(int err){
fprintf(stderr, "setup failed(errno = %d): %s", errno, strerror(errno));
}
 
int main(int argc, char **argv) {
char server_ip[16] = "";
int server_port = 0;
 
char local_ip[16] = "";
int local_port = 0;
 
int ch;
while ((ch = getopt(argc, argv, "h:p:c:o:")) != -1) {
switch (ch) {
case 'h':
printf("remote host is %s\n", optarg);
strncpy(server_ip, optarg, 15);
break;
case 'p':
printf("remote port is %s\n", optarg);
server_port = atoi(optarg);
break;
case 'c':
printf("local ip is %s\n", optarg);
strncpy(local_ip, optarg, 15);
break;
case 'o':
printf("local port is %s\n", optarg);
local_port = atoi(optarg);
 
break;
}
}
 
event_init();
event_set_fatal_callback(err_cb);
struct evhttp *evhttp_connection;
struct evhttp_request *evhttp_request;
char path[32];
 
evhttp_connection = evhttp_connection_new(server_ip, server_port);
evhttp_connection_set_local_address(evhttp_connection, local_ip);
evhttp_connection_set_local_port(evhttp_connection, local_port);
evhttp_set_timeout(evhttp_connection, 864000); // 10 day timeout
evhttp_request = evhttp_request_new(reqcb, NULL);
evhttp_request->chunk_cb = chunkcb;
sprintf(&path, "/test/%d", local_port);
 
evhttp_make_request( evhttp_connection, evhttp_request, EVHTTP_REQ_GET, path );
evhttp_connection_set_timeout(evhttp_request->evcon, 864000);
event_loop( EVLOOP_NONBLOCK );
 
usleep(SLEEP_MS * 10);
 
event_dispatch();
 
return 0;
}
view rawclient3.c hosted with ❤ by GitHub

可以看到libevent-*/include/event2/http.h內置了對綁定本地IP地址的支持:

/** sets the ip address from which http connections are made */
void evhttp_connection_set_local_address(struct evhttp_connection *evcon,
const char *address);

不用擔心端口,系統自動自動隨機挑選,除非需要特別指定:

/** sets the local port from which http connections are made */
void evhttp_connection_set_local_port(struct evhttp_connection *evcon,
ev_uint16_t port);

編譯

gcc -o client3 client3.c -levent

client3運行參數爲

  • -h 遠程主機IP地址
  • -p 遠程主機端口
  • -c 本機指定的IP地址(必須可用)
  • -o 本機指定的端口(必須可用)

測試用例,本機指定同樣的IP地址和端口,但遠程主機和IP不一樣. 
在一個測試端打開一個終端窗口1,切換到 client3對應位置

./client3 -h 192.168.190.230 -p 8000 -c 192.168.190.148 -o 4000

輸出爲

remote host is 192.168.190.230
remote port is 8000
local ip is 192.168.190.148
local port is 4000
>Chunks: 1 Bytes: 505

再打開一個測試端終端窗口2,執行:

./client3 -h 192.168.190.240 -p 9000 -c 192.168.190.148 -o 4000

窗口2程序,無法執行,自動退出。 
接着在窗口2終端繼續輸入:

./client3 -h 192.168.190.230 -p 8000 -c 192.168.190.148 -o 4001

注意,和窗口1相比,僅僅改變了端口號爲4001。但執行結果,和端口1輸出一模一樣,在等待接收數據,沒有自動退出。

剩下的,無論怎麼組合,怎麼折騰,只要一對{本機IP,本機端口}被佔用,也就意味着對應一個具體的文件句柄,那麼其它程序將不能夠再次使用。

Java怎麼綁定本地IP地址?

Java綁定就很簡單,但有些限制,不夠靈活,單純從源碼中看不出來,api doc可以告訴我們一些事情。 打開JDKAPI1_6zhCN.CHM,查看InetSocketAddress類的構造函數說明:

public InetSocketAddress(InetAddress addr, int port) 
根據 IP 地址和端口號創建套接字地址。 有效端口值介於 0 和 65535 之間。端口號 zero 允許系統在 bind 操作中挑選暫時的端口。

null 地址將分配通配符 地址。

參數: 
addr - IP 地址 
port - 端口號 
拋出: 
IllegalArgumentException - 如果 port 參數超出有效端口值的指定範圍。


public InetSocketAddress(String hostname, int port) 
根據主機名和端口號創建套接字地址。 
嘗試將主機名解析爲 InetAddress。如果嘗試失敗,則將地址標記爲未解析。

如果存在安全管理器,則將主機名用作參數調用其 checkConnect 方法,以檢查解析它的權限。這可能會導致 SecurityException 異常。

有效端口值介於 0 和 65535 之間。端口號 zero 允許系統在 bind 操作中挑選暫時的端口。

參數: hostname - 主機名 
port - 端口號 
拋出: 
IllegalArgumentException - 如果 port 參數超出有效端口值的範圍,或者主機名參數爲 null。 
SecurityException - 如果存在安全管理器,但拒絕解析主機名的權限。 
另請參見: 
isUnresolved()

InetSocketAddress的兩個構造函數都支持,看情況使用。注意int port傳遞值爲0,即可做到系統隨機挑選端口。追蹤一下源代碼,發現最終調用

private native void socketBind(InetAddress address, int port) throws IOException;

如何查看socketBind的原始C代碼,我就不清楚了,您若知曉,希望指教一下。 構造一個InetSocketAddress對象:

SocketAddress localSocketAddr = new InetSocketAddress("192.168.190.143", 0);

然後傳遞給需要位置即可。諸如使用netty連接到某個服務器上,在connect時指定遠方地址,以及本機地址

ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) Attempts a new connection with the specified remoteAddress and the specified localAddress.

Netty 客戶端連接API見: http://docs.jboss.org/netty/3.2/api/org/jboss/netty/bootstrap/ClientBootstrap.html

Linux支持綁定本機IP、端口原理

說是原理,有些牽強,因爲linux C提供瞭如何綁定函數,框架或者高級語言再怎麼封裝,在linux平臺下面,需要這麼調用:

struct sockaddr_in clnt_addr;
....
clnt_addr.sin_family = AF_INET;
clnt_addr.sin_addr.s_addr = INADDR_ANY; //綁定本機IP地址
clnt_addr.sin_port = htons(33333); //綁定本機端口
if (bind(sockfd, (struct sockaddr *) &clnt_addr,
sizeof(clnt_addr)) < 0) error("ERROR on binding");
if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) error("ERROR connecting");
.......

構造一個clnt_addr結構體,本地IP或者端口賦值,在connect之前,先bind,就這麼簡單。

更完整例子,可以參考 http://stackoverflow.com/questions/4852256/need-a-complete-snippet-example-of-binding-tcp-client-socket

有關端口的更詳細解釋,請參考《UNIX網絡編程卷1:套接字聯網API(第3版)》2.9節 端口號部分。
發佈了56 篇原創文章 · 獲贊 33 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章