zoukankan      html  css  js  c++  java
  • Socket网络编程--聊天程序(9)

      这一节应该是聊天程序的最后一节了,现在回顾我们的聊天程序,看起来还有很多功能没有实现,但是不管怎么说,都还是不错的。这一节我们将讲多服务器问题(高大上的说法就是负载问题了。)至于聊天程序的文件发送(也即二进制文件发送例如图片)和单点登陆(就是多加一个数组fd_L[],用来记录是否已经登陆过了。),这些问题就不讨论了。

      支持多服务器实现负载问题的聊天程序

      今天才知道原来我们一直使用的select来处理IO多路复用的这个函数最多只能有1024个连接,因为内部实现里面的数组就是只有1024,多了不行。什么?一个准备上万人用的聊天程序就只能1000个人?怎么可能,作为强大的服务器,肯定还有其他可以解决的办法,系统提供了一个poll和epoll等函数用来处理这个问题。还有一种办法就是创建多进程或多线程,不同的进程和线程中用一个select,就可以实现1024个以上的连接。所以只要判断conn_amount的个数如果大于1024那么就创建一个进程(线程)来继续接收更多的连接。

      可是我们今天要实现的是多个服务器,其原理跟多进程是一样的。

      程序的运行是这样的。server2和server3到Server1中注册,表示对应的服务器可以使用,然后就是各个客户端了,首先Client1发送请求通讯连接到Server1,然后由Server1发送一个可以使用的服务器(Server2或Server3)IP地址和端口给Client1,再然后由Client1向获取到的IP和端口的服务器发送连接请求。假如是连接到Server2,就可以建立通讯了。同理Client2,3,4,5都是这样建立到Server2,Server3的连接。这样5个客户端就可以分发到两个服务器了。至于分配的方法,就可以自己定义了,可以是随机分配,或者存到数据库中,如果是存到数据库中的话,那么是不是很像群功能呢?而且群里的人还是固定的。如果像上图,如何使Client1和Client4进行通讯的呢?可以判断Client是否在Server2中,如果不在就由Server2对数据转发到Server3,再由Server3发送到Client4。这样就可以了。

      不过我们这一节就没有完成那么多的功能,只是实现Client1间接链接到Server2,Client3间接连接到Server2,然后让Client1与Client3通讯。其他的服务器之间通讯就不实现了。

      好了废话不多说,代码走起。

      client.c 代码修改如下

        ...
     15 struct user
     16 {
         ... 
    19
    }; 20 21 /*下面增加多服务器代码*/ 22 struct Addr 23 { 24 char host[64]; 25 int port; 26 }; 27 28 int query_addr(struct Addr *paddr,char *phost,int port) 29 { 30 int sockfd; 31 struct Addr addr; 32 struct hostent * host; 33 struct sockaddr_in servAddr; 34 int size; 35 host=gethostbyname(phost); 36 if(host==NULL) 37 { 38 perror("host 为空"); 39 exit(-1); 40 } 41 42 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 43 { 44 perror("socket 失败"); 45 } 46 47 servAddr.sin_family=AF_INET; 48 servAddr.sin_port=htons(port); 49 servAddr.sin_addr=*((struct in_addr *)host->h_addr); 50 bzero(&(servAddr.sin_zero),8); 51 52 if(connect(sockfd,(struct sockaddr *)&servAddr,sizeof(struct sockaddr_in))==-1) 53 { 54 perror("connect 失败"); 55 exit(-1); 56 } 57 memset(paddr,0,sizeof(struct Addr)); 58 size=recv(sockfd,(char *)paddr,sizeof(struct Addr),0); 59 60 return 0; 61 } 62 63 int main(int argc,char *argv[]) 64 {     ...
    74 struct Addr addr; 75 76 77 if(argc != 5) 78 { 79 perror("use: ./client [hostname] [prot] [username] [password]"); 80 exit(-1); 81 } 82 query_addr(&addr,argv[1],atoi(argv[2])); 83 printf("从服务器获取到的IP:%s 端口:%d ",addr.host,addr.port); 84 strcpy(use.name,argv[3]); 85 strcpy(use.pwd,argv[4]); 86 87 host=gethostbyname(addr.host);      ... 104 servAddr.sin_family=AF_INET; 105 servAddr.sin_port=htons(addr.port); 106 servAddr.sin_addr=*((struct in_addr *)host->h_addr); 107 //servAddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 108 bzero(&(servAddr.sin_zero),8); 109 110 /*connect the socket*/       ... ... 168 close(sockfd); 169 //kill(0,SIGKILL);//0表示同一进程组的进程 170 171 return 0; 172 }

      这次增加了一个结构体Addr用来保存服务器的IP地址和端口号的。命令行参数填写的是super-server的IP地址和端口。然后调用query_addr函数,获取从super-server返回来的当前可用的服务器的IP地址和端口。然后在进行通讯。

      增加一个super-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 #include <unistd.h>
     16 #include <time.h>
     17 
     18 
     19 #define SERVER_PORT 12138
     20 #define BACKLOG 20
     21 #define MAX_CON_NO 10
     22 #define MAX_DATA_SIZE 4096
     23 
     24 #define MAX_ADDR 64
     25 struct Addr
     26 {
     27     char host[64];
     28     int port;
     29 };
     30 
     31 struct AddrList //保存所有可用的服务器IP地址和端口,flag表示该地址是否可用,因为服务器可能中途断开了。
     32 {
     33     int flag;
     34     struct Addr addr;
     35 };
     36 
     37 
     38 int main(int argc,char *argv[])
     39 {
     40     struct sockaddr_in clientSockaddr;
     41     int clientfd;
     42     char sendBuf[MAX_DATA_SIZE];
     43     int sendSize;
     44     int sockfd;
     45     int on;
     46     int sinSize;
     47     struct Addr addr;
     48     struct AddrList addrlist[MAX_ADDR];
     49     int addrlist_count=0;
     50     int i,ilist;
     51 
     52     memset(addrlist,0,sizeof(addrlist));
     53 
     54     if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     55     {
     56         perror("创建socket失败");
     57         exit(-1);
     58     }
     59 
     60     clientSockaddr.sin_family=AF_INET;
     61     clientSockaddr.sin_port=htons(SERVER_PORT); //super-server默认使用12138作为服务端口
     62     clientSockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
     63     bzero(&(clientSockaddr.sin_zero),8);
     64 
     65     setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
     66 
     67     if(bind(sockfd,(struct sockaddr *)&clientSockaddr,sizeof(struct sockaddr))==-1)
     68     {
     69         perror("bind 失败");
     70         exit(-1);
     71     }
     72 
     73     //backlog是积压值,对于TCP,通常建立连接时,会有3/4次握手的过程,一个client连接在完成了建立连接的握手过程,而还没有被应用层(应用程序)所响应时,这个连接被置于backlog队列中。当达到backlog队列以满时,client的连接请求会返回超时的错误。
     74     if(listen(sockfd,5)==-1)
     75     {
     76         perror("listen 失败");
     77         exit(-1);
     78     }
     79 
     80     sinSize=sizeof(clientSockaddr);
     81 
     82     for(i=0;i<MAX_ADDR;i++)
     83     {
     84         addrlist[i].flag=0;
     85     }
     86 
     87     for(i=0;i<2;i++)//这里先固定成两个,可以修改成select然后实现动态增加服务器和减少服务器
     88     {
     89         //加入进来的服务器server
     90         if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr,&sinSize))==-1)
     91         {
     92             perror("accept 失败");
     93             exit(-1);
     94         }
     95 
     96         if((sendSize=recv(clientfd,(char *)&addr,sizeof(struct Addr),0))!=sizeof(struct Addr))
     97         {
     98             perror("send 失败");
     99             exit(-1);
    100         }
    101         printf("server发过来的地址 %s:%d
    ",addr.host,addr.port);
    102         addrlist[i].flag=1;
    103         strcpy(addrlist[i].addr.host,addr.host);//保存服务器ip/端口信息到super-server中
    104         addrlist[i].addr.port=addr.port;
    105         close(clientfd);
    106     }
    107     /*
    108     addrlist[0].flag=1;
    109     addrlist[1].flag=1;
    110     strcpy(addrlist[0].addr.host,"localhost");
    111     strcpy(addrlist[1].addr.host,"localhost");
    112     addrlist[0].addr.port=12137;
    113     addrlist[1].addr.port=12139;
    114     */
    115 
    116     ilist=0;
    117     i=0;
    118     while(1)
    119     {
    120         /*分配域名/IP和端口*//*分配的方法是轮询*/
    121         i=ilist+1;
    122         while(i<MAX_ADDR)
    123         {
    124             if(addrlist[i].flag!=0)
    125             {
    126                 ilist=i;
    127                 break;
    128             }
    129             i++;
    130             i=i%MAX_ADDR;
    131         }
    132 
    133         strcpy(addr.host,addrlist[ilist].addr.host);
    134         addr.port=addrlist[ilist].addr.port;
    135         printf("发送给客户端的id=%d 域名/IP:%s  port:%d 
    ",ilist,addr.host,addr.port);
    136 
    137         if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr,&sinSize))==-1)
    138         {
    139             perror("accept 失败");
    140             exit(-1);
    141         }
    142 
    143         if((sendSize=send(clientfd,(char *)&addr,sizeof(struct Addr),0))!=sizeof(struct Addr))
    144         {
    145             perror("send 失败");
    146             exit(-1);
    147         }
    148         close(clientfd);
    149     }
    150 
    151     return 0;
    152 }

      在第87行处是使用固定两台服务器server的,这个可以修改成select或poll等进行复用,实时监听是否有新的服务器server到来或者有服务器离开。这个select版本我就不写了,看了之前的博客内容就应该会写,如果还不会那就等Socket网络编程系列的另外一个程序了,由于程序代码越来越多,调试起来比较麻烦,讲解也不太好讲解,所以就准备出新的系列了。还希望多支持啊!╮(╯3╰)╭

       第120行处,采用的分配服务器的方法是轮询。依靠生成环境的不同这里可以进行修改,比如是随机分配,依靠数据库用户表中的数据选择指定的服务器进行登陆(这个像不像玩游戏时那个分区啊,什么电信一区,网通二区。就是根据数据库判断的)。还有根据用户的IP获取用户所在的城市,然后进行服务器的分配的,以获得最佳连通效果。QQ群等等什么的都是差不多这样吧。我猜的!

      最后一个代码是server.c 

        ...
     25 struct user
     26 {
        ...
    29 }; 30 31 int MAX(int a,int b) 32 {     ...
    36 } 37 38 void print_time(char * ch,time_t *now) 39 {     ...
    43 } 44 45 46 int mysql_check_login(struct user su) 47 {      ...
    91 return 0; 92 } 93 94 //根据用户名返回该用户名在fd_A中的位置 95 //fd=-1,表示没有该用户 //fd>0 正常返回 96 int fd_ctoa(char fd_C[][32],char *ch) 97 {     ...
    109 } 110 111 /*下面部分是多服务器增加的代码*/ 112 struct Addr 113 { 114 char host[64]; 115 int port; 116 }; 117 118 int server_register(char *super_server_host,int super_server_port,struct Addr addr) 119 { 120 int sockfd; 121 struct hostent * host; 122 struct sockaddr_in servAddr; 123 int size; 124 host=gethostbyname(super_server_host); 125 if(host==NULL) 126 { 127 perror("host 为空"); 128 exit(-1); 129 } 130 131 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 132 { 133 perror("socket 失败"); 134 } 135 136 servAddr.sin_family=AF_INET; 137 servAddr.sin_port=htons(super_server_port); 138 servAddr.sin_addr=*((struct in_addr *)host->h_addr); 139 bzero(&(servAddr.sin_zero),8); 140 141 if(connect(sockfd,(struct sockaddr *)&servAddr,sizeof(struct sockaddr_in))==-1) 142 { 143 perror("connect 失败"); 144 exit(-1); 145 } 146 147 size=send(sockfd,(char *)&addr,sizeof(struct Addr),0);//传一个Addr信息过去 148 149 printf("连接到超级主机 %s:%d 上,本地打开地址 %s:%d ",super_server_host,super_server_port,addr.host,addr.port); 150 151 return 0; 152 } 153 154 155 156 int main(int argc,char *argv[]) 157 {       ...
    177 struct Addr addr; 178 179 180 if(argc != 5) 181 { 182 printf("usage: ./server [super-server host] [super-server port] [local_host] [local port] "); 183 exit(1); 184 } 185 strcpy(addr.host,argv[3]);//本机的IP或域名 186 addr.port=atoi(argv[4]);//本机的端口 187 server_register(argv[1],atoi(argv[2]),addr);//向super-server发送IP和端口,告诉super-server如果有client来连接,那就请把我的地址告诉它,让它来连接我。 188       ... 197 /*init sockaddr_in*/ 198 serverSockaddr.sin_family=AF_INET; 199 serverSockaddr.sin_port=htons(atoi(argv[4]));//改一下端口 200 serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY); 201 bzero(&(serverSockaddr.sin_zero),8); 202 203 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));       ... 234 while(1) 235 { 236 FD_ZERO(&servfd);//清空所有server的fd 237 FD_ZERO(&recvfd);//清空所有client的fd 238 FD_SET(sockfd,&servfd); 239 //timeout.tv_sec=30;//可以减少判断的次数 240 switch(select(max_servfd+1,&servfd,NULL,NULL,&timeout)) 241 { ...
    289 } 290 //FD_COPY(recvfd,servfd); 291 for(i=0;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表 292 {           ...
    297 } 298 299 switch(select(max_recvfd+1,&recvfd,NULL,NULL,&timeout)) 300 { ... ...
    408 }//end-switch 409 } 410 return 0; 411 }

      虽然我演示的时候所有的操作都是在一台机器上运行的。其实是可以多个机器同时协作运行的。修改185行处的IP就可以实现不同的机器了。

       程序的makefile

    1 main:
    2         gcc client.c -o client
    3         gcc server.c `mysql_config --cflags --libs` -o server
    4         gcc super-server.c -o super-server

      (No picture say a JB)接下来是程序运行时的截图。由于程序运行过程有点复杂,我们一步一步来。

      首先,运行super-server ,运行的命令是 ./super-server  默认打开的是12138这个端口进行监听,用于处理服务器和客户端的连接问题。

      然后就运行两个server程序,运行的命令分别是

    ./server localhost 12138 localhost 11111
    ./server localhost 12138 localhost 22222

      打开两个终端,分别输入,表示连接超级服务器super-server的12138端口,并且自己的服务器使用11111和22222进行监听。

      运行后三者的截图如下

      这样就两个服务器启动了,接下来是启动三个客户端,这样就能保证有两个是在同一个服务器中了,三者的运行命令分别如下

    ./client loclhost 12138 user1 123456
    ./client loclhost 12138 user2 123456
    ./client loclhost 12138 user3 123456

      表示连接到超级服务器super-server的12138服务端口,使用用户名密码验证。运行后截图如下

      从上图可以看到user1和user3是被分配到同一个服务器中去的。超级服务器中也实现了轮询的效果了。

      最后一步了,就是看看以前写的聊天功能还在不在了

      嗯,好了,由于client1和client3是在同一个服务器上,所以进行通讯是没有问题的,但是client2不在同一个服务器中,就通讯不了了。实现不同服务器上用户的通讯也不是很难,就是在服务器上增加一个服务器之间的转发功能就可以了。还有一个问题就是程序中为了方便,有很多地方没有进行合法性的判断,而且还有很多很多的BUG。

      

      小结:经过9天,实现了一个小小的聊天程序,有群聊功能,私聊功能,用户验证功能,指令系统功能,数据库连接问题,服务器负载问题。虽然内容没有什么高大上,但是对于一个初学者来说,想想就有点小激动。

      本系列Socket网络编程--聊天程序所有章节传送门如下:

      Socket网络编程--聊天程序(1) http://www.cnblogs.com/wunaozai/p/3870156.html
      Socket网络编程--聊天程序(2) http://www.cnblogs.com/wunaozai/p/3870194.html
      Socket网络编程--聊天程序(3) http://www.cnblogs.com/wunaozai/p/3870258.html
      Socket网络编程--聊天程序(4) http://www.cnblogs.com/wunaozai/p/3870338.html
      Socket网络编程--聊天程序(5) http://www.cnblogs.com/wunaozai/p/3871563.html
      Socket网络编程--聊天程序(6) http://www.cnblogs.com/wunaozai/p/3875506.html
      Socket网络编程--聊天程序(7) http://www.cnblogs.com/wunaozai/p/3876134.html
      Socket网络编程--聊天程序(8) http://www.cnblogs.com/wunaozai/p/3878374.html
      Socket网络编程--聊天程序(9) http://www.cnblogs.com/wunaozai/p/3880462.html

      所有开发过程中的代码: http://files.cnblogs.com/wunaozai/Socket-Chat.zip 

      因为每一个版本都是上一个版本的修改版,在学习的过程中,如果想知道这一小节增加了什么内容,可以用 vimdiff file1 file2 比较两个文件,就知道修改了哪些内容。

      

  • 相关阅读:
    ROW_NUMBER() OVER函数的基本用法
    oracle 中的next_day函数
    宽带大小与实际网速的关系:
    ora-29280 invalid directory path
    [spring]Attribute "scope" must be declared for element type "bean"
    什么是JDK,JRE,SDK,JVM以及API
    管理的常识: 让管理者发挥绩效的7个基本概念 读书笔记
    lua __index的简写
    lua中设置table={}时需要注意的坑
    摄像机旋转
  • 原文地址:https://www.cnblogs.com/wunaozai/p/3880462.html
Copyright © 2011-2022 走看看