欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

实操指南:如何在Unix系统中通过域套接字实现进程间通信(IPC之九)

最编程 2024-02-15 09:54:29
...
  • 创建 UNIX Socket

    #include <sys/socket.h>
    #include <sys/un.h>
    
    unix_socket = socket(AF_UNIX, type, 0);
    
    • type 可以是:
      • SOCK_STREAM:面向连接的数据流传输;
      • SOCK_DGRAM:面向无连接的数据报传输;
      • SOCK_SEQPACKET:面向连接的顺序数据包传输,可以按照发送的顺序传递消息;
  • 绑定一个文件路径

    • 一个命名的 UNIX domain socket 必须绑定到一个路径下;

    • 在 IPv4 下,Internet Socket 绑定 IP 地址和端口时,使用 struct sockaddr_in,在 Unix Socket 上绑定文件路径时需要使用结构 struct sockaddr_un

      struct sockaddr_un {
          sa_family_t sun_family;               /* AF_UNIX */
          char        sun_path[108];            /* Pathname */
      };
      
      • sun_family 一定是 AF_UNIX
      • sun_path 指向一个文件路径,比如 /tmp/unix_socket
    • bind()

      #include <sys/types.h>          /* See NOTES */
      #include <sys/socket.h>
      
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      
      • sockfd 是使用 socket() 建立的 UNIX Socket;
      • addr 指向 struct sockaddr_un 的指针
  • 下面代码片段创建了一个 socket 并绑定到了一个文件路径上

    ......
    int unix_sock;
    struct sockaddr_un serveraddr;
    
    unix_sock = socket(AF_UNIX, SOCK_STREAM, 0);
    
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sun_family = AF_UNIX;
    strcpy(serveraddr.sun_path, "/tmp/unix_socket");
    
    bind(unix_sock, (struct sockaddr *)&serveraddr, SUN_LEN(&serveraddr));
    ......
    
  • 建议使用宏 SUN_LEN(addr) 来计算填好文件路径的 struct sockaddr_un 的长度,因为其中的 sun_path 字段是不定长的,这个宏可以帮助我们计算出正确的结果,宏 SUN_LEN(addr) 定义在头文件 sys/un.h 中;

  • 当把一个 socket 绑定到一个文件上时,在文件系统上,这个文件会真实存在,但是这个文件不能使用 open() 打开,而只能使用 socket() 打开;

  • 建议在使用完一个命名 UNIX domain socket 后,显式地使用 unlink() 或者 remove() 将其删除,避免其残留在文件系统中;

  • 尽管我们的例子中使用的文件名在 /tmp/ 目录下,但在实际应用中我们并不建议这样做,因为 /tmp/ 目录是任何人都可以读写的,这可能会带来一些安全隐患,通常可以把这个文件建立在项目所在的目录下,相对比较安全;

  • 当我们用 bind() 将一个文件绑定到 socket 上时,相应的文件将被建立,该文件的默认权限为所绑定的 socket 的权限,通常情况下使用 socket() 建立的 socket 的权限是 0777,可以使用 fstat 获得,但这时建立的 socket 文件的默认属性是 0775;

  • 如果我们在 bind() 之前先使用 fchmod() 修改 socket 的权限,那么再使用 bind() 绑定一个文件时,其建立的文件的权限也会产生变化,下面这段代码可以演示这种权限的变化;

    ......
    #define SOCK_NAME       "./unix_socket.sock"
    ......
    int sockfd;
    struct sockaddr_un addr;
    struct stat sock_stat;
    
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    fchmod(sockfd, 0660);
    
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, SOCK_NAME);
    bind(sockfd, (struct sockaddr *)&addr, SUN_LEN(&addr));
    
    system("ls -l unix_socket.sock");
    ......
    
  • 这段代码在当前目录下生成的 socket 文件的权限为 0660,最后一行的 system("ls -l") 显示的文件清单将清楚地显示出来,你可以尝试一下,如果没有 fchmod() 这行代码,生成的文件的权限为 0775;

  • 使用 ls -l 显示文件清单时,该文件的前面有一个 s 标志,表示这是一个 socket 文件;

  • socket 以及 socket 文件是可以使用 stat()、fstat() 获取属性,同时可以使用 chmod()、fchmod() 来改变权限的;

  • 改变一个 socket 文件权限的意义在于安全性,因为其它进程是需要通过 socket 文件来使用这个 UNIX domain socket 的,那么这个进程必须要有相应的权限才行;

  • 可以使用 read()/write()、send()/recv()、sendto()/recvfrom()、sendmsg()/recvmsg() 来发送和接收数据,与 Internet Socket 下的使用方法无大的差异;

  • read()/write()

    #include <unistd.h>
    
    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);
    
    • 这对函数是文件的读/写函数,因为 socket 返回的是标准的文件描述符,所以可以使用这两个函数进行接收和发送数据;
    • 这对函数通常仅用于面向连接的 socket,也就是通常不用于 SOCK_DGRAM 类型的 socket;
    • 这对函数是比较简单的,write() 基本不会出错;
    • 使用 read() 接收数据时可能会有阻塞问题,当使用 SOCK_STREAM 时,可能还有粘包问题,不过这些问题在 Internet Socket 的 TCP 编程中也是一样存在的,并不是 UNIX Socket 所特有的,如果有这方面的问题,请参考有关资料;
  • recv()/send()

    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    
    • 这对函数理论上既可以用于有连接的 socket(AF_STREAM),也可以用于无连接的 socket(AF_DGRAM),但通常仅用于面向连接的 socket,也就是通常不用于 SOCK_DGRAM 类型的 socket;
    • 与 read()/write() 相比,这对函数多了一个参数 flags
    • 在使用 send() 发送数据时,绝大多数情况下,flags 都可以设置为 0,最常用的 falgs 设置是 MSG_DONTWAIT,但对于向 UNIX Socket 发送数据而言,如果发送的数据不是很大,是不可能产生阻塞的,所以不需要设置 MSG_DONTWAIT;
    • 在使用 recv() 接收数据时,当 socket 上没有数据时,会产生阻塞,如果不希望阻塞,可以设置 flags 为 MSG_DONTWAIT,这样可以让程序有更好的适应性;
    • 在使用 recv() 接收数据时,与 Internet Socket 的 TCP 编程一样,可能出现"粘包"问题,请参考相关资料;
  • recvfrom()/sendto()

    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                     struct sockaddr *src_addr, socklen_t *addrlen);
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, 
                   const struct sockaddr *dest_addr, socklen_t addrlen);
    
    • 这对函数通常多用于面向无连接的 socket,也就是通常仅用于 SOCK_DGRAM 类型的 socket;

    • Internet Socket 的 UDP 编程中,需要使用 struct sockaddr_in 设置对端的 IP 地址和端口,而 UNIX Socket 要使用 struct sockaddr_un 来设置对端的文件路径;

    • 源程序sendto-recvfrom.c(点击文件名下载源程序)演示了如何使用 sendto() 和 recvfrom() 进行面向报文的进程间通信;

    • 编译:gcc -Wall -g sendto-recvfrom.c -o sendto-recvfrom

    • 运行:./sendto-recvfrom

    • 运行截图:

      screenshot of running sendto-recvfrom

  • 推荐阅读