Linux网络编程

网络概述

略,因为科班都学过的哈哈哈哈哈。

网络编程基础概念

地址

其中network byte order表示网络字节序。

网络字节序

大小端转换

所以由上图可知,我们的数据从主机到网络中应该要进行从小端到大端的转换,从网络发到另一台主机时应该要进行大端到小端的转换:

这就要涉及到一组函数调用了。

ntohs:大小端转换

注意这里又要加新的头文件嗷。

来简单测试一下:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(){
  4 
  5     unsigned short s = 0x1234;
  6     printf("s = %x\n",s); //用小端
  7     printf("htons(s) = %x\n",htons(s)); //sockaddr中用大端
  8 
  9     unsigned int i = 0x12345678;
 10     printf("i = %x\n",i);
 11     printf("htonl(i) = %x\n",htonl(i));                                                                                                      
 12 }

编译运行:

inet_aton:IP地址的点分十进制格式转换32位大端整数

注意要加新的头文件嗷。

代码测试一下:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./ip 192.168.14.9
  5     ARGS_CHECK(argc,2);
  6     struct in_addr addr;                                                                                                                     
  7     //将点分十进制IP地址转换为32位大端整数
  8     inet_aton(argv[1],&addr);
  9     printf("addr = %08x\n",addr.s_addr);
 10 
 11 }   

编译运行:

接下来来编写一个完整的IP地址:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./ip 192.168.14.9 1234
  5     // 端口使用范围为1025到65535,因为1025前面的端口都被知名协议给占用了
  6     ARGS_CHECK(argc,3);
  7     struct sockaddr_in addr;                                                                                                                 
  8     addr.sin_family = AF_INET; //表示所使用的是IPV4
  9     addr.sin_port = htons(atoi(argv[2]));//要进行网络传输,小端变16位大端模式(atoi函数将string转换成数字)
 10     addr.sin_addr.s_addr = inet_addr(argv[1]);
 11     printf("port = %x\n",addr.sin_port);
 12     printf("ip = %08x\n",addr.sin_addr.s_addr);
 13 }

编译运行:

域名转换成IP

gethostbyname:获得域名对应的地址


inet_ntop是作用是将传输过来的二进制数据流给转换成点分十进制的IPV4或者IPV6地址。

基于上述接口,我们可以写程序拿到远程主机的信息了:

基于TCP的网络通信

TCP通信鸟瞰图:

socket系统调用

socket调用本质是:创建了一个用于网络通信的文件对象,并且给它分配了一个文件描述符。

当调用socket时我们会创建一个文件对象,该文件对象内部有两个缓冲区,一个叫send一个叫receive,然后我们就可以通过文件描述符来找到该文件对象,这个时候读写该文件对象其实就是在读写网络了,用法和文件差不多。

代码测试一下:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(){
  4     //创建socket套接字
  5     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
  6     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  7     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  8     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
  9     printf("sockFd = %d\n",sockFd);                          
 10     //关闭文件对象                                                                                                                           
 11     close(sockFd);
 12 }

编译运行:

connect系统调用

cpp 复制代码
  #include<43func.h>
  
  int main(int argc,char* argv[]){
      // ./connect 192.168.14.9 1234
      //创建socket套接字
      //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
      int sockFd = socket(AF_INET,SOCK_STREAM,0);
      ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
      //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
      printf("sockFd = %d\n",sockFd);
  
      //构造IP地址用sockaddr_in,这是历史遗留问题
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;//表示使用IPV4
      addr.sin_port = htons(atoi(argv[2]));
      addr.sin_addr.s_addr = inet_addr(argv[1]);
  
      //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
      //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
      //先取地址再强转
      int ret = connect(sockFd,(struct sockaddr*)&addr,sizeof(addr));
      ERROR_CHECK(ret,-1,"connect");
                                                                                                                                               
  
      //关闭文件对象
      close(sockFd);
  }

测试可以看到:

从上图可以看到,这其实就是TCP建立连接的第一次握手,因为上半张图中我们通过tcpdump命令可以监听到8080端口的数据传送情况。

我们输入的参数是本地的IP地址,即自己访问自己,所以监听到的数据发送方和传送方都是自己,其中Flag[S]就是我们之前提的SYN,表示请求建立连接,然后对方返回了一个Flag[R.]表示Reset,而后面跟的小数点表示确认收到的意思,为什么可以知道是确认了,因为该值后面有个ack 1324744859,这很明显是上面发送请求的ack的值+1,这就是在确认。

另外上图中因为我们本地发送消息的时候并未指定本地的socket端口为多少,所以是随机端口发出的,在本次测试中50188,其实接收方的端口为8080,在图中表示为http-alt,这是因为:http-alt端口号是8080,它通常被一些Web服务器软件(如Apache Tomcat)用作HTTP服务的替代端口。通过在URL中指定端口号8080,可以访问使用该端口提供的Web服务。

即是一个别名而已,换成其它的测试可能会更好理解:

可以看见此时就不会是代名词http-alt了,是访问目标机器指定的端口7777.

补充tcpdump抓包命令的使用

上面我们使用的命令是:

tcpdump -i lo port 7777

-i的意思就是指定要监听的网络接口是哪个,lo这个位置填的是要监听的网卡,网卡信息可以用ifconfig来查询:

其中lo表示本地环回地址loop back。

最后面跟的port 7777就是指定监听哪个端口的通信数据。

具体可以查看man手册。

补充TCP标志

bind系统调用:绑定一个IP:port给socket接口

代码测试:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./connect 192.168.14.9 1234
  5     //创建socket套接字
  6     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
  7     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10     printf("sockFd = %d\n",sockFd);
 11 
 12     //构造IP地址用sockaddr_in,这是历史遗留问题
 13     struct sockaddr_in addr;
 14     addr.sin_family = AF_INET;//表示使用IPV4
 15     addr.sin_port = htons(atoi(argv[2]));
 16     addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18     //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19     //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
 20     //先取地址再强转
 21     //对于bind而言,addr一定是本机地址,bind一般是给服务端用的
 22     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 23     ERROE_CHECK(ret,-1,"bind");
 24         
 25                                                                                                                                              
 26     //关闭文件对象
 27     close(sockFd);
 28 }  

运行结果:

此时虽然绑定了但还是连接不了的,因为还未监听。

listen系统调用:声明让socket当服务端来使用

当我们使用listen系统调用时,它会将socket文件对象中的发送接收缓冲区给清空,取而代之的是一个半连接队列和一个全连接队列。服务端调用listen之后,意味着这个缓冲区将不再发送和接收数据的作用,专门用来处理连接请求:

如上图所示,第一次握手成功时产生的连接放在服务端中的半连接队列里面,第二次握手成功时就已经建立了完全连接,就会放入全连接队列里面。

来验证这个事情:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./connect 192.168.14.9 1234
  5     //创建socket套接字
  6     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
  7     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10     printf("sockFd = %d\n",sockFd);
 11 
 12     //构造IP地址用sockaddr_in,这是历史遗留问题
 13     struct sockaddr_in addr;
 14     addr.sin_family = AF_INET;//表示使用IPV4
 15     addr.sin_port = htons(atoi(argv[2]));
 16     addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18     //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19     //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
 20     //先取地址再强转
 21     //对于bind而言,addr一定是本机地址,bind一般是给服务端用的
 22     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 23     ERROR_CHECK(ret,-1,"bind");
 24     //监听端口
 25     ret = listen(sockFd,10);//第二个参数只要不填负数,一般问题不大                                                                           
 26     ERROR_CHECK(ret,-1,"listen");
 27 
 28     sleep(20);//睡眠20s,来验证一下建立连接的过程
 29     //关闭文件对象
 30     close(sockFd);
 31 }

此时这个上述这个程序就俨然成了一个服务端,我们可以使用客户端来跟其进行连接啦,服务端程序如下:

cpp 复制代码
  #include<43func.h>
  
  int main(int argc,char* argv[]){
      // ./connect 192.168.14.9 1234
      //创建socket套接字
      //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
      int sockFd = socket(AF_INET,SOCK_STREAM,0);
      ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
      //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
      printf("sockFd = %d\n",sockFd);
  
      //构造IP地址用sockaddr_in,这是历史遗留问题
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;//表示使用IPV4
      addr.sin_port = htons(atoi(argv[2]));
      addr.sin_addr.s_addr = inet_addr(argv[1]);
  
      //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
      //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
      //先取地址再强转
      int ret = connect(sockFd,(struct sockaddr*)&addr,sizeof(addr));
      ERROR_CHECK(ret,-1,"connect");
                                                                                                                                               
  
      //关闭文件对象
      close(sockFd);
  }

现在我们来抓取一下客户端访问服务端的一个连接状态:

可以看见上述就是一个三次握手的过程,倒数三个信息是TCP连接关闭的连接信息不用管。

补充:DDOS攻击

DDOS攻击也就是SYN洪范攻击,就是使用大量机器向服务端发送第一次SYN请求连接,但是不返回第二次的确认连接请求,导致服务端的半连接队列空间满导致其它客户端无法再访问该客户端,这就是SYN洪范攻击。

对应的解决方式要么是扩大内存,要么就是使用SYN cookies来给每个客户端设置身份,此时我们不再需要半连接队列了,采用时间换空间的做法让CPU每回来计算一下客户端的身份,只要身份正确就给予全连接,否则就不连接,还要么就是找大企业比如阿里腾讯提供的云加速保护,但这其实也治不了这种攻击,无非是加大了攻击者的攻击成本而已,通过提高攻击成本来降低攻击者的攻击欲望。

accept系统调用:从全连接队列中取一个进行TCP连接


之前已经说过连接的事情了,但最终目的肯定是要实现通信,这就意味着服务端肯定还是需要一个发送接收缓冲区的,accept就是来干这事儿的,它从全连接队列中挑出一个连接出来创建一个新的socket称为已连接socket,那么之前的socket就叫listen socket。很明显这俩不是一个文件对象,所以这俩的文件描述符自然也不一样。

代码如下:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./connect 192.168.14.9 1234
  5     //创建socket套接字
  6     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
  7     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10     printf("sockFd = %d\n",sockFd);
 11 
 12     //构造IP地址用sockaddr_in,这是历史遗留问题
 13     struct sockaddr_in addr;
 14     addr.sin_family = AF_INET;//表示使用IPV4
 15     addr.sin_port = htons(atoi(argv[2]));
 16     addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18     //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19     //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
 20     //先取地址再强转
 21     //对于bind而言,addr一定是本机地址,bind一般是给服务端用的
 22     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 23     ERROR_CHECK(ret,-1,"bind");
 24     //监听数据
 25     ret = listen(sockFd,10);//第二个参数只要不填负数,一般问题不大
 26     ERROR_CHECK(ret,-1,"listen");
 27 
 28     //客户端地址
 29     struct sockaddr_in clientAddr;
 30     //取客户端地址长度
 31     socklen_t clientAddrLen = sizeof(clientAddr);
 32     //调用accept从全连接队列里面挑选一个连接来创建服务端的新发送接收缓冲区
 33     int netFd = accept(sockFd,(struct sockaddr*)&clientAddr,&clientAddrLen);                                                                 
 34     ERROR_CHECK(netFd,-1,"accept");
 35     printf("netFd = %d\n",netFd);
 36 
 37     //关闭文件对象
 38     close(sockFd);
 39 }

编译运行:

可以看见此时被阻塞了,正如我们之前说的,因为此时全连接队列里面还一个连接都没有,所以自然就被阻塞在了accept函数中。

此时我们使用一个客户端去进行连接会发现这阻塞就解除了:

我们还可以通过客户端地址打印其更为具体的信息:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./connect 192.168.14.9 1234
  5     //创建socket套接字
  6     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协议填0为自动获取
  7     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10     printf("sockFd = %d\n",sockFd);
 11 
 12     //构造IP地址用sockaddr_in,这是历史遗留问题
 13     struct sockaddr_in addr;
 14     addr.sin_family = AF_INET;//表示使用IPV4
 15     addr.sin_port = htons(atoi(argv[2]));
 16     addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18     //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19     //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以现在会用下面这种写法
 20     //先取地址再强转
 21     //对于bind而言,addr一定是本机地址,bind一般是给服务端用的
 22     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 23     ERROR_CHECK(ret,-1,"bind");
 24     //监听数据
 25     ret = listen(sockFd,10);//第二个参数只要不填负数,一般问题不大
 26     ERROR_CHECK(ret,-1,"listen");
 27 
 28     //客户端地址
 29     struct sockaddr_in clientAddr;
 30     //取客户端地址长度
 31     socklen_t clientAddrLen = sizeof(clientAddr);//这里只能填真实长度,不能填0或者其它的嗷,也不能不填!
 32     //调用accept从全连接队列里面挑选一个连接来创建服务端的新发送接收缓冲区
 33     int netFd = accept(sockFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
 34     ERROR_CHECK(netFd,-1,"accept");
 35     printf("netFd = %d\n",netFd);//客户端长度                                                                                                
 36     printf("client addr length = %d\n",clientAddrLen);//打印类型
 37     printf("client family = %d\n",clientAddr.sin_family);
 38     printf("client port = %d\n",ntohs(clientAddr.sin_port));//注意要从比特流中的大端转成小端显示嗷
 39     printf("client IP = %s\n",inet_ntoa(clientAddr.sin_addr));//注意要转成点分十进制更好查看IP                                               
 40 
 41 
 42     //关闭文件对象
 43     close(sockFd);
 44 }

运行结果:

类型为2就是IPV4.

如果不需要客户端的信息的话,我们也可以直接accept后面两个参数直接填NULL即可:

send和recv系统调用:读写网络缓冲区

read和write即可以操作管道文件,也可以操作socket。

而recv和send只能操作socket。

我们可以来做一下这个事情,服务端程序:

cpp 复制代码
  1 #include<43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./connect 192.168.14.9 1234
  5     //创建socket套接字
  6     //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层协
  7     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9     //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10     printf("sockFd = %d\n",sockFd);
 11 
 12     //构造IP地址用sockaddr_in,这是历史遗留问题
 13     struct sockaddr_in addr;
 14     addr.sin_family = AF_INET;//表示使用IPV4
 15     addr.sin_port = htons(atoi(argv[2]));
 16     addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18     //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19     //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所以
 20     //先取地址再强转
 21     //对于bind而言,addr一定是本机地址,bind一般是给服务端用的
 22     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 23     ERROR_CHECK(ret,-1,"bind");
 24     //监听数据
 25     ret = listen(sockFd,10);//第二个参数只要不填负数,一般问题不大
 26     ERROR_CHECK(ret,-1,"listen");
 27 
 28     //客户端地址
 29     struct sockaddr_in clientAddr;
 30     //取客户端地址长度
 31     socklen_t clientAddrLen = sizeof(clientAddr);
 32     //调用accept从全连接队列里面挑选一个连接来创建服务端的新发送接>
 33     int netFd = accept(sockFd,(struct sockaddr*)&clientAddr,&client
 34     ERROR_CHECK(netFd,-1,"accept");
 35     printf("netFd = %d\n",netFd);//客户端长度
 36     printf("client addr length = %d\n",clientAddrLen);//打印类型   
 37     printf("client family = %d\n",clientAddr.sin_family);
 38     printf("client port = %d\n",ntohs(clientAddr.sin_port));//注意>
 39     printf("client IP = %s\n",inet_ntoa(clientAddr.sin_addr));//注>
 40     //已连接的文件对象才能进行数据发送,所以我们传入netFd
 41     ret = send(netFd,"hello from server",17,0);
 42     ERROR_CHECK(ret,-1,"send");
 43     //创建读写缓冲
 44     char buf[4096] = {0};
 45     //已连接的文件对象才能进行数据接收
 46     ret = recv(netFd,buf,sizeof(buf),0);
 47     ERROR_CHECK(ret,-1,"recv");
 48     puts(buf);
 49     //关闭文件对象
 50     close(netFd);
 51     close(sockFd);
 52 }

客户端程序:

cpp 复制代码
  1   #include<43func.h>
  2   
  3   int main(int argc,char* argv[]){
  4       // ./connect 192.168.14.9 1234
  5       //创建socket套接字
  6       //参数从左往右分别是设置网路层协议IPV4,传输层协议TCP,应用层
  7       int sockFd = socket(AF_INET,SOCK_STREAM,0);
  8       ERROR_CHECK(sockFd,-1,"socket"); //若创建失败会返回-1
  9       //输出3,因为文件描述符0、1、2都被占用了自然就从3开始用了
 10       printf("sockFd = %d\n",sockFd);
 11 
 12       //构造IP地址用sockaddr_in,这是历史遗留问题
 13       struct sockaddr_in addr;
 14       addr.sin_family = AF_INET;//表示使用IPV4
 15       addr.sin_port = htons(atoi(argv[2]));
 16       addr.sin_addr.s_addr = inet_addr(argv[1]);
 17 
 18       //connect的第二个地址参数是古早时候流传下来的不好用,所以后人
 19       //搞了一个比较好用的sockaddr_in,但为了兼容之前的这个版本,所
 20       //先取地址再强转
 21       int ret = connect(sockFd,(struct sockaddr*)&addr,sizeof(addr)
 22       ERROR_CHECK(ret,-1,"connect");
 23       char buf[4096] = {0};
 24       ret = recv(sockFd,buf,sizeof(buf),0);
 25       puts(buf);
 26       ret = send(sockFd,"nihao from client",17,0);
 27   
 28       //关闭文件对象
 29       close(sockFd);
 30   }    

编译运行,注意要先启动服务端嗷,不然没有listen socket的话通信就寄了,而且本来也是服务端一直运行一直等客户端来连接的嘛:

可以看见是正常读写的。

有一个问题,如果我们让服务端发送两次数据但是不读,只让客户端读会怎样:

我们可以看见,发送了两个数据nihao和huaidan,但是客户端收到的是nihaohuaidan,这是为什么?

这是因为数据的发送实际上是由内核决定的,并不取决于我们send多少次。

send的任务只是将待发送数据拷贝到内核的发送缓冲区中,具体怎么发是由内核决定的。

所以TCP是一种字节流协议,消息与消息之间无边界。

那么如何使接收方和发送方知道相同的意义准确的信息(即有边界的消息接收),那我们就需要对这些通信过程进行一个"约定",即对通信内容做一个协议,即应用层的一个自定义协议。

一种做法是:我们在每一段消息的前面加个消息大小,这样接收方在收到消息时读取到消息大小,就往后面只读该消息大小个消息即可:

比如五个字,那么接收方就往后读五个即nihao,读到7就读七个huaidan。

利用socket实现即时聊天

框架:

服务端程序:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //服务端程序
  4 int main(int argc,char* argv[]){
  5     // ./server 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10     //地址
 11     struct sockaddr_in addr;
 12     //设置IPV4
 13     addr.sin_family = AF_INET;
 14     //将命令行中的string类型端口号转成数字然后转成大端模式在网络中>
 15     addr.sin_port = htons(atoi(argv[2]));
 16     //设置IP地址
 17     addr.sin_addr.s_addr = inet_addr(argv[1]);
 18 
 19     //绑定本机IP地址端口号,这表示其成为了一个服务端程序
 20     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 21     ERROR_CHECK(ret,-1,"bind");
 22 
 23     //监听来自客户端的连接请求
 24     ret = listen(sockFd,0);
 25     ERROR_CHECK(ret,-1,"listen");
 26 
 27     //从全连接队列里面"接受"一个连接建立TCP通道
 28     int netFd = accept(sockFd,NULL,NULL);
 29     ERROR_CHECK(netFd,-1,"accept");
 30 
 31     //处理阻塞问题
 32     //使用之前学过的IO多路复用机制,select
 33     fd_set rdset;
 34     char buf[4096] = {0};
 35 
 36     while(1){
 37         FD_ZERO(&rdset);                                           
 38         FD_SET(STDIN_FILENO,&rdset);
 39         //这里与客户端不同,使用的是netFd
 40         //这是因为netFd才是服务端真正处理通信连接的socket缓冲区
 41         FD_SET(netFd,&rdset);
 42         select(netFd+1,&rdset,NULL,NULL,NULL);
 43         if(FD_ISSET(STDIN_FILENO,&rdset)){
 44             bzero(buf,sizeof(buf));//清空缓冲区
 45             //这里不能用recv嗷,recv只能接收socket文件
 46             //但这里是从标准输入里读嗷
 47             ret = read(STDIN_FILENO,buf,sizeof(buf));
 48             //避免客户端断开服务端陷入死循环
 49             if(ret == 0){
 50                 send(netFd,"nishigehaoren",13,0);
 51                 break;
 52             }
 53             send(netFd,buf,strlen(buf),0);
 54         }
 55         if(FD_ISSET(netFd,&rdset)){
 56             bzero(buf,sizeof(buf));
 57             //避免客户端断开服务端陷入死循环
 58             ret = recv(netFd,buf,sizeof(buf),0);
 59             if(ret == 0){
 60                 puts("chat is end!");
 61                 break;
 62             }
 63             puts(buf);
 64         }
 65     }
 66 
 67     //关闭缓冲区
 68     close(sockFd);
 69     close(netFd);
 70 }

客户端程序:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //客户端程序
  4 int main(int argc,char* argv[]){
  5     // ./client 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10     //地址
 11     struct sockaddr_in addr;
 12     //设置IPV4
 13     addr.sin_family = AF_INET;
 14     //将命令行中的string类型端口号转成数字然后转成大端模式在网络中>
 15     addr.sin_port = htons(atoi(argv[2])); 
 16     //设置IP地址
 17     addr.sin_addr.s_addr = inet_addr(argv[1]);
 18     //向目标IP地址端口号建立连接
 19     int ret = connect(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 20     ERROR_CHECK(ret,-1,"connect");
 21 
 22     //处理阻塞问题
 23     //使用之前学过的IO多路复用机制,select
 24     fd_set rdset;
 25     char buf[4096] = {0};
 26 
 27     while(1){
 28         FD_ZERO(&rdset);
 29         FD_SET(STDIN_FILENO,&rdset);
 30         FD_SET(sockFd,&rdset);
 31         select(sockFd+1,&rdset,NULL,NULL,NULL);
 32         if(FD_ISSET(STDIN_FILENO,&rdset)){
 33             bzero(buf,sizeof(buf));//清空缓冲区
 34             //这里不能用recv嗷,recv只能接收socket文件
 35             //但这里是从标准输入里读嗷
 36             ret = read(STDIN_FILENO,buf,sizeof(buf));
 37             //避免服务端断开连接客户端陷入死循环
 38             if(ret == 0){
 39                 send(sockFd,"niyeshigehaoren",15,0);
 40                 break;
 41             }
 42             send(sockFd,buf,strlen(buf),0);
 43         }
 44         if(FD_ISSET(sockFd,&rdset)){
 45             bzero(buf,sizeof(buf));
 46             ret = recv(sockFd,buf,sizeof(buf),0);
 47             //避免服务端断开连接客户端陷入死循环
 48             if(ret == 0){
 49                 puts("chat is end!");
 50                 break;
 51             }
 52             puts(buf);
 53         }
 54     }
 55 
 56     //关闭缓冲区
 57     close(sockFd);
 58 }

运行结果:

补充time_wait问题

首先要知道产生原因:这是由于四次挥手导致的,主动中断连接一方要等待一段时间确保完全断开,防止旧IP地址端口重复建立新连接。

但其实明显的是客户端的端口都是随机端口发送,每次的端口都不一样,这意味着服务端的这个设定有点多余(毕竟当初设计互联网的时候并没有考虑到这种问题,四次挥手是为了解决两台电脑通信问题),好在我们是可以对其进行修改的,修改socket的属性即可。

让服务端无视time_wait

注意上图中的optval的意思是表示真时就启动该属性,为假表示不启动。

我们肯定选启动噻。

所以服务端代码修改为:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //服务端程序
  4 int main(int argc,char* argv[]){
  5     // ./server 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10 	//设置服务端无视time_wait机制
 11     int optval = 1;
 12     int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&o
 13     ERROR_CHECK(ret,-1,"setsockopt");
 14     //创建本机的IP地址
 15     struct sockaddr_in addr;
 16     //设置IPV4
 17     addr.sin_family = AF_INET;
 18     //将命令行中的string类型端口号转成数字然后转成大端模式
 19     addr.sin_port = htons(atoi(argv[2])); 
 20     //设置IP地址
 21     addr.sin_addr.s_addr = inet_addr(argv[1]);
 22 
 23     //绑定本机IP地址端口号,这表示其成为了一个服务端程序
 24     ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr)
 25     ERROR_CHECK(ret,-1,"bind");
 26 
 27     //监听来自客户端的连接请求
 28     ret = listen(sockFd,0);
 29     ERROR_CHECK(ret,-1,"listen");
 30 
 31     //从全连接队列里面"接受"一个连接建立TCP通道
 32     int netFd = accept(sockFd,NULL,NULL);
 33     ERROR_CHECK(netFd,-1,"accept");
 34 
 35     //处理阻塞问题
 36     //使用之前学过的IO多路复用机制,select 
  37     fd_set rdset;                                         
 38     char buf[4096] = {0};
 39 
 40     while(1){
 41         FD_ZERO(&rdset);
 42         FD_SET(STDIN_FILENO,&rdset);
 43         //这里与客户端不同,使用的是netFd
 44         //这是因为netFd才是服务端真正处理通信连接的socket>
 45         FD_SET(netFd,&rdset);
 46         select(netFd+1,&rdset,NULL,NULL,NULL);
 47         if(FD_ISSET(STDIN_FILENO,&rdset)){
 48             bzero(buf,sizeof(buf));//清空缓冲区
 49             //这里不能用recv嗷,recv只能接收socket文件
 50             //但这里是从标准输入里读嗷
 51             ret = read(STDIN_FILENO,buf,sizeof(buf));
 52             //避免客户端断开服务端陷入死循环
 53             if(ret == 0){
 54                 send(netFd,"nishigehaoren",13,0);
 55                 break;
 56             }
 57             send(netFd,buf,strlen(buf),0);
 58         }
 59         if(FD_ISSET(netFd,&rdset)){
 60             bzero(buf,sizeof(buf));
 61             //避免客户端断开服务端陷入死循环
 62             ret = recv(netFd,buf,sizeof(buf),0);
 63             if(ret == 0){
 64                 puts("chat is end!");
 65                 break;
 66             }
 67             puts(buf);
 68         }
 69     }
 70 
 71     //关闭缓冲区
 72     close(sockFd);
 73     close(netFd);
 74 }

编译运行:

可以看到虽然还是在time_wait状态,但我们上述添加的代码已经选择了无视bind了,所以我们可以立马再次启动服务程序而不被阻塞。

如何支持断线重连

具体就是用select的rdset去监听服务端的sockFd,因为在listen之后sockFd中有一个全连接队列和一个半连接队列,而accept本质就是一个读行为。当accept去全连接队列中挑选全连接时,全连接中若是没有数据那accept就阻塞,一旦里面有数据了那就会被accept取出来。

所以我们完全可以用select去监听服务端的sockFd,那sockFd什么时候会让select就绪呢?

显然是sockFd有数据时那么sockFd就是就绪的,那select自然也就就绪,要是sockFd没有数据那么sockFd就没有就绪自然select也就没有就绪。

总结一句话就是:如果select就绪且是sockFd导致的就绪,此时就执行accept。

代码如下:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //服务端程序
  4 int main(int argc,char* argv[]){
  5     // ./server 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10 
 11     int optval = 1;
 12     int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
 13     ERROR_CHECK(ret,-1,"setsockopt");
 14     //地址
 15     struct sockaddr_in addr;
 16     //设置IPV4
 17     addr.sin_family = AF_INET;
 18     //将命令行中的string类型端口号转成数字然后转成大端模式在网络中传输
 19     addr.sin_port = htons(atoi(argv[2]));
 20     //设置IP地址
 21     addr.sin_addr.s_addr = inet_addr(argv[1]);
 22 
 23     //绑定本机IP地址端口号,这表示其成为了一个服务端程序
 24     ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 25     ERROR_CHECK(ret,-1,"bind");
 26 
 27     //监听来自客户端的连接请求
 28     ret = listen(sockFd,0);
 29     ERROR_CHECK(ret,-1,"listen");
 30 
 31     //处理阻塞问题
 32     //使用之前学过的IO多路复用机制,select
 33     //accept要放在select后面
 34     //去使用时确保从标准输入中输入数据是在客户端建立连接之后
 35     //accept之后创建新的netFd,这个netFd加入监听---->分离监听和就绪
 36     //客户端如果断开连接以后,之前是服务端也断开,但现在服务端不退出了
 37     //要取消监听netFd                                                                      
 38     fd_set rdset;//单纯地去保存就绪的文件描述符fd
 39     char buf[4096] = {0};
 40     fd_set monitorSet;//使用一个单独的监听集合
 41     FD_ZERO(&monitorSet);
 42     FD_SET(STDIN_FILENO,&monitorSet);
 43     FD_SET(sockFd,&monitorSet);
 44     int netFd = -1;
 45     while(1){
 46         //monitorSet用来指定哪些是我们要监听的集合
 47         //rdSet用来拷贝一份monitor,然后调用select函数
 48         //它要存储哪些是select就绪之后真正就绪的文件描述符
 49         memcpy(&rdset,&monitorSet,sizeof(fd_set));
 50         select(20,&rdset,NULL,NULL,NULL);
 51         if(FD_ISSET(STDIN_FILENO,&rdset)){
 52             //if(netFd == -1){ 这几行注释删了就行,不然有bug
 53                 //puts("no client is connected!\n");
 54                 //continue;
 55             //}
 56             bzero(buf,sizeof(buf));//清空缓冲区
 57             //这里不能用recv嗷,recv只能接收socket文件
 58             //但这里是从标准输入里读嗷
 59             ret = read(STDIN_FILENO,buf,sizeof(buf));
 60             //避免客户端断开服务端陷入死循环
 61             if(ret == 0){
 62                 send(netFd,"nishigehaoren",13,0);
 63                 break;
 64             }
 65             send(netFd,buf,strlen(buf),0);
 66         }
 67         if(FD_ISSET(sockFd,&rdset)){
 68             netFd = accept(sockFd,NULL,NULL);
 69             ERROR_CHECK(netFd,-1,"accept");
 70             FD_SET(netFd,&monitorSet);
 71             puts("new connect is accepted!\n");
 72         }
 73         if(FD_ISSET(netFd,&rdset)){
 74             bzero(buf,sizeof(buf));
 75             ret = read(netFd,buf,sizeof(buf));
 76             if(ret == 0){
 77                 puts("bye bye");
 78                 //如果该客户端断开连接了
 79                 //那么我们就清除该缓冲区从监听集合中
 80                 FD_CLR(netFd,&monitorSet);
 81                 close(netFd); //再关闭该连接
 82                 netFd = -1;
 83                 continue;
 84             }
 85             puts(buf);
 86         }
 87     }
 88 
 89     //关闭缓冲区
 90     close(sockFd);
 91 }

运行结果如下:

可以看见此时客户端断开重连服务器端都是可以一直接收其连接请求的。

接下来我们让服务端不要再往标准输入里面输入东西了,我们改造该服务器,让其实现一个群聊功能。

socket实现群聊功能

代码如下:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //服务端程序
  4 int main(int argc,char* argv[]){
  5     // ./server 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10 
 11     int optval = 1;
 12     int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
 13     ERROR_CHECK(ret,-1,"setsockopt");
 14     //地址
 15     struct sockaddr_in addr;
 16     //设置IPV4
 17     addr.sin_family = AF_INET;
 18     //将命令行中的string类型端口号转成数字然后转成大端模式在网络中传输
 19     addr.sin_port = htons(atoi(argv[2]));
 20     //设置IP地址
 21     addr.sin_addr.s_addr = inet_addr(argv[1]);
 22                                                                                                                                              
 23     //绑定本机IP地址端口号,这表示其成为了一个服务端程序
 24     ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 25     ERROR_CHECK(ret,-1,"bind");
 26 
 27     //监听来自客户端的连接请求
 28     ret = listen(sockFd,0);
 29     ERROR_CHECK(ret,-1,"listen");
 30 
 31     //处理阻塞问题
 32     //使用之前学过的IO多路复用机制,select
 33     //accept要放在select后面
 34     //去使用时确保从标准输入中输入数据是在客户端建立连接之后
 35     //accept之后创建新的netFd,这个netFd加入监听---->分离监听和就绪
 36     //客户端如果断开连接以后,之前是服务端也断开,但现在服务端不退出了
 37     //要取消监听netFd
 38     fd_set rdset;//单纯地去保存就绪的文件描述符fd
 39     char buf[4096] = {0};
 40     fd_set monitorSet;//使用一个单独的监听集合
 41     FD_ZERO(&monitorSet);
 42     FD_SET(sockFd,&monitorSet);
 43     //int netFd = -1;
 44     int netFdArr[10] = {0};
 45     int curConn = 0;
 46 	/*
 			实现聊天室
 			服务端不再监听标准输入
 			服务端的功能:处理新的客户端 监听sockFd
 			处理客户端发送的消息的转发 维护netFd的数组然后进行转发
 		*/
 47     while(1){
 48         //monitorSet用来指定哪些是我们要监听的集合
 49         //rdSet用来拷贝一份monitor,然后调用select函数
 50         //它要存储哪些是select就绪之后真正就绪的文件描述符
 51         memcpy(&rdset,&monitorSet,sizeof(fd_set));
 52         select(20,&rdset,NULL,NULL,NULL);
 53         if(FD_ISSET(sockFd,&rdset)){
 54             netFdArr[curConn] = accept(sockFd,NULL,NULL);
 55             ERROR_CHECK(netFdArr[curConn],-1,"accept");
 56             FD_SET(netFdArr[curConn],&monitorSet);
 57             printf("new connect is accepted! curConn = %d\n",curConn);
 58             ++curConn;                                                                                                                       
 59         }
 60         for(int i = 0;i<curConn;i++){
 61             if(FD_ISSET(netFdArr[i],&rdset)){
 62                 bzero(buf,sizeof(buf));
 63                 recv(netFdArr[i],buf,sizeof(buf),0);
 64                 for(int j = 0;j < curConn; ++j ){
 65                     if(j == i){
 66                         continue;
 67                     }
 68                     send(netFdArr[j],buf,strlen(buf),0);
 69                 }
 70             }
 71         }
 72     }
 73                                                                                                                                              
 74     //关闭缓冲区
 75     close(sockFd);
 76 }

运行结果:

使用UDP实现即时聊天(没有TCP重要)

框架:

将要使用到的几个函数:

简单使用如下:

服务端:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./server 10.0.8.4 8080
  5     ARGS_CHECK(argc,3);
  6     //创建socket缓冲区,其中第二个参数改为SOCK_DGRAM表示使用UDP协议
  7     int sockFd = socket(AF_INET,SOCK_DGRAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket");
  9     //创建发送数据的目标地址
 10     struct sockaddr_in addr;
 11     addr.sin_family = AF_INET;
 12     addr.sin_port = htons(atoi(argv[2]));
 13     addr.sin_addr.s_addr = inet_addr(argv[1]);
 14 
 15     //绑定服务端的ip端口号
 16     int ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 17     ERROR_CHECK(ret,-1,"bind");
 18 
 19     //申请一个ip端口地址用来保存请求连接的客户端的ip端口信息
 20     struct sockaddr_in clientAddr;
 21     socklen_t clientAddrLen = sizeof(clientAddr);
 22     char buf[1024] = {0};
 23     ret = recvfrom(sockFd,buf,sizeof(buf),0,(struct sockaddr*)&clientAddr,&clientAddrLen);
 24     ERROR_CHECK(ret,-1,"recvfrom");
 25     printf("client ip = %s\n",inet_ntoa(clientAddr.sin_addr));
 26     printf("client port = %d\n",ntohs(clientAddr.sin_port));
 27 
 28     //使用sendto系统调用发送消息
 29     //与send主要的区别就是多了一个要填入的目标地址参数
 30     ret = sendto(sockFd,"hello udp",9,0,(struct sockaddr*)&addr,sizeof(addr));                                                               
 31     close(sockFd);
 32 }

客户端:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(int argc,char* argv[]){
  4     // ./client 10.0.8.4 8080
  5     ARGS_CHECK(argc,3);
  6     //创建socket缓冲区,其中第二个参数改为SOCK_DGRAM表示使用UDP协议
  7     int sockFd = socket(AF_INET,SOCK_DGRAM,0);
  8     ERROR_CHECK(sockFd,-1,"socket");
  9     //创建发送数据的目标地址
 10     struct sockaddr_in addr;
 11     addr.sin_family = AF_INET;
 12     addr.sin_port = htons(atoi(argv[2]));
 13     addr.sin_addr.s_addr = inet_addr(argv[1]);
 14     //使用sendto系统调用发送消息
 15     //与send主要的区别就是多了一个要填入的目标地址参数
 16     int ret = sendto(sockFd,"hello udp",9,0,(struct sockaddr*)&addr,sizeof(addr));
 17     close(sockFd);                                                                                                                           
 18 }

运行结果:

真正的即时通讯代码:

服务端:

cpp 复制代码
#include <43func.h>
int main(int argc, char *argv[]){
    // ./server_chat 192.168.14.9 1234
    ARGS_CHECK(argc,3);
    int sockFd = socket(AF_INET,SOCK_DGRAM,0);
    ERROR_CHECK(sockFd,-1,"socket");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    int ret = bind(sockFd,(struct sockaddr *)&addr, sizeof(addr));
    ERROR_CHECK(ret,-1,"bind");
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    char buf[1024] = {0};
    ret = recvfrom(sockFd,buf,sizeof(buf),0,(struct sockaddr *)&clientAddr, &clientAddrLen);
    ERROR_CHECK(ret,-1,"recvfrom");
    printf("buf = %s\n", buf);
    printf("client ip = %s\n", inet_ntoa(clientAddr.sin_addr));
    printf("client port = %d\n", ntohs(clientAddr.sin_port));
    fd_set rdset;
    while(1){
        FD_ZERO(&rdset);
        FD_SET(STDIN_FILENO,&rdset);
        FD_SET(sockFd,&rdset);
        select(sockFd+1,&rdset,NULL,NULL,NULL);
        puts("select returns");
        if(FD_ISSET(STDIN_FILENO,&rdset)){
            bzero(buf,sizeof(buf));
            int ret = read(STDIN_FILENO,buf,sizeof(buf));
            if(ret == 0){
                sendto(sockFd,buf,0,0,(struct sockaddr *)&clientAddr,clientAddrLen);
                break;
            }
            sendto(sockFd,buf,strlen(buf),0,(struct sockaddr *)&clientAddr,clientAddrLen);
        }
        if(FD_ISSET(sockFd,&rdset)){
            bzero(buf,sizeof(buf));
            int ret = recvfrom(sockFd,buf,sizeof(buf),0,(struct sockaddr *)&clientAddr,&clientAddrLen);
            if(ret == 0){
                break;
            }
            puts(buf);
        }
    } 
    close(sockFd);
}

客户端:

cpp 复制代码
#include <43func.h>
int main(int argc, char *argv[]){
    // ./client_chat 192.168.14.9 1234
    ARGS_CHECK(argc,3);
    int sockFd = socket(AF_INET,SOCK_DGRAM,0);
    ERROR_CHECK(sockFd,-1,"socket");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    char buf[1024] = "I am comming";
    int ret = sendto(sockFd,buf,strlen(buf),0,(struct sockaddr *)&addr, sizeof(addr));
    ERROR_CHECK(ret,-1,"recvfrom");
    fd_set rdset;
    while(1){
        FD_ZERO(&rdset);
        FD_SET(STDIN_FILENO,&rdset);
        FD_SET(sockFd,&rdset);
        select(sockFd+1,&rdset,NULL,NULL,NULL);
        puts("select returns");
        if(FD_ISSET(STDIN_FILENO,&rdset)){
            bzero(buf,sizeof(buf));
            int ret = read(STDIN_FILENO,buf,sizeof(buf));
            if(ret == 0){
                sendto(sockFd,buf,0,0,(struct sockaddr *)&addr,sizeof(addr));
                break;
            }
            sendto(sockFd,buf,strlen(buf),0,(struct sockaddr *)&addr,sizeof(addr));
        }
        if(FD_ISSET(sockFd,&rdset)){
            bzero(buf,sizeof(buf));
            socklen_t addrLen = sizeof(addr);
            int ret = recvfrom(sockFd,buf,sizeof(buf),0,(struct sockaddr *)&addr,&addrLen);
            if(ret == 0){
                break;
            }
            puts(buf);
        }
    } 
    close(sockFd);
}

基本流程和之前基于TCP的差不多,不再赘述。

UDP和TCP 的区别

select的缺陷

我们之前使用的IO多路复用机制select有缺陷:

1、fd_set的本质是一个位图,容量固定1024 (可以扩容解决,但这要重新编译内核)

2、监听和就绪用的是同一个数据结构 -> 使用很麻烦(要改的话又会很困难)

3、性能有问题,我们定义的fd_set要多次大量从用户态拷贝到内核态,大量指的是字节数,定义的字节数越大拷贝的代价越大,多次则是指在每一次调用select之前我们都要把fd_set给清空一遍,因为监听和就绪没有分离所以每次调用select之前都要重新设置监听,这就意味着要重新拷贝一次。这就是select的性能瓶颈。

4、还是性能问题,select是采用轮询的方式来查找就绪的文件描述符fd,这在海量数据少量就绪下造成的性能问题尤为严重。

正因为select有以上缺陷,所以我们推出了更加高效的IO多路复用机制:epoll。

高并发服务器的基石:epoll

epoll原理

epoll会将监听和就绪文件放在epoll文件对象中,该文件对象放在内核态中,要访问该文件对象同样要使用文件描述符。

该文件对象有两个核心结构,一个是监听集合, 一个是就绪事件队列。

要监听谁就将谁放进监听集合中,监听集合是个红黑树,每个树的节点都是一个socket。

现在就只要监听这个集合中的任意一个节点就绪即可,没有节点就绪那么epoll就阻塞;但如果某个节点就绪就将该节点拷贝到就绪事件队列,然后epoll解除阻塞,此时就将就绪事件队列整个从内核态拷贝到用户态,在用户态中我们就只需要使用循环来遍历一下就绪事件队列即可。

epoll使用步骤:

另外补充一点,select是在跨平台的,但是epoll只能在Linux上。

epoll_create系统调用:创建文件对象

记得要加系统调用的头文件嗷。

简单测试:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 int main(){
  4     //该函数的唯一参数是用来描述监听集合大小的
  5     //但现在没什么用了,填个大于0的数就行
  6     int epfd =  epoll_create(1);
  7     ERROR_CHECK(epfd,-1,"epoll_create");
  8     printf("epfd = %d\n",epfd);
  9     close(epfd);                                                                                                                             
 10 }

编译运行:

epoll_ctl系统调用:设置监听

epoll_wait系统调用:陷入阻塞

使用上述系统调用来重写我们之前的服务端程序,IO多路复用只针对服务端,所以客户端代码并不需要改:

服务端程序:

cpp 复制代码
  1 #include <43func.h>
  2 
  3 //服务端程序
  4 int main(int argc,char* argv[]){
  5     // ./server 10.0.8.4 8080
  6     ARGS_CHECK(argc,3);
  7     //建立socket缓冲区
  8     int sockFd = socket(AF_INET,SOCK_STREAM,0);
  9     ERROR_CHECK(sockFd,-1,"socket");
 10 
 11     int optval = 1;
 12     int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
 13     ERROR_CHECK(ret,-1,"setsockopt");
 14     //地址
 15     struct sockaddr_in addr;
 16     //设置IPV4
 17     addr.sin_family = AF_INET;
 18     //将命令行中的string类型端口号转成数字然后转成大端模式在网络中传输
 19     addr.sin_port = htons(atoi(argv[2]));
 20     //设置IP地址
 21     addr.sin_addr.s_addr = inet_addr(argv[1]);
 22 
 23     //绑定本机IP地址端口号,这表示其成为了一个服务端程序
 24     ret = bind(sockFd,(struct sockaddr*)&addr,sizeof(addr));
 25     ERROR_CHECK(ret,-1,"bind");
 26 
 27     //监听来自客户端的连接请求
 28     ret = listen(sockFd,0);
 29     ERROR_CHECK(ret,-1,"listen");
 30 
 31     //从全连接队列里面拿到一个就绪的建立连接
 32     int netFd = accept(sockFd,NULL,NULL);
 33     ERROR_CHECK(netFd,-1,"accept");
 34 
 35     //处理IO多路复用问题,使用epoll机制
 36     //创建epoll文件对象  
 37     int epfd = epoll_create(1);                                                                                                              
 38     ERROR_CHECK(epfd,-1,"epoll_create");
 39     //加入监听
 40     struct epoll_event event;//该结构体用来设置文件描述符的事件
 41     event.data.fd = STDIN_FILENO; //标准输入要加入监听
 42     event.events = EPOLLIN;
 43     epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&event); //把stdin以读事件加入监听                            
 44 
 45     //把netFd加入监听
 46     event.data.fd = netFd;
 47     event.events = EPOLLIN;
 48     //把已连接的socket以读事件加入监听
 49     epoll_ctl(epfd,EPOLL_CTL_ADD,netFd,&event);
 50 
 51     //循环处理就绪事件队列
 52     char buf[4096] = {0};//读写缓冲
 53     struct epoll_event readyArr[2];//就绪事件队列的内存
 54     while(1){
 55         //epoll_wait的返回值是就绪事件的个数
 56         int readyNum =  epoll_wait(epfd,readyArr,2,-1);
 57         puts("epoll_wait returns");
 58         for(int i=0;i < readyNum; i++){
 59             //如果就绪的是标准读输入
 60             if(readyArr[i].data.fd == STDIN_FILENO){
 61                 bzero(buf,sizeof(buf));
 62                 int ret = read(STDIN_FILENO,buf,sizeof(buf));
 63                 if(ret == 0){
 64                     goto end;
 65                 }
 66                 //说明有输入,那么就发送数据
 67                 send(netFd,buf,strlen(buf),0);
 68             }else if(readyArr[i].data.fd == netFd){
 69                 bzero(buf,sizeof(buf));
 70                 int ret = recv(netFd,buf,sizeof(buf),0);
 71                 if(ret == 0){
 72                     goto end;
 73                 }
 74                 puts(buf);
 75             }
 76         }
 77     }
 78 end:
 79     //关闭缓冲区
 80     close(sockFd);
 81     close(netFd);
 82     close(epfd);
 83 }

编译运行:

改写之前select断线重连问题以及新增超时断开功能

还是只要改服务端程序即可:

cpp 复制代码
#include <43func.h>
int main(int argc, char *argv[]){
    // ./server 192.168.14.9 1234
    ARGS_CHECK(argc,3);
    int sockFd = socket(AF_INET,SOCK_STREAM,0);
    ERROR_CHECK(sockFd,-1,"socket");
    int optval = 1;
    int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
    ERROR_CHECK(ret,-1,"setsockopt");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    ret = bind(sockFd,(struct sockaddr *)&addr,sizeof(addr));
    ERROR_CHECK(ret,-1,"bind");
    ret = listen(sockFd,10);
    ERROR_CHECK(ret,-1,"listen");
    int netFd = -1;
    int epfd = epoll_create(1);//创建epoll文件对象
    ERROR_CHECK(epfd,-1,"epoll_create");
    struct epoll_event event;
    event.data.fd = STDIN_FILENO;
    event.events = EPOLLIN; 
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&event);//把stdin以读事件加入监听
    event.data.fd = sockFd;
    event.events = EPOLLIN; 
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockFd,&event);//把监听socket以读事件加入监听
    char buf[4096] = {0};
    struct epoll_event readyArr[3];
    int isConnected = 0;
    time_t lastMsg,check;
    while(1){
        int readyNum = epoll_wait(epfd,readyArr,3,1000);//epoll_wait的返回值是就绪事件的个数
        puts("epoll_wait returns");
        if(readyNum == 0 && isConnected != 0){
            puts("time out");
            check = time(NULL);
            if(check-lastMsg > 5){
                send(netFd,"You are free", 12, 0);
                close(netFd);
                event.data.fd = netFd;
                event.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_DEL,netFd,&event);
                isConnected = 0;
            }
        }
        for(int i = 0; i < readyNum; ++i){
            if(readyArr[i].data.fd == STDIN_FILENO){
                if(isConnected == 0){
                    int ret = read(STDIN_FILENO,buf,sizeof(buf));
                    if(ret == 0){
                        goto end;
                    }
                }
                bzero(buf,sizeof(buf));
                int ret = read(STDIN_FILENO,buf,sizeof(buf));
                if(ret == 0){
                    goto end;
                }
                send(netFd,buf,strlen(buf),0);
            }
            else if(readyArr[i].data.fd == sockFd){
                if(isConnected != 0){
                    int noFd = accept(sockFd,NULL,NULL);
                    close(noFd);
                    continue;
                }
                netFd = accept(sockFd,NULL,NULL);
                ERROR_CHECK(netFd,-1,"accept");
                event.data.fd = netFd;
                event.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,netFd,&event);
                isConnected = 1;
                lastMsg = time(NULL);
            }
            else if(readyArr[i].data.fd == netFd){
                bzero(buf,sizeof(buf));
                int ret = recv(netFd,buf,sizeof(buf),0);
                if(ret == 0){
                    close(netFd);
                    event.data.fd = netFd;
                    event.events = EPOLLIN;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,netFd,&event);
                    isConnected = 0;
                }
                lastMsg = time(NULL);
                puts(buf);
            }
        }
    }
end:
    close(netFd);
    close(epfd);
    close(sockFd);
}

recv和read的非阻塞

fcntl系统调用:给已经打开的fd加上非阻塞属性

简单使用一下:

cpp 复制代码
#include <43func.h>
int setNonblock(int fd){
    int status = fcntl(fd,F_GETFL);
    status |= O_NONBLOCK;//既要原来的属性也要O_NONBLOCK
    int ret = fcntl(fd,F_SETFL,status);
    ERROR_CHECK(ret,-1,"fcntl");
    return 0;
}
int main(int argc,char *argv[]){
    // ./server 192.168.14.9 1234
    ARGS_CHECK(argc,3);
    int sockFd = socket(AF_INET,SOCK_STREAM,0);
    //创建一个socket 支持IPv4和TCP
    ERROR_CHECK(sockFd,-1,"socket");
    printf("sockFd = %d\n", sockFd);
    struct sockaddr_in addr;//创建时 用 sockaddr_in
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    //调用函数时,先取地址再强转
    //对于bind而言 addr一定是本地地址
    int ret = bind(sockFd,(struct sockaddr *)&addr,sizeof(addr));
    ERROR_CHECK(ret,-1,"bind");
    ret = listen(sockFd,10);
    ERROR_CHECK(ret,-1,"listen");
    struct sockaddr_in clientAddr;
    //socklen_t clientAddrLen = /*sizeof(clientAddr)*/ 0;//写0是会出问题
    socklen_t clientAddrLen = sizeof(clientAddr);//写0是会出问题
    int netFd = accept(sockFd,(struct sockaddr *)&clientAddr,&clientAddrLen);
    ERROR_CHECK(netFd,-1,"accept");
    printf("netFd = %d\n", netFd);
    printf("client addr length = %d\n", clientAddrLen);
    printf("client family = %d\n", clientAddr.sin_family);
    printf("client port = %d\n", ntohs(clientAddr.sin_port));
    printf("client ip = %s\n",inet_ntoa(clientAddr.sin_addr));
    char buf[4096] = {0};
    // //setNonblock(netFd);
    // ret = recv(netFd,buf,sizeof(buf),MSG_DONTWAIT);
    // if(ret == -1){
    //     puts("no resource now!\n");
    // }
    // ret = recv(netFd,buf,sizeof(buf),0);
    // if(ret == -1){
    //     puts("no resource now!\n");
    // }
    // //ERROR_CHECK(ret,-1,"recv");
    // // while(1){
    // //     ret = recv(netFd,buf,sizeof(buf),MSG_DONTWAIT);
    // //     //临时让本次recv是非阻塞的
    // //     if(ret == -1){
    // //         puts("no resource now!\n");
    // //         sleep(1);
    // //     }
    // //     else{
    // //         break;
    // //     }
    // // }
    bzero(buf,sizeof(buf));
    ret = recv(netFd,buf,sizeof(buf),MSG_PEEK);
    puts(buf);
    bzero(buf,sizeof(buf));
    ret = recv(netFd,buf,sizeof(buf),0);
    puts(buf);
    close(sockFd);
}

epoll的触发方式:边缘触发与水平触发

水平触发:

如图所示,可以类比为厨师给客人做菜,客人点了五个菜,那么厨师肯定要把这五个菜做完他才能转去给别人做,做菜速度有快有慢,有时候一个小时做两个,有时候一个小时做三个,但总归来说都是一直在做的,这就类比于epoll的水平触发,这会导致当客户有大量数据时epoll的重复就绪。

边缘触发:

如图所示,一样是做菜的例子,但情况不同的是厨师很可能偷懒给你做了一个菜之后就转去给另外一个人做菜了,于是你只能催他,催一次他就给你做一道,不催就不做,这种就是边缘触发。

示例:

cpp 复制代码
#include <43func.h>
int main(int argc, char *argv[]){
    // ./server 192.168.14.9 1234
    ARGS_CHECK(argc,3);
    int sockFd = socket(AF_INET,SOCK_STREAM,0);
    ERROR_CHECK(sockFd,-1,"socket");
    int optval = 1;
    int ret = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(int));
    ERROR_CHECK(ret,-1,"setsockopt");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    ret = bind(sockFd,(struct sockaddr *)&addr,sizeof(addr));
    ERROR_CHECK(ret,-1,"bind");
    ret = listen(sockFd,10);
    ERROR_CHECK(ret,-1,"listen");
    int netFd = accept(sockFd,NULL,NULL);
    ERROR_CHECK(netFd,-1,"accept");
    int epfd = epoll_create(1);//创建epoll文件对象
    ERROR_CHECK(epfd,-1,"epoll_create");
    struct epoll_event event;
    event.data.fd = STDIN_FILENO;
    event.events = EPOLLIN; 
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&event);//把stdin以读事件加入监听
    event.data.fd = netFd;
    //event.events = EPOLLIN; 
    event.events = EPOLLIN|EPOLLET; 
    epoll_ctl(epfd,EPOLL_CTL_ADD,netFd,&event);//把已连接socket以读事件加入监听
    char buf[5] = {0};
    struct epoll_event readyArr[2];
    while(1){
        int readyNum = epoll_wait(epfd,readyArr,2,-1);//epoll_wait的返回值是就绪事件的个数
        puts("epoll_wait returns");
        for(int i = 0; i < readyNum; ++i){
            if(readyArr[i].data.fd == STDIN_FILENO){
                bzero(buf,sizeof(buf));
                int ret = read(STDIN_FILENO,buf,sizeof(buf)-1);
                if(ret == 0){
                    goto end;
                }
                send(netFd,buf,strlen(buf),0);
            }
            else if(readyArr[i].data.fd == netFd){
                bzero(buf,sizeof(buf));
                //int ret = recv(netFd,buf,sizeof(buf)-1,0);
                //if(ret == 0){
                //    goto end;
                //}
                //puts(buf);
                int ret;
                while(1){
                    bzero(buf,sizeof(buf));
                    ret = recv(netFd,buf,sizeof(buf)-1,MSG_DONTWAIT);
                    puts(buf);
                    if(ret == 0 || ret == -1){
                        break;
                    }
                }
            }
        }
    }
end:
    close(netFd);
    close(epfd);
    close(sockFd);
}

补充:设置socket的属性


设置缓冲区下限的属性:

recv系统调用的MSG_PEEK属性:

当然socket的属性还有非常多,需要的时候去查对应的man手册进行查询即可,查询命令:

man 7 socket

相关推荐
tan77º16 分钟前
【项目】分布式Json-RPC框架 - 项目介绍与前置知识准备
linux·网络·分布式·网络协议·tcp/ip·rpc·json
正在努力的小河3 小时前
Linux设备树简介
linux·运维·服务器
荣光波比3 小时前
Linux(十一)——LVM磁盘配额整理
linux·运维·云计算
LLLLYYYRRRRRTT3 小时前
WordPress (LNMP 架构) 一键部署 Playbook
linux·架构·ansible·mariadb
轻松Ai享生活4 小时前
crash 进程分析流程图
linux
大路谈数字化5 小时前
Centos中内存CPU硬盘的查询
linux·运维·centos
luoqice6 小时前
linux下查看 UDP Server 端口的启用情况
linux
倔强的石头_7 小时前
【Linux指南】动静态库与链接机制:从原理到实践
linux
赏点剩饭7787 小时前
linux中的hostpath卷、nfs卷以及静态持久卷的区别
linux·运维·服务器
神鸟云8 小时前
DELL服务器 R系列 IPMI的配置
linux·运维·服务器·网络·边缘计算·pcdn