美文网首页
高级C与网络编程复习(4)—— 基本套接字函数(Elementa

高级C与网络编程复习(4)—— 基本套接字函数(Elementa

作者: SunnyQjm | 来源:发表于2020-04-24 15:41 被阅读0次
    基本 TCP 客户 / 服务器程序的套接字函数

    socket 函数

    #include <sys/socket.h>
    
    /**
    * 该函数用于创建一个socket套接字
    * @param domin    协议族/地址族
    * @param type     套接字的类型
    *                 SOCK_STREAM ==> TCP套接字
    *                 SOCK_DGRAM  ==> UDP套接字
    *                 SOCK_RAW    ==> 原始套接字
    *                 SOCK_PACKET ==> 可用于链路层访问控制
    * @param protocol 指定协议
    *
    * @return         返回一个socket描述符 sockfd
    *                 sockfd < 0  ==> 创建失败
    *                 sockfd >= 0 ==> 创建成功,之后可用该sockfd进行IO操作
    **/
    int socket(int family, int type, int protocol);
    
    • family 常值


      socket 函数的 family 常值
    • type 常值


      socket 函数的 type 常值
    • protocol 常值


      socket 函数的 AF_INET 或 AF_INET6 的常值
    • socket 函数中的 family 不是任意组合都是有效的,下面是组合效果:


      socket 函数中 family 和 type 参数的组合

    AF_XXX 和 PF_XXX

    • AF_前缀表示地址族PF_前缀表示协议族
    • 历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_值用来创建套接字,而 AF_值用于套接字地址结构。
    • 但实际上,支持多个地址族的协议从未出现过,而且头文件 <sys/socket.h> 中为一给定协议定义的 PF_值总是与此协议的 AF_值相等

    connect 函数

    /**T
    * 该函数用于建立与指定socket的连接
    * @param sockfd       一个未连接的socket的描述符
    * @param sockaddr     指向要连接的套接字的sockaddr结构体的指针
    * @param addrlen      上述sockaddr结构体的长度
    *
    * @return         成功则返回0, 失败返回-1, 错误原因存于errno 中
    **/
    int connect(int sockfd, const struct sockaddr * servaddr, int addrlen);
    
    • 如果是 TCP 套接字,调用 connect 函数将激发 TCP 的三路握手过程。而且仅在连接建立成功或出错时才返回

    • connect 错误:

      • ETIMEOUT(超时错误): TCP 客户没有收到对发出的 SYN 分节的响应
      • ECONNREFUSED(连接拒绝错误):客户在发出 SYN 分节后收到 RST 响应
        • 表明服务器主机在我们指定的端口上没有进程在等待与之连接(通常是服务器进程没有在运行,或者是客户端连接的时候指定了错误的端口号)
        • 或 TCP 向取消一个已有的连接
        • 或 TCP 接收到一个根本不存在的连接上的分节
        • 硬错误(hard error)
      • EHOSTUNREACH 或 ENETUNREACH(主机不可达或)
        • 客户咋中间的某个路由器上引发了一个 “destination unreachable”(目的地不可达)ICMP 错误
        • 并且在某个规定时间(4.4BSD 规定 75s)内仍未收到响应
        • 软错误(soft error)
    • 状态转换

      • TCP 状态转换图


        TCP 状态转换图
      • connect 函数导致当前客户套接字从 CLOSED 状态(该套接字自从由 socket 函数创建以来,一直处于 CLOSED 状态)转移到 SYN_SENT 状态

      • 如果连接成功则转移到 ESTABLISHED 状态

      • 若 connect 失败,则该套接字不可再用,必须关闭,我们不能对这样的套接字再次调用 connect 函数

      • 当循环调用函数 connect 为给定主机尝试各个 IP 地址直到有一个成功时,在每次 connect 失败后,都必须 close 当前的套接字描述符并重新调用 socket

    bind 函数

    • bind 函数把一个本地协议地址赋予一个套接字
    • 对于网际协议,协议地址是 32 位的 IPv4 地址或 128 位的 IPv6 地址与 16 位的 TCP 或 UDP 端口号的组合
    • 调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,也可以两者都指定或两者都不指定
    /****
    *  sockfd:   标识一未捆绑套接口的描述字。
    *  my_addr:  赋予套接口的地址。sockaddr结构定义如下:
    *             struct sockaddr{
    *               u_short sa_family;
    *               char sa_data[14];
    *             };
    *  addrlen:  my_addr的长度。
    *  返回值:    成功返回0,失败返回-1.
    ****/
    int bind( int sockfd , const struct sockaddr * my_addr, socklen_t addrlen);
    
    • 端口的绑定

      • 通常用于服务器在启动的时候捆绑他们众所周知的端口
      • 对于客户机,不调用 bind 绑定端口,而是在发送消息的时候由内核临时分配一个端口,这是正常的
      • 而对于服务器而言,不绑定端口是极为罕见的
    • 地址的绑定

      • 进程可以一个特定的 IP 地址绑定到它的套接字上,不过这个 IP 必须属于其所在主机的网络接口之一
      • 对于客户端而言,绑定 IP 地址就相当于为该套接字上发送 IP 数据报指定了源 IP 地址
      • 对于服务器而言,绑定 IP 地址就相当于限定该套接字只能接收那些目的地为这个 IP 地址的客户连接
    • 给 bind 函数指定要捆绑的 IP 地址和端口号产生的结果

      给 bind 函数指定要捆绑的 IP 地址和端口号产生的结果
    • 通配地址 (wildcard address)

      • IPv4: INADDR_ANY

        //IPv4
        struct sockaddr_in servaddr;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   //wildcard
        
      • IPv6: in6addr_any

        //IPv6
        struct sockaddr_in6 serv;
        serv.sin6_addr = in6addr_any;               //wildcard
        
    • 错误

      • EADDRINUSE(“Address already in use”, 地址已使用)

    listen 函数

    仅由 TCP 服务器调用,它做两件事情

    • socket 函数创建一个套接字时,它被假设为一个主动套接字 (active socket),也就是说,它是一个将调用 connect 发起连接的客户套接字。listen 函数把一个未连接的套接字转换成一个被动套接字,之后是内核应接收指向该套接字的连接请求。
    • 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数
    /**
    * 将一个未连接的套接字转换成监听套接字,这样即可以用来监听来自客户端的请求了
    * @param  sockfd     一个未连接的套接字描述符
    * @param  backlog    等待连接队列的最大长度
    **/
    int listen( int sockfd, int backlog);
    
    • 调用时机

      • 本函数通常应该在调用 socket 和 bind 这两个函数以后,并在调用 accept 函数之前调用
    • 内核为任何一个给定的监听套接字维护两个队列

      TCP为监听套接字维护的两个队列
      • 未完成连接队列(incomplete connection queue):服务器收到请求的 SYN 分节,并且正在等待完成相应的 TCP 三路握手过程。这些套接字处于 SYN_RCVD 状态
      • 已完成连接队列(completed connection queue):每个已完成 TCP 三路握手过程的客户对应其中的一项。这些套接字处于 ESTABLISHED 状态
      • 点我可查看 TCP 状态转换图
    • listen 函数的第二个参数通常指的是已完成连接队列的最大长度

    • 两个队列的建立时机

      TCP 三路握手和监听套接字的两个队列

    accept 函数

    • accpet 函数由 TCP 服务器调用,用于从已完成连接队列头返回一个已完成连接
    • 如果已完成连接队列为空,那么进程将被投入睡眠(假定套接字为默认的阻塞方式)
    /**
    * 在一个套接字的监听队列中取一个连接,如果没有,则死等
    *
    * @param sockfd    监听描述符(在调用listen之后监听来自客户端的连接)
    * @param addr      (可选)用来保存新连接的源端地址
    * @param addrlen   (可选)用来保存新连接的源端地址结构的长度
    *
    * @return          如果连接成功,则返回一个已连接的套接字描述符(用于和客户端通信)
    **/
    SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    

    fork 和 exec 函数

    • fork 函数

      #include <unistd.h>
      
      /**
      * 调用fork函数创建一个新进程,与当前进程并行执行
      *
      * @return 在子进程中为0,在父进程中为子进程ID,若出错返回-1
      **/
      pid_t fork(void);
      
      • fork 函数是 Unix 中派生新进程的唯一方法
      • 调用一次,返回两次。返回值告知当前进程是子进程还是父进程
      • 子进程可通过 getppid 获取父进程的 id
      • 父进程 fork 之前打开的所有文件描述符都会 copy 一份给子进程(各个描述符的引用计数加 1)
      • 两个典型用法:
        • 创建自身副本,每个副本并行执行各自的操作
        • 一个进程想要执行另一个程序,则 fork 一下,在子进程调用 exec 执行其它程序
    • exec 函数

      #include <unistd.h>
      
      int execl(const char* pathname, const char *arg0, ... /*(char*)*/);
      
      int execv(const char* pathname, char* const *argv[])
      
      
      • 存放在硬盘上的可执行程序文件能够被 Unix 执行的唯一方法是:由一个现有的进程调用上述 6 个 exec 函数中的一个
      • exec 把当前进程映像替换成新的程序文件,而且该程序通常从 main 函数开始执行。进程 ID 不改变
      • 我们称调用 exec 的进程为调用进程(calling process),称新执行的程序为新程序(new program)
      • 这些函数只在出错时才返回到调用跟着,否则,控制将被传递给新程序的起始点,通常就是 main 函数
      • 6 个 exec 函数的关系


        6 个 exec 函数的关系

    描述符引用计数

    • Unix 系统内核为每个文件描述符(包括 socket fd)维护一个引用计数, 这个引用计数标识当前打开着的引用该文件或套接字的描述符的个数

    • 当某个文件描述符或套接字描述符关闭的时候,不是直接关闭文件或套接字,而是引用计数减 1,当引用计数减到 0 的时候执行关闭操作

    • 需要注意的是:如果多个进程同时拥有指向同一个文件或套接字的描述符。且其中一个没有关闭(并且不再使用了),则就算其他文件描述符都关闭了,这个文件或套接字也不会关闭。就会造成内存泄露

    • 举个栗子:

      pid_t pid;
      int listenfd, connfd;
      listenfd = Socket(...);
      
      /*fill in sockaddr_in{} with server's well-know port*/
      
      Bind(listenfd, ...)
      Listen(listenfd, LISTENQ);
      for( ; ; ){
        connfd = Accept(listenfd, ...);
        if( (pid = Fork()) == 0 ){        //子进程
          Close(listenfd);
          doit(connfd);
          Close(connfd);
          exit(0);
        }
        Close(connfd);                    //父进程
      }
      
      • 上面的代码中,调用了 fork 之后,connfd 和 listenfd 在父子进程中都有一份
      • 所以在父进程中,只用处理 listenfd,故关掉 connfd
      • 在子进程中,只用处理 connfd,故关掉 listenfd
      • 试想:如果父进程中没有关闭 connfd,则就算子进程执行完毕,connfd 关联的套接字的引用计数还是不为 0,所以一直不会释放。连接多了之后,每个连接的 socket 都不释放,慢慢的服务器内存就炸了。

    close 函数

    #include <unistd.h>
    
    /**
    * 通常Unix close函数也用来光比套接字,并终止TCP序列
    *
    * @return    0 ==> 成功
    *           -1 ==> 出错
    **/
    int close(int sockfd);
    
    • 通常 close 函数的默认行为是把该套接字标记成已关闭,然后立即返回
    • 被标记的套接字不能再被进程使用,即不能 read/write
    • 然后尝试将缓存或队列中所有的 Message 发出
    • 接着就是正常的 TCP 终止序列
    • close 函数会将读和写两个方向的连接都关掉

    gesockname 和 getpeername 函数

    #include <sys/socket.h>
    
    /**
    * 返回与sockfd关联的本地协议地址
    **/
    int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
    
    /**
    * 返回与sockfd关联的外地协议地址
    **/
    int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
    
    • 其中,两个函数的后两个参数均为 Value-Result 参数
    • 可以用 getsockname 获取内核为我们分配的地址或端口号
    • getsockname 还可以用于获取某个套接字的协议族
    • 上面两个函数中的第一个参数 sockfd 必须是已连接的套接字描述符
    • 当服务器进程通过 accept 的某个进程通过调用 exec 执行程序时,getpeername 是唯一可以用来获取对端设备地址信息的函数

    相关文章

      网友评论

          本文标题:高级C与网络编程复习(4)—— 基本套接字函数(Elementa

          本文链接:https://www.haomeiwen.com/subject/asszihtx.html