TCP多客户端通信实现

来源:互联网 发布:考勤统计软件 编辑:程序博客网 时间:2024/06/05 14:37
功能要求:当客户端与服务器成功连接后,客户端自动向服务器发送一个0,服务器接收返回一个1,客户端接收返回一个2.。。。。一直到服务器向客户端发送一个9,停止连接,然后过十秒钟重新连接,并且重复相同的工作,每个客户端也做相同的工作,要求输出打印发送的数字以及对应的客户端id和对应的系统时间

 

实现方法:

       使用单进程,tcp实现。

      客户端:

              1.创建一个循环,每10s循环一次,每次循环需要close(fd)。

              2.在循环内部创建socket连接,创建网络地址,连接到服务器。

              3.先发送数据0;

              4.循环接收服务器发送过来的数据,加一,再发送给服务器。

       服务器:

              1.首先定义一个存放客户端文件描述符的数组,并初始化为-1.

              2.创建socket连接,创建网络地址,将socket连接fd绑定到网络地址。

              4.listen

              5.循环监听各文件描述符(fd,cfd[i]),如果可读,则检查是哪个文件描述符的行为。

              6.如果是fd,则表明有新客户端,连接,然后accept,新的cfd存放到数组中

              7.如果是cfd[i],则表明,已连接的客户端可读,则读取数据,并将接收到的数据+1,返回给客户端。

注意点:

       1.服务端创建的fd,需要设置为非阻塞状态。

       默认情况下,accept函数是阻塞的,就是说,在没有新连接请求来的情况下 (listen监听),accept一直在这里等,函数没有返回,也就是说,卡在这个地方而不会创建新的socket,程序不会往下运行。如果设置成非阻塞(fcntl函数),accept在没有连接请求的情况下,马上返回,也就是说,程序继续往下运行,但是accept会返回一个负数,表示客户端socket没有创建成功。

       2.select系统调用

       是用来监视多个文件描述符状态变化的。程序会停在select这里等待,直到被监视的文件描述符中某一个或者多个发生了状态改变,或者超时才返回。

       select机制中提供了一fd_set数据结构,实际上就是一long型数组,其中每一个元素都与文件描述符有关系(相当于把所有的文件描述符都存放到这个数组中),每次调用select之前都需要调用FD_ZERO清空fd_set集,然后将所有文件描述符添加到fd_set集中,当调用select时,select会检查当前fd_set中的文件描述符,将那些可读或者可写的文件描述符留下,清理掉其他没有状态变化的文件描述符,然后配合FD_ISSET找出那些文件描述符是可读或可写的,然后对这些文件描述符进行读写操作。

         函数原型:

#include <sys/select.h>  #include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds,  fd_set * readfds,  fd_set * writefds,  fd_set * exceptfds,  const struct timeval * timeout);

ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为要监视各文件中的最大文件描述符值加1。

readfds:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。

writefds:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。

exceptfds:这个文件集将监视文件集中的任何文件是否发生错误,其实,它可用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select函数返回的时候,exceptfds将清除其中的其他文件描述符,只留下标记有OOB数据的文件描述符。

timeout:本次select()的超时结束时间。这个参数至关重要,它可以使select处于三种状态:

(1)若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

(2)若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

(3)timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

函数的返回值

正值:表示监视的文件集中有文件描述符符合要求

零值:表示select监视超时

负值:表示发生了错误,错误值由errno指定。


       3.超时机制

       struct timeval timeout={4,0};设置select超时时间4s,当select与while配合使用是,每次循环都必须重新给timeout赋值

       4.r=recv(cfd,buf,sizeof(buf),MSG_DONTWAIT);

       第三个参数若使用MSG_WAITALL,表示等待接收满或者断开连接才返回。

       第三个参数若使用MSG_DONTWAIT,表示有数据就接收,没有立即返回,此时如果客户端断开连接或者发送完数据,则返回0,不过此时select还是会认为此fd可读,所以还会继续执行default,因此需要手动在服务端close(fd),并置为-1,不过下次循环是不能将该fd添加到fds否则在FD_SET时,会出现段错误,如果使用fcntl设置了fd为非阻塞模式,数据还没有发给接收端时,调用recv就会返回-1,并且errno会被设为EAGAIN.但此时连接应为正常。


客户端代码

#include<stdio.h>#include<errno.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/socket.h>#include<arpa/inet.h>#include<fcntl.h>#define SOCK_PORT7777#define SERVER_IP"192.168.0.21"#define SLEEP_TIME 3int main(){int fd,r;while(1){//每10秒重新连接服务器int send_v=0,rec_v=0;//创建socket连接fd=socket(AF_INET,SOCK_STREAM,0);if(fd==-1){printf("create socket failed\n");return -1;}    //创建网络地址struct sockaddr_in addr={};addr.sin_family=AF_INET;addr.sin_port=htons(SOCK_PORT);addr.sin_addr.s_addr=inet_addr(SERVER_IP);        //连接服务器r=connect(fd,(struct sockaddr*)&addr,sizeof(addr));if(r==-1){printf("connect:%s\n",strerror(errno));printf("connect failed\n");return -1;}printf("connect succeed\n");        //sendr=send(fd,&send_v,4,0);if(r==-1){printf("send failed\n");return -1;}while(1){        //recv  r=recv(fd,&rec_v,4,0);if(r==-1){printf("recv failed\n");return -1;}//printf("recv succeed\n");printf("rec:%d\n",rec_v);if(rec_v==9){break;}int temp_v=rec_v+1;r=send(fd,&temp_v,4,0);if(r==-1){printf("send failed\n");return -1;}printf("send:%d\n",temp_v);//sleep(1);//printf("send succeed\n");}close(fd);sleep(SLEEP_TIME);}close(fd);}

服务端

#include<stdio.h>#include<errno.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/socket.h>#include<arpa/inet.h>#include<fcntl.h>#include<netinet/in.h>#include<time.h>#define SOCK_PORT7777#define CLIENT_NUMB 20#define MAX_LISTEN_NUMB 10void getlocal_time(void){ time_t nowtime; struct tm *timeinfo; time( &nowtime ); timeinfo = localtime( &nowtime ); int year, month, day,hour,sec,min; year = timeinfo->tm_year + 1900; month = timeinfo->tm_mon + 1; day = timeinfo->tm_mday; hour=timeinfo->tm_hour; sec=timeinfo->tm_sec; min=timeinfo->tm_min; printf(" time:%d.%d.%d %d:%d:%d\n", year, month, day,hour,min,sec);}/*创建socket连接创建网络地址将文件描述符绑定到网络地址上return:成功:文件描述符fd失败:-1*/int creat_bind_socket(void){//printf("creat_bind_socket\n");int fd,r;       // create socket创建socket连接fd=socket(AF_INET,SOCK_STREAM,0);       //set this file descriptor noblock设置文件描述符fd为非堵塞fcntl(fd,F_SETFL,O_NONBLOCK);        //create network address创建网络地址    struct sockaddr_in addr={};addr.sin_family=AF_INET;addr.sin_port=htons(SOCK_PORT);    addr.sin_addr.s_addr=htonl(INADDR_ANY);//addr.sin_addr.s_addr=inet_addr("192.168.0.21");    //bound the file descriptor  to address将文件描述符fd绑定到网络地址上r=bind(fd,(struct sockaddr*)&addr,sizeof(addr));if(r==-1){printf("%s\n",strerror(errno));printf("bind failed\n");return -1;}return  fd;}/*检查各cfd是否可读,如果可读则读取数据,并发送数据input:cfd存放所有客户端文件描述符fd主文件描述符fds所有文件描述符集合i客户端在cfd数组中的序号*/int deal_read_send(int cfd[],int fd,fd_set fds,int i){//printf("deal_read_send\n");int r;int rec_v=0,send_v=0;if(cfd[i]==-1)return 0;//printf("recv cfd:%d,i:%d\n",cfd[i],i);    if(FD_ISSET(cfd[i],&fds)){// 如果cfd还在fds中表明cfd[i]可读,则读取数据int re;re=recv(cfd[i],&rec_v,4,0);if(re==-1&&EAGAIN!=errno){//注意,如果sockfd设置为非阻塞模式,数据还没有发给接收端时,调用recv就会返回-1,并且errno会被设为EAGAIN.但此时连接应为正常  printf("%s\n",strerror(errno));//print error inforprintf("recv failed\n");close(cfd[i]);cfd[i]=-1;return 0;}if(re==0){                  //if re==0 means client not send data如果客户端不再发送数据则strerror(errno);close(cfd[i]);cfd[i]=-1;printf("recv null\n");return 0;}printf("From:%d,value:%d",cfd[i],rec_v);getlocal_time();//打印时间send_v=rec_v+1;  //sendr=send(cfd[i],&send_v,4,MSG_DONTWAIT);//发送数据if(r==-1){printf("send failed\n");return 0;}    }return 0;}/*遍历cfd数组,查询没有使用的位,并返回,如果cfd数组,都被使用,则返回0*/int select_free_numb(int cfd[]){int i;for(i=0;i<CLIENT_NUMB;i++){if(cfd[i]==-1)return i;}return 0;}/*检查哪个文件描述符可读,如果是fd,则表明有新的客户端连接,调用accept获取新的客户端描述符循环遍历客户端描述符集,调用deal_read_send();处理input:fd主文件描述符fds存放所有文件描述符的集合cfd[]存放所有客户端文件描述符return:0*/int deal_select_default(int fd,fd_set fds,int cfd[]){//printf("deal_select_default\n");//检查是否有新的client连接,则acceptint new_cfd,i,r;struct sockaddr_in clientaddr={};   //store connect ip存储连接客户端ipint cliaddlen=sizeof(clientaddr);if(FD_ISSET(fd,&fds)){ //检查fd是否还在fds中,如果在,表明有新的客户端连接,则accept,否则跳过new_cfd=select_free_numb(cfd);cfd[new_cfd]=accept(fd,(struct sockaddr*)&clientaddr,&cliaddlen);//accept and return new fd if(cfd[new_cfd]==-1){printf("error:%d,%s\n",errno,strerror(errno));printf("accept failed\n");return 0;}//printf("**cfd numb:%d\n",new_cfd);printf("\nconnect from %s,client id:%d\n",inet_ntoa(clientaddr.sin_addr),cfd[new_cfd]);}for(i=0;i<CLIENT_NUMB;i++){//循环遍历客户端集中个cfd,如果可读则接收数据,并发送数据deal_read_send(cfd,fd,fds,i);}//for i=0return 0;}/*监听各文件描述符(fd,cfd[i]),如果可读则调用deal_select_default()进行处理input:fd主文件描述符cfd[]存放所有客户端文件描述符return:0*/int loop_deal_socket(int fd,int cfd[]){//printf("loop_deal_socket\n");fd_set fds;             //file descriptor set文件描述符集struct timeval timeout={4,0};  //4s超时4sint i;FD_ZERO(&fds);  //clear fd set清除fd集FD_SET(fd,&fds);// put fd into set将fd放入fd集中for(i=0;i<CLIENT_NUMB;i++){//将各cfd放入fd集中if(cfd[i]==-1)continue;FD_SET(cfd[i],&fds);}int maxfd=fd;for(i=0;i<CLIENT_NUMB;i++)maxfd=(maxfd>cfd[i])?maxfd:cfd[i];   //find the max fd //选出最大的fd//printf("select\n");switch(select(maxfd+1,&fds,NULL,NULL,&timeout)){//监视各fd,如果fd可读或者可写返回正直,如果网络错误返回-1,如果超时返回0    case -1:   //net work error网络错误     printf("caseerror:%s\n",strerror(errno));break;    case 0:  //if timeout超时    printf("time out\n");    break;    default:// if some fd can read如果可读可写等。。。//printf("default\n");deal_select_default(fd,fds,cfd);}return 0;}int main(void){int fd=-1,len,r,i;int cfd[CLIENT_NUMB];//存放各客户端fdfor(i=0;i<CLIENT_NUMB;i++){//将客户端fd集元素初始化为-1cfd[i]=-1;}if((fd=creat_bind_socket())==-1){return -1;}printf("bind succeed\n");//listen监听最多十个连接r=listen(fd,MAX_LISTEN_NUMB);if(r==-1){printf("listen failed\n");return -1;}printf("listen succeed\n");while(1){//循环监听各cfd如果可读,则读取数据,并发送数据,如果有新的客户端连接,创建新的cfd。if(loop_deal_socket(fd,cfd)){;}//sleep(1);}}




0 0
原创粉丝点击