进程间通信:消息队列与套接字详解
1. 消息队列基础
消息队列在进程间通信中展现出显著优势,相较于管道,它无需进程自行提供同步方法。只要消息队列有空间,发送者就能创建队列、放入数据,甚至在接收者启动前退出。以下是一个简单示例:
if (msgsnd(msgid, (void *)&some_data, MAX_TEXT, 0) == -1) {
fprintf(stderr, “msgsnd failed\n”);
exit(EXIT_FAILURE);
}
if (strncmp(buffer, “end”, 3) == 0) {
running = 0;
}
exit(EXIT_SUCCESS);
1.1 消息队列工作原理
发送者程序使用
msgget
创建消息队列,再用
msgsnd
添加消息;接收者通过
msgget
获取队列标识符,接收消息直至收到特殊文本“end”,最后用
msgctl
删除队列。
1.2 CD数据库应用改造
为了让CD数据库应用使用消息队列,只需替换相关文件。以下是具体步骤:
1.2.1 修订服务器函数
- 包含头文件、声明键和定义结构 :
#include “cd_data.h”
#include “cliserv.h”
#include <sys/msg.h>
#define SERVER_MQUEUE 1234
#define CLIENT_MQUEUE 4321
struct msg_passed {
long int msg_key; /* used for client pid */
message_db_t real_message;
};
- 定义队列标识符变量 :
static int serv_qid = -1;
static int cli_qid = -1;
- 创建消息队列 :
int server_starting()
{
#if DEBUG_TRACE
printf(“%d :- server_starting()\n”, getpid());
#endif
serv_qid = msgget((key_t)SERVER_MQUEUE, 0666 | IPC_CREAT);
if (serv_qid == -1) return(0);
cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666 | IPC_CREAT);
if (cli_qid == -1) return(0);
return(1);
}
- 清理资源 :
void server_ending()
{
#if DEBUG_TRACE
printf(“%d :- server_ending()\n”, getpid());
#endif
(void)msgctl(serv_qid, IPC_RMID, 0);
(void)msgctl(cli_qid, IPC_RMID, 0);
serv_qid = -1;
cli_qid = -1;
}
- 读取客户端请求 :
int read_request_from_client(message_db_t *rec_ptr)
{
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(“%d :- read_request_from_client()\n”, getpid());
#endif
if (msgrcv(serv_qid, (void *)&my_msg, sizeof(*rec_ptr), 0, 0) == -1) {
return(0);
}
*rec_ptr = my_msg.real_message;
return(1);
}
- 发送响应给客户端 :
int send_resp_to_client(const message_db_t mess_to_send)
{
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(“%d :- send_resp_to_client()\n”, getpid());
#endif
my_msg.real_message = mess_to_send;
my_msg.msg_key = mess_to_send.client_pid;
if (msgsnd(cli_qid, (void *)&my_msg, sizeof(mess_to_send), 0) == -1) {
return(0);
}
return(1);
}
1.2.2 修订客户端函数
- 获取队列标识符 :
int client_starting()
{
#if DEBUG_TRACE
printf(“%d :- client_starting\n”, getpid());
#endif
serv_qid = msgget((key_t)SERVER_MQUEUE, 0666);
if (serv_qid == -1) return(0);
cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666);
if (cli_qid == -1) return(0);
return(1);
}
- 清理资源 :
void client_ending()
{
#if DEBUG_TRACE
printf(“%d :- client_ending()\n”, getpid());
#endif
serv_qid = -1;
cli_qid = -1;
}
- 发送消息给服务器 :
int send_mess_to_server(message_db_t mess_to_send)
{
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(“%d :- send_mess_to_server()\n”, getpid());
#endif
my_msg.real_message = mess_to_send;
my_msg.msg_key = mess_to_send.client_pid;
if (msgsnd(serv_qid, (void *)&my_msg, sizeof(mess_to_send), 0) == -1) {
perror(“Message send failed”);
return(0);
}
return(1);
}
- 读取服务器响应 :
int read_resp_from_server(message_db_t *rec_ptr)
{
struct msg_passed my_msg;
#if DEBUG_TRACE
printf(“%d :- read_resp_from_server()\n”, getpid());
#endif
if (msgrcv(cli_qid, (void *)&my_msg, sizeof(*rec_ptr), getpid(), 0) == -1) {
return(0);
}
*rec_ptr = my_msg.real_message;
return(1);
}
- 定义额外函数 :
int start_resp_to_client(const message_db_t mess_to_send)
{
return(1);
}
void end_resp_to_client(void)
{
}
int start_resp_from_server(void)
{
return(1);
}
void end_resp_from_server(void)
{
}
1.3 IPC状态命令
多数Linux系统提供
ipcs
和
ipcrm
命令,用于查看和清理IPC信息与资源。
-
显示信号量状态
:使用
ipcs -s
查看,用
ipcrm -s <id>
删除。
-
显示共享内存状态
:使用
ipcs -m
查看,用
ipcrm -m <id>
删除。
-
显示消息队列状态
:使用
ipcs -q
查看,用
ipcrm -q <id>
删除。
2. 套接字基础
套接字是一种通信机制,可用于本地或跨网络的客户端/服务器系统开发。与管道不同,它明确区分客户端和服务器,能实现多个客户端连接到一个服务器。
2.1 套接字连接
可以将套接字连接类比为打进繁忙大楼的电话。服务器创建套接字并命名,等待客户端连接;客户端创建未命名套接字,连接到服务器的命名套接字。以下是套接字连接的步骤:
graph LR
A[服务器创建套接字] --> B[服务器命名套接字]
B --> C[服务器监听连接]
D[客户端创建套接字] --> E[客户端连接服务器]
C --> F[服务器接受连接]
F --> G[服务器与客户端通信]
E --> G
2.2 简单本地客户端示例
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch = ‘A’;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
address.sun_family = AF_UNIX;
strcpy(address.sun_path, “server_socket”);
len = sizeof(address);
result = connect(sockfd, (struct sockaddr *)&address, len);
if(result == -1) {
perror(“oops: client1”);
exit(1);
}
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf(“char from server = %c\n”, ch);
close(sockfd);
exit(0);
}
2.3 简单本地服务器示例
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
unlink(“server_socket”);
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, “server_socket”);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
while(1) {
char ch;
printf(“server waiting\n”);
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}
2.4 套接字属性
套接字有三个属性:域、类型和协议,还有一个用作名称的地址。
2.4.1 套接字域
常见的套接字域包括:
| 域 | 描述 |
| ---- | ---- |
| AF_UNIX | UNIX内部(文件系统套接字) |
| AF_INET | ARPA Internet协议(UNIX网络套接字) |
| AF_INET6 | IPv6协议 |
| AF_ISO | 基于ISO标准协议的网络 |
| AF_XNS | 施乐网络系统 |
2.4.2 套接字类型
- 流套接字(SOCK_STREAM) :提供有序、可靠的双向字节流,如TCP/IP连接。
- 数据报套接字(SOCK_DGRAM) :无连接,消息可能丢失或乱序,如UDP数据报。
2.4.3 套接字协议
通常由套接字类型和域决定,一般选择默认协议(0)。
2.5 创建套接字
使用
socket
系统调用创建套接字:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
2.6 套接字地址
不同套接字域有不同的地址格式,如AF_UNIX使用
sockaddr_un
结构,AF_INET使用
sockaddr_in
结构。
2.7 命名套接字
使用
bind
系统调用为套接字命名:
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);
2.8 创建套接字队列
使用
listen
系统调用创建队列:
#include <sys/socket.h>
int listen(int socket, int backlog);
2.9 接受连接
使用
accept
系统调用接受连接:
#include <sys/socket.h>
int accept(int socket, struct sockaddr *address, size_t *address_len);
2.10 请求连接
使用
connect
系统调用请求连接:
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len);
2.11 关闭套接字
使用
close
系统调用关闭套接字:
close(socket);
2.12 套接字通信
将本地套接字示例转换为网络套接字,选择未使用的端口号(如9734)。
2.13 网络客户端示例
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ch = ‘A’;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr(“127.0.0.1”);
address.sin_port = 9734;
len = sizeof(address);
result = connect(sockfd, (struct sockaddr *)&address, len);
if(result == -1) {
perror(“oops: client2”);
exit(1);
}
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf(“char from server = %c\n”, ch);
close(sockfd);
exit(0);
}
2.14 网络服务器示例
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(“127.0.0.1”);
server_address.sin_port = 9734;
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
while(1) {
char ch;
printf(“server waiting\n”);
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}
2.15 主机和网络字节序
不同计算机使用不同的字节序,为确保网络通信中整数的正确传输,需要进行字节序转换:
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
在服务器和客户端代码中应用这些函数,确保端口号的正确字节序。例如,服务器代码修改为:
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
客户端代码修改为:
address.sin_port = htons(9734);
通过以上内容,我们详细介绍了消息队列和套接字的相关知识,包括它们的工作原理、应用示例以及相关的系统调用和命令。这些知识可以帮助你开发出高效、可靠的进程间通信和网络应用程序。
3. 网络编程实践与优化
3.1 网络编程中的错误处理
在网络编程中,各种系统调用可能会失败,因此需要进行错误处理。以下是常见系统调用的错误情况及处理方式:
| 系统调用 | 可能的错误值 | 描述 |
|---|---|---|
bind
|
EBADF
| 文件描述符无效 |
ENOTSOCK
| 文件描述符不指向套接字 | |
EINVAL
| 文件描述符指向已命名的套接字 | |
EADDRNOTAVAIL
| 地址不可用 | |
EADDRINUSE
| 地址已被其他套接字绑定 | |
listen
|
EBADF
| 文件描述符无效 |
EINVAL
| 无效参数 | |
ENOTSOCK
| 文件描述符不指向套接字 | |
accept
|
EBADF
| 文件描述符无效 |
ENOTSOCK
| 文件描述符不指向套接字 | |
EWOULDBLOCK
|
设置了
O_NONBLOCK
且无待处理连接
| |
EINTR
|
进程在
accept
时被信号中断
| |
connect
|
EBADF
| 传入的文件描述符无效 |
EALREADY
| 该套接字已有连接正在进行 | |
ETIMEDOUT
| 连接超时 | |
ECONNREFUSED
| 服务器拒绝连接请求 |
3.2 异步连接处理
connect
和
accept
系统调用默认是阻塞的,但可以通过设置
O_NONBLOCK
标志来实现异步操作。以下是异步连接的处理流程:
graph LR
A[创建套接字] --> B[设置O_NONBLOCK标志]
B --> C[调用connect]
C --> D{连接是否立即成功}
D -- 是 --> E[连接成功]
D -- 否 --> F{错误是否为EINPROGRESS}
F -- 是 --> G[使用select检查套接字是否可写]
G --> H{套接字是否可写}
H -- 是 --> E
H -- 否 --> I[连接失败]
F -- 否 --> I
3.3 多客户端处理
前面的服务器示例只能一次处理一个客户端连接,为了处理多个客户端,可以使用多进程或多线程。以下是使用多进程处理多客户端的示例代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handle_client(int client_sockfd) {
char ch;
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
void sigchld_handler(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
signal(SIGCHLD, sigchld_handler);
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
while (1) {
printf("server waiting\n");
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
pid_t pid = fork();
if (pid == 0) {
close(server_sockfd);
handle_client(client_sockfd);
exit(0);
} else {
close(client_sockfd);
}
}
}
3.4 套接字选项设置
可以使用
setsockopt
函数设置套接字选项,例如设置
SO_REUSEADDR
选项允许地址重用,避免在服务器重启时出现地址已被使用的错误。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int server_sockfd;
struct sockaddr_in server_address;
int optval = 1;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
bind(server_sockfd, (struct sockaddr *)&server_address, sizeof(server_address));
listen(server_sockfd, 5);
// 后续处理...
return 0;
}
3.5 网络信息获取
在网络编程中,有时需要获取网络信息,例如主机名、IP地址等。可以使用
gethostbyname
和
gethostbyaddr
函数来实现。
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
int main() {
struct hostent *host;
struct in_addr addr;
// 通过主机名获取信息
host = gethostbyname("www.example.com");
if (host != NULL) {
printf("Host name: %s\n", host->h_name);
for (int i = 0; host->h_addr_list[i] != NULL; i++) {
addr.s_addr = *(unsigned long *)host->h_addr_list[i];
printf("IP address: %s\n", inet_ntoa(addr));
}
}
// 通过IP地址获取信息
inet_pton(AF_INET, "192.168.1.1", &addr);
host = gethostbyaddr(&addr, sizeof(addr), AF_INET);
if (host != NULL) {
printf("Host name: %s\n", host->h_name);
}
return 0;
}
4. 总结
通过对消息队列和套接字的学习,我们了解了进程间通信和网络编程的基本原理和实现方法。消息队列提供了一种方便的进程间通信方式,无需进程自行同步;套接字则可以实现本地和跨网络的客户端/服务器系统。
在网络编程中,需要注意以下几点:
1. 处理各种系统调用的错误,确保程序的健壮性。
2. 合理使用异步操作,提高程序的性能和响应能力。
3. 采用多进程或多线程处理多客户端连接,满足高并发需求。
4. 设置套接字选项,优化网络通信。
5. 正确处理主机和网络字节序,确保数据在不同计算机之间的正确传输。
掌握这些知识和技巧,可以帮助我们开发出高效、可靠的网络应用程序。
超级会员免费看
709

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



