Socket网络编程--聊天程序(4)

  上一小节讲到可以实现多客户端与服务器进行通讯,对于每一个客户端的连接请求,服务器都要分配一个进程进行处理。对于多用户连接时,服务器会受不了的,而且还很消耗资源。据说有个select函数可以用,好像还很NB的样子。

  使用select多路转换处理聊天程序

  下面摘取APUE 14.5小结 I/O多路转接

当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中循环中使用阻塞I/O:

  while((n = read(STDIN_FILENO, buf, BUFFSIZ))>0)

    if(write(STDOUT_FILENO, buf, n)!=n)

      err_sys("write error");

这种形式的阻塞I/O到处可见。但是如果必须从两个描述符读,又将如何呢?如果仍旧使用阻塞I/O,那么就可能长时间阻塞在一个描述符上,而另一个描述符虽有很多数据却不能得到及时处理。所以为了处理这种情况显然需要另一种不同的技术。

方法一:也就是上一小节使用的方法,使用多进程。每一个进程处理一个描述符

方法二:和上面相似的,使用多线程,不同的线程处理不同的描述符

方法三:仍然使用一个进程执行该程序,但使用非阻塞I/O读取数据。然后对所有的描述符进行遍历一遍,判断对应的描述符是否有数据,如果有就读取,如果没有就立即返回。这种办法就是轮询(polling)

方法四:异步I/O。其基本的思想是进程告诉内核,当一个描述符已经准备好可以进行I/O时,用一个信号通知它。

方法五:这是一种比较好的办法。叫做I/O多路转换(I/O multiplexing)。先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行I/O时,该函数才会返回。在返回时,它高数进程哪些描述符已经准备好可以进行I/O。

  poll,pselect和select这三个函数使我们能够执行I/O多路转换。本程序只使用select函数。

  #include <sys/select.h>

  int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct time val *restrict tvptr);  //返回值:准备就绪的描述符数,若超时则返回0,否则出错返回-1

  select 函数讲解
  FD_ISSET判断描述符fd是否在给定的描述符集fdset中,通常配合select函数使用,由于select函数成功返回时会将未准备好的描述符位清零。通常我们使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。

  fd_set数据类型的操作

  #include <sys/select.h>

  int FD_ISSET(int fd, fd_set *fdset);  //判断fd是否在fdset中

  void FD_CLR(int fd, fd_set *fdset);  //进fd从fdset中取出

  void FD_SET(int fd, fd_set *fdset);  //将fd放入fdset

  void FD_ZERO(fd_set *fdset);    //将fdset清空

  timeval结构分析

  struct timeval{

    long tv_sec; //seconds

    long tv_usec; //and microseconds

  };

  client.c的代码没有改

  server.c的代码如下

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <errno.h>
  4 #include <string.h>
  5 #include <netdb.h>
  6 #include <sys/types.h>
  7 #include <sys/socket.h>
  8 #include <sys/time.h>
  9 #include <sys/un.h>
 10 #include <sys/ioctl.h>
 11 #include <sys/wait.h>
 12 #include <sys/select.h>
 13 #include <netinet/in.h>
 14 #include <arpa/inet.h>
 15
 16 #define SERVER_PORT 12138
 17 #define BACKLOG 20
 18 #define MAX_CON_NO 10
 19 #define MAX_DATA_SIZE 4096
 20
 21 int MAX(int a,int b)
 22 {
 23     if(a>b) return a;
 24     return b;
 25 }
 26
 27 int main(int argc,char *argv[])
 28 {
 29     struct sockaddr_in serverSockaddr,clientSockaddr;
 30     char sendBuf[MAX_DATA_SIZE],recvBuf[MAX_DATA_SIZE];
 31     int sendSize,recvSize;
 32     int sockfd,clientfd;
 33     fd_set servfd,recvfd;//用于select处理用的
 34     int fd_A[BACKLOG+1];//保存客户端的socket描述符
 35     int conn_amount;//用于计算客户端的个数
 36     int max_servfd,max_recvfd;
 37     int on=1;
 38     socklen_t sinSize=0;
 39     char username[32];
 40     int pid;
 41     int i;
 42     struct timeval timeout;
 43
 44     if(argc != 2)
 45     {
 46         printf("usage: ./server [username]\n");
 47         exit(1);
 48     }
 49     strcpy(username,argv[1]);
 50     printf("username:%s\n",username);
 51
 52     /*establish a socket*/
 53     if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
 54     {
 55         perror("fail to establish a socket");
 56         exit(1);
 57     }
 58     printf("Success to establish a socket...\n");
 59
 60     /*init sockaddr_in*/
 61     serverSockaddr.sin_family=AF_INET;
 62     serverSockaddr.sin_port=htons(SERVER_PORT);
 63     serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
 64     bzero(&(serverSockaddr.sin_zero),8);
 65
 66     /*
 67      * SOL_SOCKET.SO_REUSEADDR 允许重用本地地址
 68      * */
 69     setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
 70
 71     /*bind socket*/
 72     if(bind(sockfd,(struct sockaddr *)&serverSockaddr,sizeof(struct sockaddr))==-1)
 73     {
 74         perror("fail to bind");
 75         exit(1);
 76     }
 77     printf("Success to bind the socket...\n");
 78
 79     /*listen on the socket*/
 80     if(listen(sockfd,BACKLOG)==-1)
 81     {
 82         perror("fail to listen");
 83         exit(1);
 84     }
 85
 86     timeout.tv_sec=1;//1秒遍历一遍
 87     timeout.tv_usec=0;
 88     sinSize=sizeof(clientSockaddr);//注意要写上,否则获取不了IP和端口
 89
 90     FD_ZERO(&servfd);//清空所有server的fd
 91     FD_ZERO(&recvfd);//清空所有client的fd
 92     FD_SET(sockfd,&servfd);
 93     conn_amount=0;
 94     max_servfd=sockfd;//记录最大的server端描述符
 95     max_recvfd=0;//记录最大的client端的socket描述符
 96     while(1)
 97     {
 98         FD_ZERO(&servfd);//清空所有server的fd
 99         FD_ZERO(&recvfd);//清空所有client的fd
100         FD_SET(sockfd,&servfd);
101         //timeout.tv_sec=30;//可以减少判断的次数
102         switch(select(max_servfd+1,&servfd,NULL,NULL,&timeout))//为什么要+1,是因为第一个参数是所有描述符中最大的描述符fd号加一,原因的话在APUE中有讲,因为内部是一个数组,第一个参数是要生成一个这样大小的数组
103         {
104             case -1:
105                 perror("select error");
106                 break;
107             case 0:
108                 //在timeout时间内,如果没有一个描述符有数据,那么就会返回0
109                 break;
110             default:
111                 //返回准备就绪的描述符数目
112                 if(FD_ISSET(sockfd,&servfd))//sockfd 有数据表示可以进行accept
113                 {
114                     /*accept a client‘s request*/
115                     if((clientfd=accept(sockfd,(struct sockaddr  *)&clientSockaddr, &sinSize))==-1)
116                     {
117                         perror("fail to accept");
118                         exit(1);
119                     }
120                     printf("Success to accpet a connection request...\n");
121                     printf(">>>>>> %s:%d join in!\n",inet_ntoa(clientSockaddr.sin_addr),ntohs(clientSockaddr.sin_port));
122                     //每加入一个客户端都向fd_A写入
123                     fd_A[conn_amount++]=clientfd;
124                     max_recvfd=MAX(max_recvfd,clientfd);
125                 }
126                 break;
127         }
128         //FD_COPY(recvfd,servfd);
129         for(i=0;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表
130         {
131             if(fd_A[i]!=0)
132             {
133                 FD_SET(fd_A[i],&recvfd);//对所有还连着服务器的客户端都放到fd_set中用于下面select的判断
134             }
135         }
136
137         switch(select(max_recvfd+1,&recvfd,NULL,NULL,&timeout))
138         {
139             case -1:
140                 //select error
141                 break;
142             case 0:
143                 //timeout
144                 break;
145             default:
146                 for(i=0;i<conn_amount;i++)
147                 {
148                     if(FD_ISSET(fd_A[i],&recvfd))
149                     {
150                         /*receive datas from client*/
151                         if((recvSize=recv(fd_A[i],recvBuf,MAX_DATA_SIZE,0))==-1)
152                         {
153                             //perror("fail to receive datas");
154                             //表示该client是关闭的
155                             printf("close\n");
156                             FD_CLR(fd_A[i],&recvfd);
157                             fd_A[i]=0;//表示该描述符已经关闭
158                         }
159                         else
160                         {
161                             printf("Client:%s\n",recvBuf);
162                             //可以判断recvBuf是否为bye来判断是否可以close
163                             memset(recvBuf,0,MAX_DATA_SIZE);
164                         }
165                     }
166                 }
167                 break;
168         }
169
170     }
171     return 0;
172 }

  运行后的截图结果

  可以看出三个客户端都可以随时连接到服务器,并且发送数据给服务器。实现的效果跟上一节的多进程实现是一样的。毕竟没有大量客户端进行连接,所以就看不出效果,从书中和网上介绍说,这样可以提高某些方面的性能。

  下一节将介绍服务器端向各个还在线的客户端进行发送数据,实现交互。然后再实现聊天室功能,大概的思路就是对接收到的数据进行转发。

  参考资料: http://www.cnblogs.com/gentleming/archive/2010/11/15/1877976.html

  

  本文出处: http://www.cnblogs.com/wunaozai/p/3870338.html

时间: 2024-10-10 05:21:28

Socket网络编程--聊天程序(4)的相关文章

Socket网络编程--聊天程序(6)

这一小节将增加一个用户的结构体,用于保存用户的用户名和密码,然后发给服务器,然后在服务器进行判断验证.这里就有一个问题,以前讲的就是发送字符串是使用char类型进行传输,然后在服务器进行用同样是字符串进行接收.然而作为一个结构体是不是也可以呢?如果有看send或recv的函数定义就知道第二个参数是void *类型,也就是说这两个函数对传入的类型其实是不做要求的,只是要你传输个地址,然后后面接一个大小就可以了.就是要send在这个地址取值,去大小为size个,然后传输.学过TCP/IP就知道,我们

Socket网络编程--聊天程序(9)

这一节应该是聊天程序的最后一节了,现在回顾我们的聊天程序,看起来还有很多功能没有实现,但是不管怎么说,都还是不错的.这一节我们将讲多服务器问题(高大上的说法就是负载问题了.)至于聊天程序的文件发送(也即二进制文件发送例如图片)和单点登陆(就是多加一个数组fd_L[],用来记录是否已经登陆过了.),这些问题就不讨论了. 支持多服务器实现负载问题的聊天程序 今天才知道原来我们一直使用的select来处理IO多路复用的这个函数最多只能有1024个连接,因为内部实现里面的数组就是只有1024,多了不行.

Socket网络编程--聊天程序(8)

上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能.就是通过服务器对客户端的数据进行转发到特定的用户上, 实现私聊功能的聊天程序 实现的技术细节是:对客户端发送的数据增加一个标识头,由于我们处理的是纯文本,所以为了讲解的方便就把标识头加到聊天信息的前面,然后在服务器中判断.如果是要在做成产品的话,因为要考虑传送纯文本,图片,文件,特定的结构体等等其他非纯文本信息,那么我们可以对一次聊天信息,发送两次数据,第一次用TCP发送一个结构体

Socket网络编程--聊天程序(7)

接上一小节,本来是计划这一节用来讲数据库的增删改查,但是在实现的过程中,出现了一点小问题,也不是技术的问题,就是在字符界面上比较不好操作.比如要注册一个帐号,就需要弄个字符界面提示,然后输入数字表示选择,在依次输入信息.(这一点,用C写过什么管理系统就知道,很是麻烦.)考虑到本程序讲的是网络编程,就不弄增删改查了,就判断一个用户名密码是否正确就行了. 首先是安装mysql和创建数据库,所用到的命令如下: 1 su root //使用root用户 2 yum install mysql-devel

Socket网络编程--聊天程序(5)

上一小节我们讲了使用select来避免使用多进程的资源浪费问题.上次只是实现了从多个客户端发送数据给服务器端,接下来就要实现从服务器端发送数据给各个服务器. 使用select多路转换处理聊天程序2 client.c 使用上一节用的那个,在那个基础上修改下面几句 66 //send-recv 一些返回指没有判断,具体可以看server.c 67 if((pid=fork())<0) 68 { 69 perror("fork error\n"); 70 } 71 else if(pi

Socket网络编程--聊天程序(3)

上一小节,已经讲到可以每个人多说话,而且还没有限制,简单的来说,我们已经完成了聊天的功能了,那么接下来我们要实现什么功能呢?一个聊天程序至少应该支持一对多的通讯吧,接下来就实现多个客户端往服务器发送数据,和服务器向多个客户端发送数据. 多对一,单向,各个客户端都可以向服务器发送数据 close函数 #include <unistd.h> int close(int sockfd); //用于关闭所打开的Socket套接字 返回值:如果为0表示成功,-1表示失败 处理的办法是每一个客户端连接到服

Socket网络编程--聊天程序(2)

上一节简单如何通过Socket创建一个连接,然后进行通信.只是每个人只能说一句话.而且还是必须说完才会接收到信息,总之是很不方便的事情.所以这一小节我们将对上一次的程序进行修改,修改成每个人可以多说话,主要是通过Linux下多进程fork实现的. 一对一,server和client是每个人都可以多说几句话 由于控制台度数据的函数fgets是阻塞函数,要每个人都可以多说话,这里我使用的是读取和发送都在不同的进程里面,使之互相没有影响. fork函数 #include <unistd.h> pid

Socket网络编程--小小网盘程序(1)

这个系列是准备讲基于Linux Socket进行文件传输.简单的文件传输就是客户端可以上传文件,可以从服务器端下载文件.就这么两个功能如果再加上身份验证,就成了FTP服务器了,如果对用户的操作再加上一些功能(如分享),就可以作为一个最简单的网盘了.想想是不是有点小激动啊. 我这一小节就不讲那么高级的东西,就先了解文件怎么传输,我们以前的聊天程序传输数据都是一次发送就完成本次的发送,因为一个sendBuf是足够的.但是对于二进制文件来说,文件的大小就不一定了,有可能很大,所以我们的Buf是不知道要

Socket网络编程--小小网盘程序(4)

在这一小节中实现了文件的下载,具体的思路是根据用户的uid和用户提供的文件名filename联合两张表,取得md5唯一标识符,然后操作这个标识符对应的文件发送给客户端. 实现下载的小小网盘程序 client.cpp增加下面这个函数以实现文件的下载. 1 int file_pull(struct Addr addr,struct User user,char *filenames) 2 { 3 struct sockaddr_in servAddr; 4 struct hostent *host;