第二部分 基本套接字编程

第3章 套接字编程简介

  • 套接字地址结构

    • IPv4套接字地址结构(又称 网络套接字地址结构)

      • sockaddr_in命名,定义在<nteinet/in.h>头文件种

      •  1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
        // POSIX定义
        struct in_addr {
              in_addr_t s_addr;	// 32位 Ipv4地址,网络字节序(大端字节序)  
        };
        struct sockaddr_in {
            uint8_t			sin_len;		// 结构长度 (16, 为增加对OSI协议的支持而随4.3BSD-Reno添加的)
            sa_family_t		sin_family;		// AF_INET
            in_port_t		sin_port;		// 16位 TCP或UDP端口号,网络字节序
        
            struct in_addr	sin_addr;
        
            char 			sin_zero[8];	// 未使用
        }
        

        imgs202202262107036.png

    • 通用套接字地址结构 从应用程序开发人员角度,其唯一用途:对指向特定于协议的套接字地址结构的指针执行类型强制转换

      • 1
        2
        3
        4
        5
        6
        7
        8
        
        struct sockaddr {
            uint8_t			sa_len;
            sa_family_t		sa_family;	// address family: AF_xxx calue
            char 			sa_data[14];
        };
        
        // bind函数的ANSI C函数原型
        int bind(int, struct sockaddr*, socklen_t);
        
    • IPv6套接字地址结构 在<netint/in.h>中定义

      •  1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
        struct in6_addr {
            uint8_t s6_addr[16];		//128位IPv6地址
        };
        #define SIN6_LEN
        struct sockaddr_in6 {
            uint8_t				sin6_len;		// 这个结构的长度(28)
            sa_family_t 		sin6_family;	// AF_INET6 (IPv4的地址簇是AF_INET)
            in_port_t			sin6_port;		//传输层端口
        
            uint32_t			sin6_flowinfo;	// flow information(32位,低20位是流标,高12位保留)
            struct in6_addr		sin6_addr;		// IPv6地址
            uint32_t			sin6_scope_id;	// 标识具有范围的地址(即 集合)
        };
        
      • 存储套接字地址结构:sockaddr_storage

        1
        2
        3
        4
        
        struct sockaddr_storage {
              uint8_t		ss_len;	//结构体长度(依赖于实现)
            sa_family_t ss_family;	// address family: AF_xxx value
        };
        

      image-20220227094348636

    • 值 - 结果参数

      • 套接字地址结构传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。

        1. 从进程到内核传递 的函数有3个:bind、connect和sendto。

          1
          2
          
          struct sockaddr_in serv;
          connect(sockfd, (SA*) &serv, sizeof(serv));
          

          image-20220227094902082

        2. 从内核到进程传递 的函数有4个:accept、recvfrom、getsockname和getpeername。这四个函数其中两个参数是指向某个套接字地质结构的指针和指向表示该结构大小的整数变量的指针。

          1
          2
          3
          4
          5
          6
          
          struct sockaddr_un cli;	// UNIX domain
          socklen_t	len;
          
          len = sizeof(cli);
          getpeername(unixfd, (SA *), &cli, &len);	// len是 值-结果 参数
          /* len可能发生改变 */
          

          套接字地址结构大小参数 从整数改成指向某个整数变量的指针,原因在于 函数调用时,作为一个值 告诉内核该结构的大小,防止内核在写该结构时越界;函数返回时,作为 一个结果 告诉进程内核在该结构中存储了多少信息。

          image-20220227095825490

  • 字节排序函数

    image-20220227102237924

    • 小端字节序 又是大多系统使用的格式,称为主机字节序;大端字节序 又称为网络字节序

    • 1
      2
      3
      4
      5
      6
      7
      
      #include <netinet/in.h>
      
      uint16_t htons(uint16_t host16bitvalue);
      uint32_t htonl(uint32_t host32bitvalue);		// 均返回网络字节序的值
      uint16_t ntohs(uint16_t net16bitvalue);
      uint32_t ntohl(uint32_t net32bitvalue);			// 均返回主机字节序的值
      // h代表host, n代表network,s代表short, l代表long
      
  • 字节操纵函数

    • 1
      2
      3
      4
      5
      6
      
      #include <strings.h>
      
      void bzero(void *dest, size_t nbytes);
      void bcopy(const void *src, void *dest, size_t nbytes);
      int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
      // b代表 字节
      
  • inet_aton、inet_addr和inet_ntoa函数 (在点分十进制数串与它长度为32位的网络字节序二进制间转换IPv4地址

    • 1
      2
      3
      4
      5
      6
      
      #include <arpa/inet.h>
      
      int inet_aton(const char *strptr, struct in_addr *addrptr);	//返回:若字符串有效则为1,否则为0
      in_addr_t inet_addr(const char *strptr);	
      // 返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE
      char *inet_ntoa(struct in_addr inaddr);	// 返回:指向一个点分十进制数串的指针
      
  • inet_pton和inet_ntop函数 (对 IPv4地址和IPv6地址都适用

    • p (presentation) 表达 和 n(numeric) 数值

    • 1
      2
      3
      4
      5
      6
      
      #include <arpa/inet.h>
      
      int inet_pton(int family, const char *strptr, void *addrptr);// 尝试转换由strptr指针所指的字符串,addrptr存放二进制结果
      // 返回:若成功则为1,若输入不是有效的表达格式则为0,若出错则为-1
      const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);// 从数值格式(addrptr) 转换到 表达格式(strptr)
      // 返回:若成功则为指向结果的指针,若出错则为NULL
      

      family参数既可以是 AF_INET, 也可以是AF_INET6。

      image-20220227104632664

  • sock_ntop和相关函数

    • inet_ntop的一个基本问题:它要求调用者传递一个指向某个二进制地址的指针,而该地址通常包含在一个套接字地址结构中,这就要求调用者必须知道这个结构的格式和地址簇。

    • 1
      2
      3
      
      #include "unp.h"	// 自定义头文件
      char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);	
      // 返回:若成功则为非空指针,若出错则为NULL
      

      image-20220227114823456

  • readn、written、readline函数

    • 1
      2
      3
      4
      5
      6
      
      // 每当 读或写 一个字节流套接字时总要使用的函数
      #include "unp.h"
      
      ssize_t readn(int filedes, void *buff, size_t nbytes);	// 从一个描述符filedes读n字节
      ssize_t written(int filedes, const void *buff, size_t nbytes);	// 往一个描述符写n字节
      ssize_t readline(int filedes, void *buff, size_t maxlen);	// 从一个描述符读文本行,一次一个字节
      

第4章 基本TCP套接字编程

image-20220227135907426

  • socket函数

    • 1
      2
      3
      4
      
      #include <sys/socket.h>
      // family表示协议簇
      int socket(int family, int type, int protocol)
      // 返回:若成功则为非负描述符,若出错则为-1
      

      image-20220227140749635

      image-20220227140807012

      image-20220227140823442

  • connect函数 (TCP客户用connect函数来建立与TCP服务器的连接)

    • 1
      2
      3
      
      #include <sys/socket.h>
      int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
      // 返回: 若成功则为0,若出错则为-1
      

      客户 在调用函数 connect 前不必非得调用 bind函数,因为如果需要的化,内核会确定源IP地址,并选择一个临时端口作为源端口。

  • bind函数 (把一个本地协议地址 赋予一个套接字) 服务器在启动时绑定一个众所周知端口

    • 1
      2
      3
      4
      
      #include <sys/socket.h>
      // myaddr 是指向特定于协议的地址结构的指针,addrlen是该地址结构的长度
      int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
      // 返回:若成功则为0, 若出错则为-1
      
  • listen函数

    • 仅由 TCP服务器调用。 本函数通常应该在调用socket和bind函数之后,并在调用accept函数之前调用。

    • 1
      2
      3
      
      #include <sys/socket.h>
      int listen(int sockfd, int backlog);
      // 返回:若成功则为0, 若出错则为-1
      

      内核为任何一个给定的监听套接字维护两个队列:

      • 未完成连接队列,每个这样的SYN分节对应其中一项:已有某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态;

      • 已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

        image-20220227142843678

        image-20220227142910524

  • accept函数

    • accept函数由TCP服务器调用,用于已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)

    • 1
      2
      3
      
      #include <sys/socket.h>
      int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
      // 返回:若成功则为非负描述符,若出错则为-1
      
  • fork和exec函数

    • 1
      2
      3
      
      #include <unistd.h>
      pid_t fork(void);	
      // 返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
      
  • 并发服务器

  • close函数

    • 1
      2
      3
      
      #include <unistd.h>
      int close(int sockfd);
      // 返回:若成功则为0,若出错则为-1
      
  • getsockname和getpeername函数

    • 1
      2
      3
      4
      
      #include <sys/socket.h>
      int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
      int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
      // 均返回:若成功则为0,若出错则为-1
      

第5章 TCP客户/服务器程序示例

  • 回射服务器

    • 客户端从标准输入读入一行文本,并写给服务器;
    • 服务器从网络输入读入这行文本,并回射给客户;
    • 客户从网络输入读入这行回射文本,并显示在标准输出上。

    image-20220226211440868

    • written readline

第6章 I/O复用:select和poll函数

IO复用典型网络应用场合:

image-20220227150432196

  • I / O 模型(5种 I / O模型)

    一个输入操作通常包括两个不同的阶段:1)等待数据准备好;2)从内核中复制数据。

    对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区; 第二步就是把数据从内核缓冲区复制到应用进程缓冲区

    • 阻塞式I / O

      • image-20220227150752200
      • 对于UDP,数据准备好读取的概念比较简单:要么整个数据包已经收到,要么还没有。
      • 在上图6-1中,进程调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中 或者 发生错误才返回。最常见的错误是 系统调用被信号中断。
    • 非阻塞式 I / O

      • image-20220228132625200
      • 当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,称为 轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,但通常是在专门提供某种功能的系统中才有。
    • I / O 复用(I/O multiplexing)(select和 poll)

      • 有了 I/O复用,可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。
      • image-20220228133116808
    • 信号驱动式 I / O (SIGIO)

      • 用信号, 让内核在描述符就绪时发送SIGIO信号通知我们,称这种模型为信号驱动式I/O
      • image-20220228133357435
      • 这种模型的优势: 等待数据包到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据包已被读取。
    • 异步 I / O (POSIX的aio_系列函数)

      • 与 信号驱动模型 的 主要区别是: 信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时 完成
      • image-20220228133935534
    • 5种I/O模型的比较

      image-20220228134403874

      前四种模型的主要区别在于第一阶段,第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞与recvfrom调用。 而异步模型在这两个阶段都要处理。

    • 同步I/O和异步I/O对比

      POSIX对这两个术语的定义:

      1. 同步I/O操作:导致请求进程阻塞,直到I/O操作完成; (前四种I/O模型即 阻塞式I/O、非阻塞式I/O、I/O复用模型和信号驱动式I/O))
      2. 异步I/O操作:不导致请求进程阻塞。 (异步I/O模型
  • select函数

    • 该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生 或 经历一段指定的时间后才唤醒它。

      1
      2
      3
      4
      
      #include <sys/select.h>
      #include <sys/time.h>
      int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
      // 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
      

      参数:

      • timeout:告知内核等待所指定描述符中的任何一个就绪可花多长时间

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        struct timeval {
              long tv_sec;	// 秒
            long tv_usec;	// 微秒
        }
        /*
        该参数有以下三种可能:
        1) 永远等下去:仅在有一个描述符准备好I/O时才返回。因此将该参数置为空指针
        2) 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过该参数指定的时间
        3) 根本不等待:检查描述符后立即返回,这称为轮询。为此该参数指定时间应为0
        */
        
      • 中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述符。 maxfdpl参数指定待测试的描述符个数,它的值是待测试的最大描述符加1

  • poll函数

    • poll提供的功能与select类似,在处理流设备时,能够提供额外的信息。

      1
      2
      3
      
      #include <poll.h>
      int poll(struct pollfd *fdarrary, unsigned long nfds, int timeout);
      // 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
      

      第一个参数是 指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

      1
      2
      3
      4
      5
      
      struct pollfd {
          int	fd;
          short events;
          short revents;
      };
      

      image-20220228143338945

      poll识别三类数据:普通(normal)、优先级带(priority band)和高优先级(high priority)。这些术语均出自基于流的实现。

第7章 套接字选项

第8章 基本UDP套接字编程

第9章 基本SCTP套接字编程

第10章 SCTP客户/服务器程序例子

第11章 名字与地址转换