5 Linux系统编程之网络编程--学习笔记

目录:

      • 1.网络相关概念
      • 2.tcp协议
      • 3.UDP协议
      • 4.网络地址转换
      • 5.tcp协议socket编程
      • 6. udp协议socket编程流程
      • 7.epoll多路复用
      • 8.总结
      • 9.进程池(以文件传输服务器为例)
      • 10.线程池

查看文件大小du -sh filename

查看磁盘相关信息df

vim 中 dG命令删除当前行到最后

md5sum 文件名:查看文件的md5码,可以唯一标识一个文件

vim中 :开始行,结束行d 删除中间的行

1.网络相关概念

  1. 网络通信才是更广泛的通信方式。

  2. 两台主机通过ip地址连接。

  3. 网络通信其实就是两台主机之间的进程通信。是通过端口号找到进程。

  4. 由一个交换机与连接在该设备上的若干个主机就构成了一个局域网。

  5. 通信双方必须遵守的规则:协议

  6. 网络协议分为两种模型:

    1. OSI参考模型(7层)(物链网应表会传):更偏向于理论研究
    2. TCP/IP四层:应传二网:更利于实际开发
  7. 为什么要把协议分层?

    分层之后,每一层只负责完成自己的任务,下层协议向上层协议提供了基础服务,上层协议使用下层协议, 不关心底层的实现细节,只需要完成各自的职能。

  8. 协议除了分层,还分为公有协议和私有协议

  9. tcp/ip是一类协议,是协议族

  10. https:http协议+ssl(安全套接字协议)

  11. 传输层两个重要协议:

    1. tcp(传输控制协议: 面向连接的,保证数据的安全可靠传输,比较慢。(常用于文件传输)
    2. udp(用户数据报协议):非面向连接,速度快,但是传输的数据不可靠(应用场景:实时类游戏上,即时聊天,在线直播)
  12. 每一台设备都由唯一的一个物理地址。

  13. 端口号的范围:[0 - 2的16次方)

  14. ipv4占4个字节,ipv6占16个字节、物理地址占6个字节,端口号占2个字节

  15. 端口号:

    1. 0-1023不是给普通用户使用的,是给一些常用的协议和应用使用的。
    2. 用户使用的范围:(1024-10000)
  16. 以太网是现有局域网最常使用的通信协议标准。

  17. 以太网帧格式数据的长度:46 - 1500字节
    5 Linux系统编程之网络编程--学习笔记_第1张图片

  18. ARP地址转换协议:把ip地址转换成物理地址(mac)

  19. arp缓存表:记录主机的ip和主机物理地址的对应关系

  20. 生存时间:TTL(time to live)指的是跳数

  21. ip数据包头长最长是60个字节。20个字节的基本头长+选项长度。
    5 Linux系统编程之网络编程--学习笔记_第2张图片

  22. 六度拓朴理论

  23. 路由器:连接两个或多个网络的硬件设备,可以解析,各种不同类型的网络传来的数据包的目的地址,在根据特定的路由算法把各数据包按照最佳路线传送到指定位置。工作在网络层。

  24. 路由表:记录两台主机之间最优的路径(类似于高德地图)

  25. 交换机:是一种基于mac地址识别,完成以太网数据帧转发的网络设备。工作在网络接口层。


2.tcp协议

5 Linux系统编程之网络编程--学习笔记_第3张图片

  1. 面向连接:正在建立连接、还是正在连接、或者正在关闭连接的状态。

  2. tcp为保证数据安全可靠的传输:序列号、确认重传(重传的次数一般为10次)、滑动窗口(规定通信双方传输数据的大小)(发送方与接收方的窗口大小不一定是一样大)。

  3. ack(Ack)确认号;ACK:确认标志位,表示确认数据信息的,tcp协议规定,三次握手建立完成之后,必须带ACK = 1标志位。

  4. SYN标志位:当SYN=1,表示请求建立连接

  5. FIN标志位:当FIN=1, 表示请求断开连接

  6. 序列号其实是一个随机值:

    1. 从安全的角度来讲:随机值不利攻击者捕获数据包。
    2. 从稳定角度来讲:防止断开重连后,延迟的数据包重新被接收。
  7. ISN = M+F (序列号生成算法)

  8. wireshark显示相对序列号的设置方式:编辑->首选项->protocols->tcp->勾选relative sequence numbers。

  9. 确认号ack是x+1的原因:

    1. 期望下一次收到的数据包的序列是x+1;
    2. SYN标志位占用一个序列号
  10. 三次握手的流程:

    1. A->B:SYN=1, seq=x;
    2. B->A: SYN=1, ACK=1, seq = y, ack=x+1;
    3. A->B: ACK=1, seq=x+1, ack=y+1;
  11. tcp协议第三次握手数据包的序列号和第一次发送数据的数据包的序列号是相同的。

  12. 两次握手能不能建立连接?

    1. 可以:前提网络必须通常。
    2. 不可以:如果网络状态不好,造成第一握手在网络中延迟,过了一段时间,主机A重传第一次握手(当前网络状态恢复正常),很快的与与主机B建立连接,数据交互完毕之后关闭连接。此时在网络中延迟的第一次握手重新到达了主机B,主机B发送第二次握手建立连接,但是主机A不会再理会这次连接,所以就造成了,主机B一直等待主机A送数据,浪费主机B的资源。
  13. tcp三次握手改成二次握手行不行?

    1. 为了防止丢失的请求报文重新到达对方。
    2. 第三次握手是对第二次握手的确认,如果少了这次确认,就无法保证服务端的序列号被客户端成功的接收。
  14. 三次握手时报文丢失:

    1. 第一次握手丢失:发送方会重传第一次握手
    2. 第二次握手丢失:发送方和接收方都可能会重传。
    3. 第三次握手丢失:
      1. 通信双方都没有数据要发送:主机B重传第二次握手
      2. 主机B要想先发送数据:主机B会重传第二次握手
      3. 主机A要想先发送数据:可以直接发送数据, 主机B直接接收数据
  15. Linux系统下2MSL是60秒

  16. tcp四次挥手:

    1. A->B:FIN=1, seq=u;

    2. B->A:ACK=1, seq=v, ack = u+1;

    3. B->A数据传输

    4. B->A:FIN=1,ACK=1, seq= w, ack=u+1;

    5. A->B:ACK=1, seq=u+1, ack= w+1
      5 Linux系统编程之网络编程--学习笔记_第4张图片

  17. 只有断开方才有TIME-WAIT状态

  18. 为什么主动断开方要有TIME-WAIT状态(2MSL)?

    1. 保证通信双方四次挥手顺利完成,能够正常的关闭tcp连接。
    2. 保证旧的报文在网络中消失。 //防止网络延时,B端重复发两次数据,第二次发后断开连接,又连接,此时阻塞数据到达A,若随机的偏移量一致,出现冲突,其实出现冲突的概率很小
  19. 四次挥手改成三次挥手行不行?

    1. 可以:

      1.前提是网络状态非常好.

      2.主机B发送完第二次挥手之后,没有数据发送给主机A,那么第二次挥手可以和第三次挥手合并。B->A:FIN=1,ACK=1,seq=v, ack=u+1;

    2. 不可以:第三次挥手数据丢失,主机B断开连接,主机A不知道B已断开连接,会等待数据,造成资源浪费。

  20. MTU 是网络传输最大报文包 = 数据的长度 + 协议的头长 1500个字节

  21. MSS 是网络传输数据最大值(不包含协议的长度)
    1500 - 20(ip头长) - 20(tcp头长) = 1460字节


3.UDP协议

5 Linux系统编程之网络编程--学习笔记_第5张图片

  1. udp协议的特点:

    1. 没有建立连接:在传输数据前不需要建立连接
    2. 不重新排序:对到达顺序混乱的数据包不进行重新排序。
    3. 没有确认:发送数据包无需等待对方确认。因此,udp协议可以随时发送数据,但是无法保证数据成功被对方接收。
  2. 可以在应用层对upd进行安全性的封装,使其传输数据安全可靠。已经有开源代码实现该功能,分别是:RUDP、RTP、UDP。

  3. 协议的选择:

    1. 对于可靠性来讲:要求可靠性高采用tcp协议,反之选择udp
    2. 从实时性的角度:对实时性要求高的选择udp协议,反之tcp
    3. 根据网络状态选择:网络状态好采用udp,网络状态差选择tcp
  4. C/S模式是传统的应用模式:含有客户端和服务端

  5. B/S模式是浏览器充当客户端。


4.网络地址转换

  1. 什么是套接口(socket):又称为套接字,是操作系统内核中的一个数据结构, 它是网络中的节点进行相互通信的门户 ,是网络进程的ID。是一个文件描述符。

  2. 套接口的组成:ip+端口号

  3. NAT技术(网络地址转换技术):内部私有的网络地址转换成合法的网络ip地址。可以有效的解决公网ip分配不足的问题。

  4. 一个完整的套接字:协议、本机IP、本机的端口、对端ip、对端的端口。

  5. tcp像管道、udp像消息队列

  6. 小端模式:内存的低地址存储数据的低字节,高地址存数据的高字节。

  7. 大端模式:内存低地址存储数据的高字节,高地址存放数据低字节。

  8. IBM公司最初存储数据采用的是大端模式。因特尔、AMD采用小端模式。

  9. 主机字节序:电脑采用什么方式存储数据的。

  10. 网络字节序:二进制值,大端模式。

  11. 在编程中,需要把主机字节序转换成网络字节序。

  12. 网络端口的主机字节序和网络字节序的相互转换

    uint16_t htons(uint16_t hostshort);
    uint16_t ntohs(uint16_t netshort);
    
  13. IPV4中ip地址主机字节序和网络字节序的相互转换

    //主机字节序转为网络字节序
    in_addr_t inet_addr(const char *cp); 
    //成功返回,转换的结果 
    //参数是ip地址 
    int inet_aton(const char *cp, struct in_addr *inp); 
    //参数1:需要转换的ip地址 
    //参数:保存转换后的网络ip地址。 
    
    
    //网络字节序转换为主机字节序
    char *inet_ntoa(struct in_addr in)
        
    //结构体原型:
    struct in_addr{
        in_addr_t s_addr; //in_addr_t其实就是unsigned int
    }
    
  14. 兼容IPV4和IPV6的IP地址转换函数

    int inet_pton(int family, const char *src, void *dst);
    
    const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);
    //参数1:地址类型,AF_INET表示是ipv4协议,AF_INET6,表示ipv6协议
    //参数2:需要转换的网络字节序,是in_addr类型
    //参数3:保存转换后的结果 
    //参数4:保存结果的大小 
    
  15. 名字地址转换(域名和IP地址的相互转换)

    //函数原型:
    struct hostent* gethostbyname(const char* hostname);
    struct hostent* gethostbyaddr(const char* addr, size_t len, int family); 
    
    //结构体原型:
    struct hostent { 
        char *h_name; /*正式主机名*/ 
        char **h_aliases; /*主机别名*/ 
        int h_addrtype; /*主机 IP 地址类型IPv4为AF_INET,ipv6为AF_INET6*/ 
        int h_length; /*主机 IP 地址字节长度,对于 IPv4 是 4 字节,即 32 位*/ 
        char **h_addr_list; /*主机的 IP 地址列表*/ 
    }
    
    //使用举例
    #include 
    int main(int argc, char **argv)
    {
       struct hostent* pHost = gethostbyname(argv[1]);
       if(NULL == pHost){
           return -1;
       }
    
       printf("h_name = %s\n", pHost->h_name);
    
       for(int i = 0; pHost->h_aliases[i] != NULL; ++i){
           printf("alias = %s\n", pHost->h_aliases[i]);
       }
    
       printf("type = %d\n", pHost->h_addrtype);
    
       printf("len = %d\n", pHost->h_length);
    
       char buf[64] = {0};
       for(int i = 0; pHost->h_addr_list[i] != NULL; ++i){
           memset(buf, 0, sizeof(buf));
           inet_ntop(pHost->h_addrtype, pHost->h_addr_list[i], buf, sizeof(buf));
           printf("buf = %s\n", buf);
       }
       return 0;
    }
    
    

5.tcp协议socket编程

1.服务端流程

  1. int socket(int domain, int type, int protocol);

    使用该函数生成一个套接口、用于与客户端建立连接时使用的。

    1. 成功返回socket文件描述符,失败返回-1
    2. 参数1:ipv4(AF_INET),或者ipv6(AF_INET6)
    3. 参数2:通信的类型,tcp,udp ,原始的(用于新的网络协议实现的测试):SOCK_STREAM/SOCK_DGRAM/SOCK_RAW
    4. 参数3:协议编号,一般为0;
  2. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    绑定本机的ip和端口号,绑定到sfd(socket函数返回值) 。

    1. 成功返回0,失败返回-1

    2. 参数1:socket函数的返回值

    3. 参数2:保存ip和端口的结构体

    4. 参数3:结构体的大小

      //结构体原型:
      struct sockaddr { 
          unsigned short sa_family; /*地址族*/ 
          char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/ 
      }; 
      struct sockaddr_in { 
          short int sin_family; /*地址族*/ 
          unsigned short int sin_port; /*端口号*/ 
          struct in_addr sin_addr; /*IP 地址*/ 
          unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/ 
      };
      
  3. int listen(int sockfd, int backlog);监听在sfd上的客户端的连接

    1. 参数1:sfd
    2. 参数2:代表同时监听的最大连接数,最大值是128,一般填5或者10
    3. 成功返回0,失败返回-1
  4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);接收对端的连接

    1. 返回值:成功返回一个新的socket文件描述符newFd,该描述符作用,是与连接上服务器的客户端进行数据交互时使用的。失败返回-1;
    2. 参数1:sfd
    3. 参数2:保存对端的ip和端口号,不关心谁连接的话填NULL
    4. 参数3:结构体长度的变量指针

      太浪漫了:accept()函数准备好了,系统调用 accept()会有点古怪的地方的!你可以想象发生这样的事情:有人从很远的地方通过一个你在侦听(listen())的端口连接(connect())到你的机器。它的连接将加入到等待接受(accept())的队列中。你调用accept()告诉它你有空闲的连接,它将返回一个新的套接字文件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口,新的在准备发送(send())和接收(recv())数据。这就是这个过程!

  5. ssize_t recv(int sockfd, void *buf, size_t len, int flags);从对端接收数据,该函数是一个阻塞性函数;

    1. 返回值:成功返回接收到的字节数,返回0的时候,代表发送方连接断开,失败返回-1
    2. 参数1:newFd
    3. 参数2:保存数据的位置
    4. 参数3:最多接收的字节数
    5. 参数4:标志位,一般置为0
  6. ssize_t send(int sockfd, const void *buf, size_t len, int flags); 向对端发送数据

    1. 返回值:成功返回成功发送的字节数,失败返回-1,
    2. 参数1:newFd
    3. 参数2:发送的数据
    4. 参数3:发送数据的大小
    5. 参数4:标志位,一般置为0;
  7. int close(int fd); 关闭文件描述符

2.客户端的流程

  1. socket

  2. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 连接服务器

    1. 返回值:成功返回0, 失败返回-1
    2. 参数1:sfd
    3. 参数2:服务器的ip和端口
    4. 参数3:结构体的大小
  3. send

  4. recv

  5. close

3.服务器客户端交流(应用select)

  1. select的底层实现:位图,只能保存0和1。FD_ZERO初始化时,位图所有的位为0。

  2. FD_SET:把需要监听的描述符加入到集合当中,位图所对应的位置为1。

  3. 如果select监控到描述符就绪,他就不去修改该描述符在位图所对应值。如果该文件描述符没有就绪,select会将该文件描述符所对应位图的值置为0。

  4. tcp_chat聊天结束后(客户端ctrl+c),服务端疯狂打印(newFd就绪)

    原因:客户端主动断开,处于close_wait状态,所以内核检测到与newFd连接的对端断开了,内核会使用newFd告诉服务端,对方已经断开,你也可以断开了。

  5. 查看网络相关状态命令: netstat -nat

  6. 客户端开的端口又叫临时端口,这个端口范围大于10000, 小于65535.

  7. 编程中goto尽量少使用

    1. goto会破坏程序的局部性。
    2. goto一般应用于嵌入式开发
  8. tcp建立三次握手的时候,内核做了哪些事情。

    1. 客户端发送第一次握手,请求建立连接,服务端接收第一次握手,并且回复第二次握手,回复完的同时,把未建立完的tcp连接加入到半连接队列里面(其实是将两次数据包加入);当客户端回复第三次握手的时候,服务端从半连接队列里面取出第一个连接与第三次握手合并成一个完整的tcp连接,并且加入到全连接队列里面。

    2. accep其实就是全连接队列里面取出第一个连接。accept函数是在内核空闲的时候从全连接队列里面取数据。
      5 Linux系统编程之网络编程--学习笔记_第6张图片

    3. listen的第二个参数就是指定全连接队列(半连接)的大小。

    4. 全连接队列(半连接)大小是由listen的第二个参数与 /proc/sys/net/core/somaxconn这个配置共同决定(取二者最小值)。

    5. 查看backlog的最大值 cat /proc/sys/net/ipv4/tcp_max_syn_backlog

  9. 设置套接口选项

    int setsockopt(int sockfd, int level, int optname,
                          const void *optval, socklen_t optlen);
    //成功返回0,失败返回-1
    //参数1:sfd
    //参数2:设置层次
    //参数3:设置的选项
    //参数4:设置的选项是否生效,大于0生效
    //参数5:对参数取sizeof
    //参数2选择SOL_SOCKET,对应参数3选择SO_REUSEADDR表示允许重用本地地址和端口
    

    SOL_SOCKET层次的选项:

    SO_RCVBUF 接收缓冲区大小 int

    SO_SNDBUF 发送缓冲区大小 int

    SO_RCVLOWAT 接收缓冲区下限 int

    SO_SNDLOWAT 发送缓冲区下限 int

    获取缓冲区大小,获取的结果是缓冲区大小的二倍。

//应用
//server.c
#include 
int main(int argc, char **argv)
{
   int sfd = socket(AF_INET, SOCK_STREAM, 0);
   ERROR_CHECK(sfd, -1, "socket");

   //保存本机的ip和端口
   struct sockaddr_in serAddr;
   memset(&serAddr, 0, sizeof(serAddr));
   serAddr.sin_family = AF_INET;
   serAddr.sin_addr.s_addr = inet_addr(argv[1]);
   serAddr.sin_port = htons(atoi(argv[2]));
   
   //设置地址可重用
   int ret = 0;
   int reuse = 1; //设置值大于0,表示生效
   ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
   
   //绑定本机的IP和端口,绑定到sfd上
   int ret = 0;
   ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));

   //监听的最大连接数是10
   ret = listen(sfd, 10);
   ERROR_CHECK(ret, -1, "listen");

   //接收连接,返回新的文件描述符,新的描述符作用,与对端进行数据交互使用的
   int newFd = 0; 

   //创建监听集合
   fd_set rdset;
   fd_set needMonitorSet;
   FD_ZERO(&needMonitorSet);
   FD_ZERO(&rdset);

   FD_SET(STDIN_FILENO, &needMonitorSet);
   FD_SET(sfd, &needMonitorSet);

   char buf[64] = {0};
   int readyNum = 0;
   while(1){
       //把需要监听的描述符加入到集合当中
       memcpy(&rdset, &needMonitorSet, sizeof(fd_set));
       readyNum = select(10, &rdset, NULL, NULL, NULL);
       ERROR_CHECK(readyNum, -1, "select");
       for(int i = 0; i < readyNum; ++i){
           //如果是标准输入就绪,就代表需要发送数据给对端
           if(FD_ISSET(STDIN_FILENO, &rdset)){
               memset(buf, 0, sizeof(buf));
               read(STDIN_FILENO, buf, sizeof(buf)); 
               send(newFd, buf, strlen(buf)-1, 0);
           }

           //如果是newFd就绪,就说明对端有数据发送给我们
           if(FD_ISSET(newFd, &rdset)){
               memset(buf, 0, sizeof(buf));
               ret = recv(newFd, buf, sizeof(buf)-1, 0);
               if(0 == ret){
                   close(newFd);
                   FD_CLR(newFd, &needMonitorSet);
                   printf("bey bey\n");
                   continue;
               }
               printf("buf = %s\n", buf);
           }
           //说明有连接到来
           if(FD_ISSET(sfd, &rdset)){
               newFd = accept(sfd, NULL, NULL);
               ERROR_CHECK(newFd, -1, "accept");
               printf("client connect\n");
               FD_SET(newFd, &needMonitorSet);
           }
       }
   }
   close(sfd);
   close(newFd);

   return 0;
}

//client.c
#include 
int main(int argc, char **argv)
{
   int sfd = socket(AF_INET, SOCK_STREAM, 0);
   ERROR_CHECK(sfd, -1, "socket");

   //保存本机的ip和端口
   struct sockaddr_in serAddr;
   memset(&serAddr, 0, sizeof(serAddr));
   serAddr.sin_family = AF_INET;
   serAddr.sin_addr.s_addr = inet_addr(argv[1]);
   serAddr.sin_port = htons(atoi(argv[2]));

   int ret = 0;
   ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
   ERROR_CHECK(ret, -1, "connect");

   //创建监听集合
   fd_set rdset;
   FD_ZERO(&rdset);

   char buf[64] = {0};
   int readyNum = 0;
   while(1){
       //把需要监听的描述符加入到集合当中
       FD_SET(STDIN_FILENO, &rdset);
       FD_SET(sfd, &rdset);
       readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
       for(int i = 0; i < readyNum; ++i){
           //如果是标准输入就绪,就代表需要发送数据给对端
           if(FD_ISSET(STDIN_FILENO, &rdset)){
               memset(buf, 0, sizeof(buf));
               read(STDIN_FILENO, buf, sizeof(buf)); 
               send(sfd, buf, strlen(buf)-1, 0);
           }

           //如果是sfd就绪,就说明对端有数据发送给我们
           if(FD_ISSET(sfd, &rdset)){
               memset(buf, 0, sizeof(buf));
               recv(sfd, buf, sizeof(buf)-1, 0);
               printf("buf = %s\n", buf);
           }
       }
   }
   close(sfd);
   return 0;
}

6. udp协议socket编程流程

1.服务端

  1. socket

  2. bind

  3. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
    //从对端接收数据,并且保存对端的ip和端口 
    
  4. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
    //向指定的ip和端口的对端发送数据 
    
  5. close

2.客户端

  1. socket

  2. sendto

  3. recvfrom

  4. close

  5. 对于upd通信来讲,服务端必须要先recvfrom,然后sendto

    因为,udp时非面向连接的,所以最开始不知道对端的ip和端口,所以先接收对方发来的而数据,并且记录对端的ip和端口。

//server.c
#include 
int main(int argc, char **argv)
{
   //udp的通信
   int sfd = socket(AF_INET, SOCK_DGRAM, 0);
   ERROR_CHECK(sfd, -1, "socket");

   //保存本机的ip和端口
   struct sockaddr_in serAddr;
   memset(&serAddr, 0, sizeof(serAddr));
   serAddr.sin_family = AF_INET;
   serAddr.sin_addr.s_addr = inet_addr(argv[1]);
   serAddr.sin_port = htons(atoi(argv[2]));

   //绑定本机的IP和端口,绑定到sfd上
   int ret = 0;
   ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
   ERROR_CHECK(ret, -1, "bind");

   struct sockaddr_in cliAddr;
   socklen_t len = sizeof(cliAddr);
   memset(&cliAddr, 0, len);

   fd_set rdset;
   FD_ZERO(&rdset);

   int readyNum = 0;
   char buf[64] = {0};

   while(1){
       FD_SET(STDIN_FILENO, &rdset);
       FD_SET(sfd, &rdset);
       readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
       ERROR_CHECK(readyNum, -1, "select");
       for(int i = 0; i < readyNum; ++i){
           if(FD_ISSET(STDIN_FILENO, &rdset)){
               memset(buf, 0, sizeof(buf));
               read(STDIN_FILENO, buf, sizeof(buf));
               ret = sendto(sfd, buf, strlen(buf)-1, 0, (struct sockaddr*)&cliAddr, len);
               ERROR_CHECK(ret, -1, "send");
           }
           if(FD_ISSET(sfd, &rdset)){
               memset(buf, 0, sizeof(buf));
               ret = recvfrom(sfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&cliAddr, &len);
               ERROR_CHECK(ret, -1, "recv");
               printf("buf = %s\n", buf);
           }
       }
   }
   close(sfd);
   return 0;
}


//client.c
#include 
int main(int argc, char **argv)
{
   //udp的通信
   int sfd = socket(AF_INET, SOCK_DGRAM, 0);
   ERROR_CHECK(sfd, -1, "socket");

   //保存本机的ip和端口
   struct sockaddr_in serAddr;
   memset(&serAddr, 0, sizeof(serAddr));
   serAddr.sin_family = AF_INET;
   serAddr.sin_addr.s_addr = inet_addr(argv[1]);
   serAddr.sin_port = htons(atoi(argv[2]));

   fd_set rdset;
   FD_ZERO(&rdset);
   socklen_t len = sizeof(serAddr);
   int ret = 0;

   int readyNum = 0;
   char buf[64] = {0};

   while(1){
       FD_SET(STDIN_FILENO, &rdset);
       FD_SET(sfd, &rdset);
       readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
       ERROR_CHECK(readyNum, -1, "select");
       for(int i = 0; i < readyNum; ++i){
           if(FD_ISSET(STDIN_FILENO, &rdset)){
               memset(buf, 0, sizeof(buf));
               read(STDIN_FILENO, buf, sizeof(buf));
               ret = sendto(sfd, buf, strlen(buf)-1, 0, (struct sockaddr*)&serAddr, len);
               ERROR_CHECK(ret, -1, "send");
           }
           if(FD_ISSET(sfd, &rdset)){
               memset(buf, 0, sizeof(buf));
               ret = recvfrom(sfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&serAddr, &len);
               ERROR_CHECK(ret, -1, "recv");
               printf("buf = %s\n", buf);
           }
       }
   }    
   close(sfd);
   return 0;
}

7.epoll多路复用

  1. 什么是epoll?为什么要引入epoll?

    1. linux为处理大批量的文件描述符,而对select与poll做的改进,它能显著提高程序在大量并发的连接中,只有少量描述符活跃的情况下,提高cpu的利用率。epoll利用一个文件描述符管理多个文件描述符。epoll号称可以监控百万级别的文件描述符。
    2. 解决select和poll不足之处。
  2. select在使用的过程的缺点:

    1. select监控文件描述符数量有限制,默认是1024;
    2. select每次都会修改监控的集合;
    3. 所以每次都需要把监听文件描述符重新加入到集合中 ;
    4. select检查就绪文件描述符时候,轮询集合;
    5. 每次都需要把监听的集合从用户态拷贝到内核态(位图保存在内核态),返回时,会从内核态拷贝数据到用户态。
  3. epoll是如何改进的?

    1. epoll监听的文件描述符没有限制,具体监控数量与内存大小有关,1GB可以监控约10w个文件描述符
    2. 引入了红黑树,把需要监听的文件描述符保存在红黑树上,一次注册永久生效。
  4. 把需要监听的文件描述符加入红黑树时,内核会给每个文件描述符注册回调函数,当该描述符就绪时,内核会调用该回调函数,将就绪的文件描述符加入到双向链表中。

  5. epoll的使用

    1.  int epoll_create(int size); 
       //成功返回一个文件描述符(管理后续需要监听的文件描述符),失败返回-1, 
       //参数:必须填大于0的值 
      
    2.  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
       //成功返回0,失败返回-1 
       //参数1:epoll_create()的返回值epfd 
       //参数2:操作epfd,EPOLL_CTL_ADD增加、EPOLL_CTL_MOD修改、EPOLL_CTL_DEL删除 
       //参数3:需要操作的文件描述符 
       //参数4:告诉内核操作文件的事件
       
       //结构体原型:
       struct epoll_event {
       	uint32_t     events;      /* Epoll events */
       	epoll_data_t data;        /* User data variable */
       };
       typedef union epoll_data {
       	void        *ptr;
       	int          fd;
       	uint32_t     u32;
       	uint64_t     u64;
       } epoll_data_t;
      

      events 可以是以下几个宏的集合:

      EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);

      EPOLLOUT:表示对应的文件描述符可以写;

      EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

      EPOLLERR:表示对应的文件描述符发生错误;

      EPOLLHUP:表示对应的文件描述符被挂断;

      EPOLLET: 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

      EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

    3.  int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) 
       
       //成功返回就绪文件描述符的数量,失败返回-1 
       //参数1:epfd 
       //参数2:结构体数组,保存就绪文件描述符 
       //参数3:结构体数组的大小 
       //参数4:epoll等待时间,如果填-1,表示epol无限等待,直到有就绪的文件描述符。
      
  6. 查看Ubuntu下,epoll能监控的文件描述符的数量命令: cat /proc/sys/fs/file-max

  7. epoll的两种触发模式

    1. LT 模式:Level_trigger(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写;

    2. ET 模式:Edge_trigger(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你,ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比 LT模式高。

    3. epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读阻塞写操作把处理多个文件描述符的任务饿死。

    4. 修改某个文件描述符为边缘触发模式,将该描述符加入epoll之前的events事件的events属性|EPOLLET就可以。

  8. 在ET模式下,当接收缓冲区比较小,需要循环接收数据,且把recv设置为非阻塞的,设置为非阻塞的后当接收完数据后,recv会返回-1,此时需判断退出循环。

    ET模式一般要配合非阻塞接口去使用 。设置非阻塞的方式如下

    1. 第一种方式设置recv为非阻塞:ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT);
    2. 第二种方式是使用fcntl函数将newFd设置为非阻塞性描述符,可以让recv函数实现非阻塞的。
  9. fcntl函数

    int fcntl(int fd, int cmd, ... /* arg */ ); 
    //修改(获取)文件描述符属性 
    //参数1:需要修改的文件描述符, 
    //参数2:修改(获取)文件描述符的操作
    //可变参数:设置的属性
    

    fcntl 函数有 5 种功能:

    1.复制一个现有的描述符(cmd=F_DUPFD)

    2.获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)

    3.获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)

    4.获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)

    5.获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)

    F_SETFL 设置文件描述符状态旗标,参数 arg 为新旗标,但只允许 O_APPEND、O_NONBLOCK 和 O_ASYNC 位的改变,其他位的改变将不受影响。

  10. 函数的阻塞性和文件描述符属性有关。

  11. epoll原理刨析

    1. 调用epoll_create函数:返回一个epfd,与此同时内核会创建eventpoll这个结构体,该结构体有两个非常重要的成员红黑树和双向链表
      1. 红黑树的作用:保存需要监听的描述符(以及监听的描述符对应的events事件[就是struct epoll_event结构体])
      2. 双向链表:保存就绪的文件描述符
    2. 调用epoll_ctl函数时:就是对红黑树操作,可以添加节点到红黑树,修改、删除。
      1. 添加、修改、删除节点到红黑树,需要不要查找?

        需要查找,logn

      2. 添加描述符到红黑树上时,有一次内存拷贝,从用户空间拷贝到内核空间,一次注册文件描述符,永久生效

      3. 当描述符添加到红黑树上时,内核会给每一个节点注册一个回调函数,当该描述符就绪的时候,会主动调用自己的回调函数,将其加入到双向链表当中。

    3. 调用epoll_wait时:去监控双向链表,如果双向链表里面有数据,取数据,返回用户空间;如果没有数据,就阻塞。
      1. 取数据时,会从内核空间拷贝数据到用户空间。
      2. 用户空间存放就绪文件描述符的结构体数组大小如何选择? 要根据实际业务逻辑
//epoll应用于服务器
#include 

void setNoBlock(int fd)
{
   int status = 0;
   status = fcntl(fd, F_GETFL);
   status |= O_NONBLOCK;
   fcntl(fd, F_SETFL, status);
}
int main(int argc, char **argv)
{
   int sfd = socket(AF_INET, SOCK_STREAM, 0);
   ERROR_CHECK(sfd, -1, "socket");

   //保存本机的ip和端口
   struct sockaddr_in serAddr;
   memset(&serAddr, 0, sizeof(serAddr));
   serAddr.sin_family = AF_INET;
   serAddr.sin_addr.s_addr = inet_addr(argv[1]);
   serAddr.sin_port = htons(atoi(argv[2]));

   //绑定本机的IP和端口,绑定到sfd上
   int ret = 0;
   ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));

   //监听的最大连接数是10
   ret = listen(sfd, 10);
   ERROR_CHECK(ret, -1, "listen");

   //接收连接,返回新的文件描述符,新的描述符作用,与对端进行数据交互使用的
   int newFd = accept(sfd, NULL, NULL);
   ERROR_CHECK(newFd, -1, "accept");

   setNoBlock(newFd);

   //创建epoll,参数必须大于0
   int epfd = epoll_create(1);
   ERROR_CHECK(epfd, -1, "epoll_create");

   struct epoll_event events, evs[2];
   memset(&events, 0, sizeof(events));
   //告诉内核监听读事件
   events.events = EPOLLIN; 
   events.data.fd = STDIN_FILENO;
   ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
   ERROR_CHECK(ret, -1, "epoll_ctl");

   events.events = EPOLLIN|EPOLLET; 
   events.data.fd = newFd;
   ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newFd, &events);
   ERROR_CHECK(ret, -1, "epoll_ctl");

   char buf[4] = {0};
   int readyNum = 0;
   while(1){
       readyNum = epoll_wait(epfd, evs, 2, -1);
       printf("after wait\n");
       for(int i = 0; i < readyNum; ++i){

           //如果是标准输入就绪,就代表需要发送数据给对端
           if(evs[i].data.fd == STDIN_FILENO){
               memset(buf, 0, sizeof(buf));
               read(STDIN_FILENO, buf, sizeof(buf)); 
               send(newFd, buf, strlen(buf)-1, 0);
           }

           //如果是newFd就绪,就说明对端有数据发送给我们
           if(evs[i].data.fd == newFd){
               memset(buf, 0, sizeof(buf));
               while(1){
                   //第一种方式设置recv为非阻塞
                   /* ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT); */

                   //使用fcntl函数将newFd设置为非阻塞性描述符,可以让recv函数
                   //实现非阻塞的
                   ret = recv(newFd, buf, sizeof(buf)-1, 0);

                   if(0 == ret){
                       close(newFd);
                       printf("bey bey\n");
                       continue;
                   }
                   if(-1 == ret){
                       break;
                   }
                   printf("buf = %s\n", buf);

               }
           }
       }
   }
   close(sfd);
   close(newFd);
   return 0;
}

8.总结

  1. select总结

    1. select的底层实现:位图
    2. select缺点:监控的文件描述符数量有限,默认是1024,这个值可以改,但是不能解决效率问题
    3. 每次调用select时,会发生用户空间到内核空间的数据拷贝,select返回时,又要从内核空间拷贝数据到用户空间
    4. select每次检查就绪描述符的时候,需要遍历所有的文件描述符
  2. epoll的总结

    1. epoll的底层实现:红黑树+双向链表
    2. epoll的优点:epoll监控的文件描述符没有限制,仅跟内存大小有关(cat /proc/sys/fs/file-max)
    3. 把需要监听的描述符加入红黑树,一次加入永久生效;
    4. 内核会给每一个加入到红黑树上的文件描述符注册一个回调函数,当就绪的时候,主动调用自己的回调函数,将其加入到双向链表中。
    5. epoll每次只检查就绪链表,不需要遍历所有文件描述符,效率高
  3. epoll的效率一定比select高?

    1. 不一定
    2. 一般认为,在并发量低,socket都比较活跃的情况下,select的效率更高
  4. epoll的超时时间:在第四个参数填,单位是毫秒,1秒=1000毫秒。

  5. epoll设置超时时间的作用:为了断开已经没有和服务端有数据交互的客户端,节约系统资源。

  6. SYN Flood攻击:攻击者利用了tcp协议的漏洞,只发送第一次握手,服务端回复第二次握手,并且把这个半连接状态加入到半连接队列里。攻击者不回复第三次握手,造成服务端半连接队列满,无法接收正常客户端的连接请求。

  7. 如何防护SYN Flood攻击

    1. 增加半连接队列长度
    2. 减少服务端重传服务端重传第二次握手的次数及时间。
    3. syn cookie
  8. UDP Flood攻击,发UDP小包

  9. ICMP Flood攻击

  10. 缓冲区的大小默认64k,所以一个tcp连接系统至少要分配128k的空间。如过业务逻辑每次的数据量比较少,那么可以减小缓冲区的大小,来节约服务器成本,增加连接数。 使用函数:

    int getsockopt(int sockfd, int level, int optname,
                          void *optval, socklen_t *optlen);
    int setsockopt(int sockfd, int level, int optname,
                          const void *optval, socklen_t optlen);
    //成功返回0,失败返回-1
    //参数1:sfd
    //参数2:设置层次
    //参数3:设置的选项
    //参数4:设置的选项是否生效,大于0生效
    //参数5:对参数取sizeof
    

    SOL_SOCKET层次的选项:

    SO_RCVBUF 接收缓冲区大小 int

    SO_SNDBUF 发送缓冲区大小 int

    SO_RCVLOWAT 接收缓冲区下限 int

    SO_SNDLOWAT 发送缓冲区下限 int

    获取缓冲区大小,获取的结果是缓冲区大小的二倍。

  11. 五种io模型

    1. 阻塞IO(blocking I/O)

      A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。

    2. 非阻塞IO(noblocking I/O)

      B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。

    3. 信号驱动IO(signal blocking I/O)

      C也在河边钓鱼,但与A、B不同的是,C比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C就会将鱼钓上来。

    4. IO多路复用(I/O multiplexing)

      D同样也在河边钓鱼,但是D生活水平比较好,D拿了很多的鱼竿,一次性有很多鱼竿在等,(对于select)而且每个鱼杆上都挂了铃铛,当铃铛响了的时候,D轮询的查找是哪个鱼杆上的铃铛响的。(对于epoll)给每个鱼杆都设置了不同的响铃方式,这样不用轮询,直接到相应鱼杆把鱼拉上来即可

    5. 异步IO(asynchronous I/O)

      E也想钓鱼,但E有事情,于是他雇来了F,让F帮他钓鱼,一旦有鱼上钩,F就钓鱼上来,然后打电话通知E钓鱼完成。


9.进程池(以文件传输服务器为例)

目的:实现多个客户端同时下载文件

  1. 好处:

    1. 降低资源消耗。通过重复利用已创建的进程降低进程创建和销毁造成的消耗。
    2. 提高响应速度。当任务到达时,任务可以不需要等到进程创建就能立即执行。
    3. 提高进程的可管理性。 进程是稀缺资源,如果无限制的创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用进程池可以进行统一的分配,调优和监控。
  2. 管道不可以传递文件描述符,只能传递描述符所对应的数值。

  3. 进程间传递文件描述符

    int socketpair(int domain, int type, int protocol, int sv[2]) 
    //成功返回0,失败返回-1
    //参数1:网络协议,AF_LOCAL
    //参数2:通信类型,SOCK_STREAM/SOCK_DGRAM
    //参数3:协议编号
    //参数4:存储创建的套接口
    

    该函数作用,创建一对套接口,这一对套接口创建的时候就是相连,类似于全双工的管道,只能在本机使用。

    sendmsg 发送描述符

    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); 
    //成功返回发送字节数,失败返回-1
    

    recvmsg接收文件描述符

    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    //成功返回接收到的字节数,失败返回-1
    

    结构体原型:

    struct msghdr { 
    	void *msg_name; /* optional address套接口地址,应用于 udp  */ 
    	socklen_t msg_namelen; /* size of address 套接口地址长度  */ 
            
    	struct iovec *msg_iov; /* scatter/gather array 指向 iovec 结构体数组  */ 
    	size_t msg_iovlen; /* # elements in msg_iov 结构体数组大小  */ 
            
    	void *msg_control; /* ancillary data, see below 附属数据,指向 cmsghdr 结构体 */ 
    	size_t msg_controllen; /* ancillary data buffer len cmsghdr 结构体的长度  */ 
            
    	int msg_flags; /* flags (unused) 标志位,没有使用(用于接收特定的标志位) */ 
    };
    
    //iovec 必须赋值 
    struct iovec { 
        void *iov_base; 
        /* Starting address 指向缓冲区的起始位置,缓冲区用于存储 writev 所接收/要发送的数据 */ 
        size_t iov_len; 
        /* Number of bytes to transfer 要传输的字节数 */
    };
    
    //附属数据结构体 cmsghdr
    struct cmsghdr { 
    	socklen_t cmsg_len; 
        /* data byte count, including header 结构体大小、这个值可由 CMSG_LEN()宏计算 */
    	int cmsg_level; /* originating protocol 原始协议,填 SOL_SOCKET*/ 
    	int cmsg_type; /* protocol-specific type 特定协议类型,填 SCM_RIGHTS */
        unsigned char cmsg_data[]; /*可变长数组,存放附属数据,使用 CMSG_DATA()接收 */
    }
    

    使用举例:

    #include 
    int sendFd(int pipeFd, int fd)
    {
        struct msghdr msg;
        memset(&msg, 0, sizeof(msg));
    
        struct iovec iov;
        memset(&iov, 0, sizeof(iov));
    
        char buf[8] = {0};
        strcpy(buf, "a");
        iov.iov_base = buf;
        iov.iov_len = strlen(buf);
    
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
    
        size_t len = CMSG_LEN(sizeof(int));
        struct cmsghdr *cmsg = (struct cmsghdr *)calloc(1, len);
        cmsg->cmsg_len = len;
        cmsg->cmsg_level = SOL_SOCKET;
        cmsg->cmsg_type = SCM_RIGHTS;
    
       *(int*)CMSG_DATA(cmsg) = fd;
    
       msg.msg_control = cmsg;
       msg.msg_controllen = len;
    
       sendmsg(pipeFd, &msg, 0);
    
       return 0;
    }
    
    int recvFd(int pipeFd, int *fd)
    {
        struct msghdr msg;
        memset(&msg, 0, sizeof(msg));
    
        struct iovec iov;
        memset(&iov, 0, sizeof(iov));
    
        char buf[8] = {0};
        strcpy(buf, "a");
        iov.iov_base = buf;
        iov.iov_len = strlen(buf);
    
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
    
        size_t len = CMSG_LEN(sizeof(int));
        struct cmsghdr *cmsg = (struct cmsghdr *)calloc(1, len);
        cmsg->cmsg_len = len;
        cmsg->cmsg_level = SOL_SOCKET;
        cmsg->cmsg_type = SCM_RIGHTS;
    
        msg.msg_control = cmsg;
        msg.msg_controllen = len;
    
        recvmsg(pipeFd, &msg, 0);
        *fd = *(int*)CMSG_DATA(cmsg);
    
        return 0;
    }
    
    int main(int argc, char **argv)
    {
        int fds[2];
        int ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
        ERROR_CHECK(ret, -1, "socketpair");
    
        if(fork()){
            close(fds[0]);
    
            int fd = open("file", O_RDWR);
            ERROR_CHECK(fd, -1, "open");
            printf("fd = %d\n", fd);
            
            sendFd(fds[1], fd);
            //write(fd, "nihao", 5);
            wait(NULL);
        }
        else{
            close(fds[1]);
    
            int nfd = 0;
    
            recvFd(fds[0], &nfd);
            printf("nfd = %d\n", nfd);
    
            char buf[64] = {0};
            read(nfd, buf, sizeof(buf));
            printf("buf = %s\n", buf);
            exit(0);
        }
        return 0;
    }
    
  4. 对于一个没有长度的数组叫可变长数组又叫柔性数组;、

    1. 柔性数组不能取sizeof
    2. 一般配合结构体使用,结构体中必须有一个明确类型的成员,而且柔性数组要放在结构体的最后面。
    3. 优先使用堆空间申请内存。
    4. cmsghdr的第一个成员:cmsg_len保存该结构的前三个成员的大小+柔性数组保存数据的大小
  5. 写iovec结构体数据使用的函数是:

    ssize_t writev(int fd, const struct iovec *iov, int iovcnt)

    1. 参数1:往哪写数据

    2. 参数2:结构体

    3. 参数3:结构体的个数

      #include 
      int main(int argc, char **argv)
      {
          struct iovec iov[2];
          memset(iov, 0, sizeof(iov));
      
          char buf1[64] = {0};
          strcpy(buf1, "hello");
      
          iov[0].iov_base = buf1;
          iov[0].iov_len = strlen(buf1);
      
          char buf2[64] = {0};
          strcpy(buf2, "world");
          iov[1].iov_base = buf2;
          iov[1].iov_len = strlen(buf2);
      
          writev(STDOUT_FILENO, iov, 2);
          return 0;
      }
      
  6. 进程池流程

    父进程的流程:

    1. 循环创建子进程,记录子进程相关信息(子进程id,子进程状态,子进程通信使用的文件描述符)

    2. 创建tcp监听套接字,等待客户端的连接

    3. 创建epoll,把需要监听的描述符添加到epoll当中

    4. 如果客户端连接服务器,使用accept函数接收这次连接请求,返回newFd, 交给空闲子进程

    5. 如果epoll监听的管道可读,表示子进程工作完成,把子进程的工作状态置为空闲。

    子进程的流程:

    1. 子进程调用recvFd阻塞在(抽象)管道上,如果管道中有数据到来,那么从recvmsg返回,服务客户端
    2. 服务完客户端之后,关闭连接,通知父进程,并且由父进程将子进程的工作状态置为非忙碌
    3. 继续等待下一次任务
  7. 客户端想要从服务器上下载文件,该文件该如何传输?

    发送方:

    1. 要打开文件,发送文件名
    2. 读取文件,发送文件内容

    接收方:

    1. 接收文件,创建同名文件
    2. 接收文件内容,把接收的内容写到文件中。
  8. 什么是tcp粘包问题:多次发送发送的数据,可以被一次接收,数据之间没有分界线,所有数据全粘在一起。

    如何去解决粘包问题?人为去规定数据的分界线。私有协议。

  9. 解决收发数据时发送方和接收方速度不匹配的方法

    1. 修改recv函数的第四个参数属性为MSG_WAITALL;

      ret = recv(sfd, buf, dataLen, MSG_WAITALL);

    2. 封装recv函数

      int recvCycle(int sockFd, void *buf, int totalSize)
      {
          int recvSize = 0;
          int ret = 0;
          while(recvSize < totalSize){
              ret = recv(sockFd, (char *)buf + recvSize, totalSize - recvSize, 0);
              recvSize += ret;
          }
          return recvSize;
      }
      
  10. 如果要发送的文件描述符关闭,则send第一次返回-1,之后触发SIGPIPE信号。

  11. 打印传输文件进度条,需要知道文件的的长度,所以要获取文件状态

    int fstat(int fd,struct stat *statbuf);
    //成功返回0,失败返回-1
    

    在打印进度条的方式:

    printf("rate = %5.2f%%\r", rate); // “\r”每次输出时从行首输出
    fflush(stdout);   //防止光标跳动太快
    
  12. 零拷贝

    1. 发送时将文件使用mmap映射到内存空间,然后send到发送缓冲区;

      接收时将文件创建好后,ftruncate()后,使用mmap映射到内存,然后用recv接收数据时直接接收到映射的地址上。

      void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 
      //成功返回指向映射空间的指针,失败-1 
      //参数1:填NULL,让系统指定分配位置 
      //参数2:映射空间大小 
      //参数3:期望的内存保护标志 PROT_READ|PROT_WRITE
      //参数4:指定映射对象的类型,是否可以被进程共享 MAP_SHARED
      //参数5:文件描述符 
      //参数6:被映射的内容起始位置 
      
    2. 使用sendfile来发送数据,不需要把文件读进内存,填文件描述符直接发送到发送缓冲区,把内核空间的数据直接拷贝到发送缓冲区,不经过用户空间;

      接收方可使用1.中的接收方法来接。

      ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 
      //参数1:往哪个文件描述符写数据 
      //参数2:从哪个文件描述符取数据 
      //参数3:文件偏移量 
      //参数4:传输数据长度 
      //sendfile只能用于发送方发送数据,不能用于接收数据 
      
    3. 创建管道int fds[2]; pipe(fds); 然后循环:使用splice()函数将源文件写到管道,再从管道读出到发送缓冲区。参数len的选择影响传输效率。

      ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); 
      //成功返回接收到的字节数,失败-1, 
      //参数1:从哪取数据 
      //参数2:偏移量 
      //参数3:数据写到哪 
      //参数4:偏移量 
      //参数5:单次写入长度,最多65536 
      //参数6:0 
      

      使用举例:

      int fds[2];
      pipe(fds);
      
      int recvLen = 0;
      while(recvLen < fileInfo.st_size){
          ret = splice(fd, 0, fds[1], 0, 65536, 0);
          ret = splice(fds[0], 0, clientFd, 0, ret, 0);
          recvLen += ret;
      }
      
  13. 实际工作中零拷贝技术用的并不多

    1. mmap和sendfile最大可以传输两个G的文件。
    2. 当今数据传输瓶颈在于网络。
    3. 零拷贝接口移植性不是特别好 。
  14. 获取时间当前时间

    int gettimeofday(struct timeval *tv, struct timezone *tz);
    
    //结构体原型
    struct timval{
        time_t tv_sec;
        suseconds_t tv_usec;
    }
    
  15. 进程池的退出

    两种退出方式:

    1. 发信号给父进程,父进程收到信号后依次杀死子进程
    2. 父进程收到信号后,给子进程发送退出标记,如果子进程正在忙碌,那么就等待忙完后退出;对于没有在忙碌,直接退出

10.线程池

5 Linux系统编程之网络编程--学习笔记_第7张图片

  1. 线程池中线程的数量不是越多越好:以下方案效率高

    1. 对io密集型,线程数量一般等于cpu核心数的二倍;
    2. 对cpu密集型,线程的数量一般等于核心数加1。
  2. 防止头文件重复包含

    1.  #ifndef ...   
       #define ...    
       //"code"    
       #endif 
      
    2. #pragma once

  3. 创建结构体时,小类型优先放到前面

你可能感兴趣的:(Linux,网络,网络协议,linux)