Unix套接字(UDS,Unix Domain Socket)

【知识简介】

在​​Linux​​系统中,有很多进程间通信方式,套接字(Socket)就是其中的一种。但传统的套接字的用法都是基于TCP/IP协议栈的,需要指定IP地址。如果不同主机上的两个进程进行通信,当然这样做没什么问题。但是,如果只需要在一台机器上的两个不同进程间通信,还要用到IP地址就有点大材小用了。

其实很多人并不一定知道,对于套接字来说,还存在一种叫做Unix域套接字的类别,专门用来解决这个问题。其API的调用方法基本上和普通TCP/IP的套接字一样,只有些许差别。

Unix域套接字用于在同一台计算机上的进程间通信,虽然因特网域套接字可用于同一目的,但是Unix域套接字的效率更高。Unix域套接字并不进行协议处理,不需要添加或删除网络报头,无需计算校验和,不需要产生顺序号,无需发送确认报文。Unix域套接字提供字节流和数据报两种数据类型,Unix域数据报服务是可靠的,不会丢失消息也不会传递出错。简单来说,Unix域套接字是套接字和管道之间的混合物。

【流程对比】

 

上边的图代表面向流的套接字,对于TCP/IP套接字来说,代表TCP协议;下边的图代表面向数据包的套接字,对于TCP/IP套接字来说,代表UDP协议。接下来,我们将特别针对普通TCP/IP套接字和Unix域套接字的不同,对这些API做一些解释。

【使用方法】

1)socket()

最开始肯定还是要创建一个套接字:

1. int socket (int domain, int type, int protocol);

 

API定义是一样的,不过这里的第一个参数,也就是域一定要设置成AF_UNIX或AF_LOCAL,而不是普通TCP/IP套接字的AF_INET。第二个参数表示套接字的类型,分为流套接字(SOCK_STREAM)和数据包套接字(SOCK_DGRAM)。不同于普通的AF_INET的Socket,由于都是在本机通过内核通信,所以SOCK_STREAM和SOCK_DGRAM都是可靠的,不会丢包也不会出现发送包的次序和接收包的次序不一致的问题。它们的区别仅仅是,SOCK_STREAM无论发送多大的数据都不会被截断,而对于SOCK_DGRAM来说,如果发送的数据超过了一个报文的最大长度,则数据会被截断。而最后一个参数,表示协议,对于Unix域套接字来说,其一定是被设置成0。因此,一般通过下面的方式创建一个Unix域套接字:

1. int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  // 流式Unix域套接字  
2. int sockfd = socket(AF_UNIX,SOCK_DGRAM, 0);    // 数据包式套接字

2)bind()

对于流式套接字的服务器端来说,在用socket()函数获得了新创建套接字的文件描述符之后,还要将其绑定到一个地址上去:

1. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

 

在Unix域套接字中,套接字的地址是以sockaddr_un结构体来表示的,其结构如下:

struct sockaddr_un {
     sa_family_t  sun_family;       /* AF_UNIX 或 AF_LOCAL */
     char         sun_path[108];    /* pathname */
}

结构体中的第一个字段必须要设置成“AF_UNIX”。而第二个字段,表示的是一个路径名。因此,要将一个Unix域套接字绑定到一个本地地址上,需要创建并初始化一个sockaddr_un结构体,并将指向这个结构体的指针作为addr参数(需要类型转换)传入bind()函数,并将addrlen参数设置成这个结构体的实际大小。

这里还要特别提一下这个路径名,其实还要分为两种,一种是普通路径名,另一种是抽象路径名。

首先来说说普通路径名,这个很好理解,就是一个基本的​​linux​​文件路径,其必须要以NULL('\0')结尾。在绑定一个Unix域套接字时,会在文件系统中的相应位置上创建一个文件,且这个文件的类型被标记为“Socket”,因此这个文件无法用open()函数打开。当不再需要这个Unix域套接字时,可以使用remove()函数或者unlink()函数将这个对应的文件删除。如果在文件系统中,已经有了一个文件和指定的路径名相同,则绑定会失败(返回错误EADDRINUSE)。所以,一个套接字只能绑定到一个路径上,同样的,一个路径也只能被一个套接字绑定。

接下来看看什么叫抽象路径名,这其实是Linux特有的一个特性,它允许将一个Unix域套接字绑定到一个名字上,且不会在文件系统中创建这个名字的文件。如果要创建一个抽象名字空间的绑定,必须要将sun_path字段的第一个字节设置成NULL('\0'),而且和普通的文件系统名字空间不同的是,系统会用sun_path除第一个字节之后余下的所有字节当做抽象名字。也就是说在解析抽象路径名时需要用到sun_path字段当中所有的字节,而不是像解析普通路径名一样,解析到第一个NULL就可以停止了。因为不会再在文件系统中创建文件了,所以对于抽象路径名来说,就不需要担心与文件系统中已存在的文件产生名字冲突的问题了,也不需要在使用完套接字之后删除附带产生的这个文件了,当套接字被关闭之后会自动删除这个抽象名。

最后再提一下权限的问题,因为要在文件系统中创建相应的文件,对于普通路径名来说,掉用bind()函数的进程必须要有路径名中目录部分的可写和可访问权限。还有,在默认情况下,在调用bind()函数时,会给所有者、组和其他用户赋予所有的权限(即777),如果想改变这个行为,可以在bind()之后再修改创建的文件的权限和属性。

3)listen()

对于流式套接字的服务器端来说,listen()函数在TCP/IP套接字和Unix域套接字中调用方式是一样的,没有区别:

int listen(int sockfd, int backlog);

4)accept()

对于流式套接字的服务器端来说,在调用bind()绑定完本地路径之后,还需要接收客户端的请求,这是通过调用accept()函数来实现的:

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

 

与普通的TCP/IP套接字不同,Unix域套接字不存在客户端地址的问题(都在一台机器上),因此这里的addr和addrlen参数都要设置成NULL。在这里,不同进程像这个服务器端进程发送的流数据是在内核里面区分的,并绑定到了accept()创建的套接字中了。而数据包套接字就没有这种对应关系,所以还是要在代码中区分出来,后面会介绍。

5)connect()

对于流式套接字的客户端来说,在用socket()函数获得了新创建套接字的文件描述符之后,就可以调用connect()函数连接服务器端了:

int connect(int sockfd, struct sockaddr *addr,int addrlen);

 

这个函数在TCP/IP套接字中和在Unix域套接字中调用方式基本相同,只不过和bind()函数一样,地址addr必须是以sockaddr_un结构体来表示。

6)read()和write()

对于流式套接字的服务器端来说,read()和write函数在TCP/IP套接字和Unix域套接字中调用方式是一样的,没有区别:

1. ssize_t read(int sockfd, void *buf, size_t length);   
2. ssize_t write(int sockfd, const void *buf, size_t length);

 

7)recvfrom()和sendto()

对于数据包事套接字来说,在服务器端recvfrom()用来接收客户端发送的请求,而在客户端这个函数用来接收服务器端发送过来的响应:

int recvfrom(int sockfd, void *buf, int length, unsigned int flags, struct sockaddr *addr, int *addrlen);

 

同时,在客户端sendto()用来向服务器端发送请求数据,而服务器端用这个函数来向客户端发送响应数据:

int sendto (int sockfd, const void *buf, int length, unsigned int flags, const struct sockaddr *addr, int addrlen);

前面也提到了,对于数据包套接字来说,服务器端在发送响应数据时是需要知道客户端到底是哪个的,从而后面可以将相应的响应数据发送给正确的客户端。而客户端也需要知道到底是向哪个服务器端发送数据,或者说接收到的响应数据到底来自哪个服务器端(当然,如果只保证和一个服务器端通信就没有这个问题)。

但是,按照普通的包套接字创建和连接的流程,只是在服务器端掉用bind()函数绑定了一个地址,而客户端并没有地址。这在流式套接字中没有问题,内核已经在服务器端调用accept()函数接收一个客户端连接时创建了一个新的套接字,从而将一一对应关系绑定到了这个新的套接字上了。所以,对于包套接字来说,在客户端还需要再调用bind()函数绑定一次,人为的创建一个客户端地址,且这个客户端路径名地址显然不能和服务器端的路径名相同。

剩下的就都和普通的TCP/IP套接字相同了,只不过地址addr必须是以sockaddr_un结构体来表示罢了。

【需要的头文件】

#include <sys/types.h>
#include <sys/socket.h>

【参考文献】

Unix域套接字(Unix Domain Socket)介绍【转】_8465449的技术博客_51CTO博客Unix域套接字(Unix Domain Socket)介绍【转】,文为博主原创文章,未经博主允许不得转载。版权声明:本文为博主原创文章,未经博主允许不得转载。在Linux系统中,有很多进程间通信方式,套接字(Socket)就https://blog.51cto.com/u_8475449/5954776

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Go语言实现unix domain socket server,使用datagram协议示例代码可以参考如下:package mainimport ( "fmt" "net" "os" )func main() { service := "./socket" udpsrv, err := net.ResolveUnixAddr("unixgram", service) checkError(err) conn, err := net.ListenUnixgram("unixgram", udpsrv) checkError(err) defer conn.Close() for { handleClient(conn) } }func handleClient(conn *net.UnixConn) { var buf [512]byte n, addr, err := conn.ReadFromUnix(buf[0:]) if err != nil { return } fmt.Println("Received ", string(buf[0:n]), " from ", addr.String()) _, err2 := conn.WriteToUnix([]byte("Hello, Client!"), addr) checkError(err2) }func checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } } ### 回答2: 下面是一个使用Go语言实现的Unix Domain Socket服务器的示例代码,使用了Datagram协议: ```go package main import ( "fmt" "net" "os" ) func main() { sockFile := "/tmp/uds.sock" // 删除旧的Unix Domain Socket文件 os.Remove(sockFile) // 创建监听Unix Domain Socket listener, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: sockFile, Net: "unixgram"}) if err != nil { fmt.Println("监听Unix Domain Socket时发生错误:", err) return } defer listener.Close() fmt.Println("等待客户端连接...") // 接收消息 buf := make([]byte, 1024) for { n, clientAddr, err := listener.ReadFromUnix(buf) if err != nil { fmt.Println("从Unix Domain Socket读取数据时发生错误:", err) return } fmt.Printf("收到来自客户端 %s 的消息: %s\n", clientAddr.String(), string(buf[:n])) // 响应客户端消息 response := []byte("收到消息") _, err = listener.WriteToUnix(response, clientAddr) if err != nil { fmt.Println("向Unix Domain Socket写入数据时发生错误:", err) return } } } ``` 该示例程序创建了一个Unix Domain Socket服务器,并使用Datagram协议监听客户端的消息。服务器首先删除旧的Unix Domain Socket文件,然后创建新的监听Unix Domain Socket,并等待客户端连接。 当服务器接收到客户端的消息时,它会打印出客户端地址和收到的消息内容,并向客户端发送一个确认消息。 注意,Unix Domain Socket只能用于本地通信,不支持网络通信。所以该示例代码在Unix地址中使用了“unixgram”和“unixgram”参数。 ### 回答3: 下面是一个使用Go语言实现unix domain socket server,并使用datagram协议的示例代码: ```go package main import ( "log" "net" "os" "os/signal" "syscall" ) func main() { // 创建一个unix domain socket,并监听在指定的路径上 socketPath := "/tmp/my_unix_socket" l, err := net.ListenUnixgram("unixgram", &net.UnixAddr{socketPath, "unixgram"}) if err != nil { log.Fatal("监听Unix套接字失败:", err) } defer os.Remove(socketPath) defer l.Close() // 创建信号处理程序,以便在收到SIGINT或SIGTERM时优雅地关闭服务器 sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigCh log.Println("接收到终止信号,正在关闭服务器..") l.Close() }() log.Println("Unix套接字服务器启动,等待接收数据..") buffer := make([]byte, 1024) for { n, addr, err := l.ReadFromUnix(buffer) if err != nil { log.Printf("从Unix套接字接收数据失败:%v\n", err) continue } log.Printf("接收到来自 %s 的数据:%s\n", addr.String(), string(buffer[:n])) // 在此处添加你的业务逻辑处理代码 // 假设响应数据是"Hello Client" response := []byte("Hello Client") _, err = l.WriteToUnix(response, addr) if err != nil { log.Printf("向Unix套接字发送数据失败:%v\n", err) continue } } } ``` 在上面的示例代码中,我们使用了net包中的`ListenUnixgram`函数来创建一个Unix域数据报套接字,指定了监听的路径`/tmp/my_unix_socket`。然后,我们使用`ReadFromUnix`函数来接收来自客户端的数据,并输出接收到的数据到日志中。 你可以在需要的地方添加你的业务逻辑处理代码,然后使用`WriteToUnix`函数将响应数据发送回客户端。 最后,我们使用信号处理程序(Signal Handler)来监听SIGINT和SIGTERM信号,以便在接收到这些信号时优雅地关闭服务器并释放占用的资源。 请注意,你需要根据你的实际需求,修改代码中的路径和处理逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值