进程间通讯的7种方式_进程间通信的几种方法-CSDN博客
- 管道 pipe(命名管道和匿名管道);
- 信号 signal;
- 共享内存;
- 消息队列;
- 信号量 semaphore;
- 套接字 socket;
1. 管道
内核提供,单工,自同步机制。
1.1 匿名管道
磁盘上无法看到,只能有亲缘关系的进程才能用匿名管道。一般用于父子进程间通信。
pipe(2) 系统调用可以创建一个匿名管道 pipefd,文件描述符 pipefd[0] 为读管道,pipefd[1] 为写管道。
#include <unistd.h>
int pipe(int pipefd[2]);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
例子,父进程通过管道发送 hello 给子进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe");
exit(1);
}
pid = fork();
if (pid > 0) {
// parent
close(pipefd[0]);
write(pipefd[1], "hello", 5);
close(pipefd[1]);
wait(NULL);
exit(0);
}
else if (pid == 0) {
// child
close(pipefd[1]);
char buf[50];
int len = read(pipefd[0], buf, 50);
printf("%d\n", len);
write(1, buf, len);
close(pipefd[0]);
exit(0);
}
else {
perror("fork");
exit(1);
}
exit(0);
}
1.2 命名管道
磁盘上能看到,文件类型为 p 的文件。
mkfifo(3) 函数可以创建一个 fifo 特殊文件(命名管道)。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
2 XSI -> SysV
2.1 消息队列,message queue
主动端,先发包的一方;被动端,先收包的一方;消息队列可以用于没有亲缘关系的进程间通信。
例子,proto.h,包含了传递的内容:
#ifndef PROTO_H__
#define PROTO_H__
#define KEYPATH "/etc/services"
#define KEYPROJ 'g'
#define NAMESIZE 32
struct msg_st
{
long mtype;
char name[NAMESIZE];
int math;
int chinese;
};
#endif
receiver.c,接收者:
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main()
{
key_t key;
int msgid;
struct msg_st rbuf;
key = ftok(KEYPATH, KEYPROJ);
if (key < 0) {
perror("ftok");
exit(1);
}
msgid = msgget(key, IPC_CREAT | 0600);
if (msgid < 0) {
perror("msgget");
exit(1);
}
while (1)
{
if (msgrcv(msgid, &rbuf, sizeof(rbuf) - sizeof(long), 0, 0) < 0)
{
perror("msgrcv");
exit(1);
}
printf("NAME = %s\n", rbuf.name);
printf("MATH = %d\n", rbuf.math);
printf("CHINESE = %d\n", rbuf.chinese);
}
msgctl(msgid, IPC_RMID, NULL);
exit(0);
}
运行接收者可以看到创建的 msg queue:
sender.c,发送者:
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
int main()
{
key_t key;
struct msg_st sbuf;
int msgid;
key = ftok(KEYPATH, KEYPROJ);
if (key < 0) {
perror("ftock");
exit(1);
}
msgid = msgget(key, 0);
if (msgid < 0)
{
perror("msgget");
exit(1);
}
sbuf.mtype = 1;
strcpy(sbuf.name, "lzp");
sbuf.math = 99;
sbuf.chinese = 100;
if (msgsnd(msgid, &sbuf, sizeof(sbuf) - sizeof(long), 0) < 0) {
perror("msgsend");
exit(1);
}
puts("ok!");
exit(0);
}
先运行 receiver,然后运行 sender 发送信息:
2.2 信号量,semaphore arrays
semget、semop、semctl;可以用于没有亲缘关系的进程间通信。
2.3 共享内存,shared memory segment
shmget、shmop、shmctl;之前可以通过 mmap 实现共享内存,不过只能在有亲缘关系的进程间通信。匿名 ipc 也只能在有亲缘关系的进程间通信;
3. 网络套接字 socket
3.1 跨主机的传输
- 字节序:大端存储(低地址处放高字节)和小端存储(低地址处放低字节,x86);主机字节序(host)和网络字节序(network);主机序转网络序,网络序转主机序;htons,htonl,ntohs,ntohl。
- 对齐:结构体中 char 类型可能会占 4 个字节;如果不对齐,那么 int 类型的存储地址可能就不是在 4 的倍数的地址上了,可能需要两次访存才能取出一个 int 类型。深入理解字节对齐-CSDN博客
- 类型长度问题:不同主机间的架构,机器字长可能不一样,结构体类型大小也可能不一样。解决方案(int32_t,int16_t ...);
socket(2) 系统调用如下:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- domain 指定协议族;
- AF_INET IPv4 Internet protocols ip(7)
- AF_INET6 IPv6 Internet protocols ipv6(7)
- type 指定通信语义;
- SOCK_STREAM(流式传输):Provides sequenced, reliable, two-way, connection-based byte streams.
- SOCK_DGRAM(报式传输) :Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
- protocol 指定协议族中的协议;
bind(2) 系统调用可以给 socket 绑定地址。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
recvfrom(2) 可以从 socket 上接收一条 message:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- recv(2) 是用于流式套接字的,是提前建立好连接的,一对一的,点对点的, 不需要记录对方的 socket 地址;
- recvfrom(2) 可以用于报式和流式套接字,每次需要传入需要通信的 socket 地址;
sendto(2) 函数可以发送 msg 到对应的 socket 地址,可以用于流式和报式传输;而 send(2) 函数不需要指定 socket 地址,只能用于事先建立好连接的流式传输。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
3.2 报式套接字(udp)
被动端(先运行):
- 取得 socket(socket);
- 给 socket 取得地址,相当于绑定本地的地址(bind);
- 收/发消息(recvfrom);
- 关闭 socket(close);
主动端:
- 取得 socket;
- 给 socket 取得地址(可省略);
- 收/发消息;
- 关闭 socket;
__attribute__((packed)):packed属性:使用该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐。即不进行对齐。
使用如下指令查看被动段的地址和端口(u代表udp):
netstat -anu
proto.h 约定传输格式和内容:
#ifndef PROTO_H__
#define PROTO_H__
#define RCVPORT 1989
#define NAMESIZE 11
// communication struct
struct msg_st
{
char name[NAMESIZE];
int math;
int ch;
} __attribute__((packed));
#endif
receiver.c 接收方, 注意多字节需要使用 ntohl() 来转换:
#include <stdio.h>
#include <stdlib.h>
#include "proto.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define IPSTRSIZE 40
int main()
{
int sd; // socket fd
struct sockaddr_in laddr;
struct sockaddr_in raddr;
struct msg_st rbuf;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
// IPV4 DGRAM UDP
sd = socket(AF_INET, SOCK_DGRAM, 0/* IPPROTO_UDP */);
if (sd < 0) {
perror("socket");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(RCVPORT);
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
if (bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bindqqq");
exit(1);
}
/* !!! */
raddr_len = sizeof(raddr);
while (1)
{
recvfrom(sd, &rbuf, sizeof(rbuf), 0, (struct sockaddr *)&raddr, &raddr_len);
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("message from %s:%d---\n", ipstr, ntohs(raddr.sin_port));
printf("name = %s\n", rbuf.name);
printf("name = %d\n", ntohl(rbuf.math));
printf("name = %d\n", ntohl(rbuf.ch));
}
close(sd);
exit(0);
}
sender.c 发送方,发送报文给对应的地址:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "proto.h"
#include <unistd.h>
int main(int argc, char *argv[])
{
int sd;
struct msg_st sbuf;
struct sockaddr_in raddr;
if (argc < 2)
{
fprintf(stderr, "usage..\n");
exit(1);
}
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0)
{
perror("socket");
exit(1);
}
//bind();
strcpy(sbuf.name, "Alan");
sbuf.math = htonl(99);
sbuf.ch = htonl(93);
raddr.sin_family = AF_INET;
raddr.sin_port = htons(RCVPORT);
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
if (sendto(sd, &sbuf, sizeof(sbuf), 0, (struct sockaddr *)&raddr, sizeof(raddr)) < 0)
{
perror("sendto");
exit(1);
}
puts("ok\n");
close(sd);
exit(0);
}
运行结果:
报式套接字还能实现多播、广播(全网广播和子网广播)、组播:
可以通过 getsockopt() 和 setsockopt() 来打开广播选项。然后将发送的目标地址改成 255.255.255.255 就可以发送广播了。
多点通信(广播、多播、组播)只能用报式套接字实现,因为流式套接字是一对一的,点对点的。
udp 会出现丢包的问题,TTL生存周期(路由跳转个数) ,丢包是由阻塞造成的,当等待队列快满的时候会发生丢包(网络太拥塞),可以使用流量控制解决(限制发送端的速率)。
3.3 流式套接字(tcp)
协议,约定双方对话的格式。
三次握手容易被ddos攻击,可以去掉半连接池,然后使用cookie(对方的ip+端口加上我放的ip+端口加上一个内核产生的令牌)。
Client 端和 Server 端:
client:
- 获取 socket;
- 给 socket 取得地址;
- 发送连接;
- 收/发消息;
- 关闭连接;
server:
- 获取 socket;
- 给 socket 取得地址;
- 将 socket 置为监听模式;
- 接受连接;
- 收/发消息;
- 关闭;
服务端(server.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
static void server_job(int sd)
{
char buf[BUFSIZE];
int len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
if (send(sd, buf, len, 0) < 0) {
perror("send()");
exit(1);
}
}
int main()
{
struct sockaddr_in laddr, raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
int sd = socket(AF_INET, SOCK_STREAM, 0/* default IPPROTO_TCP */);
if (sd < 0) {
perror("socket()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
if (bind(sd, (void *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
// listen for connections on a socket
if (listen(sd, 200) < 0)
{
perror("listen()");
exit(1);
}
raddr_len = sizeof(raddr);
while (1) {
// accept a connection on a socket
int newsd;
if ((newsd = accept(sd, (void *)&raddr, &raddr_len)) < 0) {
perror("accept()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("Client: %s : %d\n", ipstr, ntohs(raddr.sin_port));
server_job(newsd);
// close newsd
close(newsd);
}
close(sd);
exit(0);
}
运行后使用 netstat -ant 可以查看:
当连接被释放后,服务端会进入一段时间的 timewait 状态。
客户端(client.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc, char* argv[])
{
struct sockaddr_in raddr;
long long stamp;
if (argc < 2) {
fprintf(stderr, "Usage...\n");
exit(1);
}
int sd = socket(AF_INET, SOCK_STREAM, 0);
if (sd < 0) {
perror("socket()");
exit(1);
}
// initiate a connection on a socket
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
if (connect(sd, (void *)&raddr, sizeof(raddr)) < 0) {
perror("connect()");
exit(1);
}
FILE* fp = fdopen(sd, "r+");
if (fp == NULL) {
perror("fdopen()");
exit(1);
}
if (fscanf(fp, FMT_STAMP, &stamp) < 1) {
fprintf(stderr, "bad format\n");
}
else {
fprintf(stdout, "stamp = %lld\n", stamp);
}
fclose(fp);
exit(0);
}
上面这种方法有一个缺点,就是假如 server_job 的任务执行时间太长的话,需要等待 server_job 执行完后才能继续 accept 下一个连接请求,这样效率十分低,所以可以采用多线程的方式来解决,每个进程或线程处理一个连接请求。