一、问题描述
今天一个同事遇到一个问题,问题如下:
他的程序的运行操作系统是linux,作为TCP Client 端,需要连接本机上的另一程序(TCP Server端, 端口为39000)。TCP Server端程序不是并不是总是启动着,TCP Client端程序在连接不到Server端时,等待10分钟,再重新连接Server端,不断重试。昨天他遇到了这样的问题,Server端未启动,但他的程序Client端可以连接成功,造成Server端程序因为端口被占用,无法启动!
他描述完问题,我第一反应就是发生了自连接。关于自连接为什么是合法的,可以查看相关资料。
二、产生自连接程序测试程序
点击(此处)折叠或打开
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#define TRY_CNT 60000
void exec_cmd(char *cmd) {
printf("\n**********start exec cmd: %s **********\n\n", cmd);
system(cmd);
printf("\n\n**********finish exec cmd: %s **********\n", cmd);
}
int main(int argc, char *argv[]) {
int cnt ;
int sock;
int ret;
struct sockaddr_in seraddr, cliaddr;
socklen_t addrlen;
char cmd[1024];
int port;
if(argc < 2) {
printf("need port arg. usage:\nselfconntest port\n");
return 1;
}
snprintf(cmd, sizeof(cmd), "netstat -npt | grep %s", argv[1]);
port = atoi(argv[1]);
if(port < 1000 || port > 65535) {
printf("port should be 1000 - 65535\n");
return 1;
}
sock = socket(AF_INET, SOCK_STREAM, 0);
if( sock < 0 ) {
printf("socket() error. errno = %d, errstr = %s\n",
errno, strerror(errno));
return -1;
}
// init seraddr
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = ntohs(port);
if( inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr) < 0 ) {
printf("inet_pton() error");
return -2;
}
printf("remote addr:\t ip = %X, port = %u\n",
ntohl(seraddr.sin_addr.s_addr), ntohs(seraddr.sin_port));
for(cnt = 0; cnt < TRY_CNT; cnt++) {
ret = connect(sock, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if( ret == 0 ) {
printf("try %d times, connect succ.\n", cnt + 1);
// get local addr info
addrlen = sizeof(cliaddr);
ret = getsockname(sock, (struct sockaddr *)&cliaddr, &addrlen);
if( ret < 0 ) {
printf("getsockname () error. errno = %d, errstr = %s\n",
errno, strerror(errno));
} else {
printf("local addrr:\t ip = %X, port = %u\n",
ntohl(cliaddr.sin_addr.s_addr), ntohs(cliaddr.sin_port));
exec_cmd(cmd);
}
// pause();
sleep(1);
break;
}
// only print error message of first time and last time
if( cnt == 0 || cnt == TRY_CNT - 1) {
printf("connect () error. errno = %d, errstr = %s\n",
errno, strerror(errno));
}
}
printf("cnt = %d\n", cnt+1);
close(sock);
sleep(2);
exec_cmd(cmd);
return 0;
}
执行测试程序如下:
./selfconntest 39000
remote addr: ip = 7F000001, port = 39000
connect () error. errno = 111, errstr = Connection refused
try 1997 times, connect succ.
local addrr: ip = 7F000001, port = 39000
**********start exec cmd: netstat -npt | grep 39000 **********
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:39000 127.0.0.1:39000 ESTABLISHED 8512/./selfconntest
**********finish exec cmd: netstat -npt | grep 39000 **********
cnt = 1997
**********start exec cmd: netstat -npt | grep 39000 **********
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:39000 127.0.0.1:39000 TIME_WAIT -
**********finish exec cmd: netstat -npt | grep 39000 **********
三、如何避免自连接?
1. Client端连接成功后,然后判断是否是自连接。
该方法在一定程度上可行,每次connect成功后,调用getsockname(),获得本端的ip及port,判断是否等于server端端口,如果是,关闭该连接,继续循环重试。
但该方法仍然存在一个问题,就是判断出是自连接,并关闭,但因为是主动关闭,必然造成该TCP连接处于TIME_WAIT状态,如果Server端此时启动,仍然因为端口被占用导致不能启动成功。这个时间窗口,会因为你设置socket的选项,可以缩小窗口,降低发生Server端不能启动的概率,但仍然无法避免。所以Server端也应该进行相应的修改,设置SO_REUSEADDR套接口选项,并当发现端口被占用时,过一段时间重试。
这样一个问题,导致Client与Server都需要进行修改,而且Server端的开发人员还不是一个组的时候,确实有点大动干戈。
2. Client使用固定端口,该端口与Server端不同
Client端在连接前bind本地固定端口,然后再connect Server端。
这种方式能够解决问题。但仍然需要修改Client程序,如果有多个Client端,要申请多个固定的端口号。
3. 查看系统配置文件,未雨绸缪。
在linux系统,系统随机分配未绑定的客户端的端口号,是有一定规律,并可以配置的,随机本地随机端口的分配的区间是ip_local_port_range。
查看 /etc/sysctl.conf ,看一下是否有对 net.ipv4.ip_local_port_range 以及 net.ipv4.ip_local_reserved_ports的设置,如果没有,再查看/proc/sys/net/ipv4/ip_local_port_range 以及 /proc/sys/net/ipv4/ip_local_reserved_ports中的值。
我机器上的值如下:
cat /proc/sys/net/ipv4/ip_local_port_range/ip_local_port_range
32768 61000
总的来说就是,系统分配Client端的随机端口规律是:ip_local_port_range 区间的值 去掉 ip_local_reserved_ports 后剩下的端口。
未雨绸缪就是Server端程序的端口号最好不要选择ip_local_port_range区间内的端口,这样Client如果使用随机端口是在ip_local_port_range区间内,这样也就不会发生本机上的自连接。
如果Server端的端口号已经固定,并在 ip_local_port_range区间内,那么可以设置 ip_local_reserved_ports 为该Server端的端口号,那么Client就不会使用ip_local_reserved_ports中的值作为随机端口。
网友评论