【LINUX编程】一个基于C/S结构的简单通讯程序

funway: @ 2009-12-3
   
该程序是修改的一个网络上一位叫周立发的仁兄写的简单的网络通讯程序。
   
源程序可见http://www.cnitblog.com/zouzheng/archive/2007/03/27/24732.aspx
   
源程序的功能只能实现网络上一个客户端对一个服务器的相互通信。 

    多个客户端必须等待占用服务器的客户的退出才能与服务器通讯。
   
修改后实现一个类似QQ群聊的功能。多个客户端通过服务器发送消息。当然,服务器端也可发送消息给各个客户。

 

/*********************************************** 文件名:common.h 该头文件包含server.c 跟 client.c 公用的一些头文件以及自定义的数据结构 该程序的运行方法: $ ./server 8888 //开启服务器程序。8888为想要设置的端口号。当然你也可以一选用9999。 再开一个控制台: $ ./client 192.169.0.1 8888 //运行客户端。192.168.0.1为运行server程序的主机IP; 8888为该主机上一个server进程使用的端口号 再开一个控制台: $ ./client 192.168.0.1 8888 //运行另一个客户端,最大接收客户端为server.c里define的MAX_CLIENT_NUM值 . . . 注: 程序运行中可以输入 quit 代表终止程序 *************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAXBUF 1024 #define MAX_CLIENT_NUM 3 //funway:listen好像有最大限制, 但是listen connect accept之间到底有什么样的信息传递,这些最内在的东西还没弄懂。 struct client_info{ //funway:自定义的 客户端结构 int sockfd; //funway:套接口描述符。 因为linux把所有的东西都看作是文件。 //所以其实这个套接口描述符就等同于文件描述符(每打开一个文件,系统就会为其标记一个文件描述符)。 //当然,它们本质都是int。 struct sockaddr_in addr; //funway:sockaddr_in是系统定义的存储IPV4地址的数据结构(包括IP地址,端口号) }; void print_time() //funway:大印当前时间的函数,都是库函数调用 { char *wday[] = {"SUN","MON","TUE","WED","THU","FRI","SAT"}; time_t timep; struct tm *p; time(&timep); p = gmtime(&timep); printf("%d.%d.%d ",(1900+p->tm_year),(1+p->tm_mon),p->tm_mday); printf("%s %d:%d:%d/n",wday[p->tm_wday],p->tm_hour,p->tm_min,p->tm_sec); } //////////////////////////////////////////////////////////////// //funway:打印客户端 “ ip地址 [端口号] “ 的函数。 用户名什么的待实现 // void print_client(struct client_info client) { printf("client: %s[%d]",inet_ntoa(client.addr.sin_addr),ntohs(client.addr.sin_port)); //funway:inet_ntoa函数将IP地址转换成字符串返回。 //ntohs函数将实现网络字节序到机器字节序的转换。所谓字节序。。。 自己查查吧 } ////////////////////////////////////////////////////////// //funway:整理服务器端程序中包含客户端列表的clients数组; // i表示第i号客户已经退出;num表示原来在该数组中存在num个客户 // void drawback(struct client_info *clients, int i, int num) { for( ; (i + 1) < num; i++) memcpy(clients + i, clients + (i + 1), sizeof(struct client_info)); } ///////////////////////////////////////////////////////// //funway:广播msg消息给clients数组中的所有客户端 ( 除了 except代表的客户,except为-1表示所有客户均接收消息) // void broadcast_msg(struct client_info *clients,int num,int except,char *msg) { int i,len; printf("广播消息:/n"); for(i = 0; i < num; i++) { if(i == except) continue; printf(" "); print_client(clients[i]); len = send(clients[i].sockfd, msg, strlen(msg), 0); if (len > 0) printf(" ...成功/n"); else printf(" ...失败/n ...错误代码是%d,错误信息是'%s'/n", errno, strerror(errno)); } }

 

 

#include "common.h" /************关于本文档******************************************** *filename: server.c *purpose: 演示网络异步通讯,这是服务器端程序 *修改:funway @ 2009-12-3 *由原来只能一个client对一个server的聊天模式改为支持多个client于一个server互联的星形模式。 *********************************************************************/ int main(int argc, char **argv) { int sockfd,i; socklen_t len; struct sockaddr_in my_addr; unsigned int myport,client_num = 0; char buf[MAXBUF + 1]; char msg[2 * sizeof(buf)]; struct timeval tv; int retval, maxfd = -1; fd_set rfds; //funway:('fd_set') 是一组文件描述符(fd)的集合 (define在types.h跟posix_types.h两个头文件里)。 //由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量: //FD_ZERO(&set); /* 将set清零 */ 这几个宏函数定义在time.h跟posix_types.h //FD_SET(fd, &set); /* 将fd加入set */ //FD_CLR(fd, &set); /* 将fd从set中清除 */ //FD_ISSET(fd, &set); /* 如果fd在set中则真*/ /*funway:??有问题我觉得在下面的代码中该宏函数的作用其实是判断set中的fd是否有异动(读写操作)*/ struct client_info clients[MAX_CLIENT_NUM]; //funway:代表客户端的数组 struct client_info reject_client; //funway:用来暂时存储当clients数组满时仍请求连接的客户。一遍可以通知他服务器满了 if (argv[1]) myport = atoi(argv[1]); else myport = 7838; //funway:如果用户不定义端口号的话,默认使用7838端口号 if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); //funway:打印出错信息,参数会先输出,然后自动跟上系统定义的错误原因(根据一个全局的errno变量) exit(1); //funway:socket()函数返回创建的套接口描述符,前两个参数表示该套接口面向连接的TCP的style。最后一个参数通常置0。 //注意,套接口描述符并不是端口port。 ip+port可以看成是设备的地址。 //套接口描述符本质是文件描述符。下面的bind函数将会把一个套接口(文件)描述符邦定到该IP[端口]。 //这样,对该IP[端口]的所有事件都可以通过这个文件描述符来处理 } bzero(&my_addr, sizeof(my_addr)); //funway:置0函数 my_addr.sin_family = PF_INET; my_addr.sin_port = htons(myport); //funway:将16位的硬件字节转换为网络字节序号 my_addr.sin_addr.s_addr = INADDR_ANY; //funway:默认使用本机IP if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) //funway:将一个 IP地址(包括端口号) 与套接口描述符绑定 { perror("bind"); exit(1); } if (listen(sockfd, MAX_CLIENT_NUM) == -1) { //funway:监听该套接口。 //MAX_CLIENT_NUM表示最大连接数 (不过感觉这个参数没用,即使连接超过了也不报错,甚至连accept的都可以接受) perror("listen"); exit(1); } //参考:int listen(int sockfd, int backlog); //backlog是未经过处理的连接请求队列可以容纳的最大数目。 //即每一个连入请求都要进入一个连入请求队列,等待 //listen 的程序调用accept()函数来接受这个连接。当系统还没有 //调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog的 数值。 bzero(clients,sizeof(clients)); if((i = fcntl(sockfd,F_GETFL,0)) < 0) perror("fcntl(sockfd,F_GETFL,...)"); i = O_NONBLOCK; if(fcntl(sockfd,F_SETFL,i) < 0) perror("fcntl(sockfd,F_SETFL,...)"); //funway:这三行的作用就是将套接口设置为非阻塞模式。(注:fcntl是文件操作的函数。这里就可以看出,其实,套接口描述符就是文件描述符) //如果不作该设置,下面的accept函数将导致服务器阻塞 printf("/n-----服务器开启-----/n"); while (1) {//while主循环 len = sizeof(struct sockaddr); i = client_num; if(client_num < MAX_CLIENT_NUM) //客户端数目未超过连接上限时候才允许新的连接。 { if ((clients[i].sockfd = accept(sockfd, (struct sockaddr *) &clients[i].addr, &len)) == -1) ; //作空操作,或者你想打印什么东西随你 //funway:函数accept(int s,struct sockaddr * addr,int * addrlen) //accept()用来接受参数s的socket连线。参数s的socket必需先经bind()、listen()函数处理过, //当有连线进来时 accept()会返回一个新的socket描述符,往后的数据传送与读取就是经由新的socket处理, //而原来参数s的socket能继续使用 accept()来接受新的连线要求。 //连线成功时,参数addr所指的结构会被系统填入请求连接的客户端的的地址数据,参数addrlen为scokaddr的结构 长度。 //如果队列中无等待连接,且套接口为非阻塞方式,则accept()阻塞调用进程直至新的连接出现。 //如果套接口为非阻塞方式且队列中无等待连接,则accept()返回一错误代码。 else { printf("server: got connection from "); print_client(clients[i]); printf(" Socket ID %d/n",clients[i].sockfd); client_num++; } } else //funway:不再接受连接。 但是感觉这样的代码多余了。。应该在上面的哪些系统调用要有参数来表示拒绝连接亚。。?未解。。。 { if ((reject_client.sockfd = accept(sockfd, (struct sockaddr *) &reject_client.addr, &len)) == -1) ; else { printf("服务器满. 拒绝 %s[%d] 接入/n",inet_ntoa(reject_client.addr.sin_addr),ntohs(reject_client.addr.sin_port)); sprintf(msg,"#REJECT_CONNECT"); send(reject_client.sockfd, msg, strlen(msg), 0); } } /* 把集合清空 */ FD_ZERO(&rfds); /* 把标准输入句柄0加入到集合中 */ FD_SET(0, &rfds); //funway:0号文件描述符在unistd.h中define STDIN_FILENO 0。即标准输入,1号为标准输出,2号为标准出错 maxfd = 0; /* 把当前连接句柄new_fd加入到集合中 */ for(i = 0; i < client_num; i++) { FD_SET(clients[i].sockfd, &rfds); //fuwany:sockfd是当前与服务器端链接的客户的套接口描述符(本质都是文件描述符号) maxfd = clients[i].sockfd > maxfd ? clients[i].sockfd : maxfd; } /* 设置最大等待时间 */ tv.tv_sec = 1; tv.tv_usec = 0; /* 开始等待 */ retval = select(maxfd + 1, &rfds, NULL, NULL, &tv); //funway:int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout); //参数maxfd是需要监视的最大的文件描述符值+1;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合, //可写文件描述符的集合及异常文件描述符的集合。struct timeval结构用于描述一段时间长度, //如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。 //如果在这个时间内,被监听的文件集中有某个文件有异动,立即返回1 //printf("/nselect()返回%d/n",retval); if (retval == -1) { printf("服务器将退出,select出错! %s", strerror(errno)); break; } else if (retval == 0) { /* printf("没有任何消息到来,用户也没有按键,继续等待……/n"); */ continue; } else {//else 如果有哪个(文件)套接口描述符有异动 if (FD_ISSET(0, &rfds)) {//if 服务器端有输入 /* 服务器端用户按键了,则读取用户输入的内容发送出去 */ bzero(buf, MAXBUF + 1); fgets(buf, MAXBUF, stdin); if (!strncasecmp(buf, "quit", 4)) { printf("服务器终止!/n"); break; } else { sprintf(msg,"SERVER:/n"); //funway:sprintf()函数为字符串格式化。类似于printf("format") strncat(msg,buf,strlen(buf) - 1); //funway:该函数将buf的strlen-1个字节复制到msg尾部 //strlen(buf) - 1负责对用户的输入截尾,即剔除最后的/n broadcast_msg(clients,client_num,-1,msg);//广播消息msg,参数-1表示所有客户端均接受该消息 } }//if服务器端输入 for(i = 0; i < client_num; i++ ) {//for 判断并接受客户消息的循环 if (FD_ISSET(clients[i].sockfd, &rfds)) //funway:如果该客户端有输入... { /* 当前连接的socket上有消息到来则接收对方发过来的消息并显示 */ bzero(buf, MAXBUF + 1); /* 接收客户端的消息 */ len = recv(clients[i].sockfd, buf, MAXBUF, 0); //funway:这里接受的消息buf无换行符,因为在客户端的send函数中已经剔除 printf("------------------------------/n"); print_client(clients[i]); printf(" at "); print_time(); if (len > 0) { //funway:如果成功收到了用户消息,广播之!!! printf("/"%s/"/n",buf); sprintf(msg,"client: %s[%d]/n",inet_ntoa(clients[i].addr.sin_addr),ntohs(clients[i].addr.sin_port)); strcat (msg,buf); broadcast_msg(clients,client_num,i,msg); } else { if (len < 0) printf("消息接收失败!错误代码%d,错误信息'%s'/n",errno, strerror(errno)); else{ printf("对方退出/n"); //但未将该客户退出的消息广播给所有客户端,这很简单,你在可以继续实现 //接下来必须因为一个客户端的退出处理下clients[] close(clients[i].sockfd); //funway:关闭该套接口描述符,其实这个函数也是文件处理函数 drawback(clients,i,client_num); client_num--; } } printf("------------------------------/n"); }//if (FD_ISSET(clients[i].sockfd, &rfds)) }//for 判断接受客户消息循环 }//else如果有哪个(文件)套接口描述符有异动 }//while主循环 for(i = 0; i < client_num; i++ ) close(clients[i].sockfd); //funway:关闭所有被打开的套接口描述符 close(sockfd); return 0; }

 

 

#include "common.h" /************关于本文档******************************************** // *filename: client.c *purpose: 演示网络异步通讯,这是客户端程序 *修改:funway @ 2009-12-3 *由原来只能一个client对一个server的聊天模式改为支持多个client于一个server互联的星形模式。 *********************************************************************/ int main(int argc, char **argv) { int sockfd, len; struct sockaddr_in dest; char buffer[MAXBUF + 1]; char msg[2 * sizeof(buffer)]; fd_set rfds; struct timeval tv; int retval, maxfd = -1; if (argc != 3) { printf("参数格式错误!正确用法如下:/n/t/t%s IP地址 端口/n/t比如:/t%s 127.0.0.1 80/n此程序用来从某个 IP 地址的服务器某个端口接收最多 MAXBUF 个字节的消息", argv[0], argv[0]); exit(0); } /* 创建一个 socket 用于 tcp 通信 */ if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket"); exit(errno); } /* 从用户的输入参数初始化服务器端(对方)的地址和端口信息,存放在dest变量中 */ bzero(&dest, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(atoi(argv[2])); if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0) { perror(argv[1]); exit(errno); } /* 连接服务器 */ if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0) { perror("Connect "); //funway:如何在这里设置服务器满,拒绝接入的状态?...未解 exit(errno); } //printf("/033[2J"); //funway:printf的控制字,效果 清屏 = windows下的clescr() //funway:关于printf()函数的控制字可以参考 http://blog.ednchina.com/walnutcy/288214/message.aspx system("clear"); //funway:调用SHELL的清屏命令 printf("/033[31m/n...连接务器成功 ... /033[0m/n"); //funway:printf的控制字 /033[31m 表示设置显示字体为红色, //用完记的用/033[0m控制字表示关闭所有属性控制 while (1) { /* 把集合清空 */ FD_ZERO(&rfds); /* 把标准输入句柄0加入到集合中 */ FD_SET(0, &rfds); maxfd = 0; /* 把当前连接句柄sockfd加入到集合中 */ FD_SET(sockfd, &rfds); if (sockfd > maxfd) maxfd = sockfd; /* 设置最大等待时间 */ tv.tv_sec = 1; tv.tv_usec = 0; /* 开始等待 */ retval = select(maxfd + 1, &rfds, NULL, NULL, &tv); if (retval == -1) { printf("/033[31m将退出,select出错! %s/033[0m", strerror(errno)); break; } else if (retval == 0) { /* printf("没有任何消息到来,用户也没有按键,继续等待……/n"); */ continue; } else { if (FD_ISSET(sockfd, &rfds)) { /* 连接的socket上有消息到来则接收对方发过来的消息并显示 */ bzero(msg, sizeof(msg)); /* 接收对方发过来的消息,最多接收 MAXBUF 个字节 */ len = recv(sockfd, msg, MAXBUF, 0); if (len > 0) { if(strcmp(msg,"#REJECT_CONNECT") == 0) //funway:判断是否收到被拒绝的消息 { printf("/033[31m服务器满,拒绝接入/033[0m/n"); break; } printf("/033[32m------------------------------/n"); printf("%s/n",msg); printf("------------------------------/033[0m/n"); } else { if (len < 0) printf ("/033[31m消息接收失败!错误代码是%d,错误信息是'%s'/033[0m/n",errno, strerror(errno)); else printf("/033[31m服务器关闭,聊天终止!/033[0m/n"); break; } } if (FD_ISSET(0, &rfds)) { /* 用户按键了,则读取用户输入的内容发送出去 */ bzero(buffer, MAXBUF + 1); fgets(buffer, MAXBUF, stdin); if (!strncasecmp(buffer, "quit", 4)) { printf("/033[31m退出连接/033[0m/n"); break; } /* 发消息给服务器 */ len = send(sockfd, buffer, strlen(buffer) - 1, 0); if (len < 0) { printf("/033[31m消息发送失败!错误代码是%d,错误信息是'%s'/033[0m/n",errno, strerror(errno)); break; } else{ printf("/033[31m消息发送成功,%d个字节!/n", len); print_time(); printf("------------------------------/033[0m/n"); } }// if (FD_ISSET(0, &rfds)) }//retval > 0 }//while(1) /* 关闭连接 */ close(sockfd); return 0; }

你可能感兴趣的:(LINUX)