37、套接字与消息接口实现及无连接套接字详解

套接字与消息接口实现及无连接套接字详解

1. SMI的套接字实现

在实现简单消息接口(SMI)时,我们可以使用套接字来完成,并且借助之前实现的SSI函数,而非直接调用各种与套接字相关的系统调用,这样能让工作变得更加轻松。

首先是内部的 SMIQ_SKT 结构体,它非常简单,主要是因为追踪客户端的工作由 accept 系统调用完成,而且我们有SSI来处理许多细节:

typedef struct {
    SMIENTITY sq_entity;  // entity
    SSI *sq_ssip;         // structure for SSI
    struct client_id sq_client;  // client ID
    size_t sq_msgsize;    // msg size
    struct smi_msg *sq_msg;  // msg buffer
} SMIQ_SKT;

打开 SMIQ_SKT 的操作并不复杂,因为 ssi_open 完成了大部分工作:

SMIQ *smi_open_skt(const char *name, SMIENTITY entity, size_t msgsize)
{
    SMIQ_SKT *p = NULL;
    ec_null(  p = calloc(1, sizeof(SMIQ_SKT)) )
    p->sq_msgsize = msgsize + offsetof(struct smi_msg, smi_data);
    ec_null( p->sq_msg = calloc(1, p->sq_msgsize) )
    p->sq_entity = entity;
    ec_null( p->sq_ssip = ssi_open(name, entity == SMI_SERVER) )
    return (SMIQ *)p;
EC_CLEANUP_BGN
    (void)smi_close_skt((SMIQ *)p);
    return NULL;
EC_CLEANUP_END
}

关闭操作也是类似的:

bool smi_close_skt(SMIQ *sqp)
{
    SMIQ_SKT *p = (SMIQ_SKT *)sqp;
    SSI *ssip;
    if (p != NULL) {
        ssip = p->sq_ssip;
        free(p->sq_msg);
        free(p);
        if (ssip != NULL)
            ec_false( ssi_close(ssip) )
    }
    return true;
EC_CLEANUP_BGN
    return false;
EC_CLEANUP_END
}

SMI 的其他几个函数功能如下:
- smi_send_getaddr_skt :如果由服务器调用,它会保存客户端ID并返回消息地址。

bool smi_send_getaddr_skt(SMIQ *sqp, struct client_id *client,
  void **addr)
{
    SMIQ_SKT *p = (SMIQ_SKT *)sqp;
    if (p->sq_entity == SMI_SERVER)
        p->sq_client = *client;
    *addr = p->sq_msg;
    return true;
}
  • smi_send_release_skt :根据消息是来自服务器还是客户端,将消息写入客户端的文件描述符或服务器的套接字文件描述符。
bool smi_send_release_skt(SMIQ *sqp)
{
    SMIQ_SKT *p = (SMIQ_SKT *)sqp;
    int fd;
    if (p->sq_entity == SMI_SERVER)
        ec_neg1( fd = p->sq_client.c_id1 )
    else
        ec_neg1( fd = ssi_get_server_fd(p->sq_ssip) )
    ec_neg1( writeall(fd, p->sq_msg, p->sq_msgsize) )
    return true;
EC_CLEANUP_BGN
    return false;
EC_CLEANUP_END
}
  • smi_receive_getaddr_skt :服务器需要等待一个就绪的文件描述符,客户端只需获取服务器的套接字文件描述符。
bool smi_receive_getaddr_skt(SMIQ *sqp, void **addr)
{
    SMIQ_SKT *p = (SMIQ_SKT *)sqp;
    ssize_t /*nremain,*/ nread;
    int fd;
    *addr = p->sq_msg;
    while (true) {
        if (p->sq_entity == SMI_SERVER)
            ec_neg1( fd = ssi_wait_server(p->sq_ssip) )
        else
            ec_neg1( fd = ssi_get_server_fd(p->sq_ssip) )
        ec_neg1( nread = readall(fd, p->sq_msg, p->sq_msgsize) )
        if (nread == 0) {
            if (p->sq_entity == SMI_SERVER) {
                ec_false( ssi_close_fd(p->sq_ssip, fd) )
                continue;
            }
            else {
                errno = ENOMSG;
                EC_FAIL
            }
        }
        else
            break;
    }
    if (p->sq_entity == SMI_SERVER)
        p->sq_msg->smi_client.c_id1 = fd;
    return true;
EC_CLEANUP_BGN
    return false;
EC_CLEANUP_END
}
  • smi_receive_release_skt :该函数无需进行任何操作。
bool smi_receive_release_skt(SMIQ *sqp)
{
    return true;
}

通过这种 SMI 实现,之前的所有消息发送示例都可以在套接字上运行。需要注意的是,我们的 SMI 示例是为机器内的进程间通信(IPC)设计的,使用的是普通服务器名称,这意味着在 ssi_open 中,名称会被视为 AF_UNIX 域。不过,也可以轻松地将 SMI 接口用于机器间的 AF_INET 消息传递。

2. 无连接套接字

到目前为止,我们的示例和 SSI 的实现都使用了 SOCK_STREAM 类型的连接套接字。而无连接套接字则有所不同,它不使用连接,因此不会调用 listen accept

2.1 关于数据报

无连接通信使用数据报,数据报是包含目标地址的独立数据块。它不会尝试对数据报进行排序,也不能保证数据报一定会到达。相比之下, SOCK_STREAM 能保证数据的顺序和到达。

在许多应用中,数据报的顺序和到达保证并不重要。例如,在图书馆图书预订应用中,用户提交表单后发送数据报给服务器,服务器返回确认信息。即使数据报顺序被打乱,由于用户之间互不了解,也不会有投诉。如果数据报未到达,用户只需再次发送表单即可。因此,在这种应用中,数据报比建立连接再立即断开更高效。但如果用户需要交互式浏览图书目录,建立连接可能是更好的选择。

2.2 sendto recvfrom 系统调用

在之前的套接字示例中,我们使用 write (或 writeall )向套接字发送数据,这是因为我们有代表连接写入端的文件描述符。但对于无连接套接字,我们只有自己套接字的文件描述符,需要指定目标套接字地址,因此需要使用 sendto 系统调用:

#include <sys/socket.h>
ssize_t sendto(
int socket_fd,          // socket file descriptor
const void *message,    // message to send
size_t length,          // length of message
int flags,              // flags
const struct sockaddr *sa,  // target address
socklen_t sa_len        // length of target address
);
// Returns number of bytes sent or -1 on error (sets errno)

sendto 的参数 socket_fd 是发送方的套接字,接收方由参数 sa sa_len 指定。 sendto 成功返回并不意味着消息已到达,只表示本地未检测到错误。

对于无连接套接字,长度为 length 的消息被视为一个不可分割的数据报。如果套接字通道已满, sendto 通常会阻塞,直到消息全部能放入;如果设置了 O_NONBLOCK 标志且消息无法放入,则不会发送任何数据,返回 -1,并将 errno 设置为 EAGAIN EWOULDBLOCK

以下是一个使用 sendto 的示例,展示了两个对等方相互发送数据报的过程:

#define SOCKETNAME1 "SktOne"
#define SOCKETNAME2 "SktTwo"
#define MSG_SIZE 100
int main(void)
{
    struct sockaddr_un sa1, sa2;
    strcpy(sa1.sun_path, SOCKETNAME1);
    sa1.sun_family = AF_UNIX;
    strcpy(sa2.sun_path, SOCKETNAME2);
    sa2.sun_family = AF_UNIX;
    (void)unlink(SOCKETNAME1);
    (void)unlink(SOCKETNAME2);
    if (fork() == 0) { /* Peer 1 */
        int fd_skt;
        ssize_t nread;
        char msg[MSG_SIZE];
        int i;
        sleep(1); /* let peer 2 startup first */
        ec_neg1( fd_skt = socket(AF_UNIX, SOCK_DGRAM, 0) )
        ec_neg1( bind(fd_skt, (struct sockaddr *)&sa1, sizeof(sa1)) )
        for (i = 1; i <= 4; i++) {
            snprintf(msg, sizeof(msg), "Message #%d", i);
            ec_neg1( sendto(fd_skt, msg, sizeof(msg), 0,
              (struct sockaddr *)&sa2, sizeof(sa2)) )
            ec_neg1( nread = read(fd_skt, msg, sizeof(msg)) )
            if (nread != sizeof(msg)) {
                printf("Peer 1 got short message\n");
                break;
            }
            printf("Got \"%s\" back\n", msg);
        }
        ec_neg1( close(fd_skt) )
        exit(EXIT_SUCCESS);
    }
    else { /* Peer 2 */
        int fd_skt;
        ssize_t nread;
        char msg[MSG_SIZE];
        ec_neg1( fd_skt = socket(AF_UNIX, SOCK_DGRAM, 0) )
        ec_neg1( bind(fd_skt, (struct sockaddr *)&sa2, sizeof(sa2)) )
            ec_neg1( nread = read(fd_skt, msg, sizeof(msg)) )
        while (true) {
            if (nread != sizeof(msg)) {
                printf("Peer 2 got short message\n");
                break;
            }
            msg[0] = 'm';
            ec_neg1( sendto(fd_skt, msg, sizeof(msg), 0,
              (struct sockaddr *)&sa1, sizeof(sa1)) )
        }
        ec_neg1( close(fd_skt) )
        exit(EXIT_SUCCESS);
    }
EC_CLEANUP_BGN
    exit(EXIT_FAILURE);
EC_CLEANUP_END
}

在这个示例中, read 并不是一个好的选择,因为它无法告诉我们数据的发送方。因此,我们需要使用 recvfrom 系统调用:

#include <sys/socket.h>
ssize_t recvfrom(
int socket_fd,          // socket file descriptor
void *buffer,           // buffer for received message
size_t length,          // length of buffer
int flags,              // flags
struct sockaddr *sa,    // address of sender
socklen_t *sa_len       // address length
);
// Returns number of bytes received, 0, or -1 on error (sets errno)

recvfrom 的前三个参数与 read 类似,此外,它还有一个 flags 参数和两个用于接收发送方套接字地址的参数。在调用之前,需要将 sa 设置为足够大的缓冲区以保存发送方的套接字地址,并将 sa_len 设置为该缓冲区的大小。返回时, sa_len 会被设置为实际地址的大小。

recvfrom 有三个可移植的标志:
| 标志 | 描述 |
| ---- | ---- |
| MSG_OOB | 接收带外(紧急)数据 |
| MSG_PEEK | 返回消息,但不读取,供下一次读取、 recvfrom 或其他输入操作使用 |
| MSG_WAITALL | 仅适用于连接套接字,使 recvfrom 阻塞,直到请求的整个长度可用,除非同时设置了 MSG_PEEK 或调用被信号、连接终止或错误中断 |

以下是一个使用 recvfrom 的示例,展示了多个客户端向服务器发送数据报的过程:

#define SOCKETNAME_SERVER "SktOne"
#define SOCKETNAME_CLIENT "SktTwo"
static struct sockaddr_un sa_server;
#define MSG_SIZE 100
static void run_client(int nclient)
{
    struct sockaddr_un sa_client;
    int fd_skt;
    ssize_t nrecv;
    char msg[MSG_SIZE];
    int i;
    if (fork() == 0) { /* client */
        sleep(1); /* let server startup first */
        ec_neg1( fd_skt = socket(AF_UNIX, SOCK_DGRAM, 0) )
        snprintf(sa_client.sun_path, sizeof(sa_client.sun_path),
          "%s-%d", SOCKETNAME_CLIENT, nclient);
        (void)unlink(sa_client.sun_path);
        sa_client.sun_family = AF_UNIX;
        ec_neg1( bind(fd_skt, (struct sockaddr *)&sa_client,
          sizeof(sa_client)) )
        for (i = 1; i <= 4; i++) {
            snprintf(msg, sizeof(msg), "Message #%d", i);
            ec_neg1( sendto(fd_skt, msg, sizeof(msg), 0,
              (struct sockaddr *)&sa_server, sizeof(sa_server)) )
            ec_neg1( nrecv = read(fd_skt, msg, sizeof(msg)) )
            if (nrecv != sizeof(msg)) {
                printf("client got short message\n");
                break;
            }
            printf("Got \"%s\" back\n", msg);
        }
        ec_neg1( close(fd_skt) )
        exit(EXIT_SUCCESS);
    }
    return;
EC_CLEANUP_BGN
    exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static void run_server(void)
{
    int fd_skt;
    ssize_t nrecv;
    char msg[MSG_SIZE];
    struct sockaddr_storage sa;
    socklen_t sa_len;
    ec_neg1( fd_skt = socket(AF_UNIX, SOCK_DGRAM,

### 套接字与消息接口实现及无连接套接字详解

#### 2. 无连接套接字(续)
##### 2.3 `sendmsg`和`recvmsg`
除了`sendto`和`recvfrom`系统调用,还有它们的变体`sendmsg`和`recvmsg`。这两个系统调用使用相同的标志和返回值,但使用一个指向`msghdr`结构体的单一参数,该结构体包含套接字地址和消息。它们能够像`readv`和`writev`一样进行分散读取和聚集写入。

以下是`sendmsg`和`recvmsg`的函数原型:
```c
#include <sys/socket.h>
ssize_t sendmsg(
int socket_fd,          // socket file descriptor
const struct msghdr *message, // message
int flags               // flags
);
// Returns number of bytes sent or -1 on error (sets errno)

ssize_t recvmsg(
int socket_fd,          // socket file descriptor
struct msghdr *message, // message
int flags               // flags
);
// Returns number of bytes received, 0, or -1 on error (sets errno)

msghdr 结构体的定义如下:

struct msghdr {
void *msg_name;         // optional address
socklen_t msg_namelen;  // size of address
struct iovec *msg_iov;  // scatter/gather array
int msg_iovlen;         // number of elements in msg_iov
void *msg_control;      // ancillary data
socklen_t msg_controllen; // ancillary data buffer len
int msg_flags;          // flags on received message
};

对于 sendmsg
- msg_name 成员指向套接字地址, msg_namelen 是地址的长度。
- 要发送的数据通过 msg_iov 成员指向的 iovec 结构体指定,使用方式与 writev 相同。
- msg_iovlen msg_iov 数组的元素数量。
- msg_flags 成员会被忽略。

对于 recvmsg
- 可以将 msg_name 设置为 NULL 或一个长度为 msg_namelen 的缓冲区,用于接收发送方的套接字地址,类似于 recvfrom 的工作方式。返回时, msg_namelen 会被更改为实际的套接字地址长度。
- msg_iov msg_iovlen 的使用方式与 readv 相同。
- 返回的 msg_flags 成员可以设置一个可移植的标志 MSG_TRUNC ,表示消息太长无法放入提供的缓冲区。

套接字地址成员 msg_name msg_namelen 用于无连接套接字,对于连接套接字则会被忽略。另外两个成员 msg_control msg_controllen 用于与访问权限相关的辅助数据。

以下是使用 recvmsg sendmsg 重写的服务器函数示例:

static void run_server(void)
{
    int fd_skt;
    ssize_t nrecv;
    char msg[MSG_SIZE];
    struct sockaddr_storage sa;
    struct msghdr m;
    struct iovec v;
    ec_neg1( fd_skt = socket(AF_UNIX, SOCK_DGRAM, 0) )
    ec_neg1( bind(fd_skt, (struct sockaddr *)&sa_server, sizeof(sa_server)) )
    while (true) {
        memset(&m, 0, sizeof(m));
        m.msg_name = &sa;
        m.msg_namelen = sizeof(sa);
        v.iov_base = msg;
        v.iov_len = sizeof(msg);
        m.msg_iov = &v;
        m.msg_iovlen = 1;
        ec_neg1( nrecv = recvmsg(fd_skt, &m, 0) )
        if (nrecv != sizeof(msg)) {
            printf("server got short message\n");
            break;
        }
        ((char *)m.msg_iov->iov_base)[0] = 'm';
        ec_neg1( sendmsg(fd_skt, &m, 0) )
    }
    ec_neg1( close(fd_skt) )
    exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
    exit(EXIT_FAILURE);
EC_CLEANUP_END
}

虽然设置 msghdr 结构体需要额外的工作,但 recvmsg sendmsg 调用非常简单。在有大量不同消息发送到不同地址的应用中,将地址和消息数据保存在同一结构体中的优势会更加明显。

2.4 使用 connect 与无连接套接字

通常,客户端使用 connect 与服务器的套接字建立连接,但它也可以用于无连接套接字,以建立后续发送和接收操作的默认地址,这些操作不需要套接字地址参数。

调用 connect 时传入 NULL 套接字地址可以移除默认地址。

总结

本文详细介绍了简单消息接口(SMI)的套接字实现,包括 SMIQ_SKT 结构体的定义和相关函数的实现。通过使用 SSI 函数,我们可以轻松地在套接字上运行消息发送示例。

同时,我们还探讨了无连接套接字的相关内容,包括数据报的特点、 sendto recvfrom 系统调用、 sendmsg recvmsg 系统调用以及如何使用 connect 与无连接套接字。无连接套接字在某些应用场景中具有更高的效率,例如图书馆图书预订应用。

在实际应用中,我们可以根据具体需求选择合适的套接字类型和系统调用,以实现高效、可靠的网络通信。

操作步骤总结

实现SMI的套接字操作步骤
  1. 定义 SMIQ_SKT 结构体。
  2. 实现 SMIQ 打开和关闭函数: smi_open_skt smi_close_skt
  3. 实现 SMI 的发送和接收相关函数: smi_send_getaddr_skt smi_send_release_skt smi_receive_getaddr_skt smi_receive_release_skt
无连接套接字操作步骤
  1. 使用 sendto 发送数据报时:
    • 创建套接字并绑定地址。
    • 准备消息和目标地址。
    • 调用 sendto 发送消息。
  2. 使用 recvfrom 接收数据报时:
    • 创建套接字并绑定地址。
    • 准备缓冲区和地址存储。
    • 调用 recvfrom 接收消息,并获取发送方地址。
  3. 使用 sendmsg recvmsg 时:
    • 初始化 msghdr 结构体。
    • 调用 sendmsg recvmsg 进行消息发送或接收。
  4. 使用 connect 与无连接套接字时:
    • 创建套接字。
    • 调用 connect 设置默认地址(或传入 NULL 移除默认地址)。

流程图

graph TD;
    A[开始] --> B[实现SMI套接字];
    B --> C[定义SMIQ_SKT结构体];
    B --> D[实现打开和关闭函数];
    B --> E[实现发送和接收函数];
    A --> F[使用无连接套接字];
    F --> G[使用sendto发送数据报];
    F --> H[使用recvfrom接收数据报];
    F --> I[使用sendmsg和recvmsg];
    F --> J[使用connect设置默认地址];
    G --> K[创建套接字并绑定地址];
    G --> L[准备消息和目标地址];
    G --> M[调用sendto发送消息];
    H --> N[创建套接字并绑定地址];
    H --> O[准备缓冲区和地址存储];
    H --> P[调用recvfrom接收消息];
    I --> Q[初始化msghdr结构体];
    I --> R[调用sendmsg或recvmsg];
    J --> S[创建套接字];
    J --> T[调用connect设置地址];
    K --> M;
    N --> P;
    Q --> R;
    S --> T;

这个流程图展示了实现SMI套接字和使用无连接套接字的主要步骤,帮助读者更好地理解整个过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值