进程间通讯的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) {// parentclose(pipefd[0]);write(pipefd[1], "hello", 5);close(pipefd[1]);wait(NULL);exit(0);}else if (pid == 0) {// childclose(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 32struct 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 40int main()
{int sd; // socket fdstruct sockaddr_in laddr;struct sockaddr_in raddr;struct msg_st rbuf;socklen_t raddr_len;char ipstr[IPSTRSIZE];// IPV4 DGRAM UDPsd = 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 1024static 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 socketif (listen(sd, 200) < 0){perror("listen()");exit(1);}raddr_len = sizeof(raddr);while (1) {// accept a connection on a socketint 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 newsdclose(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 socketraddr.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 下一个连接请求,这样效率十分低,所以可以采用多线程的方式来解决,每个进程或线程处理一个连接请求。