1. 程式人生 > >linux下tcp socket的SO_REUSEPORT和SO_REUSEADDR

linux下tcp socket的SO_REUSEPORT和SO_REUSEADDR

SO_REUSEADDR

linux下多個tcp socket不能同時bind到一個ip:port上,但是可以bind到不同的ip相同的port上,前提是都要設定SO_REUSEADDR選項為true。否則會返回address already in use。

比如bind(127.0.0.1:80) bind(192.168.0.10:80) bind(10.0.0.12:80)這取決於有多少個網路介面,客戶端connect的時候選擇不同的ip和埠連線就可以了。

如果試圖bind到同一個ip:port上,也必須是在前一個socket關閉,連線處於time_wait狀態的時候。這種情況下大都是為了服務程式能夠快速的重新啟動,避免等待。

實際測試結果,假設我們本地是有2個ip地址127.0.0.1和10.0.0.1,服務埠為80,INADDR_ANY=0.0.0.0

如果設定了SO_REUSEADDR為1

1,先bind(0.0.0.0)成功,如果再bind(127.0.0.1)和bind(10.0.0.1)都會失敗

2,先bind(127.0.0.1)成功,如果再bind(0.0.0.0)會失敗,但bind(10.0.0.1)會成功

3,先bind(10.0.0.1)成功,如果再bind(0.0.0.0)會失敗,但bind(127.0.0.1)會成功

4,先bind(x.x.x.x)成功,如果再bind(x.x.x.x)肯定失敗

SO_REUSEPORT

linux下多個tcp socket可以同時bind到一個ip:port上,前提是都要設定SO_REUSEPORT選項為true。否則會返回address already in use。

當客戶端connect這個ip:port的時候,實際上只有一個bind的socket是有效的,其他bind的socket是無法收到connect資料的。這個socket貌似也是系統隨機挑選的,沒有嚴格順序。

實際測試結果,假設我們本地是有2個ip地址127.0.0.1和10.0.0.1,服務埠為80,INADDR_ANY=0.0.0.0

如果設定了SO_REUSEPORT為1

1,先bind(x.x.x.x)成功,如果再bind不同的ip都會成功

只是在資料的流向上會優先流向顯示指定ip地址的socket。

比如bind(0.0.0.0)和bind(10.0.0.1)之後,

如果客戶端connect(10.0.0.1),那就是第二個socket來接收資料。如果第二個socket關閉了,那則改為第一個socket來接收連線。

如果客戶端connect(127.0.0.1),呢就是第一個socket接收資料。

2,如果bind兩個相同的ip和port,那系統會根據srcip srcport dstip dstport做一個雜湊,分配到指定的cpu核心和一個指定的socket來處理。

總結:SO_REUSEADDR是一個排他性的標記,一旦某個地址和某個socket繫結,並設定了該標誌,那其他任意的socket都不能和該地址bind,包括其子地址。

一旦某個地址被bind成功了,使用該標記再bind該地址及其子地址也不會成功。唯一的例外是time_wait狀態下,可以用同一地址成功bind。

SO_REUSEPORT則是一個包容性的標記,只要打上該標記,就可以隨意的將socket和相同的或者不同的地址進行bind。然後系統會隨機分配連線到這些socket。

那linux下想利用埠複用實現轉發以及穿越防火牆的實現場景就和windows下不太一樣了。

如果應用服務程式,bind的是INADDR_ANY,切設定了SO_REUSEADDR標記,那基本上在socket層面沒法再bind該埠並接收資料了。即使先stop掉服務程式,自己先bind該埠,再啟動服務程式,服務程式也會bind失敗,進而轉發沒法實現。除非能把服務bind到localhost上,然後第三方程式bind到對外介面上,實現轉發。

如果應用服務程式,設定了SO_REUSEPORT標記,那第三方程式也可以bind該埠,這樣第三方程式就可以接收到部分連線資料並轉發給服務程式。但問題是,第三方程式沒法完全接收所有連線資料,這樣就還是一個很受限制的轉發了。


測試程式碼:

#ifdef WIN32
#include <WinSock2.h>
#include <WS2tcpip.h>
#define SO_REUSEPORT 0  // SO_REUSEPORT is not impl under windows
#pragma comment(lib, "ws2_32.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
// g++ this.cpp -std=c++11 -lpthread -o test
#endif

#include <string.h>
#include <iostream>
#include <thread>
#include <string>
#include <sstream>

struct SocketTool
{
	SocketTool()
	{
#ifdef WIN32
		WORD wVersionRequested;
		WSADATA wsaData;
		wVersionRequested = MAKEWORD(2, 2);
		WSAStartup(wVersionRequested, &wsaData);
#endif
	}

	template <typename SOCKET_TYPE>
	static int CloseSocket(SOCKET_TYPE skt)
	{
#ifdef WIN32
		return closesocket(skt);
#else
		return close(skt);
#endif
	}

	static sockaddr_in GenInAddr(const char *szIP, unsigned uPort)
	{
		sockaddr_in addr;
		memset(&addr, 0, sizeof(addr));
		addr.sin_family = AF_INET;
		addr.sin_port = htons(uPort);
#ifdef WIN32
		inet_pton(AF_INET, szIP, &addr.sin_addr);
#else
		inet_aton(szIP, &addr.sin_addr);
#endif
		return addr;
	}

	static std::string PrintInAddr(const sockaddr_in& addr)
	{
		char buf[128] = { 0 };
		inet_ntop(AF_INET, (void*)&addr.sin_addr, buf, sizeof(buf));
		auto port = ntohs(addr.sin_port);
		std::stringstream ss;
		ss << buf << ":" << port;
		return ss.str();
	}


	~SocketTool()
	{
#ifdef WIN32
		WSACleanup();
#endif
	}
};

void TestClient(const char *ip, unsigned port)
{
	SocketTool sktTool;
	bool bCotinue = true;
	auto thread = [&bCotinue, ip, port](){
		auto s = socket(AF_INET, SOCK_STREAM, 0);
		auto addr = SocketTool::GenInAddr(ip, port);
		auto ret = connect(s, (sockaddr*)&addr, sizeof(addr));
		if (ret != 0)
		{
			std::stringstream ss;
			ss << "connect " << ip << ":" << port << "failed";
			std::cout << ss.str() << std::endl;
		}
		std::stringstream ss;
		ss << "this is send from thread " << std::this_thread::get_id();
		std::string strSend = ss.str();
		do
		{
			auto r = send(s, strSend.c_str(), strSend.length(), 0);
			if (r <= 0)
			{
				std::stringstream ss;
				ss << "send " << ip << ":" << port << "failed";
				std::cout << ss.str() << std::endl;
				break;
			}
			std::this_thread::sleep_for(std::chrono::seconds(3));
			std::cout << strSend << std::endl;
		} while (bCotinue);
		SocketTool::CloseSocket(s);
	};

	for (auto i = 0; i < 5; ++i)
	{
		std::thread t(thread);
		t.detach();
	}
	std::thread t(thread);
	t.join();
}

void TestServ(const char *ip, unsigned port, unsigned reuseflag)
{
	SocketTool sktTool;
	auto serv = socket(AF_INET, SOCK_STREAM, 0);

	if (reuseflag != 0)
	{
		int reuse = 1;
		if (setsockopt(serv, SOL_SOCKET, reuseflag, (char *)&reuse, sizeof(reuse)) == -1)
		{
			std::cout << "setsockopt " << reuseflag << "failed" << std::endl;
			return;
		}
	}
	auto addr = SocketTool::GenInAddr(ip, port);
	if (bind(serv, (sockaddr*)&addr, sizeof(sockaddr)) == -1)
	{
		std::cout << "bind failed" << std::endl;
		return;
	}
	if (listen(serv, 5) == -1)
	{
		std::cout << "listen failed" << std::endl;
		return;
	}

	while (true)
	{
		sockaddr_in client_addr;
		socklen_t length = sizeof(client_addr);
		auto con = accept(serv, (struct sockaddr*)&client_addr, &length);
		if (con < 0)
		{
			std::cout << "accept failed!\n";
		}
		else
		{
			auto strClient = SocketTool::PrintInAddr(client_addr);
			std::cout << "accept from " << strClient << std::endl;
			std::thread procThread([con, strClient](){
				char buf[1024] = { 0 };
				do
				{
					int ret = recv(con, buf, 1024, 0);
					if (ret <= 0)
					{
						std::cout << strClient << " closed!" << std::endl;
						SocketTool::CloseSocket(con);
						break;
					}
					else
					{
						buf[ret] = 0;
						std::cout << strClient << ":" << buf << std::endl;
					}
				} while (true);
			});
			procThread.detach();
		}
	}
	SocketTool::CloseSocket(serv);
}

int main(int argc, char **argv)
{
	if (argc != 4)
	{
		std::cout << "usage: socket c|p|a ip port\n" 
			<< "c=client(connect(ip:port))\np=SO_REUSEPORT(not impl in windows)\na=SO_REUSEPORT" << std::endl;
		return 0;
	}
	char *ip = argv[2];
	int port = atoi(argv[3]);

	if (argv[1][0] == 'c')
	{
		TestClient(ip, port);
	}
	else if (argv[1][0] == 'p')
	{
		TestServ(ip, port, SO_REUSEPORT);
	}
	else if (argv[1][0] == 'a')
	{
		TestServ(ip, port, SO_REUSEADDR);
	}
	else
	{
		TestServ(ip, port, 0);
	}
	return 0;
}