Linux socket编程(11):Unix套接字编程及通信例子

Unix套接字是一种用于在同一台计算机上的进程间通信的一种机制。它是Linux和其他类Unix系统中的一项特性,通过在文件系统中创建特殊的套接字文件,进程可以通过这些套接字文件进行通信。

1 Unix和TCP套接字对比

Unix套接字适用于在同一计算机上运行的进程之间的通信,而TCP/IP套接字则设计用于在不同计算机上运行的程序之间通过网络进行通信。

比较因素 Unix套接字 TCP/IP套接字
全名 也被称为进程间通信(IPC)套接字 传输控制协议/互联网协议(TCP/IP)套接字
功能 用于同一台计算机上运行的进程之间通信 用于在不同计算机上运行的程序之间通信
要求 进程之间通信无需主机名和端口号 使用TCP/IP套接字进行程序通信需要主机名和端口号
速度 由于进程在同一系统上运行,避免了一些检查和操作,因此通信速度较快 相对于Unix套接字,在网络上进行通信时速度较慢

2 Unix套接字初始化流程

在Linux中进行Unix套接字编程通常涉及一系列系统调用和相关函数。下面是一个简要的介绍,以及在Unix环境中如何使用相关函数的一些例子:

1、创建套接字

int socket(int domain, int type, int protocol);
  • 在Unix域套接字编程中,domain参数通常设置为AF_UNIX。例:
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
  • type参数表示套接字的类型,可以选择SOCK_STREAMSOCK_DGRAM,取决于通信需求。

2、绑定套接字

使用bind()系统调用将套接字绑定到一个特定的文件路径。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 在Unix域套接字编程中,addr是一个指向struct sockaddr_un类型的指针,用于指定套接字的路径。例:
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/my_socket");

bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

3、监听连接

使用listen()指定套接字处于被动监听状态。

int listen(int sockfd, int backlog);

backlog参数指定等待连接的队列长度。例:

listen(sockfd, 5);

4、接受连接

使用accept()系统调用接受来自客户端的连接请求。

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

在Unix域套接字编程中,addr是一个指向struct sockaddr_un类型的指针,用于存储客户端的地址信息。

5、发送和接收数据

使用send()recv()系统调用在套接字之间传输数据。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

6、关闭套接字

使用close()系统调用关闭套接字。

int close(int sockfd);

3 例:服务端/客户端通信实现

现在通过一个例子来说明Unix套接字的使用。

功能:客户端需要一边接收用户的消息并转发给服务端,一边接收来自服务端的消息;服务端则在建立成功之后,对来自客户端的消息进行一个回显。

3.1 服务端

Unix的程序和TCP基本上一样,只是这里的地址需要指定一个本地的文件名,Unix通过这个文件来进行进程通信。

1、初始化套接字并接受连接

#define SOCKET_PATH "/tmp/unix_socket_example"
// 创建套接字
server_sock = socket(AF_UNIX, SOCK_STREAM, 0);
// 设置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SOCKET_PATH);
// 绑定套接字到地址
bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 监听连接
listen(server_sock, 5);
// 接受连接
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);

这里我们将套接字的地址设置为/tmp/unix_socket_example

2、接收消息并回显

char buffer[1024];
while (1)
{
    ssize_t received_bytes = recv(client_sock, buffer, sizeof(buffer), 0);
    if (received_bytes <= 0)
    {
        perror("Error receiving message");
        break;
    }

    printf("Received from client: %.*s", (int)received_bytes, buffer);

    if (send(client_sock, buffer, received_bytes, 0) == -1)
    {
        perror("Error sending message back to client");
        break;
    }
}

3.2 客户端

1、初始化套接字并连接到服务端

#define SOCKET_PATH "/tmp/unix_socket_example"
int client_sock;
struct sockaddr_un server_addr;
// 创建套接字
client_sock = socket(AF_UNIX, SOCK_STREAM, 0);
// 设置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SOCKET_PATH);
// 连接到服务器
connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));

2、接收用户输入并发送给服务端

char buffer[1024];
while (1)
{
    printf("Enter a message: ");
    fgets(buffer, sizeof(buffer), stdin);
    send(client_sock, buffer, strlen(buffer), 0);
    ssize_t received_bytes = recv(client_sock, buffer, sizeof(buffer), 0);
    printf("Server's response: %.*s", (int)received_bytes, buffer);
}

代码比较简单,这里就不使用selectpoll等多路I/O复用方式监听用户输入和服务端的消息了,大家可以参考我之前的文章实现。

3.3 实验结果

如下图所示,服务端收到客户端的消息后再回显给客户端:

在这里插入图片描述

前面我们的地址设置为/tmp/unix_socket_example,也就是说在tmp目录下会创建一个unix_socket_example用于进程通信:
在这里插入图片描述

  • 开头的s表示套接字文件

现在重启一下服务端,提示我们地址已经被使用:

在这里插入图片描述

我们知道在TCP通信中也有这个问题,我们可以设置地址复用:

int reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

但在Unix编程中,它是使用文件进行进程通信的,所以我们可以在服务端创建之前删除我们这个套接字文件:

在这里插入图片描述

再来运行一下服务端,发现可以创建了:

在这里插入图片描述

3.4 完整代码

1、服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/unix_socket_example"

int main() {
    int server_sock, client_sock;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
	
	unlink(SOCKET_PATH);
    // 创建套接字
    server_sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_sock == -1) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SOCKET_PATH);

    // 绑定套接字到地址
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Error binding socket");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_sock, 5) == -1) {
        perror("Error listening for connections");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening for connections...\n");

    // 接受连接
    client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
    if (client_sock == -1) {
        perror("Error accepting connection");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    printf("Connection established with a client.\n");

    // 接收和回显消息
    char buffer[1024];
    while (1) {
        // 接收来自客户端的消息
        ssize_t received_bytes = recv(client_sock, buffer, sizeof(buffer), 0);
        if (received_bytes <= 0) {
            perror("Error receiving message");
            break;
        }

        // 打印接收到的消息
        printf("Received from client: %.*s", (int)received_bytes, buffer);

        // 回显消息给客户端
        if (send(client_sock, buffer, received_bytes, 0) == -1) {
            perror("Error sending message back to client");
            break;
        }
    }

    // 关闭套接字
    close(client_sock);
    close(server_sock);
    unlink(SOCKET_PATH);

    return 0;
}

2、客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/unix_socket_example"

int main() {
    int client_sock;
    struct sockaddr_un server_addr;

    // 创建套接字
    client_sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_sock == -1) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SOCKET_PATH);

    // 连接到服务器
    if (connect(client_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Error connecting to server");
        close(client_sock);
        exit(EXIT_FAILURE);
    }

    printf("Connected to the server.\n");

    // 与用户交互,发送消息给服务器并接收回显消息
    char buffer[1024];
    while (1) {
        printf("Enter a message: ");
        fgets(buffer, sizeof(buffer), stdin);

        // 发送消息给服务器
        if (send(client_sock, buffer, strlen(buffer), 0) == -1) {
            perror("Error sending message to server");
            break;
        }

        // 接收来自服务器的回显消息
        ssize_t received_bytes = recv(client_sock, buffer, sizeof(buffer), 0);
        if (received_bytes <= 0) {
            perror("Error receiving message from server");
            break;
        }

        // 打印回显消息
        printf("Server's response: %.*s", (int)received_bytes, buffer);
    }

    // 关闭套接字
    close(client_sock);

    return 0;
}

4 TCP与Unix中connect的区别

在TCP中,如果服务器端的监听队列已满,connect()调用会阻塞,直到有连接插入为止,或者达到连接队列的最大长度。

而在Unix域套接字编程中,当客户端尝试使用connect()系统调用连接到服务器端的Unix域流式套接字时,如果服务器端的监听队列已满,connect()会立即返回ECONNREFUSED错误。这是因为Unix域套接字使用文件系统路径作为套接字地址,而服务器端在处理连接请求时,其实是在文件系统中创建一个套接字文件。如果监听队列已满,新的连接请求无法被立即处理,因此客户端会收到ECONNREFUSED错误。

以下是一些可能引起connect()返回ECONNREFUSED的情况:

  1. 服务器忙碌: 如果服务器端处理连接请求的速度不够快,导致监听队列已满,客户端可能会收到ECONNREFUSED
  2. 并发连接数过多: 如果系统中同时存在大量客户端尝试连接到服务器端,超过了服务器处理的能力,也可能导致监听队列满,从而引发ECONNREFUSED
  3. 套接字文件权限问题: 如果套接字文件所在的目录没有足够的权限,可能会导致服务器无法创建新的连接文件,从而引发ECONNREFUSED

猜你喜欢

转载自blog.csdn.net/tilblackout/article/details/134611280