项目需要同一个机器上的测试两个进程间的通信,这两个进程是通过socket来建立链路的,测试过程中发现有的时候即使server端的进程没有运行,客户端也能成功建链,而且这条链路的源目的ip和port都相同,也正是client想要连接的ip和port。开始以为是客户端程序的bug,经过走查代码发现客户端建链的流程是很标准的socket客户端程序,创建套接字调用connect接口。后来将建链的代码抽出来做测试,发现connect多次之后必定可以建一个有问题的链路。网上对这个问题的的说明不多,下面对查到的资料整理总结一下。
其实,对于没有调用bind的客户端程序,在connect函数中会为相应的套接字选择一个临时接口(源ip和port),注意此时的port是随机的所以很有可能与要连接的服务端的port相同。一旦客户端套接字对应的源ip和port与connect参数中要连接的服务端的ip和port相同,那么这个问题必现。所以,这个问题很容易用如下客户端的程序复现。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
int sockfd,numbytes;
int times = 0;
char buf[BUFSIZ];
struct sockaddr_in their_addr;
printf("break!");
while((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1);
printf("We get the sockfd~\n");
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(7617);
their_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(their_addr.sin_zero), 8);
if (bind(sockfd, (const struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1)
{
printf("bind failed!");
return 1;
}
while(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == -1){
times++;
};
printf("Get the Server~Cheers after retry %d times!\n", times);
numbytes = recv(sockfd, buf, BUFSIZ,0);//接收服务器端信息
buf[numbytes]='\0';
printf("%s",buf);
while(1)
{
printf("Entersome thing:");
scanf("%s",buf);
numbytes = send(sockfd, buf, strlen(buf), 0);
numbytes=recv(sockfd,buf,BUFSIZ,0);
buf[numbytes]='\0';
printf("received:%s\n",buf);
}
close(sockfd);
return 0;
}
下面解释下出现问题的原因(摘自https://blog.csdn.net/justlinux2010/article/details/20947609):
这种类型的连接产生的过程类似于同时打开的情况。同时打开的情况是两个机器同时向另一个机器的已知端口发送SYN段,一个机器上发送的SYN段的目的IP和端口是另一个机器上发送SYN段的套接字的本地IP和端口(注意这两个机器上没有对应端口的监听套接字),状态迁移过程如下图所示:
(划重点)这里看到的连接的建立过程只发生在一个机器、一个套接字上,但是过程几乎是一样的。我们假设套接字名称是sk,调用bind将sk套接字的本地IP绑定为192.168.56.101,本地端口绑定为9090。首先,sk向目的IP是192.168.56.101,目的端口是9090的服务器发送SYN段,在发送SYN段之前,协议栈会将sk这个套接字的目的地址设置为192.168.56.101,目的端口设置为9090。当然,这个SYN段肯定是会在本机上进行接收处理。接收到这个SYN段后,会调用__inet_lookup()来查找对应的套接字。由于这个SYN段的源目的IP和端口信息和sk套接字的信息完全匹配,所以会由sk套接字来处理。sk套接字的状态会迁移到SYN_RCVD,然后发送SYN+ACK段。这个SYN+ACK段还是会由本机上的sk套接字处理。在SYN_RCVD状态下接收到SYN+ACK段,套接字的状态会迁移到ESTABLISHED。因为此时sk套接字期望接收的序列号,要比SYN+ACK段的序列号大1,相当于接收到了重复的段,所以还要发送一个D-ACK段,表示接收到了重复的段,但是不会影响sk套接字的状态。状态迁移过程如下所示: