这一节应该是聊天程序的最后一节了,现在回顾我们的聊天程序,看起来还有很多功能没有实现,但是不管怎么说,都还是不错的。这一节我们将讲多服务器问题(高大上的说法就是负载问题了。)至于聊天程序的文件发送(也即二进制文件发送例如图片)和单点登陆(就是多加一个数组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 代码修改如下

    ...
struct user
{
    ...
19 }; /*下面增加多服务器代码*/
struct Addr
{
char host[];
int port;
}; int query_addr(struct Addr *paddr,char *phost,int port)
{
int sockfd;
struct Addr addr;
struct hostent * host;
struct sockaddr_in servAddr;
int size;
host=gethostbyname(phost);
if(host==NULL)
{
perror("host 为空");
exit(-);
} if((sockfd=socket(AF_INET,SOCK_STREAM,))==-)
{
perror("socket 失败");
} servAddr.sin_family=AF_INET;
servAddr.sin_port=htons(port);
servAddr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(servAddr.sin_zero),); if(connect(sockfd,(struct sockaddr *)&servAddr,sizeof(struct sockaddr_in))==-)
{
perror("connect 失败");
exit(-);
}
memset(paddr,,sizeof(struct Addr));
size=recv(sockfd,(char *)paddr,sizeof(struct Addr),); return ;
} int main(int argc,char *argv[])
{
    ...
struct Addr addr; if(argc != )
{
perror("use: ./client [hostname] [prot] [username] [password]");
exit(-);
}
query_addr(&addr,argv[],atoi(argv[]));
printf("从服务器获取到的IP:%s\n\t\t端口:%d\n",addr.host,addr.port);
strcpy(use.name,argv[]);
strcpy(use.pwd,argv[]); host=gethostbyname(addr.host);
     ...
servAddr.sin_family=AF_INET;
servAddr.sin_port=htons(addr.port);
servAddr.sin_addr=*((struct in_addr *)host->h_addr);
//servAddr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(servAddr.sin_zero),); /*connect the socket*/
      ... ...
close(sockfd);
//kill(0,SIGKILL);//0表示同一进程组的进程 return ;
}

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

  增加一个super-server.c文件

 #include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h> #define SERVER_PORT 12138
#define BACKLOG 20
#define MAX_CON_NO 10
#define MAX_DATA_SIZE 4096 #define MAX_ADDR 64
struct Addr
{
char host[];
int port;
}; struct AddrList //保存所有可用的服务器IP地址和端口,flag表示该地址是否可用,因为服务器可能中途断开了。
{
int flag;
struct Addr addr;
}; int main(int argc,char *argv[])
{
struct sockaddr_in clientSockaddr;
int clientfd;
char sendBuf[MAX_DATA_SIZE];
int sendSize;
int sockfd;
int on;
int sinSize;
struct Addr addr;
struct AddrList addrlist[MAX_ADDR];
int addrlist_count=;
int i,ilist; memset(addrlist,,sizeof(addrlist)); if((sockfd=socket(AF_INET,SOCK_STREAM,))==-)
{
perror("创建socket失败");
exit(-);
} clientSockaddr.sin_family=AF_INET;
clientSockaddr.sin_port=htons(SERVER_PORT); //super-server默认使用12138作为服务端口
clientSockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bzero(&(clientSockaddr.sin_zero),); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); if(bind(sockfd,(struct sockaddr *)&clientSockaddr,sizeof(struct sockaddr))==-)
{
perror("bind 失败");
exit(-);
} //backlog是积压值,对于TCP,通常建立连接时,会有3/4次握手的过程,一个client连接在完成了建立连接的握手过程,而还没有被应用层(应用程序)所响应时,这个连接被置于backlog队列中。当达到backlog队列以满时,client的连接请求会返回超时的错误。
if(listen(sockfd,)==-)
{
perror("listen 失败");
exit(-);
} sinSize=sizeof(clientSockaddr); for(i=;i<MAX_ADDR;i++)
{
addrlist[i].flag=;
} for(i=;i<;i++)//这里先固定成两个,可以修改成select然后实现动态增加服务器和减少服务器
{
//加入进来的服务器server
if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr,&sinSize))==-)
{
perror("accept 失败");
exit(-);
} if((sendSize=recv(clientfd,(char *)&addr,sizeof(struct Addr),))!=sizeof(struct Addr))
{
perror("send 失败");
exit(-);
}
printf("server发过来的地址 %s:%d\n",addr.host,addr.port);
addrlist[i].flag=;
strcpy(addrlist[i].addr.host,addr.host);//保存服务器ip/端口信息到super-server中
addrlist[i].addr.port=addr.port;
close(clientfd);
}
/*
addrlist[0].flag=1;
addrlist[1].flag=1;
strcpy(addrlist[0].addr.host,"localhost");
strcpy(addrlist[1].addr.host,"localhost");
addrlist[0].addr.port=12137;
addrlist[1].addr.port=12139;
*/ ilist=;
i=;
while()
{
/*分配域名/IP和端口*//*分配的方法是轮询*/
i=ilist+;
while(i<MAX_ADDR)
{
if(addrlist[i].flag!=)
{
ilist=i;
break;
}
i++;
i=i%MAX_ADDR;
} strcpy(addr.host,addrlist[ilist].addr.host);
addr.port=addrlist[ilist].addr.port;
printf("发送给客户端的id=%d 域名/IP:%s port:%d \n",ilist,addr.host,addr.port); if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr,&sinSize))==-)
{
perror("accept 失败");
exit(-);
} if((sendSize=send(clientfd,(char *)&addr,sizeof(struct Addr),))!=sizeof(struct Addr))
{
perror("send 失败");
exit(-);
}
close(clientfd);
} return ;
}

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

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

  最后一个代码是server.c

    ...
struct user
{
    ...
}; int MAX(int a,int b)
{
    ...
} void print_time(char * ch,time_t *now)
{
    ...
} int mysql_check_login(struct user su)
{
     ...
return ;
} //根据用户名返回该用户名在fd_A中的位置
//fd=-1,表示没有该用户 //fd>0 正常返回
int fd_ctoa(char fd_C[][],char *ch)
{
    ...
} /*下面部分是多服务器增加的代码*/
struct Addr
{
char host[];
int port;
}; int server_register(char *super_server_host,int super_server_port,struct Addr addr)
{
int sockfd;
struct hostent * host;
struct sockaddr_in servAddr;
int size;
host=gethostbyname(super_server_host);
if(host==NULL)
{
perror("host 为空");
exit(-);
} if((sockfd=socket(AF_INET,SOCK_STREAM,))==-)
{
perror("socket 失败");
} servAddr.sin_family=AF_INET;
servAddr.sin_port=htons(super_server_port);
servAddr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(servAddr.sin_zero),); if(connect(sockfd,(struct sockaddr *)&servAddr,sizeof(struct sockaddr_in))==-)
{
perror("connect 失败");
exit(-);
} size=send(sockfd,(char *)&addr,sizeof(struct Addr),);//传一个Addr信息过去 printf("连接到超级主机 %s:%d 上,本地打开地址 %s:%d\n",super_server_host,super_server_port,addr.host,addr.port); return ;
} int main(int argc,char *argv[])
{
      ...
struct Addr addr; if(argc != )
{
printf("usage: ./server [super-server host] [super-server port] [local_host] [local port]\n");
exit();
}
strcpy(addr.host,argv[]);//本机的IP或域名
addr.port=atoi(argv[]);//本机的端口
server_register(argv[],atoi(argv[]),addr);//向super-server发送IP和端口,告诉super-server如果有client来连接,那就请把我的地址告诉它,让它来连接我。       ...
/*init sockaddr_in*/
serverSockaddr.sin_family=AF_INET;
serverSockaddr.sin_port=htons(atoi(argv[]));//改一下端口
serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bzero(&(serverSockaddr.sin_zero),); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
      ...
while()
{
FD_ZERO(&servfd);//清空所有server的fd
FD_ZERO(&recvfd);//清空所有client的fd
FD_SET(sockfd,&servfd);
//timeout.tv_sec=30;//可以减少判断的次数
switch(select(max_servfd+,&servfd,NULL,NULL,&timeout))
{
...
}
//FD_COPY(recvfd,servfd);
for(i=;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表
{
          ...
} switch(select(max_recvfd+,&recvfd,NULL,NULL,&timeout))
{
... ...
}//end-switch
}
return ;
}

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

  程序的makefile

 main:
gcc client.c -o client
gcc server.c `mysql_config --cflags --libs` -o server
gcc super-server.c -o super-server

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

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

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

./server localhost  localhost
./server localhost localhost

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

  运行后三者的截图如下

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

./client loclhost  user1
./client loclhost user2
./client loclhost user3

  表示连接到超级服务器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 比较两个文件,就知道修改了哪些内容。

  

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

  1. Socket网络编程--聊天程序(1)

    很早的一段时间,看了APUE和UNPv1了解了网络编程,但是但是只是看而已,没有具体的实践,趁现在没有什么事做,就来实践了解一下网络编程.写博客保存下来,方便以后用到的时候可以查到. 此次的聊天程序是 ...

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

    上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能.就是通过服务器对客户端的数据进行转发到特定的用户上, 实现私聊功能的聊天程序 实现的 ...

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

    这一小节将增加一个用户的结构体,用于保存用户的用户名和密码,然后发给服务器,然后在服务器进行判断验证.这里就有一个问题,以前讲的就是发送字符串是使用char类型进行传输,然后在服务器进行用同样是字符串 ...

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

    接上一小节,本来是计划这一节用来讲数据库的增删改查,但是在实现的过程中,出现了一点小问题,也不是技术的问题,就是在字符界面上比较不好操作.比如要注册一个帐号,就需要弄个字符界面提示,然后输入数字表示选 ...

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

    上一小节,已经讲到可以每个人多说话,而且还没有限制,简单的来说,我们已经完成了聊天的功能了,那么接下来我们要实现什么功能呢?一个聊天程序至少应该支持一对多的通讯吧,接下来就实现多个客户端往服务器发送数 ...

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

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

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

    上一小节我们讲了使用select来避免使用多进程的资源浪费问题.上次只是实现了从多个客户端发送数据给服务器端,接下来就要实现从服务器端发送数据给各个客户端. 使用select多路转换处理聊天程序2 c ...

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

    上一节简单如何通过Socket创建一个连接,然后进行通信.只是每个人只能说一句话.而且还是必须说完才会接收到信息,总之是很不方便的事情.所以这一小节我们将对上一次的程序进行修改,修改成每个人可以多说话 ...

  9. Socket网络编程--小小网盘程序(5)

    各位好呀!这一小节应该就是这个小小网盘程序的最后一小节了,这一节将实现最后的三个功能,即列出用户在服务器中的文件列表,还有删除用户在服务器中的文件,最后的可以共享文件给好友. 列出用户在服务器中的文件 ...

随机推荐

  1. long long or int

    long long or int 很多时候long long爆空间,int有时又不够 . 在算乘法的时候,要保证乘出来的中间项也不爆long long

  2. ESLint + lint-staged 禁用老项目中的es6

    前言 ESLint作为插件化的javascript代码检测工具,为我们的平时的开发保驾护航,好处就不多说了详情查看官网. 问题 有这么一个五年前开发的老项目,机缘巧合到了我们这边来维护. 项目是zep ...

  3. 【Java并发核心四】Executor 与 ThreadPoolExecutor

    Executor 和 ThreadPoolExecutor 实现的是线程池,主要作用是支持高并发的访问处理. Executor 是一个接口,与线程池有关的大部分类都实现了此接口. ExecutorSe ...

  4. 4923: [Lydsy1706月赛]K小值查询 平衡树 非旋转Treap

    国际惯例的题面:这种维护排序序列,严格大于的进行操作的题都很套路......我们按照[0,k],(k,2k],(2k,inf)分类讨论一下就好.显然第一个区间的不会变化,第二个区间的会被平移进第一个区 ...

  5. cf348D. Turtles(LGV定理 dp)

    题意 题目链接 在\(n \times m\)有坏点的矩形中找出两条从起点到终点的不相交路径的方案数 Sol Lindström–Gessel–Viennot lemma的裸题? 这个定理是说点集\( ...

  6. 洛谷.2051.[AHOI2009]中国象棋(DP)

    题目链接 /* 每行每列不能超过2个棋子,求方案数 前面行对后面行的影响只有 放了0个.1个.2个 棋子的列数,与排列方式无关 所以设f[i][j][k]表示前i行,放了0个棋子的有j列,放了1个棋子 ...

  7. Python常见下划线

    python中常见的下划线意义 Python中常常使用下划线里对变量进行修饰,通常作为变量的前缀或者后缀出现,被修饰的变量一般存在特殊的用法: _XXX:不能被from module import _ ...

  8. JavaScript简易教程

    这是我所知道的最完整最简洁的JavaScript基础教程. 这篇文章带你尽快走进JavaScript的世界——前提是你有一些编程经验的话.本文试图描述这门语言的最小子集.我给这个子集起名叫做“Java ...

  9. [原创]H5前端性能测试工具介绍

    [原创H5前端性能测试工具介绍 一 网络抓包工具 网络抓包工具选择原则,可以捕获网络请求,抓取具体请求信息流,同时可以针对网络请包进行修改或拦截: 1.Fiddler(推荐) 2.Charles(推荐 ...

  10. C#编程(八十)---------- 异常类

    异常类 在C#里,异常处理就是C#为处理错误情况提供的一种机制.它为每种错误情况提供了定制的处理方式,并且把标志错误的代码预处理错误的代码分离开来. 对.net类来说,一般的异常类System.Exc ...