套接字与消息接口实现及无连接套接字详解
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的套接字操作步骤
-
定义
SMIQ_SKT结构体。 -
实现
SMIQ打开和关闭函数:smi_open_skt和smi_close_skt。 -
实现
SMI的发送和接收相关函数:smi_send_getaddr_skt、smi_send_release_skt、smi_receive_getaddr_skt和smi_receive_release_skt。
无连接套接字操作步骤
-
使用
sendto发送数据报时:- 创建套接字并绑定地址。
- 准备消息和目标地址。
-
调用
sendto发送消息。
-
使用
recvfrom接收数据报时:- 创建套接字并绑定地址。
- 准备缓冲区和地址存储。
-
调用
recvfrom接收消息,并获取发送方地址。
-
使用
sendmsg和recvmsg时:-
初始化
msghdr结构体。 -
调用
sendmsg或recvmsg进行消息发送或接收。
-
初始化
-
使用
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套接字和使用无连接套接字的主要步骤,帮助读者更好地理解整个过程。
超级会员免费看

被折叠的 条评论
为什么被折叠?



