24

一个端口号可以同时被两个进程绑定吗?

 3 years ago
source link: http://network.51cto.com/art/202011/631356.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

根据端口号的绑定我们分以下几种情况来讨论:

2个进程分别建立TCP server,使用同一个端口号8888

2个进程分别建立UDP server,使用同一个端口号8888

2个进程1个建立TCP server、1个建立UDP server,都使用端口号8888

1. 测试代码

我们首先编写两个简单的测试程序。

tcp.c

该程序仅仅创建tcp套接字并绑定端口号8888,没有accept建立连接操作,并且sleep(1000),让进程不要太快退出。

/*******服务器程序 TCPServer.c ************/

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define WAITBUF 10

#define RECVBUFSIZE 1024

int main(int argc, char *argv[])

{

int sockfd,new_fd,nbytes;

struct sockaddr_in server_addr;

struct sockaddr_in client_addr;

int portnumber = 8888;

socklen_t sin_size;

char hello[512];

char buffer[RECVBUFSIZE];

/*端口号不对,退出*/

/*服务器端开始建立socket描述符*/

if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)

{

fprintf(stderr,"Socket error:%s\n\a",strerror(errno));

exit(1);

}

/*服务器端填充 sockaddr结构*/

bzero(&server_addr,sizeof(struct sockaddr_in));

server_addr.sin_family=AF_INET;

/*自动填充主机IP*/

server_addr.sin_addr.s_addr=htonl(INADDR_ANY);

server_addr.sin_port=htons(portnumber);

/*捆绑sockfd描述符 进程+端口号+ip+socket*/

if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)

{

fprintf(stderr,"Bind error:%s\n\a",strerror(errno));

exit(1);

}

/*监听sockfd描述符*/

if(listen(sockfd, WAITBUF)==-1)

{

fprintf(stderr,"Listen error:%s\n\a",strerror(errno));

exit(1);

}

sleep(1000);//让程序不要这么快的退出

close(sockfd);

exit(0);

}

udp.c

该程序仅仅创建udp套接字并绑定端口号8888,没有accept建立连接操作,并且sleep(1000),让进程不要太快退出.

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define SERVER_PORT 8888

#define MAX_MSG_SIZE 1024

int main(void)

{

int sockfd;

struct sockaddr_in addr;

/* 服务器端开始建立socket描述符 */

sockfd=socket(AF_INET,SOCK_DGRAM,0);

if(sockfd<0)

{

fprintf(stderr,"Socket Error:%s\n",strerror(errno));

exit(1);

}

/* 服务器端填充 sockaddr结构 */

bzero(&addr,sizeof(struct sockaddr_in));

addr.sin_family=AF_INET;

addr.sin_addr.s_addr=htonl(INADDR_ANY);

addr.sin_port=htons(SERVER_PORT);

/* 捆绑sockfd描述符 */

if(bind(sockfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in))<0)

{

fprintf(stderr,"Bind Error:%s\n",strerror(errno));

exit(1);

}

sleep(1000);

close(sockfd);

}

编译

gcc tcp.c -o tcp

gcc udp.c -o udp

2. 执行结果

1).2个进程分别建立TCP server

情况1执行结果

从结果可知,第二个进程绑定端口号8888绑定失败。

2).2个进程分别建立UDP server

情况2执行结果

从结果可知,第二个进程绑定端口号8888绑定失败。

3).1个建立TCP server、1个建立UDP server

情况3执行结果

用netstat命令查看信息。

netstat

从结果可知,该种情形,两个进程分别绑定成功。

3. 结果分析

由上述结果可知:TCP、UDP可以同时绑定一个端口8888,但是一个端口在同一时刻不可以被TCP或者UDP绑定2次。原因如下:

tcp的端口不是物理概念,仅仅是协议栈中的两个字节;

TCP和UDP的端口完全没有任何关系,完全有可能又有一种XXP基于IP,也有端口的概念,这是完全可能的;

TCP和UDP传输协议监听同一个端口后,接收数据互不影响,不冲突。因为数据接收时时根据五元组**{传输协议,源IP,目的IP,源端口,目的端口}**判断接受者的。

二、端口号的一些其他知识点

1. 端口号的作用

端口号可以用来标识同一个主机上通信的不同应用程序,端口号+IP地址就可以组成一个套接字,用来标识一个进程。

2. 端口号的应用场景

在TCP/IP协议中,用“源IP地址”,“目的IP地址”,“源端口号”,“目的端口号”,协议号(IP协议的协议号为4,TCP的协议号为6)这样的一个五元组来标识一个通信,通信的双方在发送消息时,消息的头部会带着这样的五元组。

3. 端口范围划分

(1)0~1023:知名端口号,是留着备用的,一把都是用于协议,例如HTTP、FTP、SSH ;

(2)1024~65535:是操作系统动态分配的端口号,客户端程序的端口号,就是由操作糸统从这个范围来分配的,在TCP与UDP的套接字通信中,客户端的端口号就是在此范围中。

4. 知名的端口号与端口号对应的服务器

比如:

HTTP服务器:80

FTP服务器:21

ps:FTP有一个控制连接和一个数据连接,所以FTP是有两个端口号的,控制连接的端口号是21,数据连接的端口号是20,但是如果FTP的端口号默认是21,如果指明FTP有两个端口号的话,那就是21和20,否则FTP服务器的端口号就是21

TELNET服务器:23

SSH服务器:22

HTTPS:443

WEB服务器:25

5. 在linux中如何查看知名端口号?

cat /etc/services

6. 一个进程是否可以bind多个端口号?

可以

因为一个进程可以打开多个文件描述符,而每个文件描述符都对应一个端口号,所以一个进程可以绑定多个端口号。

Linux内核会给每一个socket分配一个唯一的文件描述符,进程通过该文件描述符来区分对应的套接字。

7. 一个端口号是否可以被多个进程绑定?

同种协议通常不可以,但有一种情况可以。

ps:如果进程先绑定一个端口号,然后在fork一个子进程,这样的话就可以是实现多个进程绑定一个端口号,但是两个不同的进程绑定同一个端口号是不可以的。

三、SO_REUSEADDR有什么用处和怎么使用?

当两个socket的address和port相冲突,而我们又想重用地址和端口,则旧的socket和新的socket都要已经被设置了SO_REUSEADDR特性,只有两者之一有这个特性还是有问题的。

SO_REUSEADDR可以用在以下四种情况下。(摘自《Unix网络编程》卷一,即UNPv1)

当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时【4次握手】,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。

一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。

SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用。server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项。TCP,先调用close()的一方会进入TIME_WAIT状态。

4次握手顺序见下图:

4次握手

SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。

SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。

SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。

SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。

SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。

SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。

SO_REUSEPORT选项有如下语义:此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。

如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。

使用这两个套接口选项的建议:在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项;当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。

设置方法如下:

if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,

(const void *)&nOptval , sizeof(int)) < 0)

...

Q:编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?

A:这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。

一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用 SO_REUSEADDR 选项。

举例

例子1:测试上面第一种情况。

#include

#include

#include

#include

#include

#define MAXLINE 100

int main(int argc, char** argv)

{

int listenfd,connfd;

struct sockaddr_in servaddr;

char buff[MAXLINE+1];

time_t ticks;

unsigned short port;

int flag=1,len=sizeof(int);

port=10013;

if( (listenfd=socket(AF_INET,SOCK_STREAM,0)) == -1)

{

perror("socket");

exit(1);

}

bzero(&servaddr,sizeof(servaddr));

servaddr.sin_family=AF_INET;

servaddr.sin_addr.s_addr=htonl(INADDR_ANY);

servaddr.sin_port=htons(port);

if( setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)

{

perror("setsockopt");

exit(1);

}

if( bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)

{

perror("bind");

exit(1);

}

else

printf("bind call OK!\n");

if( listen(listenfd,5) == -1)

{

perror("listen");

exit(1);

}

for(;;)

{

if( (connfd=accept(listenfd,(struct sockaddr*)NULL,NULL)) == -1)

{

perror("accept");

exit(1);

}

if( fork() == 0)/*child process*/

{

close(listenfd);/*关闭监听套接字,子进程不需要。*/

ticks=time(NULL);

snprintf(buff,100,"%.24s\r\n",ctime(&ticks));

write(connfd,buff,strlen(buff));

close(connfd);

sleep(1);

execlp("run",NULL);

perror("execlp");

exit(1);

}

close(connfd);

exit(0);/* end parent*/

}

}

gcc 123.c -o run

sudo cp run /sbin

sudo chmod 777 /sbin/run

测试:编译为run程序,放到一个自己PATH环境变量里的某个路径里,例如$HOME/bin,运行run,然后telnet localhost 10013看结果。

第一步 运行程序,此时程序阻塞在accept()这个位置。

第二步 重新打开一个终端,执行以下命令。

第三步:可以看到次异步运行的程序退出,并打印了bind call OK! 说明子进程被执行,并且成功绑定了端口10013,验证了第一种情况。

第二种情况我没有环境测,所以就不给测试程序了,大家有条件的可以自己写一个来测试一下。

测试第三种情况的程序 读取本地ip地址

ifconfig

可以得到本地地址为:

eth0 : 192.168.43.171

lo : 127.0.0.1

测试代码

#include

#include

#include

#include

#include

#define MAXLINE 100

int main(int argc, char** argv)

{

int fd1,fd2;

struct sockaddr_in servaddr1,servaddr2;

char buff[MAXLINE+1];

time_t ticks;

unsigned short port;

int flag=1,len=sizeof(int);

port=10013;

if( (fd1=socket(AF_INET,SOCK_STREAM,0)) == -1)

{

perror("socket");

exit(1);

}

if( (fd2=socket(AF_INET,SOCK_STREAM,0)) == -1)

{

perror("socket");

exit(1);

}

bzero(&servaddr1,sizeof(servaddr1));

bzero(&servaddr2,sizeof(servaddr2));

servaddr1.sin_family=AF_INET;

servaddr2.sin_family=AF_INET;

if( inet_pton(AF_INET, "127.0.0.1", &servaddr1.sin_addr) <= 0)

{

printf("inet_pton() call error:127.0.0.1\n");

exit(1);

}

if( inet_pton(AF_INET, "192.168.43.171", &servaddr2.sin_addr) <= 0)

{

printf("inet_pton() call error:128.160.1.230\n");

exit(1);

}

servaddr1.sin_port=htons(port);

servaddr2.sin_port=htons(port);

if( setsockopt(fd1, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)

{

perror("setsockopt");

exit(1);

}

if( setsockopt(fd2, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)

{

perror("setsockopt");

exit(1);

}

if( bind(fd1,(struct sockaddr*)&servaddr1,sizeof(servaddr1)) == -1)

{

perror("bind fd1");

exit(1);

}

if( bind(fd2,(struct sockaddr*)&servaddr2,sizeof(servaddr2)) == -1)

{

perror("bind fd2");

exit(1);

}

printf("bind fd1 and fd2 OK!\n");

/*put other process here*/

getchar();

exit(0);/* end */

}

执行结果

结果

由于第四种情况只用于UDP的多播,和TCP的使用没多大关系,所以就不写测试例子了。自己有兴趣的可以写。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK