问题:
如果一台机器上同时进行make check会出现只有一个能成功,另一个会终止的情况。经过调研发现,make check的底层代码中,make check的端口被写死了,不会自动识别空闲端口号,导致在同时进行make check时端口被占用之后,无法完成。
通过底层代码可以发现,各端口号是固定的。
在make check的过程中,用gdb进行调试,并对statrt_node函数打断点,可以发现端口号一直为50852。
为了解决上述问题,需要进行自动识别空闲的端口,使端口被抢占后,能使用其他端口进行make check。
idea1:
-
编写一个函数去判断是否端口被占用(通过bind去判断?)
-
被占用就把端口号做变更,进行下一次判断(如何变更端口号?是用rand()在范围内随机,还是通过取余加减的计算方式寻找端口号?端口号得在1025~65535)
-
当端口号没被占用,就把其赋值给所需端口
(调研之后)idea2:
-
windows下有API可以调用,来获取空闲端口
可以使用GetTcpTable()、GetUdpTable()获得已经占用的端口列表
IPHLPAPI_DLL_LINKAGE ULONG GetTcpTable(
[out] PMIB_TCPTABLE TcpTable,
[in, out] PULONG SizePointer,
[in] BOOL Order
);
IPHLPAPI_DLL_LINKAGE ULONG GetUdpTable(
[out] PMIB_UDPTABLE UdpTable,
[in, out] PULONG SizePointer,
[in] BOOL Order
); -
通过上面两个api封装成接口,
uint16_t FindAvailableTcpPort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP);
uint16_t FindAvailableUdpPort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP);
uint16_t FindAvailablePort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP);
这三个接口分别的作用是:获得Tcp空闲端口,获得Udp空闲端口,获得Tcp和Udp都空闲的端口。
- 通过接口来获得空闲端口
#include <Windows.h>
#include <WinSock.h>
#include <tcpmib.h>
#include <IPHlpApi.h>
#include <vector>
#include <memory>
#include <algorithm>
#include <iostream>
#pragma comment(lib, "WS2_32.lib")
#pragma comment(lib, "IPHlpApi.lib")
#define PORT_DOWN 49152
#define PORT_UP 65535
static std::vector<uint16_t> GetAllTcpConnectionsPort()
{
std::vector<uint16_t> ret;
ULONG size = 0;
GetTcpTable(NULL, &size, TRUE);
std::unique_ptr<char[]> buffer(new char[size]);
PMIB_TCPTABLE tcpTable = reinterpret_cast<PMIB_TCPTABLE>(buffer.get());
if (GetTcpTable(tcpTable, &size, FALSE) == NO_ERROR)
for (size_t i = 0; i < tcpTable->dwNumEntries; i++)
ret.push_back(ntohs((uint16_t)tcpTable->table[i].dwLocalPort));
std::sort(std::begin(ret), std::end(ret));
return ret;
}
static std::vector<uint16_t> GetAllUdpConnectionsPort()
{
std::vector<uint16_t> ret;
ULONG size = 0;
GetUdpTable(NULL, &size, TRUE);
std::unique_ptr<char[]> buffer(new char[size]);
PMIB_UDPTABLE udpTable = reinterpret_cast<PMIB_UDPTABLE>(buffer.get());
if (GetUdpTable(udpTable, &size, FALSE) == NO_ERROR)
for (size_t i = 0; i < udpTable->dwNumEntries; i++)
ret.push_back(ntohs((uint16_t)udpTable->table[i].dwLocalPort));
std::sort(std::begin(ret), std::end(ret));
return ret;
}
uint16_t FindAvailableTcpPort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP)
{
auto vec = GetAllTcpConnectionsPort();
for (uint16_t port = begin; port != end; ++port)
if (!std::binary_search(std::begin(vec), std::end(vec), port))
return port;
return 0;
}
uint16_t FindAvailableUdpPort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP)
{
auto vec = GetAllUdpConnectionsPort();
for (uint16_t port = begin; port != end; ++port)
if (!std::binary_search(std::begin(vec), std::end(vec), port))
return port;
return 0;
}
uint16_t FindAvailablePort(uint16_t begin = PORT_DOWN, uint16_t end = PORT_UP)
{
auto vecTcp = GetAllTcpConnectionsPort(),
vecUdp = GetAllUdpConnectionsPort();
for (uint16_t port = begin; port != end; ++port)
if (!std::binary_search(std::begin(vecTcp), std::end(vecTcp), port) &&
!std::binary_search(std::begin(vecUdp), std::end(vecUdp), port))
return port;
return 0;
}
int main()
{
{
WSADATA wsaData;
WSAStartup(0x0201, &wsaData);
}
std::cout << "Tcp Available port : " << FindAvailableTcpPort() << "\n";
std::cout << "Udp Available port : " << FindAvailableUdpPort() << "\n";
std::cout << "Available port : " << FindAvailablePort() << "\n";
{
WSACleanup();
}
return 0;
}
该方法是在windows下实现的,GetTcpTable()、GetUdpTable()两个函数是windows特有的,在linux下没有,所以该方法无法使用。
idea3:
根据idea1,调研发现,可以借助socket实现,当我们使用socket去bind端口时,如果将端口号sin_port定为0,系统则会自动给我们一个当前可用的端口,关闭socket后,这个端口就是我们需要的。
具体步骤:
-
winsock初始化
-
初始化一个AF_INET、SOCK_STREAM的套接字
-
sockaddr_in的属性设置中将port定为0
-
bind成功后,使用getsockname获取对应信息,拿到系统分配的sin_port
-
关闭该套接字,返回上一步中的sin_port
代码实现:
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")
bool getAvaliablePort(unsigned short& port) {
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) {
return false;
}
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = 0;
struct sockaddr_in connAddr;
int len = sizeof connAddr;
if (::bind(sock, (SOCKADDR*)&addr, sizeof addr) != 0) {
return false;
}
if (getsockname(sock, (SOCKADDR*)&connAddr, &len) != 0) {
return false;
}
port = ntohs(connAddr.sin_port);
if (closesocket(sock) != 0) {
return false;
}
return true;
}
尽管该方法,是在windows下进行实现的,但是socket的思路可以运用在linux环境中,所以借鉴该方法的思路,可以在linux下进行尝试。
idea4:
依据idea3的思路,使用socket方式,借助bind()函数来寻找空闲端口号。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
while (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
fprintf(stderr, "Failed to bind to port %d: %s\n", PORT, strerror(errno));
fprintf(stderr, "Trying to bind to a different port\n");
addr.sin_port = htons(ntohs(addr.sin_port) + 1);
}
printf("Listening on port %d...\n", ntohs(addr.sin_port));
if (listen(sock, 5) == -1) {
perror("listen failed");
exit(EXIT_FAILURE);
}
// 等待连接并处理...
close(sock);
return 0;
}
idea5:
idea4是在linux下进行识别空闲端口的一种方法,但是调研中还发现了一种很取巧的方式。
netstat -an | grep :xxxx
该shell命令是过滤出xxxx监听端口,如果xxxx端口没有被使用,则过滤出来的是空。C语言调用system函数执行该命令,如果端口被占用,则system的返回值为0,如果未被使用,则返回值为256,这样我们就可以根据system函数的返回值来确认端口有没有被使用。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT_START 5000
#define PORT_END 6000
int main()
{
int i_random_port[2] = {0};
time_t t;
char psz_port_cmd[BUFF_SIZE];
srand((unsigned)time(&t));
int i=0;
while(1)
{
i_random_port[i] = rand() % (PORT_END - PORT_START + 1) + PORT_START;
sprintf(psz_port_cmd, "netstat -an | grep :%d > /dev/null", i_random_port[i]);
printf("%d\n",system(psz_port_cmd));
if(i ==1 && system(psz_port_cmd) && i_random_port[1] != i_random_port[0])
{
break;
}
else if(system(psz_port_cmd) && i==0)
{
i++;
}
}
printf("port=%d\n", i_random_port[0]);
printf("port=%d\n", i_random_port[1]);
}
打印结果如下:
256
256
port=5708
port=5745
对比选择合适的方法
通过结合实际生产情况,可以对比idea4和idea5的优缺点。
idea | 优点 | 缺点 |
---|---|---|
4 | 兼容性更好,适合大部分的机器 | 识别一个端口就需要创建一个socket去循环bind()端口,效率较低 |
5 | 使用system()函数调用netstat命令,更方便快捷 | 兼容性较低,不是所有机器都有netstat命令 |
经过筛选,最终采用idea4,使用socket来解决识别空闲端口号的问题。
实际解决方法
为了解决多个端口的写死问题,采用封装识别空闲端口号的api,在具体代码中调用该api,可以保证每个端口号都是可以动态变化的。
方法测试
为了测试编写的代码能在实际中正常使用,就得确保如果端口号被占用,就会自动识别其他端口号。
采用测试的方法是使用“netstat -nlp | grep 端口号”来启动一个端口号50849,加一条printf()函数,打印make check的测试结果端口号50852被占用之后的输出结果。
先占用端口号50849
进行make check测试
可以发现,当50849端口被占用后,会自动识别到50859,端口占用问题被解决。