TCP/IP網路重複型伺服器通訊軟體設計

來源:互聯網
上載者:User
摘要:本文介紹一種新型的基於訊息佇列的重複型伺服器通訊軟體的設計方法,不同於並髮型伺服器和一般的重複型伺服器通訊軟體,這種新的軟體具有產生的子進 程數少的優點,並且容易對客戶機與伺服器的串連進行管理,適用於客戶機數量較多和隨機資料通訊的情況,能夠有效地提高伺服器的運行效率。

  關鍵詞:TCP/IP網路 重複型伺服器通訊軟體 通訊端 串連 共用記憶體 訊息佇列

  並發伺服器與重複伺服器的區別

   一般TCP/IP伺服器通訊軟體都是並髮型的,即是由一個守護進程負責監聽客戶機的串連請求,然後再由守護進程產生一個或多個子進程與客戶機具體建立連 接以完成通訊,其缺點是隨著串連的客戶機數量的增多,產生的通訊子進程數量會越來越多,在客戶機數量較多的應用場合勢必影響伺服器的運行效率。一般的重複 伺服器指的是伺服器在接收客戶機的串連請求後即與之建立串連,然後要在處理完與客戶機的通訊任務後才能再去接收另一客戶機的請求串連,其優點是不必產生通 信子進程,缺點是客戶機在每次通訊之前都要與伺服器建立串連,開銷過大,不能用於隨機的資料通訊和繁忙的業務處理。

  本文提出的新型的 重複型伺服器不同於一般的重複伺服器,它摒棄了上述兩類伺服器的缺點綜合其優點,該伺服器通訊軟體具有一般重複伺服器的特徵但又能處理客戶機的隨機訪問, 在客戶機數量多且業務繁忙的應用場合將發揮其優勢。重複型伺服器通訊軟體只用三個進程就可完成與所有客戶機建立串連,並始終保持這些串連。

  重複型伺服器通訊軟體與客戶機建立串連的方法

  基本思路

   當第一台客戶機向伺服器請求串連時,伺服器的守護進程與之建立初始串連(L0),客戶機利用L0向伺服器發送兩個連接埠號碼,守護進程將客戶機的IP地址和 連接埠號碼登記在共用記憶體的記錄中,然後關閉L0。由守護進程產生的兩個通訊子進程從共用記憶體中獲得客戶機IP地址及連接埠號碼後,分別向客戶機請求串連,建立一 個從客戶機讀的串連(L1)和一個往客戶機寫的串連(L2),並將兩個串連的通訊端的控制代碼記錄在共用記憶體中。當另一台客戶機請求串連時,守護進程不再產生 通訊子進程,只是將客戶機IP地址和連接埠號碼同樣登記在共用記憶體中。通訊子進程在一個大迴圈中先查詢共用記憶體中是否有新的記錄,如果有則與這一台客戶機建立 串連,然後輪詢所有已建立的串連的讀通訊端,查看是否有資料可讀,有則讀取資料,同時標明該資料是從共用記憶體中的哪條記錄上的讀通訊端中獲得的,再由另一 個通訊子進程根據這個記錄的編號從共用記憶體中獲得對應的寫通訊端,最後將結果資料往該通訊端寫往客戶機。

  建立串連

   ⑴ 伺服器通訊軟體的初始進程首先建立公用連接埠上的通訊端,並在該通訊端上建立監聽隊列,同時產生一個守護進程(Daemon)tcp_s,然後初始進程就退 出運行。守護進程在函數accept處堵塞住直到有客戶機的串連請求,一有串連請求即調用server函數處理,然後繼續迴圈等待另一台客戶機的請求。因 為TCP/IP在串連被拆除後為了避免出現重複串連的現象,一般是將串連放在過時串連表中,串連在拆除後若要避免處於TIME_WAIT狀態(過時連 接),可調用setsockopt設定通訊端的linger延時標誌,同時將延時時間設定為0。伺服器在/etc/services檔案中要登記一個全域 公認的公用連接埠號碼:tcp_server 2000/tcp。

struct servent *sp;
struct sockaddr_in peeraddr_in,myaddr_in;
linkf=0;
sp=getservbyname("tcp_server","tcp");
ls=socket(AF_INET,SOCK_STREAM,0); /* 建立監聽通訊端 */
myaddr_in.sin_addr.s_addr=INADDR_ANY;
myaddr_in.sin_port=sp->s_port; /* 公用連接埠號碼 */
bind(ls,&myaddr_in,sizeof(struct sockaddr_in));
listen(ls,5);
qid3=msgget(MSGKEY3,0x1ff); /* 獲得訊息佇列的標誌號 */
qid4=msgget(MSGKEY4,0x1ff);
signal(SIGCLD,SIG_IGN); /* 避免子進程在退出後變為僵死進程 */
addrlen=sizeof(struct sockaddr_in);
lingerlen=sizeof(struct linger);
linger.l_onoff=1;
linger.l_linger=0;
setpgrp();
switch(fork()){ /* 產生Daemon */
case -1:exit(1);
case 0: /* Daemon */
for(;;){
s=accept(ls,&peeraddr_in,&addrlen);
setsockopt(s,SOL_SOCKET,SO_LINGER,&linger,lingerlen);
server();
close(s);
}
default:
fprintf(stderr,"初始進程退出,由守護進程監聽客戶機的串連請求./n");
}

   ⑵ 客戶機以這樣的形式運行通訊程式tcp_c:tcp_c rhostname,rhostname為客戶機所要已連線的服務器主機名稱。客戶機上的/etc/services檔案中也要登記:tcp_server 2000/tcp,公用連接埠號碼2000要與伺服器一樣。

int qid1,qid2,s_c1,s_c2,cport1,cport2;
struct servent *sp;
struct hostent *hp;
memset((char *)&myaddr_in,0,sizeof(struct sockaddr_in));
memset((char *)&peeraddr_in,0,sizeof(struct sockaddr_in));
addrlen=sizeof(struct sockaddr_in);
sp=getservbyname("tcp_server","tcp");
hp=gethostbyname(argv[1]); /* 從/etc/hosts中擷取伺服器的IP地址 */
qid1=msgget(MSGKEY1,0x1ff);
qid2=msgget(MSGKEY2,0x1ff);
cport1=6000;
s=rresvport(&cport1);
peeraddr_in.sin_family=hp->h_addrtype;
bcopy(hp->h_addr_list[0],(caddr_t)&peeraddr_in.sin_addr,hp->h_length);
peeraddr_in.sin_port=sp->s_port;
connect(s,(struct sockaddr *)&peeraddr_in,sizeof(peeraddr_in));
cport1--;
s_c1=rresvport(&cport1);
cport2=cport1;
s_c2=rresvport(&cport2);
sprintf(cportstr,"%dx%d",cport1,cport2);
write(s,cportstr,strlen(cportstr)+1);
close(s);

   先給變數cport1置一個整數後調用rresvport函數,該函數先檢查連接埠號碼cport1是否已被佔用,如果已被佔用就減一再試,直到找到一個未 用的連接埠號碼,然後產生一個通訊端,將該通訊端與連接埠號碼相聯形成客戶機端的半相關,接下調用connect函數向伺服器發出串連請求。客戶機在發出串連請求 之前,已用函數gethostbyname和getservbyname獲得了伺服器的IP地址及其公用連接埠號碼,這樣就形成了一個完整的相關,可建立起與 伺服器的初始串連。接下來再建立兩個通訊端s_c1和s_c2,利用初始串連將客戶機的兩個通訊端的連接埠號碼以字串的形式發送給伺服器,這時初始串連的任 務已經完成就可將其關閉。以上就完成了與伺服器的初始串連,接下來客戶機等待伺服器的兩次串連請求。

  ⑶ tcp_s的監聽隊列在收到客戶機發來的串連請求後,由server函數讀出客戶機發送來的兩個連接埠號碼,並在第一次調用時產生兩個通訊子進程tcp_s1 和tcp_s2,以後就不再產生,這是與並發伺服器最大的不同。tcp_s進程將客戶機的兩個連接埠號碼和IP 位址以記錄的形式登記在共用記憶體最後一條記錄中,子進程通過共用記憶體獲得這兩個連接埠號碼,然後再分別與客戶機建立串連。tcp_s繼續處於監聽狀態,以便響 應其他客戶機的串連請求。兩個子進程都應該關閉從父進程繼承來的但又沒有使用的通訊端s。

server(){
int f;char c;
cport1=cport2=f=0;
for(;;){
read(s,&c,1);
if(c==0) break;
if(c=='x'){
f=1;continue;
}
if(f) cport2=(cport2*10)+(c-'0');
else cport1=(cport1*10)+(c-'0');
}
/* 在共用記憶體中登記客戶機連接埠號碼和IP地址 */
shm_login(cport1,cport2,peeraddr_in.sin_addr.s_addr);
if(linkf==0){ /* 只產生兩個子進程 */
if(fork()==0){ /* 子進程tcp_s2 */
close(s);Server_Send();
}else
if(fork()==0){ /* 子進程tcp_s1 */
close(s);Server_Receive();
}
}
linkf=1;
}

  共用記憶體的結構如下,通訊子進程tcp_s1從s_socket1讀,tcp_s2往對應的s_socket2寫。

struct s_linkinfo{
int id; /* 串連的標誌號,從1開始順序編號 */
int s_socket1; /* 伺服器的讀通訊端 */
int linkf1; /* 與客戶機的cport1串連標誌,0:未建立串連,1:已經串連 */
int cport1; /* 客戶機的第一個連接埠號碼 */
int s_socket2; /* 伺服器的寫通訊端 */
int linkf2; /* 與客戶機的cport2串連標誌 */
int cport2; /* 客戶機的第二個連接埠號碼 */
u_long client_addr; /* 客戶機IP地址 */
char flag; /* 共用記憶體佔用標誌,'i':已佔用,'o':未佔用 */
};

  ⑷ tcp_c用listen(s_c1,5)在通訊端s_c1上建立客戶機的第一個監聽隊列,等待伺服器的串連請求。在與伺服器建立第一個串連後,再用listen(s_c2,5)建立第二個監聽隊列,與伺服器建立第二個串連。

listen(s_c1,5);
s_w=accept(s_c1,&peeraddr_in,&addrlen);
close(s_c1); /*只允許接收一次串連請求*/
linger.l_onoff=1;linger.l_linger=0;
setsockopt(s_w,SOL_SOCKET,SO_LINGER,&linger,sizeof(struct linger));
listen(s_c2,5);
s_r=accept(s_c2,&peeraddr_in,&addrlen);
close(s_c2);
setsockopt(s_r,SOL_SOCKET,SO_LINGER,&linger,sizeof(struct linger));

   ⑸ 進程tcp_s1調用函數Server_Receive在一個迴圈中不斷查詢是否又有新的客戶機登記在共用記憶體中,方法是判斷共用記憶體中最後一條記錄的 linkf1標誌是否為0,如果為0就調函數connect_to_client與客戶機建立第一個串連,然後輪詢所有的讀通訊端,有資料則讀,沒有資料 則讀下一個讀通訊端。

Server_Receive(){
int s1,len,i,linkn,linkf1,n;
struct msg_buf *buf,mbuf;
buf=&mbuf;
for(;;){
linkn=shm_info(0,GETLINKN);
linkf1=shm_info(linkn,GETLINKF1);
if(linkf1==0){
if((i=connect_to_client(linkn,1))<0){
shm_logout(linkn);continue;
}
}
for(n=1;n<=linkn;n++){
s1=shm_info(n,GETS1);
i=read(s1,buf,MSGSIZE);
if(i==0){
fprintf(stderr,"A client exit!/n");
shutdown(s1,1);close(s1);
shm_logout(n);
linkn--;continue;
}
if(i==-1) continue;
buf->mtype=MSGTYPE;buf->sid=n;
len=strlen(buf->mdata);
fprintf(stderr,"mdata=%s/n",buf->mdata);
i=msgsnd(qid3,buf,len+BUFCTLSIZE+1,0);
}
}
}

   由於已將讀通訊端的讀取標誌設為O_NDELAY,所以沒有資料可讀時read函數就返回-1不會堵塞住。這樣我們才能接收到客戶機隨機的資料發送同時 也才能及時響應新的客戶機的串連請求,這是重複伺服器得以實現的關鍵所在。如果read函數返回0則表示客戶機通訊程式已退出或者別的原因,比如客戶機關 機或網路通訊故障等,此時就要從共用記憶體中清除相應客戶機的記錄。在建立串連時如果出現上述故障也要從共用記憶體中清除相應客戶機的記錄。在有資料可讀時就 將sid標誌設定為n,表示資料是從第n台客戶機讀取的,這樣子進程tcp_s2才可根據訊息的sid標誌往第n台客戶機寫資料。

  ⑹ 進程tcp_s2調用函數Server_Send,在一個迴圈中不斷查詢是否又有新的客戶機串連登記在共用記憶體中,方法是判斷共用記憶體中最後一條記錄的 linkf2標誌是否為0,如果為0就調用函數connect_to_client與客戶機建立第二個串連,然後再從訊息佇列中讀資料。因為只有一個 tcp_s2進程在讀訊息佇列,所以就不必對訊息進行區別,有資料則讀。再按照訊息的sid標誌從共用記憶體中查出寫通訊端,然後將資料往該通訊端寫。由於 該寫通訊端是在進程tcp_s2內建立的,所以只要簡單地使用通訊端的控制代碼即可訪問該通訊端。函數msgrcv要設定IPC_NOWAIT標誌以免在沒有 資料時堵塞住,這樣才能繼續執行下面的程式以便及時地與下一台客戶機建立串連,這也是一個關鍵的地方。tcp_s2調用函數Server_Send用於數 據發送,tcp_s1則調用函數Server_Recvice用於資料接收。

Server_Send(){
int s2,linkn,linkf2,i;
struct msg_buf *buf,mbuf;
buf=&mbuf;
for(;;){
linkn=shm_info(0,GETLINKN);
linkf2=shm_info(linkn,GETLINKF2);
if(linkf2==0){
if((i=connect_to_client(linkn,2))<0){
shm_logout(linkn);continue;
}
}
i=msgrcv(qid4,buf,MSGSIZE,MSGTYPE,0x1ff|IPC_NOWAIT);
if(i==-1) continue;
s2=shm_info(buf->sid,GETS2);
if(write(s2,buf,i+1)!=i+1){
perror("write");close(s2);
}
}
}

   函數connect_to_client(n,type)表示伺服器與第n台客戶機建立第type次串連。該函數由兩個子進程同時調用,分別從共用記憶體 中查出客戶機的IP地址和連接埠號碼後與客戶機建立串連,建立的串連分別處於各個子進程自己的資料空間中,彼此並不相通,所以又要用到共用記憶體,將串連的套接 字控制代碼登記在共用記憶體中,使得與同一台客戶機建立串連的兩個通訊端形成一一對應的關係。

  這樣tcp_s2才可根據資料讀入的通訊端去 查詢出對應的寫通訊端,才能正確地將處理結果發送給對應的客戶機。tcp_s1以type=1調用該函數,使用共用記憶體中第n條記錄的cport1和客戶 機IP地址與客戶機建立第一個串連,同時將這一串連伺服器方的通訊端(讀通訊端)登記在共用記憶體第n條記錄的s_socket1中,同時將串連標誌 linkf1置1。

  tcp_s2以type=2調用該函數,使用共用記憶體中第n條記錄的cport2和客戶機IP地址與客戶機建立第 二條串連,同樣也要將這一串連伺服器方的通訊端(寫通訊端)登記在共用記憶體第n條記錄的s_socket2中,將串連標誌linkf2置1。因為該函數由 兩個子進程同時調用,為了保持進程間同步,當type=2時必需等到第n條記錄的linkf1為1時才能繼續執行,即必須先建立第一個串連才能再建立第二 個串連,這是由客戶機通訊程式決定的,因為客戶機通訊程式是先監聽並建立起第一個串連後再監聽並建立第二個串連。子進程tcp_s1和tcp_s2通過共 享記憶體實現處理序間通訊,在實際應用中總是使用共用記憶體的最後一條記錄。

②:(5991,5990,168.1.1.71) ┌─────┐①:(5991,5990) 168.1.1.21
┌─────────────┤ 守護進程 ├←─────────┐┌─────┐
│ │ tcp_s │ 初始串連L0 ││ Client 1 │
│ 共用記憶體 └─────┘ │├──┬──┤
│ id s1 linkf1 cport1 s2 linkf2 cport2 IP_Address flag ││5999│5998│
│ ┌─┬──┬──┬──┬──┬──┬──┬─────┬─┐│└──┴──┘
│ │1 │ 12 │ 1 │5999│ 13 │ 1 │5998│168.1.1.21│i ││ 168.1.1.22
│ ├─┼──┼──┼──┼──┼──┼──┼─────┼─┤│┌─────┐
│ │2 │ 14 │ 1 │5995│ 17 │ 1 │5994│168.1.1.22│i │││ Clinet 2 │
│ ├─┼──┼──┼──┼──┼──┼──┼─────┼─┤│├──┬──┤
└→┤3 │0/22│0/1 │5991│0/23│0/1 │5990│168.1.1.71│i │││5995│5994│
└─┴──┼──┴┬─┴──┼──┴┬─┴─────┴─┘│└──┴──┘
⑤:(22,1)↑ │ ↑ ↓⑥:(5990,168.1.1.71)│ 168.1.1.71
│ │ │ └─────┐ │┌─────┐
│ │ │⑧:(23,1) ┌──┴┬─┐ └┤ Client 3 │
│ │ └──────┤ │13│ ├──┬──┤
│ ↓③:(5991,168.1.1.71) │通訊 ├─┤ │5991│5990│
│┌──┴┬─┐ │子進程│17│ └┬─┴─┬┘
└┤ │12│ │tcp_s2├─┤ │ L2↑⑦
│通訊 ├─┤ │ │23├───┼───┘
│子進程│14│ └───┴─┘ │
│tcp_s1├─┤L1 (讀通訊端22) (寫通訊端23) │
│ │22├←─────────────────┘
└───┴─┘④

圖1 伺服器和客戶機建立串連的過程

  這裡必須置通訊端的讀取標誌位O_NDELAY,這樣在讀資料時如果沒有資料可讀read函數就不會堵塞住,這是重複型伺服器能夠實現的關鍵。因為UNIX系統將通訊端與普通檔案等同處理,所以就能夠使用設定檔案標誌的函數fcntl來處理通訊端。

int connect_to_client(n,type){
u_long client_addr; /* type=1,2 */
int s2,cport,sport,i;
if(type==2){
for(;;) if(shm_info(n,GETLINKF1)==1) break;
}
sport=6000-1;s2=rresvport(&sport);
cport=shm_info(n,GETCPORT1+type-1);
client_addr=shm_info(n,GETCADDR);
peeraddr_in.sin_port=htons((short)cport);
peeraddr_in.sin_addr.s_addr=client_addr;
connect(s2,(struct sockaddr *)&peeraddr_in,sizeof(peeraddr_in));
flags=fcntl(s2,F_GETFL,0);
fcntl(s2,F_SETFL,flags|O_NDELAY);
if(type==1) i=shm_update(n,s2,0,1,0);
if(type==2) i=shm_update(n,0,s2,0,1);
return(i);
}

   ⑺ tcp_c在接收到伺服器的兩個串連後,產生子進程tcp_c1調用函數Client_Receive用於接收資料,tcp_c則調用函數 Client_Send用於發送資料。如果函數Client_Receive從迴圈中退出,就說明伺服器通訊軟體已退出,於是子進程在退出之前要先殺掉父 進程。

cpid=getpid(); /* 父進程的進程號 */
if(fork()==0){ /* tcp_c1 */
close(s_w);
Client_Receive();
sprintf(cmdline,"kill -9 %d",cpid);
system(cmdline);
}else{
close(s_r);
Client_Send();
}

  客戶機伺服器接收和發送資料的方法

  資料的傳送過程

硬體劃分:

├←─── 伺服器 ───→┼← 網路 →┼←── 客戶機 ──→┤
┌──┐⑥┌──┐⑦┌──┐
┌→┤qid4├→┤ L2 ├→┤qid2├─┐
⑤│ └──┘ └──┘ └──┘ ↓⑧
┌──┐ ┌──┴──┐ ┌──→ ┌──┴──┐ ┌────┐
│ DB ├←→┤s_process │ │ │c_process ├←→┤終端使用者│
└──┘ └──┬──┘ └─── └──┬──┘ └────┘
④↑ ┌──┐ ┌──┐ ┌──┐ │①
└─┤qid3├←┤ L1 ├←┤qid1├←┘
軟體劃分: └──┘③└──┘②└──┘
├←─ s_process ──→┼←tcp_s→┼←tcp_c→┼← c_process →┤

圖2 資料在客戶機伺服器之間傳遞的全過程

  其中s_process和c_process是分別運行在伺服器上的伺服器業務程式和運行在客戶機上的客戶業務進程。qid3,qid4和qid1,qid2是分別存在於伺服器及客戶機上的訊息佇列。

  tcp_s和tcp_c是分別運行在伺服器和客戶機上的通訊軟體。在客戶機和伺服器之間建立的兩條串連是L1和L2,其中L1專用於客戶機至伺服器,L2專用於伺服器至客戶機。

  下面敘述圖2中所示的資料傳遞過程,同時介紹用於資料接收和發送的四個函數。因為業務程式不知何時可以接收或發送訊息,所以這四個函數都存在一個迴圈不斷地試圖接收或發送資料。表示訊息的資料結構是sg_buf,訊息由訊息類別mtype及本文段mdata組成。

   本文段中存放的資料是無結構的,必須定義一種資料結構(struct),用結構中的各變數對mdata進行劃分,從而使mdata中的資料可以被理解和 使用。還可將mdata前面的一部分地區划出來重新命名用作其他用途。訊息在整個資料傳遞的過程中起類似“載體”的作用。

#define MSGSIZE 200
struct msg_buf{
long mtype; /* 訊息類別 */
long cpid; /* 客戶業務進程標識號 */
long sid; /* 共用記憶體記錄編號 */
long msgid; /* 訊息編號 */
char mdata[MSGSIZE-16]; /* 資料區 */
}

  ① 客戶業務程式c_process從終端使用者接收資料,先存放在一個結構中,然後將該結構的內容依照一定的格式拷入buf->mdata中,然後將buf以訊息的形式放入訊息佇列qid1中。

pidc=getpid();/* c_process的進程號 */
buf->mtype=1; /* 訊息類別都為1 */
buf->sid=0; /* sid在客戶機沒用 */
buf->msgid=++msgid;
buf->cpid=pidc;
msgsnd(qid1,buf,MSGSIZE,0);

  ② 進程tcp_c調用函數Client_Send從qid1中取得訊息,然後往L1寫給伺服器。從qid1中取訊息時對訊息並不予於區別,凡在qid1中的訊息都要由進程tcp_c來發送。

for(;;){ /* 取mtype=1的訊息 */
msgrcv(qid1,buf,MSGSIZE,1,0);
write(s_w,buf,i+1);
}

  ③ 進程tcp_s1調用函數Server_Receive從L1讀資料至buf中,將buf作為訊息放入qid3中。

for(n=1;n<=linkn;n++){
s1=shm_info(n,GETS1);
i=read(s1,buf,MSGSIZE);
if(i==-1) continue;
if(i==0) ... /* 判斷出客戶機已退出 */
/* n是s1在共用記憶體登記項的編號 */
buf->sid=n;
msgsnd(qid3,buf,MSGSIZE,0);
}

   ④ 伺服器業務程式s_process從訊息佇列qid3中接收訊息到buf,然後將buf->mdata轉成結構,根據結構的內容對資料庫進行操作。 s_process處在一個迴圈中,一有訊息就取走去作訊息所要求的操作,對訊息並不加以區別。如果沒有訊息函數msgrcv就處於堵塞狀態。

  ⑤ s_process根據訊息的內容訪問資料庫後將結果放在一個結構中,然後將該結構的內容拷到buf->mdata中,再將緩衝區buf以訊息的形式放於訊息佇列qid4中,最後s_process又要繼續迴圈再去接收新的訊息。

for(;;){
msgrcv(qid3,buf,MSGSIZE,1,0);
... ...
/* 解釋buf->mdata的內容,對資料庫進行操作後再將結果存放在buf->mdata中 */
buf->mtype=1;
msgsnd(qid4,buf,MSGSIZE,0);
}

  ⑥ 進程tcp_s2調用Server_Send從qid4中取走mtype=1的第一個訊息,往L2寫回客戶機。

for(;;){
i=msgrcv(qid4,buf,MSGSIZE,1,0);
if(i==-1) continue;
s2=shm_info(buf->sid,GETS2);
write(s2,buf,i+1);
}

   ⑦ 進程tcp_c1調用函數Client_Receive從L2讀資料到buf中,將buf作為訊息放入qid2中。如果函數read返回0則表示伺服器通 信程式已經退出,於是就中斷迴圈。這裡必須將訊息的類別mtype設定為客戶業務進程的進程號cpid,便於客戶業務程式識別。

for(;;){
i=read(s_r,buf,MSGSIZE);
if(i==0){
close(s_r);return(1);
}
buf->mtype=buf->cpid;
msgsnd(qid2,buf,i+1,0);
}

   ⑧ 客戶業務程式c_process從訊息佇列qid2中取走mtype=pidc(自身進程號)的第一個訊息放入緩衝區buf中,再將buf-> mdata中的資料劃分為結構,對該結構作處理後將最終結果顯示給使用者。 在①中c_process將資料發出後要在什麼時候到qid2中去拿結果呢? 方法是一就訊息發送出去後客戶業務程式馬上就到qid2中去拿結果,若沒有給自己的訊息則堵塞住直到訊息到來。這裡程式設計成在堵塞20秒後發出時鐘警 報,調用函數overtime作出逾時反應。當時鐘警報時如果函數msgrcv正處於堵塞狀態也會退出並返回-1。

  這裡就又存在一個 問題,c_process在發送一個新訊息後可能先接收到上一個因逾時而未能被接收到的訊息,解決這一問題最簡單的方法就是發送訊息之前給每個訊息編號, 如果接收到的訊息的編號與發送的訊息的編號不同則將訊息從訊息佇列中刪除,或者將訊息取出後放在某一地方另行處理,然後繼續等待接收正確編號的訊息。刪除 訊息的方法很簡單,只要從訊息佇列中將訊息取出就可以了。如果進程c_process被殺則遲到的訊息由於其mtype表示的c_process已經不在 運行,所以將會始終存在於訊息佇列中,直到客戶機關機,因此在必要時也要對這些無主的訊息作善後處理。

alarm(20);
signal(SIGALRM,overtime);
for(;;){
i=msgrcv(qid2,buf,MSGSIZE,pidc,0);
if(i==-1) break;
if(buf->msgid==msgid) break;
}
alarm(0);
printf("%s/n",buf->mdata);

overtime(int sig){
strcpy(buf->mdata,"overtime");
}

  兩個關鍵問題的解決方案

   通常一台伺服器要串連多台客戶機,而每台客戶機由於支援多使用者方式就會同時運行多個c_process進程。伺服器如何準確地將訊息送給哪一台客戶機? 另外一台客戶機上啟動並執行每一個c_process進程如何正確地擷取發送給自己的訊息? 這是兩個關鍵的問題。 第一個問題在前面已經講述過,主要是通過訊息的sid標誌來區別的。第二個問題是這樣解決,在第①步時c_process進程先將自身的進程號pidc放 在buf->cpid中,該值在以後的傳輸過程中保持不變,在第⑦步再將cpid賦值給訊息類別mtype。這樣在第⑧時c_process進程就 從訊息佇列qid2中取走訊息類別mtype等於其自身進程號pidc的訊息,而不會錯將送給同一客戶機別的c_process進程的訊息拿走。(圖3)

┌──────────────┐ ┌────────────┐
│Server ┌───┤ ├───┐ ┌─────┐│
│ │tcp_s │ ┌────┤tcp_c ├┐│c_process2││
│ ┌─────┐ └─┬─┤ │ ├───┤│└─────┘│
│ │s_process │┌───┴┐│ │ ┌─→┤tcp_c1││┌─────┐│
│ │服務程式 ││共用記憶體││ │ │ L2├─┬─┘││c_process1││
│ └─┬─┬─┘└───┬┘│ │ │ │ ↓⑦ │└───┬┬┘│
│ ⑤↓ ↑④ ┌─┴─┤L1 │ │ │ │ └─┐ │↑⑧│
│┌──┘ │ ┌─┤tcp_s1├←──┘ │ │ │ ②↑ ││ │
││┌──┬┼┐③│ │ ├←┐L1' │ │ │┌──┬┼┐①││ │
│││qid3│ ├←┘ ├───┤ │ │ │ ││qid1│ ├←┘│ │
││├──┼─┤ ┌┤tcp_s2├─┼───┘ │ │├──┼─┤ │ │
│││qid4│ ┼→─┘│ ├┐│┌────┐│ ││qid2│ ┼──┘ │
││└──┴┬┘⑥ └───┤│└┤ ││ │└──┴┬┘ │
│└────┘ │└→┤Client2 ││ └────┘ Client1 │
└──────────────┘ L2'└────┘└────────────┘

圖3 訊息在伺服器和客戶機內傳送的過程

  訊息佇列與共用記憶體

   在運行伺服器通訊軟體之前應先建立共用記憶體和訊息佇列,建立共用記憶體的方法見文獻[3]。本文共用到四個共用記憶體操作函數:shm_login (cport1,cport2,client_addr)在共用記憶體中申請一條記錄將三個參數登記其中,並將flag標誌設為'i'表示已經佔用,同時根 據記錄的位置賦值給記錄編號id。shm_logout(id)將共用記憶體中第id條記錄刪除,並將後面的記錄前移,重新計算各條記錄的編號。 shm_info(id,type)根據type查詢第id條記錄的內容,比如type為GETS1時表示要查詢s_socket1的值,當type等於 GETLINKN時統計共用記憶體的記錄總數。shm_update(id,s_socket1,s_socket2,linkf1,linkf2)修改第 id條記錄的內容,如果某個參數為零則不修改這個參數,如shm_update(n,s2,0,1,0)只修改s_socket1和linkf1的值,其 餘內容不作修改。在業務繁忙的情況下,有必要擴大訊息佇列的儲存容量,下面的例子將訊息佇列qid3的容量擴大兩倍。

struct msqid_ds sbuf1,*sbuf;int qid3;
sbuf=&sbuf1;
qid3=msgget(MSGKEY3,02000);
msgctl(qid1,IPC_STAT,sbuf);
sbuf->msg_qbytes*=2;
msgctl(qid3,IPC_SET,sbuf);

  其他問題的討論

   由於將伺服器與客戶機的串連登記在共用記憶體中,所以可以控制伺服器與客戶機的串連次數,在伺服器接收到客戶機的串連請求後可以先查詢共用記憶體,如果與同 一台客戶機建立的串連次數已達到限定的數量時,伺服器的守護進程就可以關閉掉已與客戶機建立起來的初始串連,同時不再將客戶機的連接埠號碼和IP地址登記在共 享記憶體中,這樣子進程也將不會再與客戶機建立串連了。

  另外這種重複型伺服器通訊軟體使用一個唯讀通訊端和一個唯寫的通訊端,由於一 個通訊端都有獨立的讀緩衝區和寫緩衝區,長度都是24k。於是唯讀通訊端就不會用到寫緩衝區,唯寫的通訊端就不會用到讀緩衝區,為了節省系統資源有必要 將通訊端設定成只有一個緩衝區,比如將唯讀通訊端的寫緩衝區長度設定為0。

int i,bufsize;
i=sizeof(int);
getsockopt(ls,SOL_SOCKET,SO_SNDBUF,&bufsize,&i);
fprintf(stderr,"size=%d/n",bufsize);
bufsize=0;
setsockopt(ls,SOL_SOCKET,SO_SNDBUF,&bufsize,i);
getsockopt(ls,SOL_SOCKET,SO_SNDBUF,&bufsize,&i);
fprintf(stderr,"size=%d/n",bufsize);

   在圖2所示的僅是應用模式中的一種,本文提到的重複型伺服器通訊軟體還可用於更複雜的情況。比如當客戶機要與另一台客戶機通訊時就可用伺服器作為中轉 站,從而不必在客戶機之間建立串連。 比如通訊子進程tcp_s1查詢出目的客戶機登記在共用記憶體第x條記錄中,就將接收到的訊息的sid置為x,這樣子進程tcp_s2就可將訊息送往第x台 客戶機,當然源客戶機在發送的訊息中應指明目的客戶機的IP地址。這在客戶機之間通訊並不頻繁的情況下很有用,因為這樣就可減少所有的客戶機都要相互建立 串連的系統開銷,有利於提高整個網路的運行效率。在某種特定的應用場合伺服器在收到客戶機的服務要求後,但因某種原因暫不能處理,於是就將訊息存放起來, 要等到條件成熟時伺服器才能處理客戶請求並將結果返回給客戶機,此時客戶機就不能認為這也是一個遲到的訊息,應另行處理。
 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.