macOS跨进程通信: Unix Domain Socket 创建实例

macOS跨进程通信: Unix Domain Socket 创建实例

一: 简介

Socket 是 网络传输的抽象概念。

一般我们常用的有Tcp Socket和 UDP Scoket, 和类Unix 系统(包括Mac)独有的 Unix Domain Socket(UDX)。

  • Tcp Socket 能够跨电脑进行通信,即使是在同一个电脑下的多进程间通信,也会通过网卡进行数据传输,如果本地网卡的环回网络被禁用, 则会导致通信失败。
  • Unix Domain Socket,使用的是Liunx 系统中万物皆文件的概念,和有名管道的操作差不多,都是在文本创建一个特有的文件,用来在两个进程间通信,两个进程分别写入和读取文件流中的数据,达到传输的目的。 和Tcp Socket不一样的是不用借助网卡通信,限制比较小,传输的效率高。

    这里主要针对 Unix Domain Socket进行研究.


    在终端使用 ls -ll /tmp/

    可以看到红圈中我们demo创建的Unix Domain Socket 文件。

    Unix Domain Socket 会在 在第一列将会显示类型 s

    这里还有其他类型的文件。其中p表示命名管道文件,d表示目录文件,l表示符号连接文件,-表示普通文件,s表示socket文件,c表示字符设备文件,b表示块设备文件。

    二:主要函数

    1. int socket (int domain, int type, int protocol) 创建socket 对象

    • domain 选择 AF_UNIX, 代表 unix domain socket
    • type. 选择SOCK_STREAM, socket 流
    • protocol 填0, 由系统选择

      2. int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)

      将socket 绑定到对应 ip 和 端口上

      • sockfd 前面返回的描述符
      • myaddr 包含 通信 对象 路径的struct, 这里创建的是 /tmp/jimbo_udx_server.sock
      • addrlen前一个stuct的长度

        3. int listen(int sockfd, int backlog)

        调用后,本地socket 文件的状态变更

        • sockfd 前面返回的描述符
        • backlog 此socket 接收的客户端的数量

          4. int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen)

          阻塞式等待客户端接入,客户端接入后返回。

          传入server的 sockfd,返回接入后的sockfd.

          后面两个参数代表接口客户端的地址及struct长度

          5. int read(int sockfd, void *buf, int len, unsigned int flags)

          接收客户端发来的数据

          6. int write(int sockfd, const void *msg, int len, int flags)

          服务器 往 客户端/服务器发送数据

          7. int close(int sockfd) 或 Windows的 7. int closesocket(int sockfd)

          关闭连接

          三:demo代码

          如下图,创建了两个进程,分别为服务器App, 客户端App.

          UI 上点击发送按钮。 收到消息后可以在 控制台查看 输出。

          1. 服务器端主要逻辑

          • 主要创建了socket一个 AF_UNIX和SOCK_STREAM 组合的socket

          • remove(...) 删除以前的sock 文件

          • bind 将文件路径和 socket 对象绑定在一起

          • listen() 开始监听

          • 启动子线程,在线程内 阻塞等待客户端连接(accept),和接收客户端消息(read)

          • 启动客户端进程。 客户端内进行连接到这个服务器

          • 点击ui上的发送按钮,往客户端发送消息

            主要代码: ViewController.mm 文件代码

            //
            //  ViewController.m
            //  Sockct_UDX_MainApp
            //
            //  Created by jimbo on 2024/1/5.
            //
            #import "ViewController.h"
            #include const char * s_sock_path = "/tmp/jimbo_udx_server.sock";
            @interface ViewController ()
            @property (weak) IBOutlet NSTextField *textLabel;
            @property (nonatomic, assign) int sfd;
            @end
            @implementation ViewController
            - (void)viewDidLoad { [super viewDidLoad];
                
                /**
                 int socket(int domain, int type, int protocol)
                 AF_UNIX VS AF_INET(ipv4 tcp)
                 SOCK_STREAM VS SOCK_DGRAM
                 当protocol为0时,会自动选择type类型对应的默认协议。
                 */
                int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
                self.sfd = sfd;
                
                if (sfd == -1) { perror("socket create failed!");
                    return;
                }
                // 删除所有与路径名一致的既有文件,这样才能将 socket 绑定到这个路径名上
                  if (remove(s_sock_path) == -1 && errno != ENOENT){ perror("remove failed");
                      return;
                  }
                
                struct sockaddr_un addr  = {0};
                addr.sun_family = AF_UNIX;
                strcpy(addr.sun_path, s_sock_path);
                
                //bind 的时候会在文件路径创建响应的s_sock_path文件. (UI app 需要关闭沙盒才能有权限访问对应的路径)
                //当使用ls –ll列出时,UNIX domain socket 在第一列将会显示类型 s
                //扩展一下,这个位置还可以有其他几种选项:p、d、l、s、c、b和-:
                //其中p表示命名管道文件,d表示目录文件,l表示符号连接文件,-表示普通文件,s表示socket文件,c表示字符设备文件,b表示块设备文件。
                int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un));
                if (ret == -1) { perror("bind faild!");
                    return;
                }
                
                ret = listen(sfd, 5);
                if (ret == -1) { perror("listen failed!");
                    return;
                }
                
                NSThread *th = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadWorker) object:nil];
                [th setName:@"udx thread"];
                [th start];
                NSLog(@"---start");
                
                
                //启动子进程 app client
                
                   //启动子进程
                NSURL *subAppURL = [[NSBundle mainBundle] URLForResource:@"Sockct_UDX_SubApp" withExtension:@"app"];
                [[NSWorkspace sharedWorkspace] openURL:subAppURL configuration:[NSWorkspaceOpenConfiguration configuration] completionHandler:nil];
            }
            - (void)subThreadWorker { NSLog(@"---subThreadWorker");
                
                ssize_t numRed = 0;
                static const int buffer_size = 100;
                char buffer[buffer_size];
                
                while (self.sfd != -1) { printf("服务器等待客户端%i连接...\n", self.sfd);
                    //接受新链接, 并得到新的id
                    self.sfd = accept(self.sfd, NULL, NULL);
                    printf("收到客户端连接。 sfd:%i\n", self.sfd);
                    
                    if (self.sfd == -1) { perror("这是一个无效的连接!");
                        break;
                    }
                    
                    while ((numRed = read(self.sfd, buffer, buffer_size)) > 0) { printf("服务器收到客户端发的数据: %s\n", buffer);
                    }
                    if (numRed == -1) { perror("numRed == -1!");
                        break;
                    }
                    if (close(self.sfd) == -1) { perror("close faild!");
                        break;
                    }
                    printf("for over!\n");
                }
                
                printf("sub thread over!\n");
                
            //    exit(0);
            }
            - (IBAction)sendMsgToClient:(id)sender { const  char *backBuffer = [self.textLabel.stringValue UTF8String];
                ssize_t sendLen =  write(self.sfd, backBuffer, strlen(backBuffer)+1);
                if (sendLen < 0) { printf("error:%i\n", errno);
                    perror("服务器发送给客户端失败!reason:");
                } else { printf("服务器发送给客户端成功!len:%zi\n", sendLen);
                }
            }
            @end
            

            2. 客户端主要逻辑

            • 主要创建了socket一个 AF_UNIX和SOCK_STREAM 组合的socket
            • connect(...) 使用带服务器创建的sock 路径/tmp/jimbo_udx_server.sock 的结构体,和 socket 对象进行连接。 这样双方通信就建立了
            • read(...)在子线程 阻塞式 等待服务器的消息.
            • write(..) UI 按钮点击后,往服务器发消息

              主要代码: ViewController.mm 文件代码

              //
              //  ViewController.m
              //  Sockct_UDX_SubApp
              //
              //  Created by jimbo on 2024/1/5.
              //
              #import "ViewController.h"
              #include const char * s_sock_path = "/tmp/jimbo_udx_server.sock";
              @interface ViewController ()
              @property (weak) IBOutlet NSTextField *textLabel;
              @property (nonatomic, assign) int sfd;
              @end
              @implementation ViewController
              - (void)viewDidLoad { [super viewDidLoad];
                  int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
                  self.sfd = sfd;
                  
                  if (sfd == -1) { perror("socket create failed!");
                      return;
                  }
                  
                  /*
                  // client 也可以同事绑定一个路径。自己又当客户端又当服务器
                  
                  const char * s_sock_path_client = "/tmp/jimbo_udx_client.sock";
                   
                  // 删除所有与路径名一致的既有文件,这样才能将 socket 绑定到这个路径名上
                  if (remove(s_sock_path_client) == -1 && errno != ENOENT){
                      perror("remove jimbo_udx_client.sock failed");
                      return;
                  }
                  
                  struct sockaddr_un addr_client  = {0};
                  addr_client.sun_family = AF_UNIX;
                  strcpy(addr_client.sun_path, s_sock_path_client);
                  
                  //bind 的时候会在文件路径创建响应的s_sock_path文件. (UI app 需要关闭沙盒才能有权限访问对应的路径)
                  //当使用ls –l列出时,UNIX domain socket 在第一列将会显示类型 s
                  //扩展一下,这个位置还可以有其他几种选项:p、d、l、s、c、b和-:
                  //其中p表示命名管道文件,d表示目录文件,l表示符号连接文件,-表示普通文件,s表示socket文件,c表示字符设备文件,b表示块设备文件。
                  int ret = bind(sfd, (struct sockaddr *)&addr_client, sizeof(struct sockaddr_un));
                  if (ret == -1) {
                      perror("bind  addr_client faild!");
                      return;
                  }
                  */
                  struct sockaddr_un addr = {0};
                  addr.sun_family = AF_UNIX;
                  strcpy(addr.sun_path, s_sock_path);
                  
                  NSLog(@"准备connect");
                  int ret = connect(self.sfd, (const struct sockaddr *)&addr, sizeof(addr));
                  if (ret == -1) { perror("connect faild!");
                      return;
                  }
                  NSLog(@"connect成功");
                  
                  
                  [NSThread detachNewThreadWithBlock:^{ //单独线程监听服务器发回来的消息
                      static const int buffer_size = 100;
                      char buffer[buffer_size];
                      while (self.sfd != -1) { printf("等待服务的回调...\n");
                          ssize_t len = read(self.sfd, buffer, buffer_size);
                          printf("收到的服务器回馈长度:%zi\n", len);
                          if (len <= 0) { printf("read error:%i\n", errno);
                              perror("read failed");
              //                assert(false); //需要判断是否服务器已经断开了的情况。
                              exit(0);
                          }else { printf("服务器返回的数据:%s\n", buffer);
                          }
                      }
                      printf("等待服务器回调线程结束!\n");
                  }];
                  
              }
              - (IBAction)sendMegToServer:(id)sender { //发送消息
                  const char *buf = [self.textLabel.stringValue UTF8String];
                  size_t numWrite = strlen(buf) + 1;
                  
                  ssize_t writeSize = write(self.sfd, buf, numWrite);
                  printf("numWrite: %zu writeSize:%zi\n", numWrite, writeSize);
                  if (writeSize == -1) { perror("write failed!");
                      return;
                  }
              }
              - (void)dealloc { if (self.sfd > 0) { NSLog(@"关闭 sfd");
                      close(self.sfd);
                      self.sfd = -1;
                  }
              }
              @end