SOCK_SEQPACKET Unix套接字上的空数据包

时间:2018-07-22 18:08:26

标签: c posix unix-socket

我正在Unix套接字上使用SOCK_SEQPACKET类型。

我用来阅读的代码是经典的

ssize_t recv_size = recv(sd, buffer, sizeof(buffer), 0);
if (recv_size < 0) {
    handle_error("recv", errno);
} else if (recv_size > 0) {
    handle_packet(buffer, recv_size);
} else {
    // recv_size == 0 => peer closed socket.
    handle_end_of_stream();
}

虽然这很好用,但我注意到它无法区分套接字关闭和大小为0的消息。换句话说,如果在另一端,我发出如下调用序列:

send(sd, "hello", strlen("hello"), 0);
send(sd, "", 0, 0);
send(sd, "world", strlen("world"), 0);

…阅读器将只收到"hello"并通过套接字关闭对第二条消息作出反应,而完全丢失了"world"消息。

我想知道这两种情况之间是否存在某种歧义。

2 个答案:

答案 0 :(得分:1)

如果您在两端进行某种“确认”功能怎么办。例如,代替以下代码:handle_end_of_stream:

Item

<如果连接仍在,您会收到“ UNIQUE_RESPONSE” ,如果不是,则可以确定另一端已关闭。只需在“确认”功能中过滤掉某种“ UNIQUE_MESSAGE”和“ UNIQUE_RESPONSE”即可。

答案 1 :(得分:1)

正如我在评论中提到的那样,零长度的seqpackets(以及零长度的数据报)的行为可能很奇怪,通常会误认为是断开连接。因此,我绝对建议不要出于任何目的使用零长度seqpackets或数据报。

为了说明主要问题并探索细节,我创建了两个测试程序。首先是 receive.c ,它在Unix域seqpacket套接字上侦听,接受一个连接,并描述接收的内容:

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <signal.h>
#include <string.h>
#include <poll.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    done = 1;
}

static int install_done(int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

static inline unsigned int digit(const int c)
{
    switch (c) {
    case '0': return 0;
    case '1': return 1;
    case '2': return 2;
    case '3': return 3;
    case '4': return 4;
    case '5': return 5;
    case '6': return 6;
    case '7': return 7;
    case '8': return 8;
    case '9': return 9;
    case 'A': case 'a': return 10;
    case 'B': case 'b': return 11;
    case 'C': case 'c': return 12;
    case 'D': case 'd': return 13;
    case 'E': case 'e': return 14;
    case 'F': case 'f': return 15;
    default:  return 16;
    }
}

static inline unsigned int octbyte(const char *src)
{
    if (src) {
        const unsigned int  o0 = digit(src[0]);
        if (o0 < 4) {
            const unsigned int  o1 = digit(src[1]);
            if (o1 < 8) {
                const unsigned int  o2 = digit(src[2]);
                if (o2 < 8)
                    return o0*64 + o1*8 + o2;
            }
        }
    }
    return 256;
}

static inline unsigned int hexbyte(const char *src)
{
    if (src) {
        const unsigned int  hi = digit(src[0]);
        if (hi < 16) {
            const unsigned int  lo = digit(src[1]);
            if (lo < 16)
                return 16*hi + lo;
        }
    }
    return 256;
}

size_t set_unix_path(const char *src, struct sockaddr_un *addr)
{
    char         *dst = addr->sun_path;
    char *const   end = addr->sun_path + sizeof (addr->sun_path) - 1;
    unsigned int  byte;

    if (!src || !addr)
        return 0;

    memset(addr, 0, sizeof *addr);
    addr->sun_family = AF_UNIX;
    while (*src && dst < end)
        if (*src == '\\')
            switch (*(++src)) {
            case '0':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else {
                    *(dst++) = '\0';
                    src++;
                }
                break;
            case '1':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case '2':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case '3':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case 'x':
                byte = hexbyte(src + 1);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case 'a':  *(dst++) = '\a'; src++; break;
            case 'b':  *(dst++) = '\b'; src++; break;
            case 't':  *(dst++) = '\t'; src++; break;
            case 'n':  *(dst++) = '\n'; src++; break;
            case 'v':  *(dst++) = '\v'; src++; break;
            case 'f':  *(dst++) = '\f'; src++; break;
            case 'r':  *(dst++) = '\r'; src++; break;
            case '\\': *(dst++) = '\\'; src++; break;
            default:   *(dst++) = '\\';
            }
        else
            *(dst++) = *(src++);

    *(dst++) = '\0';

    return (size_t)(dst - (char *)addr);
}


int main(int argc, char *argv[])
{
    struct sockaddr_un  addr, conn;
    socklen_t           addrlen, connlen;
    int                 socketfd, connfd;

    if (argc != 2 || !argv[1][0] || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s SOCKET_PATH\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    socketfd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    if (socketfd == -1) {
        fprintf(stderr, "Cannot create an Unix domain seqpacket socket: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    addrlen = set_unix_path(argv[1], &addr);
    if (bind(socketfd, (const struct sockaddr *)&addr, addrlen) == -1) {
        fprintf(stderr, "Cannot bind to %s: %s.\n", argv[1], strerror(errno));
        close(socketfd);
        return EXIT_FAILURE;
    }

    if (listen(socketfd, 1) == -1) {
        fprintf(stderr, "Cannot listen for incoming connections: %s.\n", strerror(errno));
        close(socketfd);
        return EXIT_FAILURE;
    }

    memset(&conn, 0, sizeof conn);
    connlen = sizeof conn;
    connfd = accept(socketfd, (struct sockaddr *)&conn, &connlen);
    if (connfd == -1) {
        close(socketfd);
        fprintf(stderr, "Canceled.\n");
        return EXIT_SUCCESS;
    }

    if (connlen > 0)
        fprintf(stderr, "Connected, peer address size is %d.\n", (int)connlen);
    else
        fprintf(stderr, "Connected; no peer address.\n");

    while (!done) {
        char     buffer[65536];
        ssize_t  n;
        int      r;

        n = recv(connfd, buffer, sizeof buffer, 0);
        if (n > 0)
            fprintf(stderr, "Received %zd bytes.\n", n);
        else
        if (n == 0) {
            struct pollfd  fds[1];

            fds[0].fd = connfd;
            fds[0].events = 0;
            fds[0].revents = 0;
            r = poll(fds, 1, 0);
            if (r > 0 && (fds[0].revents & POLLHUP)) {
                fprintf(stderr, "Disconnected (revents = %d).\n", fds[0].revents);
                break;
            } else
            if (r > 0)
                fprintf(stderr, "recv() == 0, poll() == %d, revents == %d\n", r, fds[0].revents); 
            else
            if (r == 0)
                fprintf(stderr, "Received a zero-byte seqpacket.\n");
            else
                fprintf(stderr, "recv() == 0, poll() == %d, revents == %d\n", r, fds[0].revents);
        }
    }

    close(connfd);
    close(socketfd);
    return EXIT_SUCCESS;
}

您可以使用例如gcc -Wall -O2 receive.c -o receive。要运行,请给它提供Unix域名地址以进行监听。在Linux中,您可以通过在地址前面加上\0来使用抽象名称空间。例如,通过运行./receive '\0example'。否则,套接字地址将在文件系统中可见,并且您需要在使用相同的套接字地址再次运行rm之前,将其删除(就好像它是一个文件,使用./receive)。

我们还需要一个实用程序来发送seqpackets。以下 send.c 非常相似(重复使用了许多相同的代码)。您指定要连接的Unix域地址,以及一个或多个seqpacket长度。您还可以指定以毫秒为单位的延迟(只需在-前面添加;即,负整数是以毫秒为单位的延迟):

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <signal.h>
#include <string.h>
#include <poll.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    done = 1;
}

static int install_done(int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

static inline unsigned int digit(const int c)
{
    switch (c) {
    case '0': return 0;
    case '1': return 1;
    case '2': return 2;
    case '3': return 3;
    case '4': return 4;
    case '5': return 5;
    case '6': return 6;
    case '7': return 7;
    case '8': return 8;
    case '9': return 9;
    case 'A': case 'a': return 10;
    case 'B': case 'b': return 11;
    case 'C': case 'c': return 12;
    case 'D': case 'd': return 13;
    case 'E': case 'e': return 14;
    case 'F': case 'f': return 15;
    default:  return 16;
    }
}

static inline unsigned int octbyte(const char *src)
{
    if (src) {
        const unsigned int  o0 = digit(src[0]);
        if (o0 < 4) {
            const unsigned int  o1 = digit(src[1]);
            if (o1 < 8) {
                const unsigned int  o2 = digit(src[2]);
                if (o2 < 8)
                    return o0*64 + o1*8 + o2;
            }
        }
    }
    return 256;
}

static inline unsigned int hexbyte(const char *src)
{
    if (src) {
        const unsigned int  hi = digit(src[0]);
        if (hi < 16) {
            const unsigned int  lo = digit(src[1]);
            if (lo < 16)
                return 16*hi + lo;
        }
    }
    return 256;
}

size_t set_unix_path(const char *src, struct sockaddr_un *addr)
{
    char         *dst = addr->sun_path;
    char *const   end = addr->sun_path + sizeof (addr->sun_path) - 1;
    unsigned int  byte;

    if (!src || !addr)
        return 0;

    memset(addr, 0, sizeof *addr);
    addr->sun_family = AF_UNIX;
    while (*src && dst < end)
        if (*src == '\\')
            switch (*(++src)) {
            case '0':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else {
                    *(dst++) = '\0';
                    src++;
                }
                break;
            case '1':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case '2':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case '3':
                byte = octbyte(src);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case 'x':
                byte = hexbyte(src + 1);
                if (byte < 256) {
                    *(dst++) = byte;
                    src += 3;
                } else
                    *(dst++) = '\\';
                break;
            case 'a':  *(dst++) = '\a'; src++; break;
            case 'b':  *(dst++) = '\b'; src++; break;
            case 't':  *(dst++) = '\t'; src++; break;
            case 'n':  *(dst++) = '\n'; src++; break;
            case 'v':  *(dst++) = '\v'; src++; break;
            case 'f':  *(dst++) = '\f'; src++; break;
            case 'r':  *(dst++) = '\r'; src++; break;
            case '\\': *(dst++) = '\\'; src++; break;
            default:   *(dst++) = '\\';
            }
        else
            *(dst++) = *(src++);

    *(dst++) = '\0';

    return (size_t)(dst - (char *)addr);
}

static inline long sleep_ms(const long ms)
{
    struct timespec  t;

    if (ms > 0) {
        t.tv_sec = ms / 1000;
        t.tv_nsec = (ms % 1000) * 1000000;
        if (nanosleep(&t, &t) == -1 && errno == EINTR)
            return 1000 * (unsigned long)(t.tv_sec)
                 + (unsigned long)(t.tv_nsec / 1000000);
        return 0;
    } else
        return ms;
}

static int parse_long(const char *src, long *dst)
{
    const char *end = src;
    long        val;

    if (!src || !*src)
        return errno = EINVAL;

    errno = 0;
    val = strtol(src, (char **)&end, 0);
    if (errno)
        return errno;

    if (end == src)
        return errno = EINVAL;

    while (*end == '\t' || *end == '\n' || *end == '\v' ||
           *end == '\f' || *end == '\r' || *end == ' ')
        end++;

    if (*end)
        return errno = EINVAL;

    if (dst)
        *dst = val;

    return 0;
}

int main(int argc, char *argv[])
{
    char                buffer[65536];
    struct sockaddr_un  conn;
    socklen_t           connlen;
    int                 connfd, arg;
    ssize_t             n;
    long                val, left;

    if (argc < 3 || !argv[1][0] || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s SOCKET_PATH [ LEN | -MS ] ...\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "All arguments except the first one, SOCKET_PATH, are integers.\n");
        fprintf(stderr, "A positive integer causes a seqpacket of that length to be sent,\n");
        fprintf(stderr, "a negative value causes a delay (magnitude in milliseconds).\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Fill buffer with some data. Anything works. */
    {
        size_t  i = sizeof buffer;
        while (i-->0)
            buffer[i] = (i*i) ^ i;
    }

    connfd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    if (connfd == -1) {
        fprintf(stderr, "Cannot create an Unix domain seqpacket socket: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    connlen = set_unix_path(argv[1], &conn);
    if (connect(connfd, (const struct sockaddr *)&conn, connlen) == -1) {
        fprintf(stderr, "Cannot connect to %s: %s.\n", argv[1], strerror(errno));
        close(connfd);
        return EXIT_FAILURE;
    }

    /* To avoid output affecting the timing, fully buffer stdout. */
    setvbuf(stdout, NULL, _IOFBF, 65536);

    for (arg = 2; arg < argc; arg++)
        if (parse_long(argv[arg], &val)) {
            fprintf(stderr, "%s: Not an integer.\n", argv[arg]);
            close(connfd);
            return EXIT_FAILURE;
        } else
        if (val > (long)sizeof buffer) {
            fprintf(stderr, "%s: Seqpacket size too large. Current limit is %zu.\n", argv[arg], sizeof buffer);
            close(connfd);
            return EXIT_FAILURE;
        } else
        if (val >= 0) {
            n = send(connfd, buffer, (size_t)val, 0);
            if (n == (ssize_t)val)
                printf("Sent %ld-byte seqpacket successfully.\n", val);
            else
            if (n != (ssize_t)val && n >= 0)
                fprintf(stderr, "Sent %zd bytes of a %ld-byte seqpacket.\n", n, val);
            else
            if (n < -1) {
                fprintf(stderr, "C library bug: send() returned %zd.\n", n);
                close(connfd);
                return EXIT_FAILURE;
            } else
            if (n == -1) {
                fprintf(stderr, "Send failed: %s.\n", strerror(errno));
                close(connfd);
                return EXIT_FAILURE;
            }
        } else {
            left = sleep_ms(-val);
            if (left)
                fprintf(stderr, "Slept %ld milliseconds (out of %ld ms).\n", -val-left, -val);
            else
                printf("Slept %ld milliseconds.\n", -val);
        }

    if (close(connfd) == -1) {
        fprintf(stderr, "Error closing connection: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    printf("All done, connection closed.\n"); 
    fflush(stdout);

    return EXIT_SUCCESS;
}

使用例如gcc -Wall -O2 send.c -o send

为了进行测试,建议您使用两个终端窗口。一个运行send,另一个运行receive。为简单起见,我将并排显示相应的命令和输出。运行的计算机是Core i5 7200U笔记本电脑(HP EliteBook 830),运行Ubuntu 16.04.4 LTS,64位Linux内核版本4.15.0-24-通用,使用GCC-5.4.0 20160609(5.4 .0-6ubuntu1〜16.04.10)和上述命令(gcc -Wall -O2)。

当我们在最终发送之前使用一小段延迟时,一切似乎都可以正常工作:

$ ./send '\0example' 1 0 3 0 0 -1 6   │   $ ./receive '\0example'
                                      │   Connected, peer address size is 2.
Sent 1-byte seqpacket successfully.   │   Received 1 bytes.
Sent 0-byte seqpacket successfully.   │   Received a zero-byte seqpacket.
Sent 3-byte seqpacket successfully.   │   Received 3 bytes.
Sent 0-byte seqpacket successfully.   │   Received a zero-byte seqpacket.
Sent 0-byte seqpacket successfully.   │   Received a zero-byte seqpacket.
Slept 1 milliseconds.                 │  
Sent 6-byte seqpacket successfully.   │   Received 6 bytes.
All done, connection closed.          │   Disconnected (revents = 16).

但是,当发件人发送最后几个seqpackets(以零长度的seqpackets开始)且之间没有任何延迟时,我会观察到:

$ ./send '\0example' 1 0 3 0 0 6      │   ./receive '\0example'
                                      │   Connected, peer address size is 2.
Sent 1-byte seqpacket successfully.   │   Received 1 bytes.
Sent 0-byte seqpacket successfully.   │   Received a zero-byte seqpacket.
Sent 3-byte seqpacket successfully.   │   Received 3 bytes.
Sent 0-byte seqpacket successfully.   │   
Sent 0-byte seqpacket successfully.   │   
Sent 6-byte seqpacket successfully.   │   
All done, connection closed.          │  Disconnected (revents = 16).

查看如何丢失两个零字节seqpacket和6字节seqpacket(因为poll()返回了revents == POLLHUP。(POLLHUP == 0x0010 == 16,因此任何一次都没有设置其他标志。)

我个人不确定这是否是 bug 。在我看来,这是公正的,这表明使用零长度seqpackets是有问题的,应该避免。

(对等地址长度为2以上,因为发件人未绑定任何地址,因此使用了未命名的Unix域套接字地址(如man unix手册页中所述)。我认为这很重要,但我还是以防万一。)

讨论了一种通过MSG_EOR解决当前问题的方法(因为recvmsg()应该在{{1的MSG_EOR字段中添加msg_flags }}结构;并且由于“应该为所有seqpackets设置它”,即使是零长度的seqpackets,这也是从输入端/读取侧关闭/断开连接检测零长度seqpackets的可靠方法。 2007年5月的Linux内核邮件列表(和linux-netdev列表)。(存档的thread at Marc.info is here) 但是,根据最初的发布者Sam Kumar的说法,在Linux Unix域seqpacket套接字中,msghdr没有设置或传递。讨论没有进行到任何地方。在我阅读本文时,没有人能确定预期的行为。

查看Unix domain sockets的Linux内核更改日志,自该线程以来(截至2018年7月23日)也没有任何相关更改。


以上程序是一站式编写,未经审查;因此,他们很容易在其中包含错误或想法。如果您发现任何问题或获得了截然不同的结果(但请注意有时很难复制基于时序的效果),请在评论中告知我,以便我检查并在必要时进行修复。