這一節應該是聊天程序的最后一節了,現在回顧我們的聊天程序,看起來還有很多功能沒有實現,但是不管怎么說,都還是不錯的。這一節我們將講多服務器問題(高大上的說法就是負載問題了。)至於聊天程序的文件發送(也即二進制文件發送例如圖片)和單點登陸(就是多加一個數組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\n\t\t端口:%d\n",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\n",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 \n",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\n",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]\n"); 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 比較兩個文件,就知道修改了哪些內容。