• 《TCP/IP网络编程》阅读笔记--I/O复用


    目录

    1--基于I/O复用的服务器

    2--select()函数

    3--基于I/O复用的回声服务器端

    4--send()和recv()函数的常用可选项

    5--readv()和writev()函数


    1--基于I/O复用的服务器

            多进程服务器端具有以下缺点:当有多个客户端发起连接请求时,就会创建多个进程来分别处理客户端的请求,创建多个进程往往需要付出巨大的代价;

            I/O复用的服务器端可以减少进程数,无论连接多少个客户端,提供服务的进程都只有 1 个;

    2--select()函数

            select() 函数可以将多个文件描述符集中到一起来统一监视,监视文件描述符可以视为监视 socket;集中多个文件描述符时需要按照接收传输异常三种情况进行区分;

            select() 通过 fd_set 数组变量来执行监视操作,fd_set 中文件描述符(索引)对应的位(值)被设置为 1 时,表明该文件描述符是监视对象;

    1. // 对 fd_set 数组的常用操作
    2. FD_ZERO(fd_set* fd_set); // 将 fd_set 变量的所有位初始化为0
    3. FD_SET(int fd, fd_set* fdset); // 在参数 fdset 指向的变量中注册文件描述符fd的信息
    4. FD_CLR(int fd, fd_set* fdset); // 从参数 fdset 指向的变量中消除文件描述符fd的信息
    5. FD_ISSET(int fd, fd_set* fdset); // 若参数 fdset 指向的变量中包含文件描述符fd的信息,则返回true

    1. #include
    2. #include
    3. int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
    4. // 成功时返回大于 0 的值,值为发生事件的文件描述符数;失败时返回 -1;超时返回 0
    5. // maxfd 表示监视对象文件描述符的数量
    6. // readset 表示将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
    7. // writeset 表示将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
    8. // exceptset 表示将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
    9. // timeout 表示调用 select() 函数后,为防止陷入无限阻塞的状态,传递超时信息

            select() 函数只有在监视的文件描述符发生变化时才返回,如果未发生变化就会进入阻塞状态;通过指定超时事件可以防止无限阻塞的情况,即使文件描述符中未发生变化,当超过了指定事件,就会从函数中返回,返回值为 0;

            当调用 select() 函数时,除了发生变化的文件描述符之外,所有原来值为 1 的位均会变为 0,因此可知值仍为 1 的文件描述符发生了变化

    1. // gcc select.c -o select
    2. // ./select
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define BUF_SIZE 30
    8. int main(int argc, char* argv[]){
    9. fd_set reads, temps;
    10. int result, str_len;
    11. char buf[BUF_SIZE];
    12. struct timeval timeout;
    13. FD_ZERO(&reads); // 初始化fd_set变量
    14. FD_SET(0, &reads); // 将文件描述符 0 对应的位设置为1,表示监视标准输入(文件描述符0对应标准输入stdin)
    15. while(1){
    16. temps = reads; // 记录初始值,新循环时重新初始化为初始值
    17. timeout.tv_sec = 5; // 超时时间设置为 5s
    18. timeout.tv_usec = 0;
    19. result = select(1, &temps, 0, 0, &timeout); // 5s内监视是否有标准输入时间发生
    20. if(result == -1){
    21. puts("select() error!");
    22. break;
    23. }
    24. else if(result == 0){ // 返回值为0表示超时
    25. puts("Time-out!");
    26. }
    27. else{
    28. if(FD_ISSET(0, &temps)){ // 验证是否是标准输入发生了变化,打印标准输入的内容
    29. str_len = read(0, buf, BUF_SIZE);
    30. buf[str_len] = 0;
    31. printf("message from console: %s", buf);
    32. }
    33. }
    34. }
    35. return 0;
    36. }

    3--基于I/O复用的回声服务器端

    1. // gcc echo_selectserv.c -o echo_selectserv
    2. // ./echo_selectserv 9190
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define BUF_SIZE 100
    12. void error_handling(char *buf){
    13. fputs(buf, stderr);
    14. fputc('\n', stderr);
    15. exit(1);
    16. }
    17. int main(int argc, char* argv[]){
    18. int serv_sock, clnt_sock;
    19. struct sockaddr_in serv_adr, clnt_adr;
    20. struct timeval timeout;
    21. fd_set reads, cpy_reads;
    22. socklen_t adr_sz;
    23. int fd_max, str_len, fd_num, i;
    24. char buf[BUF_SIZE];
    25. if(argc != 2){
    26. printf("Usage : %s \n", argv[0]);
    27. exit(1);
    28. }
    29. serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    30. memset(&serv_adr, 0, sizeof(serv_adr));
    31. serv_adr.sin_family = AF_INET;
    32. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    33. serv_adr.sin_port = htons(atoi(argv[1]));
    34. if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1){
    35. error_handling("bind() error");
    36. }
    37. if(listen(serv_sock, 5) == -1){
    38. error_handling("listen() error");
    39. }
    40. FD_ZERO(&reads); // 初始化fd_set变量
    41. FD_SET(serv_sock, &reads); // 监视 serv_sock
    42. fd_max = serv_sock;
    43. while(1){
    44. cpy_reads = reads; // 记录初始值
    45. timeout.tv_sec = 5; // 设置超时时间
    46. timeout.tv_usec = 5000;
    47. if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1){
    48. break;
    49. }
    50. if(fd_num = 0) continue; // 判断是否是超时
    51. // 真的有事件发生,执行以下代码
    52. for(i = 0; i < fd_max + 1; i++){
    53. if(FD_ISSET(i, &cpy_reads)){ // 查找发生状态变化的文件描述符
    54. if(i == serv_sock){ // 服务器端socket有变化,执行受理连接请求
    55. adr_sz = sizeof(clnt_adr);
    56. clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
    57. FD_SET(clnt_sock, &reads); // 将客户端socket注册到fd_set变量中
    58. if(fd_max < clnt_sock){
    59. fd_max = clnt_sock;
    60. }
    61. printf("connected client: %d \n", clnt_sock);
    62. }
    63. else{ // 不是服务器端socket发生变化,表明有要接收的数据
    64. str_len = read(i, buf, BUF_SIZE);
    65. if(str_len == 0){ // 接收的是 EOF,表明要断开连接
    66. FD_CLR(i, &reads);
    67. close(i);
    68. printf("closed client: %d \n", i);
    69. }
    70. else{ // 接收的是真实数据
    71. write(i, buf, str_len); // 将接收到的数据返回客户端,实现回声功能
    72. }
    73. }
    74. }
    75. }
    76. }
    77. close(serv_sock);
    78. return 0;
    79. }

    4--send()和recv()函数的常用可选项

    1. #include
    2. ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
    3. ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
    4. // flags 表示可选项信息

            通常情况下,我们会将 send() 和 recv() 的可选项参数设置为 0,但其实际拥有以下可选项:

    ① MSG_OOB 表示用于传输带外数据(send、recv)

    ② MSG_PEEK 表示验证输入缓冲中是否存在接收的数据(recv)

    ③ MSG_DONTROUTE 表示数据传输过程中不参照路由表,在本地网络中寻找目的地(send)

    ④ MSG_DONTWAIT 表示调用 I/O 函数时不阻塞,用于使用非阻塞 I/O(send、recv)

    ⑤ MSG_WAITALL 表示防止函数返回,直到接收全部请求的字节数(recv)

            MSG_OOB 用于发送带外数据的紧急消息,操作系统收到紧急消息时,将产生 SIGURG 信号,并调用注册的信号处理函数;

    1. // gcc oob_recv.c -o oob_recv
    2. // ./oob_recv 9190
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define BUF_SIZE 30
    12. int acpt_sock;
    13. int recv_sock;
    14. void error_handling(char *message){
    15. fputs(message, stderr);
    16. fputc('\n', stderr);
    17. exit(1);
    18. }
    19. void urg_handler(int signo){
    20. int str_len;
    21. char buf[BUF_SIZE];
    22. str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB);
    23. buf[str_len] = 0;
    24. printf("Urgent message: %s \n", buf);
    25. }
    26. int main(int argc, char* argv[]){
    27. struct sockaddr_in recv_adr, serv_adr;
    28. int str_len, state;
    29. socklen_t serv_adr_sz;
    30. struct sigaction act;
    31. char buf[BUF_SIZE];
    32. if(argc != 2){
    33. printf("Usage : %s \n", argv[0]);
    34. exit(1);
    35. }
    36. act.sa_handler = urg_handler; //设置信号的处理函数
    37. sigemptyset(&act.sa_mask);
    38. act.sa_flags = 0;
    39. acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
    40. memset(&recv_adr, 0, sizeof(recv_adr));
    41. recv_adr.sin_family = AF_INET;
    42. recv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    43. recv_adr.sin_port = htons(atoi(argv[1]));
    44. if(bind(acpt_sock, (struct sockaddr*) &recv_adr, sizeof(recv_adr)) == -1){
    45. error_handling("bind() error");
    46. }
    47. listen(acpt_sock, 5);
    48. serv_adr_sz = sizeof(serv_adr);
    49. recv_sock = accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
    50. // getpid() 返回进程ID
    51. // recv_sock发生SIGUGR信号,需要有确定的处理进程(假设创建了多个进程)来调用信号处理函数
    52. // fcntl() 将 getpid() 返回的进程作为 SIGUGR 信号的处理进程
    53. fcntl(recv_sock, F_SETOWN, getpid());
    54. state = sigaction(SIGURG, &act, 0); // 发生SIGURG信号时,调用urg_handler()函数
    55. while((str_len = recv(recv_sock, buf, sizeof(buf)-1, 0)) != 0){
    56. if(str_len == -1){
    57. continue;
    58. }
    59. buf[str_len] = 0;
    60. puts(buf);
    61. }
    62. close(recv_sock);
    63. close(acpt_sock);
    64. return 0;
    65. }
    1. // gcc oob_send.c -o oob_send
    2. // ./oob_send 127.0.0.1 9190
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define BUF_SIZE 30
    10. void error_handling(char *message){
    11. fputs(message, stderr);
    12. fputc('\n', stderr);
    13. exit(1);
    14. }
    15. int main(int argc, char *argv[]){
    16. int sock;
    17. struct sockaddr_in recv_adr;
    18. if(argc != 3){
    19. printf("Usage : %s \n", argv[0]);
    20. exit(1);
    21. }
    22. sock = socket(PF_INET, SOCK_STREAM, 0);
    23. memset(&recv_adr, 0, sizeof(recv_adr));
    24. recv_adr.sin_family = AF_INET;
    25. recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    26. recv_adr.sin_port = htons(atoi(argv[2]));
    27. if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1){
    28. error_handling("connect() error!");
    29. }
    30. write(sock, "123", strlen("123"));
    31. send(sock, "4", strlen("4"), MSG_OOB); // 紧急传输数据
    32. write(sock, "567", strlen("567"));
    33. send(sock, "890", strlen("890"), MSG_OOB); // 紧急传输数据
    34. close(sock);
    35. return 0;
    36. }

            同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,可以验证输入缓冲中是否存在接收的数据,同时由于设置了 MSG_PEEK 选项,则调用 recv 函数时,即使读取了输入缓冲的数据也不会删除输入缓冲的数据(也就是说下一次读取时,还可以读到上一次读取的数据);

    1. // gcc peek_recv.c -o peek_recv
    2. // ./peek_recv 9190
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define BUF_SIZE 30
    10. void error_handling(char *message){
    11. fputs(message, stderr);
    12. fputc('\n', stderr);
    13. exit(1);
    14. }
    15. int main(int argc, char* argv[]){
    16. int acpt_sock, recv_sock;
    17. struct sockaddr_in acpt_adr, recv_adr;
    18. int str_len, state;
    19. socklen_t recv_adr_sz;
    20. char buf[BUF_SIZE];
    21. if(argc != 2){
    22. printf("Usage : %s \n", argv[0]);
    23. exit(1);
    24. }
    25. acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
    26. memset(&acpt_adr, 0, sizeof(acpt_adr));
    27. acpt_adr.sin_family = AF_INET;
    28. acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    29. acpt_adr.sin_port = htons(atoi(argv[1]));
    30. if(bind(acpt_sock, (struct sockaddr*) &acpt_adr, sizeof(acpt_adr)) == -1){
    31. error_handling("bind() error");
    32. }
    33. listen(acpt_sock, 5);
    34. recv_adr_sz = sizeof(recv_adr);
    35. recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);
    36. while(1){
    37. // 设置 MSG_PEEK|MSG_DONTWAIT 选项,即使不存在待读取的数据,也不会进入阻塞状态
    38. // 假设存在待读取的数据,则读取且不删除输入缓冲的数据,因此下次仍可以读取
    39. str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK|MSG_DONTWAIT);
    40. if(str_len > 0){
    41. break;
    42. }
    43. }
    44. buf[str_len] = 0;
    45. printf("Buffering %d bytes: %s \n", str_len, buf);
    46. // 读取上一次留在输入缓冲的数据
    47. str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
    48. buf[str_len] = 0;
    49. printf("Read again: %s \n", buf);
    50. close(acpt_sock);
    51. close(recv_sock);
    52. return 0;
    53. }
    1. // gcc peek_send.c -o peek_send
    2. // ./peek_send 127.0.0.1 9190
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. void error_handling(char *message){
    10. fputs(message, stderr);
    11. fputc('\n', stderr);
    12. exit(1);
    13. }
    14. int main(int argc, char *argv[]){
    15. int sock;
    16. struct sockaddr_in recv_adr;
    17. if(argc != 3){
    18. printf("Usage : %s \n", argv[0]);
    19. exit(1);
    20. }
    21. sock = socket(PF_INET, SOCK_STREAM, 0);
    22. memset(&recv_adr, 0, sizeof(recv_adr));
    23. recv_adr.sin_family = AF_INET;
    24. recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    25. recv_adr.sin_port = htons(atoi(argv[2]));
    26. if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1){
    27. error_handling("connect() error!");
    28. }
    29. write(sock, "123", strlen("123"));
    30. close(sock);
    31. return 0;
    32. }

    5--readv()和writev()函数

            readv() 和 writev() 函数对数据进行整合后,再进行读取和发送;即通过 writev() 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv() 函数可以由多个缓冲分别接收;

    1. #include
    2. ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
    3. // 成功时返回发送的字节数,失败时返回 -1
    4. // filedes 表示文件描述符
    5. // iov 表示 iovec 结构体数组的地址值
    6. // iovcnt 表示向第二个参数传递的数组长度
    7. ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
    8. // 成功时返回接收的字节数,失败时返回 -1
    9. // iovec结构体
    10. struct iovec{
    11. void* iov_base; // 缓冲地址
    12. size_t iov_len; // 缓冲大小
    13. }

    代码实例:

    1. // gcc writev.c -o write
    2. // ./write
    3. #include
    4. #include
    5. int main(int argc, char* argv[]){
    6. struct iovec vec[2];
    7. char buf1[] = "ABCDEFG";
    8. char buf2[] = "1234567";
    9. int str_len;
    10. vec[0].iov_base = buf1;
    11. vec[0].iov_len = 3;
    12. vec[1].iov_base = buf2;
    13. vec[1].iov_len = 4;
    14. str_len = writev(1, vec, 2); // 向文件描述符1写数据,即向标准输出写数据
    15. puts("");
    16. printf("Write bytes: %d \n", str_len);
    17. return 0;
    18. }

    1. // gcc readv.c -o readv
    2. // ./readv
    3. #include
    4. #include
    5. #define BUF_SIZE 100
    6. int main(int argc, char *argv[]){
    7. struct iovec vec[2];
    8. char buf1[BUF_SIZE] = {0,};
    9. char buf2[BUF_SIZE] = {0,};
    10. int str_len;
    11. vec[0].iov_base = buf1;
    12. vec[0].iov_len = 5; // 设置最多保存5个字节
    13. vec[1].iov_base = buf2;
    14. vec[1].iov_len = BUF_SIZE;
    15. str_len = readv(0, vec, 2); // 向标准输入(文件描述符0)读数据
    16. printf("Read bytes: %d \n", str_len);
    17. printf("First message: %s \n", buf1);
    18. printf("Second message: %s \n", buf2);
    19. return 0;
    20. }

  • 相关阅读:
    [MySQL]存储过程与函数
    Pimpl 与 unique_ptr 的问题
    Android 12之启动画面Splash Screens(二) -- framework原理
    2022最新Java面试笔试题目分享,Java高级工程师面试题
    双指针--浅试
    三数之和(LeetCode 15)
    PD虚拟机(Parallels Desktop)2024mac苹果电脑19免费版下载
    ARM day9
    详解单例模式
    栈的概念及用法
  • 原文地址:https://blog.csdn.net/weixin_43863869/article/details/132794581