【TCP Loop Connect】TCP回环连接

业务场景描述

TCP回环连接,通俗点讲就是,TCP自己连上了自己。
是不是有点不可思议?!

好了,先说下业务场景。

同一台服务器上部署两个服务,一个是基于C++ - ACE库编写的服务端,一个是基于Java-Netty编写的客户端。服务端bind端口20002,CLIENT端提供重连机制。

服务端与客户端进程间通过Socket通信,服务端是择时启动和运行,客户端是全天候运行,客户端不断尝试连接服务端,由于两者在同一台机器上,当服务端不存在时并且人品好到客户端创建的这个Socket端口刚好随机到这个服务端端口相同时,就连接上了,客户端以为自己连上了服务端,但实际上只是连上了自己,但实际上程序是不工作的。

临时解决方案

问题发生场景:当第二天重新启动服务端时,启动失败,查看日志信息:端口号[20002]已被占用
开始怀疑,昨天关闭服务端时,未能将服务端彻底关闭,导致端口号未释放,那先查看进程吧:

ps -ef | grep sched

哎,进程不存在啊!说明确实已经关闭了!

那么,是谁占用了20002端口号呢?

netstat -anp | grep 20002

回显信息:

tcp6 0 0 192.168.80.10:20002 192.168.80.10:20002 ESTABLISHED 24276/java

可以确认是被Java客户端占用了,但是为什么会占用呢?它只是去连接,而没有bind该端口号,好吧,先记下现象,再去分析和解决。

临时解决方案:
先解决服务端启动失败的问题吧,毕竟是线上环境,你得让它先正常工作啊!

## kill掉java客户端
kill -9 24276

再启动Java客户端,服务端,然后一切都正常了。

问题重现

什么情况下会发生TCP重连?

Connection is established from HOST to HOST on the same IP
Destination TCP Port has to be from range of ephemeral ports configured on OS
Listen port is unbound

  • 连接是由主机与主机在同一个IP上建立的。
  • 目标TCP端口在操作系统上配置的临时端口的范围内。
  • 监听端口是自由状态

自测程序一:固定端口号

socket.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int, char**)
{
        int s = (int)socket(PF_INET, SOCK_STREAM, 0);
        if (s != ~0)
        {
                int opt = 1;
                setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(&opt));
                struct sockaddr_in sockAddr;
                memset(&sockAddr, 0, sizeof(sockAddr));
                sockAddr.sin_family = AF_INET;
                sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
                sockAddr.sin_port = htons(20002);
                if (bind(s, (struct sockaddr*)&sockAddr, sizeof(sockAddr)) == 0)
                {
                        if (connect(s, (struct sockaddr*)&sockAddr, sizeof(sockAddr)) == 0)
                        {
                                printf("Connect succeeded!\r\n");
                                const char* lpWord = "Hello World!";
                                if (send(s, lpWord, (int)strlen(lpWord), 0) > 0)
                                {
                                        char buffer[20];
                                        memset(buffer, 0, sizeof(buffer));
                                        if (recv(s, buffer, sizeof(buffer) - 1, 0) > 0)
                                        {
                                                printf("%s\r\n", buffer);
                                                getchar();
                                        }
                                }
                        }
                        else
                        {
                                printf("Connect failed!\r\n");
                        }
                }
                else
                {
                        printf("Can't bind to port 20002!\r\n");
                }
                close(s);
        }
        else
        {
                printf("Can't create socket!\r\n");
        }
        return 0;
}

启动两个终端,编译:

g++ socket.cpp

终端一,执行: ./a.out
这里写图片描述
终端二,执行:netstat -na | grep 20002
这里写图片描述
终端二,执行: ./a.out
这里写图片描述

自测程序二:重连多少次可以做到TCP回环连接

tcploop.c

/* tcploop.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <netdb.h>
#include <string.h>

int main(int argc, char *argv[])
{
        int port = 0;
        int sckfd = 0;
        int opt = 1;
        struct sockaddr_in *remote;
        char *ip = "127.0.0.1";
        int rc;
        long n = 0;

        if(argc != 2){
                printf("Usage: %s <listen port>\n", argv[0]);
                exit(1);
        }
        port = atoi(argv[1]);

        /* Set to Localhost:Port */
        remote = (struct sockaddr_in *)malloc(sizeof(struct sockaddr_in *));
        remote->sin_family = AF_INET;
        assert(inet_pton(AF_INET, ip, (void *)(&(remote->sin_addr.s_addr))) > 0);
        remote->sin_port = htons(port);

        /* create socket */
        assert((sckfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) > 0);
        assert(setsockopt(sckfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) >= 0);

        printf("Trying to connect..."); fflush(stdout);
        while(1) {
                n++;
                rc = connect(sckfd, (struct sockaddr *)remote, sizeof(struct sockaddr));
                if(rc < 0){
                        if(n % 1000 == 0) { printf("."); fflush(stdout); }
                        continue;
                }
                else {
                        printf("Connected after %ld tries\n", n);
                        break;
                }
        }
        /* Wait for Enter */
        printf("Press Enter to Continue...\n");
        getchar();
        close(sckfd);
        return 0;
}

样例输出:

  • Show how Ephemeral port range is configured
$ cat /proc/sys/net/ipv4/ip_local_port_range 
32768   61000
  • Invocation of tcploop with port from that range (33333)
$ ./tcploop 33333
Trying to connect..............Connected after 11263 tries
Press Enter to Continue...
  • lsof output for port 33333 in the other terminal
$ lsof -n | grep 33333
COMMAND     PID  USER   FD      TYPE     DEVICE  SIZE/OFF       NODE NAME
tcploop    3440 dixie    3u     IPv4     163396       0t0        TCP 127.0.0.1:33333->127.0.0.1:33333 (ESTABLISHED)

总结

  • 尽量避免服务端与客户端部署在同一台服务器
  • 服务端/客户端应用都不应该使用临时端口作为服务端侦听端口
  • TCP客户端有重连机制时,每个新连接均建立socket连接,操作系统均分配一个新的临时端口号时,TCP回环连接的概率会大大增加。

参考文章

https://www.zhihu.com/question/51438786
http://www.rampa.sk/static/tcpLoopConnect.html
http://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html

猜你喜欢

转载自blog.csdn.net/changqing5818/article/details/80021780
tcp