一般TCP/IP服務器通信軟件都是并發型的,即是由一個守護進程負責監聽客戶機的連接請求,然后再由守護進程生成一個或多個子進程與客戶機具體建立連接以完成通信,其缺點是隨著連接的客戶機數量的增多,生成的通信子進程數量會越來越多,在客戶機數量較多的應用場合勢必影響服務器的運行效率。一般的重復服務器指的是服務器在接收客戶機的連接請求后即與之建立連接,然后要在處理完與客戶機的通信任務后才能再去接收另一客戶機的請求連接,其優點是不必生成通信子進程,缺點是客戶機在每次通信之前都要與服務器建立連接,開銷過大,不能用于隨機的數據通信和繁忙的業務處理。
本文提出的新型的重復型服務器不同于一般的重復服務器,它摒棄了上述兩類服務器的缺點綜合其優點,該服務器通信軟件具有一般重復服務器的特征但又能處理客戶機的隨機訪問,在客戶機數量多且業務繁忙的應用場合將發揮其優勢。重復型服務器通信軟件只用三個進程就可完成與所有客戶機建立連接,并始終保持這些連接。
重復型服務器通信軟件與客戶機建立連接的方法
基本思路
當第一臺客戶機向服務器請求連接時,服務器的守護進程與之建立初始連接(L0),客戶機利用L0向服務器發送兩個端口號,守護進程將客戶機的IP地址和端口號登記在共享內存的記錄中,然后關閉L0。由守護進程生成的兩個通信子進程從共享內存中獲得客戶機IP地址及端口號后,分別向客戶機請求連接,建立一個從客戶機讀的連接(L1)和一個往客戶機寫的連接(L2),并將兩個連接的套接字的句柄記錄在共享內存中。當另一臺客戶機請求連接時,守護進程不再生成通信子進程,只是將客戶機IP地址和端口號同樣登記在共享內存中。通信子進程在一個大循環中先查詢共享內存中是否有新的記錄,如果有則與這一臺客戶機建立連接,然后輪詢所有已建立的連接的讀套接字,查看是否有數據可讀,有則讀取數據,同時標明該數據是從共享內存中的哪條記錄上的讀套接字中獲得的,再由另一個通信子進程根據這個記錄的編號從共享內存中獲得對應的寫套接字,最后將結果數據往該套接字寫往客戶機。 2.2 建立連接
?、?服務器通信軟件的初始進程首先建立公用端口上的套接字,并在該套接字上建立監聽隊列,同時生成一個守護進程(Daemon)tcp_s,然后初始進程就退出運行。守護進程在函數aclearcase/" target="_blank" >ccept處堵塞住直到有客戶機的連接請求,一有連接請求即調用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為客戶機所要連接的服務器主機名??蛻魴C上的/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函數向服務器發出連接請求??蛻魴C在發出連接請求之前,已用函數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); overtime(int sig){ |
┌──────────────┐ ┌────────────┐ │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地址。這在客戶機之間通信并不頻繁的情況下很有用,因為這樣就可減少所有的客戶機都要相互建立連接的系統開銷,有利于提高整個網絡的運行效率。在某種特定的應用場合服務器在收到客戶機的服務請求后,但因某種原因暫不能處理,于是就將消息存放起來,要等到條件成熟時服務器才能處理客戶請求并將結果返回給客戶機,此時客戶機就不能認為這也是一個遲到的消息,應另行處理