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:我的线程池是在实验楼学的,那个是收费课程,想着直接发出来不太好,就只这样说一下思路。

posted @ 2019-03-13 13:14  Thanks_up  阅读(6533)  评论(0编辑  收藏  举报