macOS跨进程通信: Unix Domain Socket 创建实例
一: 简介
一般我们常用的有
Tcp Socket 能够跨电脑进行通信,即使是在同一个电脑下的多进程间通信,也会通过网卡进行数据传输,如果本地网卡的环回网络被禁用, 则会导致通信失败。Unix Domain Socket ,使用的是Liunx 系统中万物皆文件的概念,和有名管道的操作差不多,都是在文本创建一个特有的文件,用来在两个进程间通信,两个进程分别写入和读取文件流中的数据,达到传输的目的。 和Tcp Socket 不一样的是不用借助网卡通信,限制比较小,传输的效率高。
这里主要针对
在终端使用
可以看到红圈中我们demo创建的
这里还有其他类型的文件。其中
二:主要函数
1. int socket (int domain, int type, int protocol) 创建socket 对象
domain 选择AF_UNIX , 代表 unix domain sockettype . 选择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)
阻塞式等待客户端接入,客户端接入后返回。
传入
后面两个参数代表接口客户端的地址及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 <sys/socket.h> 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连接... ", self.sfd); //接受新链接, 并得到新的id self.sfd = accept(self.sfd, NULL, NULL); printf("收到客户端连接。 sfd:%i ", self.sfd); if (self.sfd == -1) { perror("这是一个无效的连接!"); break; } while ((numRed = read(self.sfd, buffer, buffer_size)) > 0) { printf("服务器收到客户端发的数据: %s ", buffer); } if (numRed == -1) { perror("numRed == -1!"); break; } if (close(self.sfd) == -1) { perror("close faild!"); break; } printf("for over! "); } printf("sub thread over! "); // 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 ", errno); perror("服务器发送给客户端失败!reason:"); } else { printf("服务器发送给客户端成功!len:%zi ", sendLen); } } @end
2. 客户端主要逻辑
- 主要创建了
socket 一个AF_UNIX 和SOCK_STREAM 组合的socket connect(...) 使用带服务器创建的sock 路径/tmp/jimbo_udx_server.sock 的结构体,和 socket 对象进行连接。 这样双方通信就建立了read(...) 在子线程 阻塞式 等待服务器的消息.write(..) UI 按钮点击后,往服务器发消息
主要代码:
// // ViewController.m // Sockct_UDX_SubApp // // Created by jimbo on 2024/1/5. // #import "ViewController.h" #include <sys/socket.h> 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("等待服务的回调... "); ssize_t len = read(self.sfd, buffer, buffer_size); printf("收到的服务器回馈长度:%zi ", len); if (len <= 0) { printf("read error:%i ", errno); perror("read failed"); // assert(false); //需要判断是否服务器已经断开了的情况。 exit(0); }else { printf("服务器返回的数据:%s ", buffer); } } printf("等待服务器回调线程结束! "); }]; } - (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 ", numWrite, writeSize); if (writeSize == -1) { perror("write failed!"); return; } } - (void)dealloc { if (self.sfd > 0) { NSLog(@"关闭 sfd"); close(self.sfd); self.sfd = -1; } } @end