C++ socket 網絡編程 簡單聊天室


操作系統里的進程通訊方式有6種:(有名/匿名)管道、信號、消息隊列、信號量、內存(最快)、套接字(最常用),這里我們來介紹用socket來實現進程通訊。

 

1、簡單實現一個單向發送與接收

這是套接字的工作流程

(對於有時間想慢慢看的推薦這篇博客:https://www.cnblogs.com/kefeiGame/p/7246942.html)

 (不想自己畫一遍,直接用別人的)

 

我們現在先來實現套接字對同一主機的通訊。(代碼注釋比較全

服務器(虛擬機[Ubuntu]):

  1 #include <unistd.h>
  2 #include <string.h>
  3 #include <iostream>
  4 #include <arpa/inet.h>
  5 #include <sys/socket.h>
  6 
  7 #define MYPORT 1223///開應一個端口
  8 #define IP "**.**.**.**"///你用的服務器的IPv4地址,這里我用了虛擬機(ubuntu)的地址
  9 #define BACKLOG 10
 10 #define getLen(zero) sizeof(zero) / sizeof(zero[0]) ///得到數組最大大小
 11 using namespace std;
 12 
 13 int main() {
 14     int sockfd, new_fd;
 15     struct sockaddr_in my_addr;
 16     puts("SERVER:");
 17     if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) {
 18         ///socket()函數發生錯誤則返回-1,否則會返回套接字文件描述符
 19         ///對於int socket(int domain, int type, int protocol);中的參數想要詳細了解可以看這篇博客:https://blog.csdn.net/liuxingen/article/details/44995467
 20 
 21         perror("socket():");///顯示錯誤
 22         return 0;
 23     }
 24     my_addr.sin_family = AF_INET;///通訊在IPv4網絡通信范圍內
 25     my_addr.sin_port = htons(MYPORT);///我的端口
 26     my_addr.sin_addr.s_addr = inet_addr(IP);///用來得到一個32位的IPv4地址,inet_addr將"127.0.0.1"轉換成s_addr的無符號整型。
 27     bzero(&(my_addr.sin_zero), getLen(my_addr.sin_zero));///sin_zero是為了讓sockaddr與sockaddr_in兩個數據結構保持大小相同而保留的空字節。
 28 
 29     /**
 30         借用以下代碼得到了my_addr.sin_addr.s_addr的類型是無符號整型
 31         unsigned int a;
 32         if(typeid(a) == typeid(my_addr.sin_addr.s_addr)){
 33             puts("Yes");
 34         }
 35     **/
 36 
 37 
 38     if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {///bind()函數將套接字與該IP:端口綁定起來。
 39         perror("bind():");
 40         return 0;
 41     }
 42     if(listen(sockfd, BACKLOG) == -1) {///啟動監聽,等待接入請求,BACKLOG是在進入隊列中允許的連接數目
 43         perror("listen():");
 44         return 0;
 45     }
 46 
 47     socklen_t sin_size;
 48     struct sockaddr_in their_addr;
 49     if((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
 50         ///當你監聽到一個來自客戶端的connect請求時,要選擇是將他放在請求隊列里還是允許其連接,我這里寫的其實是單進客戶的,所以說無等待。
 51         ///這個函數還返回了一個新的套接字,用於與該進程通訊。
 52         ///還有一點是之前推薦的c++中的socket編程(入門),該博客里寫的sin_size類型是int,可是實際上我在linux的C++環境下出現錯誤,類型要是socklen_t。
 53         perror("accept():");
 54         return 0;
 55     }
 56     printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr));///inet_ntoa可以將inet_addr函數得到的無符號整型轉為字符串IP
 57 
 58     char str[1007];
 59 
 60     while(1) {///循環發送 以endS結束與這一進程的通訊,endS也作為客戶端停止工作的標志送出
 61         puts("send:");
 62         scanf("%s", str);
 63         if(send(new_fd, str, strlen(str), 0) == -1) {
 64             ///send()函數,new_fd是accept返回的套接字文件描述符,str就你要發送的數據,數據長度,對於最后一位flag
 65             /// flags取值有:
 66             /// 0: 與write()無異(我自己也不知道什么意思,大概就是常規操作,以下提供幾種flag值的定義,然后下面是這類宏定義的源碼)
 67 
 68 ///            MSG_DONTROUTE 繞過路由表查找。
 69 ///            MSG_DONTWAIT 僅本操作非阻塞。
 70 ///            MSG_OOB 發送或接收帶外數據。
 71 ///            MSG_PEEK 窺看外來消息。
 72 ///            MSG_WAITALL 等待所有數據。
 73 ///
 74 ///            源碼里沒找到0x00的定義,所以說我將其當作默認參數
 75 ///            enum
 76 ///              {
 77 ///                MSG_OOB             = 0x01, /// Process out-of-band data.
 78 ///            #define MSG_OOB         MSG_OOB
 79 ///                MSG_PEEK            = 0x02, /// Peek at incoming messages.
 80 ///            #define MSG_PEEK        MSG_PEEK
 81 ///                MSG_DONTROUTE       = 0x04, /// Don't use local routing.
 82 ///            #define MSG_DONTROUTE   MSG_DONTROUTE
 83 ///            #ifdef __USE_GNU
 84 ///                /// DECnet uses a different name.
 85 ///                MSG_TRYHARD         = MSG_DONTROUTE,
 86 ///            # define MSG_TRYHARD    MSG_DONTROUTE
 87 ///            #endif
 88 ///                MSG_CTRUNC          = 0x08, /// Control data lost before delivery.
 89 ///            #define MSG_CTRUNC      MSG_CTRUNC
 90 ///                MSG_PROXY           = 0x10,  /// Supply or ask second address.
 91 ///            #define MSG_PROXY       MSG_PROXY
 92 ///                MSG_TRUNC           = 0x20,
 93 ///            #define MSG_TRUNC       MSG_TRUNC
 94 ///                MSG_DONTWAIT        = 0x40,  /// Nonblocking IO.
 95 ///            #define MSG_DONTWAIT    MSG_DONTWAIT
 96 ///                MSG_EOR             = 0x80,  /// End of record.
 97 ///            #define MSG_EOR         MSG_EOR
 98 ///                MSG_WAITALL         = 0x100,  /// Wait for a full request.
 99 ///            #define MSG_WAITALL     MSG_WAITALL
100 ///                MSG_FIN             = 0x200,
101 ///            #define MSG_FIN         MSG_FIN
102 ///                MSG_SYN             = 0x400,
103 ///            #define MSG_SYN         MSG_SYN
104 ///                MSG_CONFIRM         = 0x800,  /// Confirm path validity.
105 ///            #define MSG_CONFIRM     MSG_CONFIRM
106 ///                MSG_RST             = 0x1000,
107 ///            #define MSG_RST         MSG_RST
108 ///                MSG_ERRQUEUE        = 0x2000,  /// Fetch message from error queue.
109 ///            #define MSG_ERRQUEUE    MSG_ERRQUEUE
110 ///                MSG_NOSIGNAL        = 0x4000,  /// Do not generate SIGPIPE.
111 ///            #define MSG_NOSIGNAL    MSG_NOSIGNAL
112 ///                MSG_MORE            = 0x8000,   /// Sender will send more.
113 ///            #define MSG_MORE        MSG_MORE
114 ///                MSG_WAITFORONE      = 0x10000,  /// Wait for at least one packet to return.
115 ///            #define MSG_WAITFORONE  MSG_WAITFORONE
116 ///                MSG_BATCH           = 0x40000,  /// sendmmsg: more messages coming.
117 ///            #define MSG_BATCH       MSG_BATCH
118 ///                MSG_ZEROCOPY        = 0x4000000, /// Use user data in kernel path.
119 ///            #define MSG_ZEROCOPY    MSG_ZEROCOPY
120 ///                MSG_FASTOPEN        = 0x20000000, /// Send data in TCP SYN.
121 ///            #define MSG_FASTOPEN    MSG_FASTOPEN
122 ///
123 ///                MSG_CMSG_CLOEXEC    = 0x40000000    /// Set close_on_exit for file
124 ///                                                       ///descriptor received through
125 ///                                                       ///SCM_RIGHTS.
126 ///            #define MSG_CMSG_CLOEXEC MSG_CMSG_CLOEXEC
127 ///              };
128 
129             perror("send():");
130             close(new_fd);///發送失敗就關閉該通訊
131             return 0;
132         }
133         if(!strcmp("endS", str))
134             break;
135     }
136     close(new_fd);///正常結束要關閉這些已建立的套接字
137     close(sockfd);
138 
139     return 0;
140 }
linux環境的服務端

 客戶端(虛擬機[Ubuntu]):(linux環境的客戶端)

 1 #include <unistd.h>
 2 #include <string.h>
 3 #include <iostream>
 4 #include <arpa/inet.h>
 5 #include <sys/socket.h>
 6 
 7 #define PORT 1223/// 客戶機連接遠程主機的端口
 8 #define MAXDATASIZE 100 /// 每次可以接收的最大字節
 9 #define IP "**.**.**.**"
10 #define getLen(zero) sizeof(zero)/sizeof(zero[0])
11 using namespace std;
12 
13 int main( ) {
14 
15     int sockfd, numbytes;
16     char buf[MAXDATASIZE];///緩存接收內容
17     struct sockaddr_in their_addr;///和my_addr用法差不多
18 
19     puts("USER:");
20     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
21         perror("socket():");
22         return 0;
23     }
24 
25     their_addr.sin_family = AF_INET;
26     their_addr.sin_port = htons(PORT);
27 
28     their_addr.sin_addr.s_addr = inet_addr(IP);
29     bzero(&(their_addr.sin_zero),getLen(their_addr.sin_zero));
30     if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {
31         ///在客戶端這里我們不需要綁定什么東西,因為我們只要向目的IP:端口發起連接請求
32 
33         perror("connect():");
34         return 0;
35     }
36     while(1) {///循環接收
37         if((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {///recv函數,套接字文件描述符,接收到這字符串里,最大長度,flag(之前有解釋);
38             perror("recv():");
39             return 0;
40         }
41         buf[numbytes] = '\0';
42         if(!strcmp(buf, "endS")) {///接收到endS兩邊一起結束
43             break;
44         }
45         cout<<"Received: "<<buf<<endl;///輸出接收的字符
46     }
47     close(sockfd);
48     return 0;
49 
50 }
linux環境的客戶端

 

接下來我們把這個客戶端移植到windows操作系統下,代碼肯定是要有小改動的。但是這個是最后的操作,我們一步步來:

讓虛擬機和本機能夠ping通(這個我一開始在網絡上找 博客,然后都沒用,后面把虛擬機的虛擬網絡編輯器恢復默認就可以了,所以說建議自己嘗試解決);

因為主機和虛擬機可以用IP地址(IPv4)ping通,也就是可以訪問該ip,那么我們的服務器就要在那個客戶端(主機)可訪問的IP上拿一個端口出來用來通訊。

所以說我們服務器的IP地址要選ifconfig指令里看到的虛擬機里的IPv4地址。

接下來開始移植,其實基本思想和代碼結構完全沒變。

客戶端(windows)

 1 #include <iostream>
 2 #include <stdlib.h>
 3 #include <winsock2.h>
 4 #pragma comment(lib,"ws2_32.lib")
 5 ///我在codeblocks下不可以運行這些是因為這個libws2_32.a找不到
 6 ///解決方法:Settings->compiler->Global compiler settings->(找到)Linker settings(橫着排開的目錄)->Add->去MinGW/lib找到libws2_32.a就可以了
 7 
 8 
 9 #define PORT 1223/// 客戶機連接遠程主機的端口
10 #define MAXDATASIZE 100 /// 每次可以接收的最大字節
11 using namespace std;
12 
13 int main( ) {
14     WORD sockVersion = MAKEWORD(2,2);
15     WSADATA wsaData;
16     if(WSAStartup(sockVersion, &wsaData)!=0){
17         return 0;
18     }
19     ///windows環境下的Winsock是要初始化的;即:固定代碼。
20 
21     int sockfd, numbytes;
22     char buf[MAXDATASIZE];
23     struct hostent *he;
24     struct sockaddr_in their_addr;
25 
26     puts("USER:");
27     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
28         cout<<WSAGetLastError()<<endl;///這個可以輸出WSAError號
29         perror("socket");
30         return 0;
31     }
32 
33     their_addr.sin_family = AF_INET;
34     their_addr.sin_port = htons(PORT);
35 
36     their_addr.sin_addr.s_addr = inet_addr("**.**.**.**");
37     memset(their_addr.sin_zero, 0, sizeof(their_addr.sin_zero));
38     if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {
39         perror("connect");
40         return 0;
41     }
42     while(1) {
43         if((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
44             perror("recv");
45             return 0;
46         }
47         buf[numbytes] = '\0';
48         if(!strcmp(buf, "endS")) {
49             break;
50         }
51         cout<<"Received: "<<buf<<endl;
52     }
53     closesocket(sockfd);///函數不同
54     return 0;
55 }
windows環境的客戶端

試着通訊,應該是沒問題的!!(至少本地沒問題)

不想用windows客戶端的人可以用telnet [ip] [port]指令來連接到服務器,前提是服務器使用的ip地址是你能ping通的ip地址

 

 2、實現一個可以發送接收的客戶端以及轉發消息的服務端

一個聊天室很明顯是有多個客戶端在一個服務器的協助下進行聊天,就是一個人發一句消息,服務器向所有人發送一遍消息,所有人的客戶端接收消息,也就是服務器負責接收轉發,客戶端也是接收和發送。

 

接下來我們就要學習一下怎么收發同時進行了,為了實現這一方法,我們可以這樣:

我對一個事件(如讀或寫),不等到發生或者異常就不結束他,服務器讀時一直等待客戶端的寫,反之同理,很顯然,這不現實,可能對方並不想回你,但你想發信息給他,但是你此時就做不到。

那么我們就要用I/O多路復用模型了,我先用最簡單的select來實現多路復用。(下面簡單的從源碼解釋了poll和select的工作方式)

關於select()函數
這個函數的作用是每次輪詢一遍保存好的套接字集合看是否有事件發生(讀、寫、異常)。
但是因為select每次可以傳入的文件描述符集大小只有1024位,所以說這個函數能監聽的大小只有1024,至於為什么是1024呢,我們來看看源碼對fd_set的定義:
    typedef long int __fd_mask;
    #define __FD_SETSIZE 1024
    #define _NFDBITS (8*(int)sizeof(__fd_mask))
    
這個是fd_set內的成員,上面有所需宏定義
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    得到的結論是,成員為:long int fds_bits[1024 / __NFDBITS],long int字節數為x
    則該變量字節數為 x*1024/x,即不管64位機還是32位機都是1024位。所以說最多保存1024個文件描述符。

poll函數與select函數的不同則是他不是用這樣的壓位的方式來保存文件描述符,它采用的結構體如下:
    struct pollfd{
    int fd;///文件描述符
    short int events;///輪詢時關心的事件種類,種類在下面給源碼
    short int revents;///實際發生的事件
};
事件的定義
#define POLLIN        0x001        /* 有數據要讀 */
#define POLLPRI        0x002        /*有緊急數據要讀  */
#define POLLOUT        0x004        /*現在寫入不會阻塞 */
# define POLLRDNORM    0x040        /* 可以讀正常數據*/
# define POLLRDBAND        0x080        /* 可以讀取優先數據 */
# define POLLWRNORM    0x100        /* 現在寫入不會阻塞  */
# define POLLWRBAND    0x200        /* 可以寫入優先數據  */
    /* These are extensions for Linux.  */
# define POLLMSG    0x400    
# define POLLREMOVE    0x1000
# define POLLRDHUP    0x2000這三個是linux的擴展,有興趣自己去查,注釋是源碼里的說明

#define POLLERR        0x008        /*錯誤條件  */
#define POLLHUP        0x010        /* 掛起  */
#define POLLNVAL    0x020        /* 無效輪詢請求  */
看了這些宏定義,接下來就是他的用法:
我關心這個對象的讀取狀態那么 client.events = POLLIN;
我關系讀和寫的話client.events = POLLIN | POLLOUT;
判斷可讀 client.revents & POLLIN 為true就是可讀;

之前select用的是一個fd_set 這個poll函數則是傳入一個pollfd 指針(即可以是數組)進去:
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
第一個剛剛說明過,第二個是最大文件描述符大小+1,第三個是毫秒等待;
這個函數返回:大於零即是發生的事件個數,為零則是超時,-1異常;

這個函數的用法理解了select的用法其實是一樣的,當連接的客戶端多的時候會產生很大的延遲,因為是每次都輪詢的,這個缺點和select一樣;
select和poll的分析

 

select就是將你關系的文件描述符集以及想得到的(讀、寫、異常)結果集以及等待事件給他,返回給你發生事件的文件描述符的個數,以及(讀、寫、異常)結果集給你來判斷要對該客戶端執行何種操作。

直接看服務器代碼:(linux環境服務端)

  1 #include <string>
  2 #include <errno.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <iostream>
  6 #include <arpa/inet.h>
  7 #include <sys/socket.h>
  8 #include <sys/select.h>
  9 
 10 #define lisnum 10///最大連接客戶端
 11 #define myport 1223///隨意的一個(>1024)端口號
 12 #define maxnum 1007///最大字節接收數
 13 #define myip "**.**.**.**"///這個ip可以被ping到就可以用
 14 using namespace std;
 15 
 16 class keyNode{///這個類存儲了連接上的客戶端分配的文件描述符以及客戶端的昵稱
 17 public:
 18     int clientfd;
 19     string name;
 20     keyNode(): clientfd(0), name(""){}///構造
 21     void init() {///初始化
 22         this->clientfd=0;
 23         this->name="";
 24     }
 25     keyNode& operator = (const keyNode& tmp) {///重載拷貝賦值
 26         if(&tmp != this) {
 27             this->clientfd=tmp.clientfd;
 28             this->name=tmp.name;
 29         }
 30         return *this;
 31     }
 32 };
 33 
 34 inline void init(const keyNode client[], const int &sockfd) {///初始化,關閉所有客戶端連接
 35     for(int i = 0; i < lisnum; ++ i) {
 36         if(client[i].clientfd != 0) {
 37             close(client[i].clientfd);
 38         }
 39     }
 40     close(sockfd);
 41 }
 42 
 43 inline void allSend(const keyNode client[], const char buffer[], const int &maxn, const int &now) {
 44 ///將信息送到除了發送者以外的所有客戶端上,參數:client[]是自定義類數組,buffer[]是傳輸字符串,maxn是目前連接數,now是發送者於client[]里的下標
 45     if(buffer[0] == '\0')///長度為0不轉發
 46         return ;
 47     for(int i = 0; i < maxn; ++ i) {
 48         if(i != now)
 49             send(client[i].clientfd, buffer, strlen(buffer), 0);
 50     }
 51 }
 52 
 53 int main() {
 54     int sockfd;
 55     struct sockaddr_in my_addr;
 56     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
 57         perror("socket()");
 58         return 0;
 59     }
 60     cout<<"socket ok"<<endl;
 61     my_addr.sin_family = AF_INET;
 62     my_addr.sin_port = htons(myport);
 63     my_addr.sin_addr.s_addr = inet_addr(myip);
 64 
 65     memset(my_addr.sin_zero, 0, sizeof(my_addr.sin_zero));
 66     if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
 67         perror("bind()");
 68         return 0;
 69     }
 70     puts("bind ok");
 71     if(listen(sockfd, lisnum) == -1) {
 72         perror("lisnum()");
 73         return 0;
 74     }
 75     cout<<"listen ok"<<endl;
 76     /****************與之前的無異***************************/
 77 
 78     fd_set clientfd;///select
 79     clientfd.
 80     int maxsock;///
 81     keyNode IDkey[lisnum];///保存客戶端的文件描述符和昵稱
 82 
 83     int cntfd = 0;///記錄客戶端個數
 84     maxsock = sockfd;//
 85     char buffer[maxnum];
 86     string res;///用來記錄一些臨時值傳輸
 87     int len = 0;///保存接收字符的長度
 88     while(true) {
 89         FD_ZERO(&clientfd);///將文件描述符集置零
 90 
 91         FD_SET(sockfd, &clientfd);///源碼中是將0~1023大小的文件描述符直接保存到相應的位里(該位置1)
 92 
 93         for(int i = 0; i < lisnum; ++i) {///將目前有的客戶端的文件描述符也加入到集合中
 94             if(IDkey[i].clientfd != 0) {
 95                 FD_SET(IDkey[i].clientfd, &clientfd);
 96             }
 97         }
 98         if(select(maxsock + 1, &clientfd, NULL, NULL, NULL) < 0) {
 99         ///自己沒找到select的源碼,於是去百度了一波:https://blog.csdn.net/u010601662/article/details/78922557
100         ///第一個參數是所有文件描述符的最大值+1
101         ///第二三四個參數指向文件描述符集(讀、寫、異常集)   ///只有讀文件描述集的原因是因為我們的服務器只需要讀客戶端發的消息,然后轉發給其他客戶端
102         ///第四個是等待時間,該類有long    tv_sec;秒  long    tv_usec;毫秒 兩個參數
103             perror("select()");
104             break;
105         }
106 
107         for(int i = 0; i < cntfd; ++i) {
108             if(FD_ISSET(IDkey[i].clientfd, &clientfd)) {///檢查關心的文件描述符是否有讀事件發生
109                 len = recv(IDkey[i].clientfd, buffer, maxnum, 0);
110                 if(len <= 0) {///發生錯誤 否則正常情況會返回接收到的流的長度
111                     close(IDkey[i].clientfd);
112                     FD_CLR(IDkey[i].clientfd, &clientfd);///將該文件描述符刪除
113 
114                     IDkey[i] = IDkey[--cntfd];///最后一位填補上他的空位,保持有用的信息全部連着
115                     IDkey[cntfd].init();
116                 } else {
117                     buffer[len] = '\0';
118                     res = IDkey[i].name + ": " + buffer;///信息發送格式。
119                     allSend(IDkey, res.c_str(), cntfd, i);///全廣播
120                 }
121             }
122         }
123 
124         if(FD_ISSET(sockfd, &clientfd)) {///檢查服務端的文件描述符是否有讀事件,有的話表示有新連接請求
125             struct sockaddr_in client_addr;
126             socklen_t sizes=1;
127             int sock_client = accept(sockfd, (struct sockaddr*)(&client_addr), &sizes);///接受
128             if(sock_client < 0) {
129                 perror("accept()");
130                 continue;
131             }
132             if(cntfd < lisnum) {///只有小於限定大小才讓添加
133                 IDkey[cntfd++].clientfd = sock_client;
134 
135                 strcpy(buffer, "this is server!\n");
136                 send(sock_client, buffer, strlen(buffer), 0);///提示信息
137                 ///cout<<"new connection client["<<cntfd - 1<<"] "<<inet_ntoa(client_addr.sin_addr)<<":"<<ntohs(client_addr.sin_port)<<endl;
138                 memset(buffer, 0, sizeof(buffer));
139                 len = recv(sock_client, buffer, maxnum, 0);
140                 if(len < 0) {
141                     perror("revc()");
142                     init(IDkey, sockfd);
143                     return 0;
144                 }
145                 buffer[len] = '\0';
146                 IDkey[cntfd - 1].name = buffer;///我的客戶端默認連上第一件事是發送昵稱(簡陋的實現方法)
147                 strcat(buffer, " join the chatroom\n");
148                 allSend(IDkey, buffer, cntfd, cntfd - 1);///對客戶端提示有人加入了聊天室
149                 maxsock = max(maxsock, sock_client);///更新文件描述符最大值
150             } else {
151                 cout<<"over the max connections"<<endl;
152             }
153         }
154     }
155     init(IDkey, sockfd);///全部斷開
156     return 0;
157 }
linux環境的服務端

 

 我們搭建好了一個服務端,也就是說現在就差一堆客戶端了!那么我們聊天的時候當然是用中文了,但是linux系統下命令行顯示中文的編碼會亂碼!我也懶得設置什么,於是就想直接寫個windows的服務端來用。

windows環境客戶端:(自己根據自己的IDE來多開客戶端吧!)

 1 #include <thread>
 2 #include <iostream>
 3 #include <stdlib.h>
 4 #include <winsock2.h>
 5 #pragma comment(lib,"ws2_32.lib")
 6 
 7 
 8 #define PORT 1223
 9 #define maxnum 100
10 using namespace std;
11 
12 
13 
14 void recvMsg(const int &sockfd) {///接收信息
15     char buf[maxnum];
16     while(true) {
17         int num = recv(sockfd, buf, maxnum, 0);
18         if(num == -1) {
19             perror("recv()");
20             return ;
21         }
22         buf[num]='\0';
23         cout<<buf<<endl;
24     }
25     return ;
26 }
27 void init(const int &sockfd) {///發送初始化信息,即昵稱
28     char str[10] = "Thanks_up";
29     if(send(sockfd, str, strlen(str), 0) == -1) {
30         perror("send()");
31     }
32     return ;
33 }
34 
35 int main( ) {
36     WORD sockVersion = MAKEWORD(2,2);
37     WSADATA wsaData;
38     if(WSAStartup(sockVersion, &wsaData)!=0){
39         return 0;
40     }
41 
42     int sockfd, numbytes;
43     char buf[maxnum];
44     struct hostent *he;
45 
46     struct sockaddr_in their_addr;
47 
48     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
49         cout<<WSAGetLastError()<<endl;
50         perror("socket");
51         return 0;
52     }
53 
54     their_addr.sin_family = AF_INET;
55     their_addr.sin_port = htons(PORT);
56     their_addr.sin_addr.s_addr = inet_addr("**.**.**.**");
57     memset(their_addr.sin_zero, 0, sizeof(their_addr.sin_zero));
58 
59     if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {
60         perror("connect");
61         return 0;
62     }
63     char str[1007];
64     init(sockfd);
65     thread taskRecv(recvMsg, sockfd);///將讀和寫分成兩個線程來執行,在分得的時間片內可以看似並行的完成讀寫任務
66     taskRecv.detach();///讓這個線程不阻塞
67     while(true) {
68         scanf("%s", str);
69         if(send(sockfd, str, strlen(str), 0) == -1) {
70             perror("send()");
71             break;
72         }
73     }
74     closesocket(sockfd);
75     return 0;
76 }
windows環境的客戶端

我測試是沒問題的

 

留下一個linux的客戶端:

 1 #include <unistd.h>
 2 #include <string.h>
 3 #include <iostream>
 4 #include <pthread.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define PORT 1223
 9 #define maxnum 100
10 #define IP "192.168.50.130"
11 #define getLen(zero) sizeof(zero)/sizeof(zero[0])
12 using namespace std;
13 
14 void init(const int &sockfd){
15     if(send(sockfd, "xiejin", 9, 0) == -1) {
16         perror("send()");
17         close(sockfd);
18         return ;
19     }
20 }
21 
22 void* recvMsg(void* sockid) {
23     const int sockfd=*((int*)sockid);
24     char buf[maxnum];
25     while(true) {
26         int num = recv(sockfd, buf, maxnum, 0);
27         if(num == -1) {
28             perror("recv()");
29             return 0;
30         }
31         buf[num]='\0';
32         cout/**<<"recv: "**/<<buf<<endl;
33     }
34     return 0;
35 }
36 void* sendMsg(void* sockid) {
37     const int sockfd=*((int*)sockid);
38     char str[maxnum];
39     while(true) {
40         scanf("%s", str);
41         if(send(sockfd, str, strlen(str), 0) == -1) {
42             perror("send()");
43             close(sockfd);
44             return 0;
45         }
46     }
47     return 0;
48 }
49 
50 int main() {
51 
52     int sockfd, numbytes;
53     struct sockaddr_in their_addr;
54 
55     puts("USER:");
56     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
57         perror("socket():");
58         return 0;
59     }
60 
61     their_addr.sin_family = AF_INET;
62     their_addr.sin_port = htons(PORT);
63 
64     their_addr.sin_addr.s_addr = inet_addr(IP);
65     bzero(&(their_addr.sin_zero),getLen(their_addr.sin_zero));
66     if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) {
67         perror("connect():");
68         return 0;
69     }
70 
71     init(sockfd);
72 
73     pthread_t thread[2];
74     if(pthread_create(&thread[0], NULL, recvMsg, (void *)&(sockfd)) != 0) {
75         perror("pthread_create()");
76         return 0;
77     }
78     if(pthread_create(&thread[1], NULL, sendMsg, (void *)&(sockfd)) != 0) {
79         perror("pthread_create()");
80         return 0;
81     }
82     while(true);
83     pthread_exit(NULL);
84     close(sockfd);
85     return 0;
86 }
linux環境的客戶端

 

 

既然select的實現方法寫完了,那么根據select和poll的相似性我們也可以很輕松的將其更改,想學可以去參考他人的博客,這里就不多做解釋了;

 

3、提升服務器的處理能力

對於一個服務器要是聊天的人一多就會出現嚴重延遲是絕對不可以的,也就是一個個輪詢的方式是費時費力的,那么我們會想辦法解決這個問題。

這就涉及到了接下來要講的epoll。

epoll的底層維護是一顆紅黑樹,查找和刪除修改等等操作都是log級別的,所有很快,具體來說就是一顆紅黑樹,里面有很多FD,此時來了一個事件,我在樹上快速查找有沒有與之對應的FD,有就將其添加至list里。然后由下面講的epoll_wait去等,等待list不為空、收到信號、超時這三種條件后返回一個值。

epoll的操作主要需要這3個接口函數:

  int epoll_create(int size);///size是要監聽的數目,創建好epoll句柄后,它會占用一個文件描述符,所以說在epoll用完要close(),不然文件描述符可能被耗盡;

  int epoll_ctl(int epfd, int op, int fd,  struct epoll_event *event);

  /**參數1是上一個函數返回值,2是操作

  參數2是操作,操作有三種,EPOLL_CTL_ADD(添加FD),EPOLL_CTL_MOD(修改已添加的FD),EPOLL_CTL_DEL(刪除一個FD)

  參數3是要對其操作的FD

  參數4用來告訴內核需要監聽的事件。()

///epoll_event結構體:
struct epoll_event {
    unsigned int events;///關注的事件
    epoll_data_t data; ///在意這個的用法於是去百度了源碼,如果源碼沒錯的話,這個data應該是沒被使用過,所以說傳入什么參數會原樣返回(有誤的話請指出,謝謝)
};
typedef union epoll_data {
    void *ptr;
    int fd;
    unsigned int u32;
    unsigned long int u64;
}epoll_event_t;
/**
關注的事件:(其他幾種有興趣就去查查)
EPOLLIN = 0x001,///表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)
EPOLLPRI = 0x002,///表示對應的文件描述符有緊急的數據可讀
EPOLLOUT = 0x004,///表示對應的文件描述符可以寫
EPOLLRDNORM = 0x040,
EPOLLRDBAND = 0x080,
EPOLLWRNORM = 0x100,
EPOLLWRBAND = 0x200,
EPOLLMSG = 0x400,
EPOLLERR = 0x008,///表示對應的文件描述符發生錯誤
EPOLLHUP = 0x010,///表示對應的文件描述符被掛斷
EPOLLRDHUP = 0x2000,
EPOLLEXCLUSIVE = 1u << 28,
EPOLLWAKEUP = 1u << 29,
EPOLLONESHOT = 1u << 30,///只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
EPOLLET = 1u << 31///將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的
**/
struct epoll_event詳細

  **/

  int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);///返回產生事件的個數,且maxevents不能大於epoll_create()的size,timeout是ms為單位,為0立即返回,-1將不確定或永久阻塞。一般返回0表示超時

這個是一個實例,將上面的select改為了epoll的做法,但是因為嫌麻煩就沒寫昵稱的接收,客戶端可以繼續用上面select的客戶端。

值得注意的是linux2.6才有開始有epoll的方法。(有一些可能知道ET以及LT的小伙伴可能會發現我在發消息前沒有將events修改為EPOLLOUT之類的,但是我默認的情況是緩沖區不會滿,也就是不需要通知,只管發送,然后客戶端一直接收)

  1 #include <set>
  2 #include <vector>
  3 #include <string>
  4 #include <errno.h>
  5 #include <unistd.h>
  6 #include <string.h>
  7 #include <iostream>
  8 #include <sys/epoll.h>
  9 #include <arpa/inet.h>
 10 #include <sys/socket.h>
 11 
 12 
 13 #define LISNUM 10
 14 #define MYPORT 1223
 15 #define MAXLEN 1007
 16 #define MYIP "**.**.**.**"
 17 using namespace std;
 18 
 19 set<int> socketset;///維護現有的socketfd,用於全發送
 20 vector<int> deletefd;///存儲異常的文件描述符
 21 
 22 int socketBind( );///socket()、bind()
 23 
 24 void doEpoll(int &sockfd);
 25 
 26 void handleEvents(int &epollfd, struct epoll_event *events, int &num, int &sockfd, char *buffer);
 27 
 28 void handleAccept(int &epollfd, int &sockfd);
 29 
 30 void handleRecv(int &epollfd, int &sockfd, char *buffer);
 31 
 32 void allSend(char buffer[], int &nowfd, int &epollfd);
 33 
 34 void handleSend(int &epollfd, int &sockfd, char *buffer);
 35 
 36 void addEvent(int &epollfd, int &sockfd, int state);
 37 
 38 void deleteEvent(int &epollfd, int sockfd, int state);
 39 
 40 void modifyEvent(int &epollfd, int &sockfd, int state);
 41 
 42 int main( ) {
 43     int sockfd = socketBind( );
 44     if(listen(sockfd, LISNUM) == -1) {
 45         perror("listen()");
 46         return 0;
 47     }
 48     cout<<"listen ok"<<endl;
 49     doEpoll(sockfd);
 50     return 0;
 51 }
 52 
 53 int socketBind( ){///socket()、bind()
 54     int sockfd;
 55     struct sockaddr_in my_addr;
 56     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
 57         perror("socket()");
 58         exit(1);
 59     }
 60     cout<<"socket ok"<<endl;
 61     my_addr.sin_family = AF_INET;
 62     my_addr.sin_port = htons(MYPORT);
 63     my_addr.sin_addr.s_addr = inet_addr(MYIP);
 64 
 65     memset(my_addr.sin_zero, 0, sizeof(my_addr.sin_zero));
 66 
 67     if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
 68         perror("bind()");
 69         exit(1);
 70     }
 71     puts("bind ok");
 72     return sockfd;
 73 }
 74 
 75 void doEpoll(int &sockfd) {
 76     int epollfd = epoll_create(LISNUM);///創建好一個epoll后會產生一個fd值
 77     struct epoll_event events[LISNUM];
 78 
 79 
 80     int ret;
 81     char buffer[MAXLEN]={};
 82     addEvent(epollfd, sockfd, EPOLLIN);///對sockfd這個連接,我們關心的是是否有客戶端要連接他,所以說要將讀事件設為關心
 83 
 84     while(true) {///持續執行
 85         ret = epoll_wait(epollfd, events, LISNUM, -1);
 86         handleEvents(epollfd, events, ret, sockfd, buffer);///對得到的事件進行處理
 87     }
 88     close(epollfd);
 89 }
 90 
 91 void handleEvents(int &epollfd, struct epoll_event *events, int &num, int &sockfd, char *buffer){
 92     int listenfd;
 93     for(int i = 0; i < num; ++i) {
 94         listenfd = events[i].data.fd;
 95         if((listenfd == sockfd)&&(events[i].events & EPOLLIN)) {
 96             handleAccept(epollfd, sockfd);///處理客戶端連接請求
 97         } else if(events[i].events & EPOLLIN) {
 98             handleRecv(epollfd, listenfd, buffer);///處理客戶端發送的信息
 99 
100         }
101     }
102 }
103 
104 void handleAccept(int &epollfd, int &sockfd) {
105     int clientfd;
106     struct sockaddr_in clientaddr;
107     socklen_t clientaddrlen = 1;
108     if((clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &clientaddrlen)) == -1) {
109         perror("accept()");
110     } else {
111         socketset.insert(clientfd);
112         addEvent(epollfd, clientfd, EPOLLIN);///處理連接,我們關心這個連接的讀事件
113     }
114 }
115 
116 void handleRecv(int &epollfd, int &sockfd, char *buffer) {
117     int len = recv(sockfd, buffer, MAXLEN, 0);
118     if(len <= 0) {
119         perror("recv()");
120         socketset.erase(sockfd);
121         deleteEvent(epollfd, sockfd, EPOLLIN);
122     } else {
123         cout<<buffer<<endl;
124         allSend(buffer, sockfd, epollfd);///成功接收到一個字符串就轉發給全部客戶端
125     }
126 }
127 
128 void allSend(char buffer[], int &nowfd, int &epollfd) {
129     ///modifyEvent(epollfd, nowfd, EPOLLOUT);
130     if(buffer[0] == '\0')
131         return ;
132     for(auto it = socketset.begin(); it != socketset.end() ; ++ it) {
133         if(*it != nowfd){
134             cout<<"__"<<buffer<<"________"<<endl;
135             if(send(*it, buffer, strlen(buffer), 0) == -1) {
136                 perror("send()");
137                 deletefd.push_back(*it);///直接erase會導致迭代器失效
138 
139                 deleteEvent(epollfd, *it, EPOLLIN);
140             }
141         }
142     }
143     for(size_t i = 0; i < deletefd.size(); ++i) { ///單獨刪除
144         socketset.erase(deletefd[i]);
145     }
146     deletefd.clear();
147     ///modifyEvent(epollfd, nowfd, EPOLLIN);
148     memset(buffer, 0, MAXLEN);
149 }
150 
151 void addEvent(int &epollfd, int &sockfd, int state) {
152     struct epoll_event ev;
153     ev.events=state;
154     ev.data.fd = sockfd;
155     epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
156 }
157 
158 void deleteEvent(int &epollfd, int sockfd, int state) {
159     struct epoll_event ev;
160     close(sockfd);
161     ev.events=state;
162     ev.data.fd = sockfd;
163     epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, &ev);
164 }
165 
166 void modifyEvent(int &epollfd, int &sockfd, int state) {
167     struct epoll_event ev;
168     ev.events=state;
169     ev.data.fd = sockfd;
170     epoll_ctl(epollfd, EPOLL_CTL_MOD, sockfd, &ev);
171 }
linux環境的服務端

 

4、繼續提升處理能力

  我們知道了epoll是很優秀的I/O多路復用的方法了,但是其實還是有問題的,最大的問題就是無並發。(並發,在操作系統中,是指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行)

  為什么無並發是最大的問題呢!我們可以想象,默認情況下,其實大部分函數都是阻塞的,即:一個函數沒執行完畢那么程序就不能繼續運行下去。

  那么為了解決這個問題我們就會想用讓他們並發,比如我的代碼里的allSend其實可以讓他一遍自個兒傳去的,還有接收也一樣,以及accept。那么我們就可以接收到一條消息就新建一個線程用來allSend給其他客戶端,其他操作同理。(一般我們一個線程里只處理一個socket,因為每一個socket都是阻塞的)(這里談到的方法就是Reactor模式,只不過我只不過是泛泛之談,具體可以看這里:https://www.cnblogs.com/doit8791/p/7461479.html)

  怎么做確實極大的提高了效率,因為程序不再是單線程一直被某些操作所阻塞的狀態了。但是如果連接數高的情況下呢??很明顯,我們要一直開線程和關線程,雖然說線程的創建以及銷毀開銷遠遠小於進程的創建銷毀的開銷,但是數量一大也會需要大量的系統資源,系統可能吃不消。那么我們就要限制創建線程的數量,此時我們就要引入一個線程池的概念。

  線程池是一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可並發執行的任務。這避免了在處理短時間任務時創建與銷毀線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。(百度百科)

PS:我的線程池是在實驗樓學的,那個是收費課程,想着直接發出來不太好,就只這樣說一下思路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM