AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

TCP/IP网络重复型服务器通信软件的设计

51自学网 http://www.wanshiok.com

  ⑶ 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();
}

 

 
 

上一篇:使用C++异常来取代exit()函数  下一篇:Shell实现Unix进程间信息交换的几种方法