【计算机网络】网络编程接口 Socket API 解读(6)

2023-09-13 15:42:51

         Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。

        本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。


recv

recv()           遵循 POSIX.1 - 2008

1.库

标准 c 库,libc, -lc

2.头文件

<sys/socket.h>

3.接口定义

       ssize_t recv(int sockfd, void buf[.len], size_t len,
                        int flags);

       ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
                        int flags,
                        struct sockaddr *_Nullable restrict src_addr,
                        socklen_t *_Nullable restrict addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

4.接口描述

        recv()、recvfrom()、recvmsg() 调用用来从套接字接收消息,它们都可以用在连接或非连接套接字上。我们首先描述几个系统调用共同的特性,然后在介绍它们的差别。

        recv() 和 read(2) 的唯一区别就是是否有 flags 标记,当 recv() 的标记为 0 时,它基本上等同于 read(2)(具体参见注意部分)。同样的,下面调用:

recv(sockfd, buf, len, flags);

        等同于

recvfrom(sockfd, buf, len, flags, NULL, NULL);

         三个调用都在成功时返回消息的长度,如果提供的 buffer 盛不下消息,那么超出的消息可能会被遗弃,这要取决于接收套接字的类型。

        如果套接字上没有消息,那么接收调用会一直等到有消息到来,除非套接字是非阻塞的(参考 fcntl(2) ),这时会返回  -1 并将 errno 设置为 EAGAIN 或者 EWOULDBLOCK。接收调用正常情况只要有可用数据就会返回,接近请求的数据量,而不是一直等到接收到所有的请求数据量。

        应用程序可以使用 select(2)、poll(2)、epoll(7) 来决定套接字上有更多数据发生的时机。

        flags 参数

        flags 是下面值的位或值:

        MSG_CMSG_CLOEXEC (只有 recvmsg() 可用)

        设置接收文件描述符的异常关闭标记,通过 UNIX 域文件描述符的 SCM_RIGHTS 操作实现。这个标记的用途和 O_CLOEXEC 类似。

        MSG_DONTWAIT

        使能非阻塞操作,如果操作要阻塞,那么调用会报出 EAGAIN、EWOULDBLOCK 错误。这个和设置 O_NONBLOCK 标记类似(通过 fcntl(2) F_SETFL 操作),不过 MSG_DONTWAIT 只对本次调用管用,而 O_NONBLOCK 是设置到了文件描述符上,这样就会影响所有调用进程的所有线程以及其他持有该套接字句柄的进程。

        MSG_ERRQUEQUE

        这个标记指定了排队错误应该被套接字错误队列接收,错误信息会以依赖具体协议的类型来传递(对于 IPv4 是 IP_RECVERR)。用户应该提供足够的 buffer 大小。导致错误的原始报文的载荷大小以正常数据的 msg_iovec 格式传递,导致错误的原始报文的地址以 msg_name 形式提供。

        错误以 sock_extended_err 结构提供:

                  #define SO_EE_ORIGIN_NONE    0
                  #define SO_EE_ORIGIN_LOCAL   1
                  #define SO_EE_ORIGIN_ICMP    2
                  #define SO_EE_ORIGIN_ICMP6   3

                  struct sock_extended_err
                  {
                      uint32_t ee_errno;   /* Error number */
                      uint8_t  ee_origin;  /* Where the error originated */
                      uint8_t  ee_type;    /* Type */
                      uint8_t  ee_code;    /* Code */
                      uint8_t  ee_pad;     /* Padding */
                      uint32_t ee_info;    /* Additional information */
                      uint32_t ee_data;    /* Other data */
                      /* More data may follow */
                  };

                  struct sockaddr *SO_EE_OFFENDER(struct sock_extended_err *);

        ee_errno 包含了排队错误的 errno 值,ee_origin 是错误发源地的代码,其他域都是协议相关的。宏 SO_EE_OFFENDER 作为辅助信息返回错误发生点的网络对象地址。如果地址未知,那么 sockaddr 中的 sa_family 会包含 AF_UNSPEC,其他域为未知值。导致错误的报文的载荷以正常数据传递。

        对于本地错误,不传递地址(可以通过 cmsghdr 的 cmsg_len 值来确认)。 收到错误时,msghdr 会设置 MSG_ERRQUEQUE 标记。错误传递后,套接字错误码会根据下一个队列错误重新生成,在下一个套接字操作发生时传递。

        MSG_OOB

        这个标记请求接收通常不会在正常数据量中接到的带外数据。一些协议会将加速数据放在正常数据队列的前面,这就会导致这个标记没办法在这些协议中使用。

        MSG_PEEK

        这个标记指定从接收队列的头部接收数据,并且不会将数据从队列中移除。因此,下一次接收调用会返回相同的值。

        MSG_TRUNC

        对于原始协议(AF_PACKET),Internet datagram、netlink、UNIX datagram、sequenced-packet 套接字会返回实际的分组或报文长度,即使它比提供的 buffer 大。

        对于网络流套接字,参考 tcp(7)。

        MSG_WAITALL

        这个标记请求操作一直等到请求大小完全满足为止。然后,当信号、错误、连接断开、后面接收数据和之前的数据类型不同等发生时,调用仍然可能返回少于请求大小的数据。

recvfrom()

        recvfrom() 将收到的消息放到缓冲区 buf 中,调用者必须通过 len 参数指定 buf 的大小。

        如果 src_addr 不是 NULL,底层协议提供了消息的原地址,那么原地址会填到 src_addr 中,这种情况下 addrlen 是一个输入输出参数。调用前,它应该被初始化为 src_addr 缓冲区的大小,返回时会有原地址的实际大小更新。如果提供的缓冲器太小,那么返回地址就会被截断,这种情况下,addrlen 的值就会比提供的值大。

        如果调用者对原地址不感兴趣,那么 src_addr 和 addrlen 都应该被设置为 NULL。

recv()

        recv() 通常只能用于连接的套接字(参考 connect(2)),它相当于下面的调用:

           recvfrom(fd, buf, len, flags, NULL, 0);

recvmsg()

         recvmsg() 调用使用 msghdr 结构来减少需要传递参数的个数,结构体在 <sys/socket.h> 中定义,如下:

           struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags on received message */
           };

        msg_name 域指向用户分配的缓冲区,用来存放非连接套接字的源地址,调用者应该通过 msg_namelen 来设置缓冲器的大小,一旦成功返回,msg_namelen 会被设置为源地址的实际大小。如果应用不关心源地址,那么 msg_name 可以设置为 NULL。

        msg_iov 和 msg_iovlen 描述 scatter-gather 方式的区域(就是一些类分散的缓冲区列表),在 readv(2) 中有讨论。 

        msg_control 域具有 msg_controllen 长度,是一个为其他协议控制消息或者各种辅助数据准备的缓冲区。当 recvmsg() 调用时,msg_controllen 应指定 msg_control 缓冲区可用大小,一旦成功返回,它将包含控制消息序列的大小。消息格式如下:

           struct cmsghdr {
               size_t cmsg_len;    /* Data byte count, including header
                                      (type is socklen_t in POSIX) */
               int    cmsg_level;  /* Originating protocol */
               int    cmsg_type;   /* Protocol-specific type */
           /* followed by
               unsigned char cmsg_data[]; */
           };

        辅助数据应该只能被 cmsg(3) 中定义的宏来访问。

        作为例子,Linux 使用这个辅助数据机制在 UNIX 域套接字上传递扩展错误、IP 选项、文件描述符,参考 unix(7) 和 ip(7)。

        msghdr 中的 msg_fags 域会在 recvmsg() 返回时更新,它可能包含以下一些标记:

        MSG_EOR

        指示记录结束,记录中所有数据都已返回(通常用在 SOCK_SEQPACKET 中)。

        MSG_TRUNC

        指示数据报文的结尾部分因为大于提供的缓冲区大小而被丢弃。

        MSG_OOB

        指示有带外或者加速数据到达

5.返回值

        调用会返回接收到数据的字节数。

        发生错误时,返回 -1,并设置errno 来指示错误类型。

        当流套接字对端自己关闭了,那么将返回 0(传统意义的 EOF 返回)。

        各个域中的数据报文套接字允许 0 长度报文,当这样的报文收到时,返回的值就是 0。

        在流套接字请求接收 0 个字节时,返回值也可能是 0。

        错误值定义如下:

EAGAIN/EWOULDBLOCK如果套接字被标记为非阻塞并且接收操作打算阻塞,或者设置了超时值,在数据到达前发生了超时。POSIX.1 允许使用两个错误值的任何一个,也不假设两个值相等,这就需要应用检查对两个错误都进行检查。
EBADFsockfd 参数是一个非法的文件描述符
ECONNREFUSED远程主机拒绝网络连接(通常是没有运行请求的服务)
EFAULT接收缓冲区指针指向进程外地址
EINTR接收操作在数据来临前被传递来的信号打断
EINVAL参数不合法
ENOMEM无法申请 recvmsg() 的内存
ENOTCONN套接字是一个面向连接的套接字,但是没有连接(参考 connect(2) 和 accept(2))
ENOTSOCK文件描述符不是一个套接字

6.注意

       如果有 0 长度报文处于等待,那么 read(2) 和 标记为 0 的recv() 的处理行为是不同的。read(2) 没有任何影响(报文还在等待中),而 recv() 会消耗掉报文。

        参考 recvmmsg(2) 来看 Linux 系统特定的系统调用来在一次调用中处理多个报文。

7.代码

   Server program

       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <string.h>
       #include <sys/socket.h>
       #include <sys/types.h>
       #include <unistd.h>

       #define BUF_SIZE 500

       int
       main(int argc, char *argv[])
       {
           int                      sfd, s;
           char                     buf[BUF_SIZE];
           ssize_t                  nread;
           socklen_t                peer_addrlen;
           struct addrinfo          hints;
           struct addrinfo          *result, *rp;
           struct sockaddr_storage  peer_addr;

           if (argc != 2) {
               fprintf(stderr, "Usage: %s port\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           memset(&hints, 0, sizeof(hints));
           hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
           hints.ai_flags = AI_PASSIVE;    /* For wildcard IP address */
           hints.ai_protocol = 0;          /* Any protocol */
           hints.ai_canonname = NULL;
           hints.ai_addr = NULL;
           hints.ai_next = NULL;

           s = getaddrinfo(NULL, argv[1], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }

           /* getaddrinfo() returns a list of address structures.
              Try each address until we successfully bind(2).
              If socket(2) (or bind(2)) fails, we (close the socket
              and) try the next address. */

           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;

               if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
                   break;                  /* Success */

               close(sfd);
           }

           freeaddrinfo(result);           /* No longer needed */

           if (rp == NULL) {               /* No address succeeded */
               fprintf(stderr, "Could not bind\n");
               exit(EXIT_FAILURE);
           }

           /* Read datagrams and echo them back to sender. */

           for (;;) {
               char host[NI_MAXHOST], service[NI_MAXSERV];

               peer_addrlen = sizeof(peer_addr);
               nread = recvfrom(sfd, buf, BUF_SIZE, 0,
                                (struct sockaddr *) &peer_addr, &peer_addrlen);
               if (nread == -1)
                   continue;               /* Ignore failed request */

               s = getnameinfo((struct sockaddr *) &peer_addr,
                               peer_addrlen, host, NI_MAXHOST,
                               service, NI_MAXSERV, NI_NUMERICSERV);
               if (s == 0)
                   printf("Received %zd bytes from %s:%s\n",
                          nread, host, service);
               else
                   fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));

               if (sendto(sfd, buf, nread, 0, (struct sockaddr *) &peer_addr,
                          peer_addrlen) != nread)
               {
                   fprintf(stderr, "Error sending response\n");
               }
           }
       }
   Client program

       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <string.h>
       #include <sys/socket.h>
       #include <sys/types.h>
       #include <unistd.h>

       #define BUF_SIZE 500

       int
       main(int argc, char *argv[])
       {
           int              sfd, s;
           char             buf[BUF_SIZE];
           size_t           len;
           ssize_t          nread;
           struct addrinfo  hints;
           struct addrinfo  *result, *rp;

           if (argc < 3) {
               fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           /* Obtain address(es) matching host/port. */

           memset(&hints, 0, sizeof(hints));
           hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
           hints.ai_flags = 0;
           hints.ai_protocol = 0;          /* Any protocol */

           s = getaddrinfo(argv[1], argv[2], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }

           /* getaddrinfo() returns a list of address structures.
              Try each address until we successfully connect(2).
              If socket(2) (or connect(2)) fails, we (close the socket
              and) try the next address. */

           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;

               if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
                   break;                  /* Success */

               close(sfd);
           }

           freeaddrinfo(result);           /* No longer needed */

           if (rp == NULL) {               /* No address succeeded */
               fprintf(stderr, "Could not connect\n");
               exit(EXIT_FAILURE);
           }

           /* Send remaining command-line arguments as separate
              datagrams, and read responses from server. */

           for (size_t j = 3; j < argc; j++) {
               len = strlen(argv[j]) + 1;
                       /* +1 for terminating null byte */

               if (len > BUF_SIZE) {
                   fprintf(stderr,
                           "Ignoring long message in argument %zu\n", j);
                   continue;
               }

               if (write(sfd, argv[j], len) != len) {
                   fprintf(stderr, "partial/failed write\n");
                   exit(EXIT_FAILURE);
               }

               nread = read(sfd, buf, BUF_SIZE);
               if (nread == -1) {
                   perror("read");
                   exit(EXIT_FAILURE);
               }

               printf("Received %zd bytes: %s\n", nread, buf);
           }

           exit(EXIT_SUCCESS);
       }

下一篇 【计算机网络】网络编程接口 Socket API 解读(7)​​​​​​​

更多推荐

Spring Authorization Server入门 (十八) Vue项目使用PKCE模式对接认证服务

Vue单页面项目使用授权码模式对接流程说明以下流程摘抄自官网在本例中为授权代码流程。授权码流程的步骤如下:客户端通过重定向到授权端点来发起OAuth2请求。对于公共客户端,此步骤包括生成code_verifier并计算code_challenge,然后将其作为查询参数发送。如果用户未通过身份验证,授权服务器将重定向到登

PostgreSQL16源码包编译安装

一、安装环境操作系统:CentOSLinuxrelease7.8.2003(Core)PostgreSQL版本:16服务器IP地址:192.168.0.244Firewalld关闭、selinux关闭笔者本次选用最新v16版本进行部署二、pg数据库安装包下载下载地址:https://www.postgresql.org

第八章 排序

一、插入排序不带哨兵voidInsertSort(intA[],intn){inti,j,temp;for(i=1;i<n;i++){if(A[i]<A[i-1]){temp=A[i];for(j=i-1;j>=0&&A[j]>temp;--j){A[j+1]=A[j];}A[j+1]=temp;}}}带哨兵voidI

二分与前缀和

目录🍈前言❤二分🌹二分🌼数的范围🌼数的三次方根🌼特殊数字🌼机器人跳跃问题🌼四平方和🌼分巧克力🌹前缀和🌼前缀和🌼子矩阵的和🌼激光炸弹🌼K倍区间🍈前言❤二分整数二分模板中,一个比较坑的点,就是C++整数向下取整的机制,考虑到这点,你才能写出AC100%的代码关键在于1,对if()后面条件的判断2

SAP CRM 模块:概述,体系结构

前言CRM代表“客户关系管理”,是一组有助于以有组织的方式管理客户关系的方法和工具。在当今竞争激烈的商业环境中,顶级公司的注意力越来越集中于其最有价值的资产–客户。因此,这些公司需要一种合适的软件解决方案来迎合其客户,该解决方案易于使用,易于定制,完全集成并且可以灵活实施。客户关系管理是一种用于优化客户互动的业务策略。

MD5加密算法

1、简介MD5在90年代初由MIT的计算机科学实验室和RSADataSecurityInc发明,经MD2、MD3和MD4发展而来。MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理

编程小白如何学习RPA,0基础学习RPA攻略!

对于编程小白来说,学习RPA(机器人流程自动化)可能会感到有些无从下手。然而,只要按照一定的步骤和策略进行学习,从0开始掌握RPA并非难事。一、了解RPA的基本概念和优势在开始学习RPA之前,首先需要了解RPA的基本概念、特点和优势。RPA是一种使用自动化工具模拟人类在计算机上执行操作的技术,它可以帮助企业实现重复性、

第36章_瑞萨MCU零基础入门系列教程之步进电机控制实验

本教程基于韦东山百问网出的DShanMCU-RA6M5开发板进行编写,需要的同学可以在这里获取:https://item.taobao.com/item.htm?id=728461040949配套资料获取:https://renesas-docs.100ask.net瑞萨MCU零基础入门系列教程汇总:https://b

Spring Boot 简介与入门

🌷🍁博主猫头虎带您GotoNewWorld.✨🍁🦄博客首页——猫头虎的博客🎐🐳《面试题大全专栏》文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺🌊《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐🌊《100天精通Golang(基础入门篇)》学会Golang语言,畅玩云原生,走遍大

数据结构:树和二叉树之-堆排列 (万字详解)

目录树概念及结构1.1树的概念1.2树的表示​编辑2.二叉树概念及结构2.1概念2.2数据结构中的二叉树:​编辑2.3特殊的二叉树:​编辑2.4二叉树的存储结构2.4.1顺序存储:2.4.2链式存储:二叉树的实现及大小堆排列1功能展示2定义基本结构3初始化4打印5销毁6插入7向上调整8交换两数组元素之间的值9删除10向

Kubernetes(K8S)集群部署

目录一、创建3台虚拟机二、为每台虚拟机安装Docker三、安装kubelet3.1安装要求3.2为每台服务器完成前置设置3.3为每台服务器安装kubelet、kubeadm、kubectl四、使用kubeadm引导集群4.1master服务器4.2node1、node2服务器4.3初始化主节点4.4work节点加入集群

热文推荐