主要内容:
1、管道
2、XSI/SysV
3、网络套接字socket
1、管道
管道是一种比较老的进程间通信的方式,最早出现于Unix系统种。把一个进程连接到另一个进程的一个数据流称为一个管道。
管道就是操作系统通过文件的形式,给两个进程创建一份共享资源。也就是两个进程共用一块内核缓冲区,一个进程从里面读取数据另外一个进程往其中写数据,进而达到通信的效果。管道可以分为匿名管道和命名管道。
匿名管道:供具有血缘关系的进程进行进程间通信,常见于父子进程。匿名管道之所以能够通信就是因为子进程继承了父进程的文件描述表,从而看到了相同的struct file结构体,进而能够进行进程间的通信。
命名管道:匿名管道的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件,它就是命名管道。
匿名管道可以用pipe()来创建
#include <unistd.h>
int pipe(int pipefd[2]);
//pipefd数组中的pipefd[0]和pipe[1]就是打开的管道的读端和写段
//成功返回0,失败返回-1并设置errno
我们写个小例子来看看怎么用:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BUFSIZE 1024
int main()
{
int pipefd[2];
pid_t pid;
char buf[BUFSIZE];
int len;
if (pipe(pipefd) < 0)
{
perror("pipe()");
exit(1);
}
pid = fork();
if (pid < 0) // 创建子进程失败
{
perror("fork()");
exit(1);
}
if (pid == 0) // 子进程 读管道
{
// 关闭写端
close(pipefd[1]);
// 读管道
len = read(pipefd[0], buf, BUFSIZE);
// 输出到标准输出
write(1, buf, len);
// 关闭读端
close(pipefd[0]);
exit(0);
}
else // 父进程 写管道
{
// 关闭读端
close(pipefd[0]);
// 写管道
write(pipefd[1], "Hello world\n", 12);
// 关闭写端
close(pipefd[1]);
// 收尸
wait(NULL);
// 父进程退出
exit(0);
}
}
运行结果:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/pipe$ ./pipe
Hello world
命名管道可以用mkfifo()创建,用法和匿名差不多这里就不做过多演示。
2、XSI/SysV
三种方式:
1、消息队列
2、信号量数组
3、共享内存
它们所涉及的函数都是有规律的,命名方式都是xxxget(创建)、xxxop(使用操作)、xxxctl(控制)。其中的xxx就可以是msg(消息队列)、sem(信号量数组)、shm(共享内存)
1、消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//获得消息队列
//key值可以通过ftok获得
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//发送
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);//接收
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//控制消息队列,其中的cmd的值可以参考man手册
下面我们来实现一个消息队列的实例,我们的思路是分三个文件:proto.h用于规定协议、snder.c作为发送方、rcver.c作为接收方。
注意:rcver.c作为接收方要先运行起来!
下面分别实现一下:
proto.h
#ifndef PROTO_H__
#define PROTO_H__
#define KEYPATH "/etc/services"
#define KEYPROJ 'l' // 这儿随便传一个整型数就是了,这里我用有符号的,所以用字符ascii码
#define NAMESIZE 32
struct msg_st
{
long mtype; // 这个玩意儿先不解释什么意思,后面会用到再解释
char name[NAMESIZE];
int math;
int chinese;
};
#endif
rcver.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <signal.h>
#include "proto.h"
typedef void (*sighandler_t)(int);
int msgid;
// 这个信号处理函数用来接收到ctrl+c时退出程序同时摧毁消息队列
static void handler(int p)
{
msgctl(msgid, IPC_RMID, NULL); // 使用IPC_RMID的时候就不用传参,最后一个参数写NULL即可
exit(0);
}
int main()
{
key_t key;
struct msg_st myst;
// 我们给ctrl+c注册个行为,不然while循环打断后消息队列没有释放
signal(SIGINT, handler);
// 首先获得key值
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, &myst, sizeof(myst) - sizeof(long), 0, 0) < 0) // 倒数第二个参数是否选择性接收,我们全接收写0。没有特殊要求,最后一个参数写0
{
perror("msgrcv()");
exit(1);
}
// 打印到屏幕上
printf("name is %s\n", myst.name);
printf("math is %d\n", myst.math);
printf("chinese is %d\n", myst.chinese);
}
exit(0);
}
snder.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <string.h>
#include "proto.h"
int main()
{
key_t key;
int msgid;
struct msg_st myst;
// 首先获得key值
key = ftok(KEYPATH, KEYPROJ);
if (key < 0)
{
perror("ftok()");
exit(1);
}
// 消息队列已由接收方创建,发送方不需要再创建消息队列,直接获取即可
msgid = msgget(key, 0); // 直接获取即可
if (msgid < 0)
{
perror("msgget()");
exit(1);
}
// 给消息写入内容
strcpy(myst.name, "gengenshuai");
myst.math = 100;
myst.chinese = 99;
// 发送消息
if (msgsnd(msgid, &myst, sizeof(myst) - sizeof(long), 0) < 0)
{
perror("msgsnd()");
exit(1);
}
// 发送完毕
puts("send over!\n");
exit(0);
}
我们先运行rcver.c:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/msg/basic$ ./rcver
成功运行起来接收方,现在用ipcs查看当前消息队列是否创建成功:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/msg/basic$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
0x6c0500d8 0 liugenyi 600 0 0
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 6 liugenyi 600 16384 1 dest
0x00000000 9 liugenyi 600 524288 2 dest
0x00000000 13 liugenyi 600 524288 2 dest
0x00000000 14 liugenyi 600 2129920 2 dest
0x00000000 15 liugenyi 600 2129920 2 dest
0x00000000 18 liugenyi 600 524288 2 dest
------ Semaphore Arrays --------
key semid owner perms nsems
创建成功,接下来运行snder.c:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/msg/basic$ ./snder
send over!
查看接收方:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/msg/basic$ ./rcver
name is gengenshuai
math is 100
chinese is 99
没有毛病!
2、信号量数组
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);//获得信号量数组
int semop(int semid, struct sembuf *sops, size_t nsops);//操作信号量数组
int semctl(int semid, int semnum, int cmd, ...);//控制(可以用来初始化或者销毁)
我们直接在之前的高级IO中文件锁的例子里来使用信号量数组改写程序,直接看怎么用(我们这里只用信号量数组里一个元素,并且值是1,其实就相当于互斥的用法了对吧。至于信号量数组里多个元素的实现大家可以用银行家算法去完成下):
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#define PROCNUM 20 // 进程数量
#define BUFSIZE 1024
#define FILENAME "/tmp/out"
static int semid;
// p操作,资源量--
static void p(void)
{
struct sembuf sbuf;
sbuf.sem_num = 0; // 要操作的信号量数组的下标
sbuf.sem_op = -1; //-1代表减一操作
sbuf.sem_flg = 0; // 特殊要求无
while (semop(semid, &sbuf, 1) < 0)
{
if (errno != EAGAIN || errno != EINTR)
{
perror("semop()");
exit(1);
}
}
}
// v操作,资源量++
static void v(void)
{
struct sembuf sbuf;
sbuf.sem_num = 0; // 要操作的信号量数组的下标
sbuf.sem_op = 1; // 1代表加一操作
sbuf.sem_flg = 0; // 特殊要求无
while (semop(semid, &sbuf, 1) < 0)
{
if (errno != EAGAIN || errno != EINTR)
{
perror("semop()");
exit(1);
}
}
}
static void func_add(void)
{
FILE *fp;
int fd;
char buf[BUFSIZ];
// 打开文件
fp = fopen(FILENAME, "r+"); // 读写,并且保证文件存在,因此用r+
if (fp == NULL)
{
perror("fopen()");
exit(1);
}
// P操作
p();
// 读文件
fgets(buf, BUFSIZ, fp);
// 定向到文件开头
fseek(fp, 0, SEEK_SET);
sleep(1);
// 写文件
fprintf(fp, "%d\n", atoi(buf) + 1);
// 文件是全缓冲,注意刷新
fflush(fp);
// 文件解锁
lockf(fd, F_ULOCK, 0);
// V操作
v();
// 关闭文件流
fclose(fp);
// 线程终止
return;
}
int main()
{
pid_t pid;
int i;
// 获得信号量数组
semid = semget(IPC_PRIVATE, 1, 0600); // 我们不需要用key,直接用匿名方式创建即可。数组大小1个写1即可,最后跟权限。
if (semid < 0)
{
perror("semget()");
exit(1);
}
// 初始化信号量数组赋初值1
if (semctl(semid, 0, SETVAL, 1) < 0)
{
perror("semctl");
exit(1);
}
for (i = 0; i < PROCNUM; ++i) // 创建20个进程
{
pid = fork();
if (pid < 0) // 创建子进程失败
{
// 我这里笼统报错退出,其实应该去收尸成功创建的子进程
perror("fork()");
exit(1);
}
if (pid == 0) // 子进程
{
func_add();
exit(0);
}
}
// 父进程收尸子进程
for (i = 0; i < PROCNUM; ++i)
{
wait(NULL);
}
// 摧毁信号量数组
semctl(semid, 0, IPC_RMID);
exit(0);
}
运行结果:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/sem$ echo 1 > /tmp/out
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/sem$ cat /tmp/out
1
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/sem$ ./add
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/sem$ cat /tmp/out
21
3、共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);//获得共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);//映射共享内存
int shmdt(const void *shmaddr);//解除共享内存映射
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//控制
这个共享内存其实比较类似于之前我们用mmap实现的父子进程,现在我们用共享内存来重构:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define MEMSIZE 1024
int main()
{
int shmid;
char *ptr;
pid_t pid;
// 创建共享内存
shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600); // 不需要key值,直接匿名创建即可,最后跟上权限
if (shmid < 0)
{
perror("shmget()");
exit(1);
}
// 创建子进程
pid = fork();
if (pid < 0) // 创建子进程失败
{
perror("fork()");
exit(1);
}
if (pid == 0) // 子进程写
{
// 进行共享内存的映射
ptr = shmat(shmid, NULL, 0); // 这儿写NULL表示系统帮我找位置,我就不人为指定了
if (ptr == (void *)-1) // man手册上的返回值这样写的我们就这样写
{
perror("shmat()");
exit(1);
}
strcpy(ptr, "Hello World !");
printf("child ptraddress :%x\n", (unsigned int)ptr);
// 子进程解除映射
shmdt(ptr);
// 子进程退出
exit(0);
}
else // 父进程读
{
// 先等待收尸,确保子进程写了东西进去
wait(NULL);
// 父进程映射
ptr = shmat(shmid, NULL, 0);
if (ptr == (void *)-1) // man手册上的返回值这样写的我们就这样写
{
perror("shmat()");
exit(1);
}
printf("parent ptraddress :%x\n", (unsigned int)ptr);
puts(ptr);
// 父进程解除映射
shmdt(ptr);
// 父进程摧毁共享内存
shmctl(shmid, IPC_RMID, NULL);
// 父进程退出
exit(0);
}
}
运行结果:
liugenyi@liugenyi-virtual-machine:~/linuxsty/ipc/xsi/shm$ ./shm
child ptraddress :513da000
parent ptraddress :513da000
Hello World !
3、网络套接字socket
网络套接字主要用于跨主机的传输问题。
先来想想我们在跨主机传输的时候需要注意哪些问题?
1、字节序问题
大端存储:低地址处放高字节
小端存储:低地址处放低字节
主机字节序:host
网络字节序:network
主机字节序和网络字节序之间的转换可以描述为xxtoxx:htons(主机字节序转换为short网络字节序)、htonl(主机字节序转换为long网络字节序)、ntohs、ntohl
2、对齐问题
这个是我们以前大学学习c语言的时候,老师们会让我们去算一个struct的空间占用大小,应试考试的时候我们只需要把里面的成员分别占多大空间,然后再加起来即可,但是实际我们编译器会进行对齐,这个问题大家不怎么清楚的可以专门去搜搜对齐问题,这里的重心放在网络编程就不做太多解释,反正我们在这里需要知道的是我们要禁用对齐功能!
3、类型长度问题
比如int在32位和在64位下的占据字节数不同,那假如我是16位机器,int给你,你是32位数据,那咋解析对吧
解决办法(很简单,写清楚嘛):int32_t、uint32_t、int64_t、int8_t、uint8_t
4、socket
什么是socket?socket套接字就是一种通信机制,socket屏蔽了各个协议的通信细节,提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一方便的使用tcp/ip协议的功能。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain:协议族
//type:传输的类型
//protoco:协议
//用一句口水话来说就是,我要用domain协议族中的protocol协议,来完成type类型的传输
/*
domain:只是局部,完整可见man手册
Name Purpose Man page
AF_UNIX Local communication unix(7)
AF_LOCAL Synonym for AF_UNIX
AF_INET IPv4 Internet protocols ip(7)
AF_AX25 Amateur radio AX.25 protocol ax25(4)
AF_IPX IPX - Novell protocols
AF_APPLETALK AppleTalk ddp(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_DECnet DECet protocol sockets
AF_KEY Key management protocol, originally developed for usage with IPsec
AF_NETLINK Kernel user interface device netlink(7)
AF_PACKET Low-level packet interface packet(7)
*/
/*
type:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call.
SOCK_RAW Provides raw network protocol access.
SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET Obsolete and should not be used in new programs; see packet(7).
*/
5、报式套接字
我们先来说下整体的流程:
被动端(先运行)
a、取得socket
b、给socket取得地址
c、收/发消息
d、关闭socket
主动端
a、取得socket
b、给socket取得地址(可省略)
c、收/发消息
d、关闭socket
6、流式套接字
接下来我们写个小例子看看:
写小例子之前我们先man一下会用到的几个函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:套接字fd
//addr:地址结构体,其中包括使用协议、ip地址、端口信息,详细见man手册,一会儿我下面的例子会用到,可以直接看
//addrlen:大小
#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);
//sockfd:套接字fd
//buf:接收的数据
//len:接收的数据大小
//flags:特殊要求(没有就写0)
//src_addr:对端结构体,里面包括对端的使用协议、ip地址、端口信息,详细见man手册,一会儿我下面的例子会用到,可以直接看
//addrlen:结构体长度
#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);
下面我们实现小例子:
proto.h
#ifndef PROTO_H__
#define PROTO_h__
#define RCVPORT "1989"
#define NAMESIZE 11
struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
} __attribute__((packed));
#endif
rcvev.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPBUFSIZE 40
int main()
{
struct sockaddr_in laddr, raddr; // local address(本地地址)remote address(远端地址)
struct msg_st rbuf;
socklen_t st;
char ipbuf[IPBUFSIZE];
int sfd;
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0); // 最后一个写0表示用AF_INET协议族里面的默认协议(UDP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 2、地址赋值绑定
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr); // inet_pton()这个函数可以完成ip地址点分十进制向大整数的转换;"0.0.0.0"可以匹配任何地址,也就是当前环境下我们的ip地址是多少那它就是多少
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 3、接收消息
// 注意,st的初始化非常重要!!!!
// 如果不初始化就会出错,但是只有第一次会出错,后面就会正确,因为第一次会发现地址不符,但经过通讯之后就会发现对端的正确地址
st = sizeof(raddr);
while (1)
{
recvfrom(sfd, &rbuf, sizeof(rbuf), 0, (void *)&raddr, &st);
inet_ntop(AF_INET, &raddr.sin_addr, ipbuf, IPBUFSIZE); // 把大整数ip地址转化为点分十进制
printf("------MESSAGER FROM------ %s:%d\n", ipbuf, ntohs(raddr.sin_port));
printf("name:%s\n", rbuf.name);
printf("chinese:%d\n", ntohl(rbuf.chinese));
printf("math:%d\n", ntohl(rbuf.math));
}
// 4、关闭
close(sfd);
exit(0);
}
snder.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
int main(int argc, char **argv)
{
struct msg_st sbuf;
struct sockaddr_in radd;
int sfd;
// 参数检查
if (argc != 2)
{
fprintf(stderr, "please use right agrc!\n");
exit(1);
}
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 2、给socket注册/绑定地址(可省略)---bind
// 3、发送消息
// 消息内容赋值
strcpy(sbuf.name, "liugenyi");
sbuf.chinese = htonl(95);
sbuf.math = htonl(100);
// 地址赋值
radd.sin_family = AF_INET;
radd.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, argv[1], &radd.sin_addr);
if (sendto(sfd, &sbuf, sizeof(sbuf), 0, (void *)&radd, sizeof(radd)) < 0)
{
perror("sendto()");
exit(1);
}
puts("send ok!");
// 4、关闭
close(sfd);
exit(0);
}
首先我们运行接收方: 然后发送方发送消息,这里我方便放效果图,就在本地演示,因此就发给自己127.0.0.1
结果没有问题。
但是在上述程序中我们把NAMESIZE写死为11,这就很死板,太长的名字发不了,太短的又会产生浪费,因此我们进行一波优化(想法是通过变长结构体实现):
proto.h
#ifndef PROTO_H__
#define PROTO_h__
#define RCVPORT "1989"
#define NAMEMAX (512 - 8 - 8) // 512是UDP推荐长度,其中一个8是结构体中的math和Chinese大小,另外一个8是头部大小
struct msg_st
{
uint32_t math;
uint32_t chinese;
uint8_t name[1];
} __attribute__((packed));
#endif
rcvev.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPBUFSIZE 40
int main()
{
struct sockaddr_in laddr, raddr; // local address(本地地址)remote address(远端地址)
struct msg_st *rbufp; // 接收方也用指针来接收
socklen_t st;
char ipbuf[IPBUFSIZE];
int sfd;
int size;
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0); // 最后一个写0表示用AF_INET协议族里面的默认协议(UDP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 申请空间
size = sizeof(struct msg_st) + NAMEMAX - 1; //-1是因为结构体中数组写的1
rbufp = malloc(size);
if (rbufp == NULL)
{
perror("malloc()");
exit(1);
}
// 2、地址赋值绑定
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr); // inet_pton()这个函数可以完成ip地址点分十进制向大整数的转换;"0.0.0.0"可以匹配任何地址,也就是当前环境下我们的ip地址是多少那它就是多少
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 3、接收消息
// 注意,st的初始化非常重要!!!!
// 如果不初始化就会出错,但是只有第一次会出错,后面就会正确,因为第一次会发现地址不符,但经过通讯之后就会发现对端的正确地址
st = sizeof(raddr);
while (1)
{
recvfrom(sfd, rbufp, size, 0, (void *)&raddr, &st);
inet_ntop(AF_INET, &raddr.sin_addr, ipbuf, IPBUFSIZE); // 把大整数ip地址转化为点分十进制
printf("------MESSAGER FROM------ %s:%d\n", ipbuf, ntohs(raddr.sin_port));
printf("name:%s\n", rbufp->name);
printf("chinese:%d\n", ntohl(rbufp->chinese));
printf("math:%d\n", ntohl(rbufp->math));
}
// 4、关闭
close(sfd);
exit(0);
}
snder.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
int main(int argc, char **argv)
{
struct msg_st *sbufp; // 这里得用指针了,因为结构体中的实际长度已经不是定义里的长度了
struct sockaddr_in radd;
int sfd;
int size;
// 参数检查
if (argc != 3)
{
fprintf(stderr, "please use right agrc!\n");
exit(1);
}
if (strlen(argv[2]) > NAMEMAX)
{
fprintf(stderr, "name is too long!\n");
exit(1);
}
// 申请空间
size = sizeof(struct msg_st) + strlen(argv[2]);
sbufp = malloc(size);
if (sbufp == NULL)
{
perror("malloc()");
exit(1);
}
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 2、给socket注册/绑定地址(可省略)---bind
// 3、发送消息
// 消息内容赋值
strcpy(sbufp->name, argv[2]);
sbufp->chinese = htonl(95);
sbufp->math = htonl(100);
// 地址赋值
radd.sin_family = AF_INET;
radd.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, argv[1], &radd.sin_addr);
if (sendto(sfd, sbufp, size, 0, (void *)&radd, sizeof(radd)) < 0)
{
perror("sendto()");
exit(1);
}
puts("send ok!");
// 4、关闭
close(sfd);
exit(0);
}
看看结果:
报式套接字中涉及到多点通信:广播(全网广播、子网广播),多播/组播,这些的含义就是计网的知识了,这里我就不做过多解释,直接应用。
这里演示下广播,我们就将就上面那个例子来改一下,但是涉及到类型的变更就需要用到关于设置socket的函数了:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
//我这里就一句话来表达这些参数啥意思
//对哪个socket(sockfd)的哪一层(level),名字是什么(optname),给什么参数(optcal),参数长度是多少(optlen)
//具体的optname、optcal等信息大家可以自行man手册7socket等进行查看,在socket options里
对snder.c修改如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
int main()
{
struct msg_st sbuf;
struct sockaddr_in radd;
int sfd;
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 给socket设置属性(广播)
int flag = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 2、给socket注册/绑定地址(可省略)---bind
// 3、发送消息
// 消息内容赋值
strcpy(sbuf.name, "liugenyi");
sbuf.chinese = htonl(95);
sbuf.math = htonl(100);
// 地址赋值
radd.sin_family = AF_INET;
radd.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, "255.255.255.255", &radd.sin_addr); // 广播
if (sendto(sfd, &sbuf, sizeof(sbuf), 0, (void *)&radd, sizeof(radd)) < 0)
{
perror("sendto()");
exit(1);
}
puts("send ok!");
// 4、关闭
close(sfd);
exit(0);
}
运行结果:
下面再来把它换成组播实现下,逻辑大体应该是这样:发送方创建组播组,接收方加入组播组:
proto.h
#ifndef PROTO_H__
#define PROTO_h__
#define RCVPORT "1989"
#define NAMESIZE 11
#define MGROUP "224.2.2.2" // 多播地址
struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
} __attribute__((packed));
#endif
snder.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <netinet/in.h>
#include "proto.h"
int main()
{
struct msg_st sbuf;
struct sockaddr_in radd;
int sfd;
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd < 0)
{
perror("socket()");
exit(1);
}
struct ip_mreqn mreq;
inet_pton(AF_INET, MGROUP, &mreq.imr_multiaddr);
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("ens33");
// 创建组播
if (setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 2、给socket注册/绑定地址(可省略)---bind
// 3、发送消息
// 消息内容赋值
strcpy(sbuf.name, "liugenyi");
sbuf.chinese = htonl(95);
sbuf.math = htonl(100);
// 地址赋值
radd.sin_family = AF_INET;
radd.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, MGROUP, &radd.sin_addr); // 组播
if (sendto(sfd, &sbuf, sizeof(sbuf), 0, (void *)&radd, sizeof(radd)) < 0)
{
perror("sendto()");
exit(1);
}
puts("send ok!");
// 4、关闭
close(sfd);
exit(0);
}
rcver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <net/if.h>
#include "proto.h"
#define IPBUFSIZE 40
int main()
{
struct sockaddr_in laddr, raddr; // local address(本地地址)remote address(远端地址)
struct msg_st rbuf;
socklen_t st;
char ipbuf[IPBUFSIZE];
int sfd;
// 1、获得socket
sfd = socket(AF_INET, SOCK_DGRAM, 0); // 最后一个写0表示用AF_INET协议族里面的默认协议(UDP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 2、地址赋值绑定
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr); // inet_pton()这个函数可以完成ip地址点分十进制向大整数的转换;"0.0.0.0"可以匹配任何地址,也就是当前环境下我们的ip地址是多少那它就是多少
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 加入组播
struct ip_mreqn mreq;
inet_pton(AF_INET, MGROUP, &mreq.imr_multiaddr);
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("ens33");
if (setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 3、接收消息
// 注意,st的初始化非常重要!!!!
// 如果不初始化就会出错,但是只有第一次会出错,后面就会正确,因为第一次会发现地址不符,但经过通讯之后就会发现对端的正确地址
st = sizeof(raddr);
while (1)
{
recvfrom(sfd, &rbuf, sizeof(rbuf), 0, (void *)&raddr, &st);
inet_ntop(AF_INET, &raddr.sin_addr, ipbuf, IPBUFSIZE); // 把大整数ip地址转化为点分十进制
printf("------MESSAGER FROM------ %s:%d\n", ipbuf, ntohs(raddr.sin_port));
printf("name:%s\n", rbuf.name);
printf("chinese:%d\n", ntohl(rbuf.chinese));
printf("math:%d\n", ntohl(rbuf.math));
}
// 4、关闭
close(sfd);
exit(0);
}
如果我们还没有改写rcver.c就运行的话,那结果如下:很显然收不到消息,因为接收方都没加入组播组 rcver.c改写之后(即加入组之后),即可成功收到消息:
6、流式套接字
整体流程:
被动端:
a、获取socket
b、给socket取得地址
c、将socket置为监听模式
d、接收连接
e、收/发消息
f、关闭
主动端:
a、获取socket
b、给socket取得地址(可省)
c、发送消c
d、收/发消息
e、关闭
补充函数:
// listen - listen for connections on a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//sockfd:监听的socket
//backlog:监听队列大小
// accept a connection on a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addren);
//sockfd:socket
//addr:对端地址
//addren:结构体长度
// initiate a connection on a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);// 同上accept用法一致
下面我们实现下基本版流式套接字:
proto.h
#ifndef PROTO_H__
#define PROTO_H__
#define SERVERPORT "2001"
#define FMT_STAMP "%lld\r\n"
#endif
server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>
#define IPSTRSIZE 40
#define BUFSIZE 1024
#include "proto.h"
static void server_job(int sfd)
{
char buf[BUFSIZE];
int len;
len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
if (send(sfd, buf, len, 0) < 0)
{
perror("send()");
exit(1);
}
}
int main()
{
int sfd, newsfd;
struct sockaddr_in laddr, raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
// 获取socket
sfd = socket(AF_INET, SOCK_STREAM, 0); // 使用流式套接字的默认协议(即TCP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 给socket加个属性,在程序通过ctrl+c结束时能快速释放端口资源
int flag = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 给socket绑定地址
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 设置监听模式
if (listen(sfd, 200) < 0)
{
perror("listen()");
exit(1);
}
// 接收连接
raddr_len = sizeof(raddr);
while (1)
{
newsfd = accept(sfd, (void *)&raddr, &raddr_len);
if (newsfd < 0)
{
//....这里可以用newsfd进行信号判断,假错就continue,我这里就省略了
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(newsfd);
close(newsfd);
}
// 关闭连接
close(sfd);
exit(0);
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "proto.h"
int main(int argc, char **argv)
{
int sfd;
struct sockaddr_in raddr;
FILE *fp;
long long stamp;
// 参数检查
if (argc != 2)
{
fprintf(stderr, "please use right argc!\n");
exit(1);
}
// 获得socket
sfd = socket(AF_INET, SOCK_STREAM, 0); // 使用流式套接字的默认协议(即TCP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// bind(可省略)
// 连接
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
if (connect(sfd, (void *)&raddr, sizeof(raddr)) < 0)
{
perror("connect()");
exit(1);
}
// recv 这里当然可以用recv来接收,但是我这里用其他方法来让大家切身感受下Linux理念的魅力
// close
// 第一部分io我们就说过这个函数可以用于将一个文件描述符的操作转换为流的操作
fp = fdopen(sfd, "r+");
if (fp == NULL)
{
perror("fdopen()");
exit(1);
}
if (fscanf(fp, FMT_STAMP, &stamp) < 1)
{
fprintf(stderr, "bad format!\n");
exit(1);
}
else
{
fprintf(stdout, "%lld\n", stamp);
}
fclose(fp);
close(sfd);
exit(0);
}
运行结果:
结果没有问题,但是我们想一下,如果server端接收一个连接后,都会为它服务10秒(就比如在上面的server_job中加一个sleep(10)),那这10秒钟期间万一还有其他的几千个请求来访问怎么办呢?因此我们可以用之前的并发的思想来优化一下程序,在server进行accept之后,我们就可以进行fork操作,这样每有一次请求就可以派一个子进程去进行server_job。但是这其实也并不是很好,原因和我们之前筛质数的程序差不多,子进程上限数量问题,因此,这里我用静态进程池来实现:proto.h和client.c都不用变,改写下server.c即可:
server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>
#define IPSTRSIZE 40
#define BUFSIZE 1024
#define PROCNUM 4 // 进程数量
#include "proto.h"
static void server_loop(int sfd);
static void server_job(int sfd)
{
char buf[BUFSIZE];
int len;
len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
if (send(sfd, buf, len, 0) < 0)
{
perror("send()");
exit(1);
}
}
int main()
{
int sfd, newsfd;
int i;
pid_t pid;
struct sockaddr_in laddr;
// 获取socket
sfd = socket(AF_INET, SOCK_STREAM, 0); // 使用流式套接字的默认协议(即TCP)
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 给socket加个属性,在程序通过ctrl+c结束时能快速释放端口资源
int flag = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 给socket绑定地址
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 设置监听模式
if (listen(sfd, 200) < 0)
{
perror("listen()");
exit(1);
}
// 创建子进程
for (i = 0; i < PROCNUM; ++i)
{
pid = fork();
// 创建子进程失败
if (pid < 0)
{
perror("fork()");
exit(1);
}
// 如果是子进程
if (pid == 0)
{
server_loop(sfd);
exit(0); // 我这里让子进程一直干活,所以这个其实也执行不到
}
}
// 收尸
for (i = 0; i < PROCNUM; ++i)
{
wait(NULL);
}
close(sfd);
exit(0);
}
static void server_loop(int sfd)
{
struct sockaddr_in raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
int newsfd;
// 接收连接
raddr_len = sizeof(raddr);
while (1)
{
newsfd = accept(sfd, (void *)&raddr, &raddr_len);
if (newsfd < 0)
{
//....这里可以用newsfd进行信号判断,假错就continue,我这里就省略了
perror("accept()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("[%d]client:%s:%d\n", getpid(), ipstr, ntohs(raddr.sin_port));
server_job(newsfd);
close(newsfd);
}
}
结果:其实我自己演示效果不大明显,大家可以用多台机器去访问server效果就会明显
但其实我们也知道,静态进程池的弊端,不够弹性灵活,要是可以实现这种就挺好的:我进程池里的进程弹性变化,在保证一个上限(最大进程数量)的前提下,根据现在访问量来灵活改变进程池中的进程数量。
那我们来实现一下动态进程池:proto.h和client.c不用变,我们改写server.c:
server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <time.h>
#include "proto.h"
#define MINSPARESERVER 5 // 最小空闲server数量
#define MAXSPARESERVER 10 // 最大空闲server数量
#define MAXCLIENT 20 // 最大支持连接的client
#define SIG_NOTIFY SIGUSR2
#define LINEBUFSIZE 1024
enum
{
STATE_IDLE = 0,
STATE_BUSY
};
struct server_st
{
pid_t pid;
int state;
};
static struct server_st *serverpool;
static int idle_count = 0, busy_count = 0;
static int sfd;
static int slot;
static void usr2_handler(int s)
{
return;
}
static void server_job(int pos)
{
int ppid;
int client_sfd;
struct sockaddr_in raddr;
socklen_t raddr_len;
time_t stamp;
int len;
char linebuf[LINEBUFSIZE];
ppid = getppid();
while (1)
{
// 置为空闲态
serverpool[slot].state = STATE_IDLE;
// 给父进程发信号
kill(ppid, SIG_NOTIFY);
// 接收连接
raddr_len = sizeof(raddr);
client_sfd = accept(sfd, (void *)&raddr, &raddr_len);
if (client_sfd < 0)
{
if (errno != EINTR || errno != EAGAIN)
{
perror("accept()");
exit(1);
}
continue;
}
// 状态置为忙碌
serverpool[slot].state = STATE_BUSY;
kill(ppid, SIG_NOTIFY);
stamp = (long long)time(NULL);
len = snprintf(linebuf, LINEBUFSIZE, FMT_STAMP, stamp);
send(client_sfd, linebuf, len, 0);
/*if error*/
sleep(5);
close(client_sfd);
}
}
static int add_1_server(void)
{
pid_t pid;
if (idle_count + busy_count >= MAXCLIENT)
{
return -1;
}
// 找空位
for (slot = 0; slot < MAXCLIENT; ++slot)
{
if (serverpool[slot].pid == -1)
{
break;
}
}
serverpool[slot].state = STATE_IDLE;
pid = fork();
// 创建子进程失败
if (pid < 0)
{
perror("fork()");
exit(1);
}
// 子进程
if (pid == 0)
{
// 接收连接并发送时间
server_job(slot);
exit(0);
}
// 父进程
else
{
serverpool[slot].pid = pid;
idle_count++;
return 0;
}
}
static int del_1_server(void)
{
if (idle_count == 0)
{
return -1;
}
// 找一个子进程干掉
for (slot = 0; slot < MAXCLIENT; slot++)
{
if (serverpool[slot].pid != -1 && serverpool[slot].state == STATE_IDLE)
{
kill(serverpool[slot].pid, SIGTERM);
serverpool[slot].pid = -1;
idle_count--;
break;
}
}
return 0;
}
// 扫描pool
static int scan_pool(void)
{
int i;
int idle = 0, busy = 0;
for (i = 0; i < MAXCLIENT; ++i)
{
if (serverpool[i].pid == -1)
{
continue;
}
if (kill(serverpool[i].pid, 0))
{
serverpool[i].pid = -1;
continue;
}
if (serverpool[i].state == STATE_IDLE)
{
idle++;
}
else if (serverpool[i].state == STATE_BUSY)
{
busy++;
}
else
{
fprintf(stderr, "unknown state!\n");
// 注意这里的结束最好用_exit()或者abort()
// 因为这种错误比较严重,别刷新流,直接退出
_exit(1);
}
}
idle_count = idle;
busy_count = busy;
return 0;
}
int main()
{
int val;
int i;
struct sigaction sa, oldsa;
struct sockaddr_in laddr;
sigset_t set, oldset;
// 忽略SIGCHLD信号
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, &oldsa);
// 定义我们自己的信号
sa.sa_handler = usr2_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIG_NOTIFY, &sa, &oldsa);
// 让信号进入block
sigemptyset(&set);
sigaddset(&set, SIG_NOTIFY);
sigprocmask(SIG_BLOCK, &set, &oldset);
// 申请空间(这里我就不用malloc了,用我们之前讲过的内存映射mmap来申请)
serverpool = mmap(NULL, sizeof(struct server_st) * MAXCLIENT, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (serverpool == MAP_FAILED)
{
perror("mmap()");
exit(1);
}
// 初始化
for (i = 0; i < MAXCLIENT; i++)
{
serverpool[i].pid = -1;
}
// 获得socket
sfd = socket(AF_INET, SOCK_STREAM, 0); // 使用默认协议tcp
if (sfd < 0)
{
perror("socket()");
exit(1);
}
// 设置socket属性
val = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)))
{
perror("setsockopt()");
exit(1);
}
// 给socket绑地址
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr);
if (bind(sfd, (void *)&laddr, sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
// 监听
if (listen(sfd, 100) < 0)
{
perror("listen()");
exit(1);
}
for (i = 0; i < MINSPARESERVER; ++i)
{
// 首先增加server到最低数量
add_1_server();
}
while (1)
{
sigsuspend(&oldset);
// 扫描进程池
scan_pool();
// 操作进程池
if (idle_count > MAXSPARESERVER)
{
for (i = 0; i < (idle_count - MAXSPARESERVER); ++i)
{
// 删除server
del_1_server();
}
}
if (idle_count < MAXSPARESERVER)
{
for (i = 0; i < (MAXSPARESERVER - idle_count); ++i)
{
// 增加server
add_1_server();
}
}
// 打印pool
for (i = 0; i < MAXCLIENT; ++i)
{
if (serverpool[i].pid == -1)
{
putchar(' ');
}
else if (serverpool[i].state == STATE_IDLE)
{
putchar('.');
}
else
{
putchar('x');
}
}
putchar('\n');
}
// 恢复之前的信号
sigprocmask(SIG_SETMASK, &oldset, NULL);
exit(0);
}
运行结果: 大家每创建一个client去访问就可以发现服务端的x(忙碌增多),减少一个client就可以发现.(空闲增多)