Linux 有一个准则:一切皆文件。所以在 Linux 中,文件 I/O 函数 read 和 write 就是金字招牌,哪里都可以使用。但是在 Windows 中,文件 I/O 函数和套接字 I/O 函数需要被严格区分使用。现在将它们总结一下放到一个表格里

Windows 系统 Linux 系统
文件 I/O 函数:fopen 和 fwrite 文件 I/O 函数:open、read 和 write
套接字函数:send 和 recv 套接字函数:(write 和 read) 或 (send 和 recv)或 (writev 和 readv)

在 Windows 中,使用 send 和 recv 来传递数据,在 Linux 中同样也有这套函数。两者其实并没有什么差别。

send 函数原型

  1. #include <sys/socket.h>
  2. // 成功时返回发送的字节数,失败时返回 -1
  3. ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);

函数参数:

  • sockfd:表示与数据传输对象连接的套接字文件描述符
  • buf:保存待传输数据的缓冲地址值
  • nbytes:待传输的字节数
  • flags:传输数据时指定的可选项信息

recv 函数原型

  1. #include <sys/socket.h>
  2. // 成功时返回接收的字节数,收到 EOF 时返回 0,失败时返回 -1
  3. ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);

函数参数:

  • sockfd:表示与数据接收对象连接的套接字文件描述符
  • buf:保存接收数据的缓冲地址值
  • nbytes:可接收的最大字节数
  • flags:接收数据时指定的可选项信息

send 函数 recv 函数的最后一个参数是收发数据时的可选项,该可选项可利用 位或 (bit OR) 运算同时传递多个信息。下表是 flags 的种类及其含义

可选项 含义 send() recv()
MSG_OOB 用于传输带外数据
MSG_PEEK 验证输入缓冲中是否存在接收的数据
MSG_DONTROUTE 数据传输过程中不参照路由(Routing)表,在本地(Local)
网络中寻找目的地
MSG_DONTWAIT 调用 I/O 函数时不阻塞,用于使用非阻塞 I/O
MSG_WAITALL 防止函数返回,直到接收全部请求的字节数

不同操作系统对上述可选项的支持也不同,所以需要对实际开发中采用的操作系统有一定了解才能使用不同可选项。下面介绍几种不受操作系统影响的可选项进行讲解,表示其可以移植。

可移植可选项:MSG_OOB 发送和接收紧急消息

发送紧急消息

MSG_OOB 可选项用于 “带外数据” 紧急消息。拿一个例子:假设医院里有很多病人在等待看病,此时若有急诊患者该怎么办?

“当然应该优先处理。”

如果急诊患者较多,就需要得到等待看病的普通病人的谅解。因此,医院一般会设立单独的急诊室。需要紧急处理时,应采用不同的处理方法和通道。MSG_OOB 可选项就用于创建特殊发送方法和通道,然后发送紧急消息。下列示例通过 MSG_OOB 可选项收发数据:

  1. // oob_send.c
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <sys/socket.h>
  7. #include <arpa/inet.h>
  8. #define BUF_SIZE 30
  9. void error_handling(char *message);
  10. int main(int argc, char *argv[]) {
  11. // 1.检查输入
  12. if (argc != 3) {
  13. printf("Usage: %s <IP> <port> \n", argv[0]);
  14. }
  15. // 2.初始化套接字
  16. int sock;
  17. struct sockaddr_in recv_adr;
  18. sock = socket(PF_INET, SOCK_STREAM, 0);
  19. memset(&recv_adr, 0, sizeof(recv_adr));
  20. recv_adr.sin_family = AF_INET;
  21. recv_adr.sin_addr.s_addr = inet_addr(argv[1]); // argv[1]: 要连接的服务器的 IP
  22. recv_adr.sin_port = htons(atoi(argv[2])); // argv[2]: 服务器开启的端口号
  23. // 3.向发起连接请求
  24. if (connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
  25. error_handling("connect() error!");
  26. write(sock, "123", strlen("123"));
  27. send(sock, "4", strlen("4"), MSG_OOB); // 加急消息
  28. write(sock, "567", strlen("567"));
  29. send(sock, "890", strlen("890"), MSG_OOB); // 加急消息
  30. close(sock);
  31. return 0;
  32. }
  33. void error_handling(char *message) {
  34. fputs(message, stderr);
  35. fputc('\n', stderr);
  36. exit(1);
  37. }

在上述代码中,使用 MSG_OOB 可以把重要的消息先发出去。

接收紧急消息

相比于紧急消息的传输,紧急消息的接收过程要相对复杂一些。

  1. // oob_recv.c
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <signal.h>
  7. #include <sys/socket.h>
  8. #include <netinet/in.h>
  9. #include <fcntl.h>
  10. #define BUF_SIZE 30
  11. void error_handling(char *message);
  12. void urg_handler(int signo);
  13. int acpt_sock;
  14. int recv_sock;
  15. int main(int argc, char *argv[]) {
  16. // 1.检查输入格式
  17. if (argc != 2) {
  18. printf("Usage: %s <port> \n", argv[0]);
  19. exit(1);
  20. }
  21. // 2.初始化套接字
  22. struct sockaddr_in recv_adr, serv_adr;
  23. acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
  24. memset(&recv_adr, 0, sizeof(recv_adr));
  25. recv_adr.sin_family = AF_INET; // IPV4
  26. recv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 不限定客户端 IP
  27. recv_adr.sin_port = htons(atoi(argv[1])); // 开放端口 argv[1]
  28. // 3.收到 MSG_OOB 紧急消息时,操作系统将产生 SIGURE,并调用注册的信号处理函数
  29. struct sigaction act;
  30. int state;
  31. act.sa_handler = urg_handler; // 信号处理函数
  32. sigemptyset(&act.sa_mask);
  33. act.sa_flags = 0;
  34. state = sigaction(SIGURG, &act, 0);
  35. // 4.bind() 函数将套接字与特定的 IP 地址和端口绑定起来
  36. if (bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
  37. error_handling("bind() error");
  38. // 5.listen() 函数可以让套接字进入被动监听状态
  39. listen(acpt_sock, 5);
  40. // 6.为连接的客户端分配套接字
  41. socklen_t serv_adr_sz;
  42. recv_sock = accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
  43. // 7.调用 fcntl 函数
  44. fcntl(recv_sock, F_SETOWN, getpid());
  45. // 8.接收客户端发送的数据
  46. int str_len;
  47. char buf[BUF_SIZE];
  48. while ((str_len=recv(recv_sock, buf, sizeof(buf), 0)) != 0) {
  49. if (str_len == -1)
  50. continue;
  51. buf[str_len] = 0;
  52. puts(buf);
  53. }
  54. close(recv_sock);
  55. close(acpt_sock);
  56. return 0;
  57. }
  58. void urg_handler(int signo) {
  59. int str_len;
  60. char buf[BUF_SIZE];
  61. str_len = recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);
  62. buf[str_len] = 0;
  63. printf("Urgent message: %s \n", buf);
  64. }
  65. void error_handling(char *message) {
  66. fputs(message, stderr);
  67. fputc('\n', stderr);
  68. exit(1);
  69. }

和原来的子进程结束,就会发出 SIGCHLD ,操作系统就会调用处理函数类似;这里套接字接收到紧急消息以后,使用紧急接收函数 str_len = recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB); 来处理。

关于 fcntl 函数

  1. fcntl(recv_sock, F_SETOWN, getpid());

fcntl 函数用于控制文件描述符,上述调用语句的含义为:“将文件描述符” recv_sock 指向的套接字拥有者 (F_SETOWN) 改为把 getpid 函数返回值用作 ID 的进程。

什么是套接字拥有者呢?操作系统实际创建并管理套接字,所以从严格意义上来说,“套接字拥有者” 是操作系统。但是这里的 “拥有者” 是指负责套接字所有事物的主体。

所以上述函数在这里的含义为:“文件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为将 getpid 函数返回值用作 ID 的进程”。

看到这里仍旧是一脸懵逼吧,这都是什么玩意?不要着急,继续往下看

在多进程服务器学习过,通过 fork 函数创建子进程并同时复制文件描述符,这样会有多个进程有一个 1 个套接字的文件描述符。如果这个时候有紧急消息传入的时候,我们是选取哪个进程的 urg_handler 函数来处理呢?

而这就是 fcntl 的作用,这个时候处理的进程应该是 getpid 函数返回的调用此函数的进程 ID。

§ Linux 中的 send & recv - 图1

运行紧急发送和接收程序

将上述 oob_send.c 和 oob_recv.c 编译运行,先运行服务器端程序,使其进入 listen 状态,不然服务器端的请求连接会出错。

服务器端的信息如下所示:

§ Linux 中的 send & recv - 图2

可以看到,通过 MSG_OOB 可选项传递的数据在读取时只能读取 1 个字节,剩下的数据通过普通输入函数读取。这是因为 TCP 并不存在真正意义的 “带外数据”(带外数据:通过完全不同的通信路径传输的数据),但是 TCP 提供一种紧急模式进行传输。

TCP 紧急模式

§ Linux 中的 send & recv - 图3 MSG_OOB 可选项有如下作用:

“嗨!这里有数据需要紧急处理,别磨蹭了!”

MSG_OOB 的真正意义:督促数据接收对象尽快处理数据,这也就是紧急模式的全部内容。并且 TCP 保持传输顺序的传输特性依旧成立。

§ Linux 中的 send & recv - 图4 当有紧急消息的时候,需要做的事情有两件:

  • 尽快传输
  • 尽快被对象接收处理

传输会和网速有关系,这里不可控因素很多。而尽快被对象接收处理就对应前面的 urg_handler 信号处理函数,下面看一下上述紧急发送消息的发送阶段。

§ Linux 中的 send & recv - 图5

如果将缓冲最左端的位置视作偏移量为 0,字符 0 则保存在偏移量为 2 的位置。字符 0 右侧偏移量为 2 的位置存有紧急指针。紧急指针指向紧急消息的下一个位置,同时向对方主机(这里为服务器端)传递如下消息:

“紧急指针指向的偏移量 3 之前的部分就是紧急消息!”

但是,却并不知道,紧急消息到底是 “890”、“90” 还是 “0”,书上说这个功能不重要,因为我们所做的事情只是督促对方接收数据,这里理解不了!!!

可移植选项 MSG_PEEK & MSG_DONTWAIT 检查输入缓冲

同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,验证输入缓冲中是否存在接受的数据。设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了输入缓冲的数据也不会删除。

因此,该选项通常与 MSG_DONTWAIT 合作,用于调用以非阻塞方式验证待读数据存在与否的函数。下面看个例子:

客户端发送:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <sys/socket.h>
  6. #include <arpa/inet.h>
  7. void error_handling(char *message);
  8. int main(int argc, char *argv[]) {
  9. // 1.检查输入
  10. if (argc != 3) {
  11. printf("Usage: %s<IP><port> \n", argv[0]);
  12. exit(1);
  13. }
  14. // 2.初始化套接字描述符
  15. int sock;
  16. struct sockaddr_in send_adr;
  17. sock = socket(PF_INET, SOCK_STREAM, 0);
  18. memset(&send_adr, 0, sizeof(send_adr));
  19. send_adr.sin_family = AF_INET;
  20. send_adr.sin_addr.s_addr = inet_addr(argv[1]);
  21. send_adr.sin_port = htons(atoi(argv[2]));
  22. // 3.发起请求
  23. if (connect(sock, (struct sockaddr*)&send_adr, sizeof(send_adr)) == -1)
  24. error_handling("connect() error");
  25. write(sock, "123", strlen("123"));
  26. close(sock);
  27. return 0;
  28. }
  29. void error_handling(char *message) {
  30. fputs(message, stderr);
  31. fputc('\n', stderr);
  32. exit(1);
  33. }

服务器端接收:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <sys/socket.h>
  6. #include <arpa/inet.h>
  7. #define BUF_SIZE 30
  8. void error_handling(char *message);
  9. int main(int argc, char *argv[]) {
  10. // 1.检查输入
  11. if (argc != 2) {
  12. printf("Usage: %s <port>", argv[0]);
  13. exit(1);
  14. }
  15. // 2.初始化套接字描述符
  16. int acpt_sock, recv_sock;
  17. struct sockaddr_in acpt_adr, recv_adr;
  18. acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
  19. memset(&acpt_adr, 0, sizeof(acpt_adr));
  20. acpt_adr.sin_family = AF_INET;
  21. acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
  22. acpt_adr.sin_port = htons(atoi(argv[1]));
  23. // 3.bind绑定套接字与端口
  24. if (bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr)) == -1)
  25. error_handling("bind() error");
  26. // 4.listen监听
  27. listen(acpt_sock, 5);
  28. socklen_t recv_adr_sz;
  29. recv_adr_sz = sizeof(recv_adr);
  30. recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);
  31. int str_len;
  32. char buf[BUF_SIZE];
  33. while(1) {
  34. str_len = recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT);
  35. if (str_len > 0)
  36. break;
  37. }
  38. buf[str_len] = 0;
  39. printf("Buffering %d bytes: %s \n", str_len, buf);
  40. str_len = recv(recv_sock, buf, sizeof(buf)-1, 0);
  41. buf[str_len] = 0;
  42. printf("Read again: %s \n", buf);
  43. close(acpt_sock);
  44. close(recv_sock);
  45. return 0;
  46. }
  47. void error_handling(char *message) {
  48. fputs(message, stderr);
  49. fputc('\n', stderr);
  50. exit(1);
  51. }

运行以上两个程序:

客户端:

§ Linux 中的 send & recv - 图6

服务器端:

§ Linux 中的 send & recv - 图7

通过上述运行结果可以发现,仅发送 1 次的数据被读取了 2 次,因为第一次调用 recv 函数时设置了 MSG_PEEK 可选项。