韌館-LearnHouse

[C/Unix]實作伺服器連線備援機制

其實紀錄這篇文章,我本身就有點怕怕的,怕我把程式的錯誤觀念帶給其他人

身為寫程式的人,我除了大學有修過程式課,買過上課要用的書,就再也沒買過程式相關的書籍

這三年來,寫程式可以說都是從網路上看過很多的code自習出來的

但就是如此,讓我發現在寫新一代語言(Java、Android、C#)的時候,網路上不乏參考的文章

而遇到C/C++或早期的WindowsMobile資料就顯得很少,甚至幾乎都要從大陸那篇才能得到想要的資訊

秉著吃水果拜樹頭,既然我是從網路學寫程式,我也分享一些我的經驗,但也許觀念會有錯,所以有錯的地方還請前輩們指教。

有時候Main Server可能會掛掉,所以通常會有一台Backup Server,在Man Server連線不上時,可以更換成連線到Backup Server

但也有可能連Backup Server也會掛點,所以程式最好能在Main Server與Backup Server互換,嘗試連線一段時間後,再發出連線TimeOut

以下的程式碼就是這個理念,我是在IBM的AIX撰寫以下程式:

bool isConnected = false;//是否與Server連接上
long startTime = 0;//紀錄發起連線時間
int SockCount = 0;//嘗試連線次數
int ret = 0;
struct timeval checktv;	//設定輪詢timeout時間

struct sockaddr_in SockAddr;
struct in_addr TCPAddr;
int            TCPPort;
TCPAddr.s_addr = inet_addr("192.168.1.100"); //Main Server
TCPPort        = atoi("1234");

SockAddr.sin_family    = AF_INET;
SockAddr.sin_port   = TCPPort;
SockAddr.sin_addr   = TCPAddr;

checktv.tv_sec  = 2;//輪詢時間2秒
checktv.tv_usec = 0;
startTime = ITime().asSeconds();//紀錄開始連線時間

bSocket = socket(AF_INET, SOCK_STREAM, 0);

while( isConnected )//無法連線return -1
{
	fcntl(bSocket, F_SETFL, O_NONBLOCK);//設定Socket成non-blocking
	ret = connect( bSocket, ( struct sockaddr * )&SockAddr, sizeof( SockAddr ) );
	fd_set writefds;
	FD_ZERO( &writefds );
	FD_SET( bSocket, &writefds );

	if(select( bSocket + 1, NULL, &writefds, NULL, &checktv ) <= 0)
	{
		SockCount++;    //嘗試次數+1
		if(getSockCount == 5)//嘗試連線失敗五次後換Backup Server
		{
			TCPAddr.s_addr = inet_addr("192.168.1.200");
			TCPPort        = atoi("1234");

			SockAddr.sin_family    = AF_INET;
			SockAddr.sin_port   = TCPPort;
			SockAddr.sin_addr   = TCPAddr;

			bSocket = socket(AF_INET, SOCK_STREAM, 0);
		}

		if(getSockCount == 10)//再嘗試連線失敗五次又換回Main Server
		{
			TCPAddr.s_addr = inet_addr("192.168.1.100");
			TCPPort        = atoi("1234");

			SockAddr.sin_family    = AF_INET;
			SockAddr.sin_port   = TCPPort;
			SockAddr.sin_addr   = TCPAddr;

			bSocket = socket(AF_INET, SOCK_STREAM, 0);
			getSockCount = 0;//重設計數器
		}

		if( (ITime().asSeconds() - startTime) >= 40 )//設定最大嘗試連線時間
		{
			close( bSocket );
			break;
		}
		continue;
	}
	isConnected = true;//成功連結到Server
}
以下省略.......

以上程式要特別注意的是,connect屬於blocking的function,在呼叫後如果欲連結的Server連不上
程式會block住一直等待Server回應,大約40~60秒才會回傳-1,確定連線失敗

所以如果要Main與Backup Server切換,就需把連線改成non-blocking,因此要加入fcntl(bSocket, F_SETFL, O_NONBLOCK)

而要注意的是,在Unix上是fcntl,ioctlsocket是WinSocket的函式,我一開始就傻傻的呼叫ioctlsocket = ="

另外要達成這樣的效果,最大的功臣非select莫屬了。那什麼是select呢?

他可以用來幫你檢查一整組(set)的物件是否有可以讀或寫的資料,可用在IO的輪詢上。若實作在網路上,就可以用來檢查 socket 是否已成功連線、接收到資料或對方是否已經關閉socket。

它的函式原型如下:

int PASCAL FAR select (
 IN int nfds,
 IN OUT fd_set FAR *readfds,
 IN OUT fd_set FAR *writefds,
 IN OUT fd_set FAR *exceptfds,
 IN const struct timeval FAR *timeout);

nfds 要被檢查的 Sockets
readfds 檢查Sockets是否有資料可讀
writefds 檢查Sockets是否資料可寫
exceptfds 檢查Sockets是否錯誤
timeout 檢查Sockets等待的時間

成功的話會回傳符合條件的Socket總數,若Timeout發生會回傳0,失敗的話會回傳-1。(在Windows的環境可以透過呼叫WSAGetLastError()得知SOCKET_ERROR原因)

再來,由於參數readfds、writefds與exceptfds都是傳參考呼叫(called by reference)

是將參數的記憶體位址告訴select函式,而select函式則會利用這些值來做運算,最後並將結果再寫回這些參數的位址中

所以這些參數的值在傳入前和函式回返後,可能會不同,因此每次呼叫select前,對這些參數一定要重新設定它們的值。

FD_ZERO(*set) 清空集合(readfds、writefds、exceptfds)的值
FD_SET(s, *set) 將Socket加到集合中
FD_CLR(s, *set) 將Socket從集合中刪除
FD_ISSET(s, *set) 檢查Socket是否存在於集合中

select最後一個參數就是設定每次檢查的timeout時間

struct timeval checktv; 宣告時間的結構
checktv.tv_sec = 0;    等待秒數
checktv.tv_usec = 500000; 等待微秒數,注意是微秒喔!!!
若將此結構放入select參數,則會等待500毫秒才會發生timeout返回。

另外如果用來做輪詢(polling),在Windows上使用時要特別注意,若checktv.tv_sec與checktv.tv_usec都設置為0,就如同while(1)無窮迴圈

由於UNIX系統的設計與Windows不同,屬於Tims Sharing的方式,而在Windows的系統上可能不會呼叫到Blocking Hook函式來釋放控制權,造成系統停住

最後要注意的是,呼叫select集合要放對參數,要做connect輪詢要放writefds參數的位置(第三個參數),要做recv接收封包檢查要放readfds(第二個參數)。

放錯位置效果可是意義與效果完全不同,這也是我白痴的經驗.............

2012年2 月 posted by admin in 程式&軟體 and have No Comments

Place your comment

Please fill your data and comment below.
名稱:
信箱:
網站:
您的評論: